rippled
Loading...
Searching...
No Matches
Subscribe_test.cpp
1#include <test/jtx.h>
2#include <test/jtx/WSClient.h>
3#include <test/jtx/envconfig.h>
4
5#include <xrpld/app/main/LoadManager.h>
6#include <xrpld/app/misc/LoadFeeTrack.h>
7#include <xrpld/app/misc/NetworkOPs.h>
8#include <xrpld/core/ConfigSections.h>
9
10#include <xrpl/beast/unit_test.h>
11#include <xrpl/json/json_value.h>
12#include <xrpl/protocol/Feature.h>
13#include <xrpl/protocol/jss.h>
14
15#include <tuple>
16
17namespace xrpl {
18namespace test {
19
21{
22public:
23 void
25 {
26 using namespace std::chrono_literals;
27 using namespace jtx;
28 Env env(*this);
29 auto wsc = makeWSClient(env.app().config());
30 Json::Value stream;
31
32 {
33 // RPC subscribe to server stream
34 stream[jss::streams] = Json::arrayValue;
35 stream[jss::streams].append("server");
36 auto jv = wsc->invoke("subscribe", stream);
37 if (wsc->version() == 2)
38 {
39 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
40 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
41 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
42 }
43 BEAST_EXPECT(jv[jss::status] == "success");
44 }
45
46 // here we forcibly stop the load manager because it can (rarely but
47 // every-so-often) cause fees to raise or lower AFTER we've called the
48 // first findMsg but BEFORE we unsubscribe, thus causing the final
49 // findMsg check to fail since there is one unprocessed ws msg created
50 // by the loadmanager
51 env.app().getLoadManager().stop();
52 {
53 // Raise fee to cause an update
54 auto& feeTrack = env.app().getFeeTrack();
55 for (int i = 0; i < 5; ++i)
56 feeTrack.raiseLocalFee();
57 env.app().getOPs().reportFeeChange();
58
59 // Check stream update
60 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { return jv[jss::type] == "serverStatus"; }));
61 }
62
63 {
64 // RPC unsubscribe
65 auto jv = wsc->invoke("unsubscribe", stream);
66 if (wsc->version() == 2)
67 {
68 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
69 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
70 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
71 }
72 BEAST_EXPECT(jv[jss::status] == "success");
73 }
74
75 {
76 // Raise fee to cause an update
77 auto& feeTrack = env.app().getFeeTrack();
78 for (int i = 0; i < 5; ++i)
79 feeTrack.raiseLocalFee();
80 env.app().getOPs().reportFeeChange();
81
82 // Check stream update
83 auto jvo = wsc->getMsg(10ms);
84 BEAST_EXPECTS(!jvo, "getMsg: " + to_string(jvo.value()));
85 }
86 }
87
88 void
90 {
91 using namespace std::chrono_literals;
92 using namespace jtx;
93 Env env(*this);
94 auto wsc = makeWSClient(env.app().config());
95 Json::Value stream;
96
97 {
98 // RPC subscribe to ledger stream
99 stream[jss::streams] = Json::arrayValue;
100 stream[jss::streams].append("ledger");
101 auto jv = wsc->invoke("subscribe", stream);
102 if (wsc->version() == 2)
103 {
104 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
105 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
106 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
107 }
108 BEAST_EXPECT(jv[jss::result][jss::ledger_index] == 2);
109 BEAST_EXPECT(jv[jss::result][jss::network_id] == env.app().config().NETWORK_ID);
110 }
111
112 {
113 // Accept a ledger
114 env.close();
115
116 // Check stream update
117 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
118 return jv[jss::ledger_index] == 3 && jv[jss::network_id] == env.app().config().NETWORK_ID;
119 }));
120 }
121
122 {
123 // Accept another ledger
124 env.close();
125
126 // Check stream update
127 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
128 return jv[jss::ledger_index] == 4 && jv[jss::network_id] == env.app().config().NETWORK_ID;
129 }));
130 }
131
132 // RPC unsubscribe
133 auto jv = wsc->invoke("unsubscribe", stream);
134 if (wsc->version() == 2)
135 {
136 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
137 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
138 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
139 }
140 BEAST_EXPECT(jv[jss::status] == "success");
141 }
142
143 void
145 {
146 using namespace std::chrono_literals;
147 using namespace jtx;
148 Env env(*this);
149 auto baseFee = env.current()->fees().base.drops();
150 auto wsc = makeWSClient(env.app().config());
151 Json::Value stream;
152
153 {
154 // RPC subscribe to transactions stream
155 stream[jss::streams] = Json::arrayValue;
156 stream[jss::streams].append("transactions");
157 auto jv = wsc->invoke("subscribe", stream);
158 if (wsc->version() == 2)
159 {
160 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
161 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
162 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
163 }
164 BEAST_EXPECT(jv[jss::status] == "success");
165 }
166
167 {
168 env.fund(XRP(10000), "alice");
169 env.close();
170
171 // Check stream update for payment transaction
172 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
173 return jv[jss::meta]["AffectedNodes"][1u]["CreatedNode"]["NewFields"][jss::Account] //
174 == Account("alice").human() &&
175 jv[jss::transaction][jss::TransactionType] //
176 == jss::Payment &&
177 jv[jss::transaction][jss::DeliverMax] //
178 == std::to_string(10000000000 + baseFee) &&
179 jv[jss::transaction][jss::Fee] //
180 == std::to_string(baseFee) &&
181 jv[jss::transaction][jss::Sequence] //
182 == 1;
183 }));
184
185 // Check stream update for accountset transaction
186 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
187 return jv[jss::meta]["AffectedNodes"][0u]["ModifiedNode"]["FinalFields"][jss::Account] ==
188 Account("alice").human();
189 }));
190
191 env.fund(XRP(10000), "bob");
192 env.close();
193
194 // Check stream update for payment transaction
195 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
196 return jv[jss::meta]["AffectedNodes"][1u]["CreatedNode"]["NewFields"][jss::Account] //
197 == Account("bob").human() &&
198 jv[jss::transaction][jss::TransactionType] //
199 == jss::Payment &&
200 jv[jss::transaction][jss::DeliverMax] //
201 == std::to_string(10000000000 + baseFee) &&
202 jv[jss::transaction][jss::Fee] //
203 == std::to_string(baseFee) &&
204 jv[jss::transaction][jss::Sequence] //
205 == 2;
206 }));
207
208 // Check stream update for accountset transaction
209 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
210 return jv[jss::meta]["AffectedNodes"][0u]["ModifiedNode"]["FinalFields"][jss::Account] ==
211 Account("bob").human();
212 }));
213 }
214
215 {
216 // RPC unsubscribe
217 auto jv = wsc->invoke("unsubscribe", stream);
218 if (wsc->version() == 2)
219 {
220 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
221 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
222 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
223 }
224 BEAST_EXPECT(jv[jss::status] == "success");
225 }
226
227 {
228 // RPC subscribe to accounts stream
229 stream = Json::objectValue;
230 stream[jss::accounts] = Json::arrayValue;
231 stream[jss::accounts].append(Account("alice").human());
232 auto jv = wsc->invoke("subscribe", stream);
233 if (wsc->version() == 2)
234 {
235 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
236 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
237 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
238 }
239 BEAST_EXPECT(jv[jss::status] == "success");
240 }
241
242 {
243 // Transaction that does not affect stream
244 env.fund(XRP(10000), "carol");
245 env.close();
246 BEAST_EXPECT(!wsc->getMsg(10ms));
247
248 // Transactions concerning alice
249 env.trust(Account("bob")["USD"](100), "alice");
250 env.close();
251
252 // Check stream updates
253 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
254 return jv[jss::meta]["AffectedNodes"][1u]["ModifiedNode"]["FinalFields"][jss::Account] ==
255 Account("alice").human();
256 }));
257
258 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
259 return jv[jss::meta]["AffectedNodes"][1u]["CreatedNode"]["NewFields"]["LowLimit"][jss::issuer] ==
260 Account("alice").human();
261 }));
262 }
263
264 // RPC unsubscribe
265 auto jv = wsc->invoke("unsubscribe", stream);
266 if (wsc->version() == 2)
267 {
268 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
269 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
270 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
271 }
272 BEAST_EXPECT(jv[jss::status] == "success");
273 }
274
275 void
277 {
278 testcase("transactions API version 2");
279
280 using namespace std::chrono_literals;
281 using namespace jtx;
282 Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
283 cfg->FEES.reference_fee = 10;
284 return cfg;
285 }));
286 auto wsc = makeWSClient(env.app().config());
288
289 {
290 // RPC subscribe to transactions stream
291 stream[jss::api_version] = 2;
292 stream[jss::streams] = Json::arrayValue;
293 stream[jss::streams].append("transactions");
294 auto jv = wsc->invoke("subscribe", stream);
295 if (wsc->version() == 2)
296 {
297 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
298 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
299 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
300 }
301 BEAST_EXPECT(jv[jss::status] == "success");
302 }
303
304 {
305 env.fund(XRP(10000), "alice");
306 env.close();
307
308 // Check stream update for payment transaction
309 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
310 return jv[jss::meta]["AffectedNodes"][1u]["CreatedNode"]["NewFields"][jss::Account] //
311 == Account("alice").human() &&
312 jv[jss::close_time_iso] //
313 == "2000-01-01T00:00:10Z" &&
314 jv[jss::validated] == true && //
315 jv[jss::ledger_hash] ==
316 "0F1A9E0C109ADEF6DA2BDE19217C12BBEC57174CBDBD212B0EBDC1CEDB"
317 "853185" && //
318 !jv[jss::inLedger] &&
319 jv[jss::ledger_index] == 3 && //
320 jv[jss::tx_json][jss::TransactionType] //
321 == jss::Payment &&
322 jv[jss::tx_json][jss::DeliverMax] //
323 == "10000000010" &&
324 !jv[jss::tx_json].isMember(jss::Amount) &&
325 jv[jss::tx_json][jss::Fee] //
326 == "10" &&
327 jv[jss::tx_json][jss::Sequence] //
328 == 1;
329 }));
330
331 // Check stream update for accountset transaction
332 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
333 return jv[jss::meta]["AffectedNodes"][0u]["ModifiedNode"]["FinalFields"][jss::Account] ==
334 Account("alice").human();
335 }));
336 }
337
338 {
339 // RPC unsubscribe
340 auto jv = wsc->invoke("unsubscribe", stream);
341 if (wsc->version() == 2)
342 {
343 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
344 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
345 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
346 }
347 BEAST_EXPECT(jv[jss::status] == "success");
348 }
349 }
350
351 void
353 {
354 using namespace jtx;
355 Env env(*this);
356 auto wsc = makeWSClient(env.app().config());
357 Json::Value stream;
358
359 {
360 // RPC subscribe to manifests stream
361 stream[jss::streams] = Json::arrayValue;
362 stream[jss::streams].append("manifests");
363 auto jv = wsc->invoke("subscribe", stream);
364 if (wsc->version() == 2)
365 {
366 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
367 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
368 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
369 }
370 BEAST_EXPECT(jv[jss::status] == "success");
371 }
372
373 // RPC unsubscribe
374 auto jv = wsc->invoke("unsubscribe", stream);
375 if (wsc->version() == 2)
376 {
377 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
378 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
379 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
380 }
381 BEAST_EXPECT(jv[jss::status] == "success");
382 }
383
384 void
386 {
387 using namespace jtx;
388
389 Env env{*this, envconfig(validator, ""), features};
390 auto& cfg = env.app().config();
391 if (!BEAST_EXPECT(cfg.section(SECTION_VALIDATION_SEED).empty()))
392 return;
393 auto const parsedseed = parseBase58<Seed>(cfg.section(SECTION_VALIDATION_SEED).values()[0]);
394 if (!BEAST_EXPECT(parsedseed))
395 return;
396
397 std::string const valPublicKey = toBase58(
400
401 auto wsc = makeWSClient(env.app().config());
402 Json::Value stream;
403
404 {
405 // RPC subscribe to validations stream
406 stream[jss::streams] = Json::arrayValue;
407 stream[jss::streams].append("validations");
408 auto jv = wsc->invoke("subscribe", stream);
409 if (wsc->version() == 2)
410 {
411 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
412 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
413 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
414 }
415 BEAST_EXPECT(jv[jss::status] == "success");
416 }
417
418 {
419 // Lambda to check ledger validations from the stream.
420 auto validValidationFields = [&env, &valPublicKey](Json::Value const& jv) {
421 if (jv[jss::type] != "validationReceived")
422 return false;
423
424 if (jv[jss::validation_public_key].asString() != valPublicKey)
425 return false;
426
427 if (jv[jss::ledger_hash] != to_string(env.closed()->header().hash))
428 return false;
429
430 if (jv[jss::ledger_index] != std::to_string(env.closed()->header().seq))
431 return false;
432
433 if (jv[jss::flags] != (vfFullyCanonicalSig | vfFullValidation))
434 return false;
435
436 if (jv[jss::full] != true)
437 return false;
438
439 if (jv.isMember(jss::load_fee))
440 return false;
441
442 if (!jv.isMember(jss::signature))
443 return false;
444
445 if (!jv.isMember(jss::signing_time))
446 return false;
447
448 if (!jv.isMember(jss::cookie))
449 return false;
450
451 if (!jv.isMember(jss::validated_hash))
452 return false;
453
454 uint32_t netID = env.app().config().NETWORK_ID;
455 if (!jv.isMember(jss::network_id) || jv[jss::network_id] != netID)
456 return false;
457
458 // Certain fields are only added on a flag ledger.
459 bool const isFlagLedger = (env.closed()->header().seq + 1) % 256 == 0;
460
461 if (jv.isMember(jss::server_version) != isFlagLedger)
462 return false;
463
464 if (jv.isMember(jss::reserve_base) != isFlagLedger)
465 return false;
466
467 if (jv.isMember(jss::reserve_inc) != isFlagLedger)
468 return false;
469
470 return true;
471 };
472
473 // Check stream update. Look at enough stream entries so we see
474 // at least one flag ledger.
475 while (env.closed()->header().seq < 300)
476 {
477 env.close();
478 using namespace std::chrono_literals;
479 BEAST_EXPECT(wsc->findMsg(5s, validValidationFields));
480 }
481 }
482
483 // RPC unsubscribe
484 auto jv = wsc->invoke("unsubscribe", stream);
485 if (wsc->version() == 2)
486 {
487 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
488 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
489 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
490 }
491 BEAST_EXPECT(jv[jss::status] == "success");
492 }
493
494 void
496 {
497 using namespace jtx;
498 testcase("Subscribe by url");
499 Env env{*this};
500
501 Json::Value jv;
502 jv[jss::url] = "http://localhost/events";
503 jv[jss::url_username] = "admin";
504 jv[jss::url_password] = "password";
505 jv[jss::streams] = Json::arrayValue;
506 jv[jss::streams][0u] = "validations";
507 auto jr = env.rpc("json", "subscribe", to_string(jv))[jss::result];
508 BEAST_EXPECT(jr[jss::status] == "success");
509
510 jv[jss::streams][0u] = "ledger";
511 jr = env.rpc("json", "subscribe", to_string(jv))[jss::result];
512 BEAST_EXPECT(jr[jss::status] == "success");
513 BEAST_EXPECT(jr[jss::network_id] == env.app().config().NETWORK_ID);
514
515 jr = env.rpc("json", "unsubscribe", to_string(jv))[jss::result];
516 BEAST_EXPECT(jr[jss::status] == "success");
517
518 jv[jss::streams][0u] = "validations";
519 jr = env.rpc("json", "unsubscribe", to_string(jv))[jss::result];
520 BEAST_EXPECT(jr[jss::status] == "success");
521 }
522
523 void
524 testSubErrors(bool subscribe)
525 {
526 using namespace jtx;
527 auto const method = subscribe ? "subscribe" : "unsubscribe";
528 testcase << "Error cases for " << method;
529
530 Env env{*this};
531 auto wsc = makeWSClient(env.app().config());
532
533 {
534 auto jr = env.rpc("json", method, "{}")[jss::result];
535 BEAST_EXPECT(jr[jss::error] == "invalidParams");
536 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
537 }
538
539 {
540 Json::Value jv;
541 jv[jss::url] = "not-a-url";
542 jv[jss::username] = "admin";
543 jv[jss::password] = "password";
544 auto jr = env.rpc("json", method, to_string(jv))[jss::result];
545 if (subscribe)
546 {
547 BEAST_EXPECT(jr[jss::error] == "invalidParams");
548 BEAST_EXPECT(jr[jss::error_message] == "Failed to parse url.");
549 }
550 // else TODO: why isn't this an error for unsubscribe ?
551 // (findRpcSub returns null)
552 }
553
554 {
555 Json::Value jv;
556 jv[jss::url] = "ftp://scheme.not.supported.tld";
557 auto jr = env.rpc("json", method, to_string(jv))[jss::result];
558 if (subscribe)
559 {
560 BEAST_EXPECT(jr[jss::error] == "invalidParams");
561 BEAST_EXPECT(jr[jss::error_message] == "Only http and https is supported.");
562 }
563 }
564
565 {
566 Env env_nonadmin{*this, no_admin(envconfig())};
567 Json::Value jv;
568 jv[jss::url] = "no-url";
569 auto jr = env_nonadmin.rpc("json", method, to_string(jv))[jss::result];
570 BEAST_EXPECT(jr[jss::error] == "noPermission");
571 BEAST_EXPECT(jr[jss::error_message] == "You don't have permission for this command.");
572 }
573
579 "",
582
583 for (auto const& f : {jss::accounts_proposed, jss::accounts})
584 {
585 for (auto const& nonArray : nonArrays)
586 {
587 Json::Value jv;
588 jv[f] = nonArray;
589 auto jr = wsc->invoke(method, jv)[jss::result];
590 BEAST_EXPECT(jr[jss::error] == "invalidParams");
591 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
592 }
593
594 {
595 Json::Value jv;
596 jv[f] = Json::arrayValue;
597 auto jr = wsc->invoke(method, jv)[jss::result];
598 BEAST_EXPECT(jr[jss::error] == "actMalformed");
599 BEAST_EXPECT(jr[jss::error_message] == "Account malformed.");
600 }
601 }
602
603 for (auto const& nonArray : nonArrays)
604 {
605 Json::Value jv;
606 jv[jss::books] = nonArray;
607 auto jr = wsc->invoke(method, jv)[jss::result];
608 BEAST_EXPECT(jr[jss::error] == "invalidParams");
609 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
610 }
611
612 {
613 Json::Value jv;
614 jv[jss::books] = Json::arrayValue;
615 jv[jss::books][0u] = 1;
616 auto jr = wsc->invoke(method, jv)[jss::result];
617 BEAST_EXPECT(jr[jss::error] == "invalidParams");
618 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
619 }
620
621 {
622 Json::Value jv;
623 jv[jss::books] = Json::arrayValue;
624 jv[jss::books][0u] = Json::objectValue;
625 jv[jss::books][0u][jss::taker_gets] = Json::objectValue;
626 jv[jss::books][0u][jss::taker_pays] = Json::objectValue;
627 auto jr = wsc->invoke(method, jv)[jss::result];
628 BEAST_EXPECT(jr[jss::error] == "srcCurMalformed");
629 BEAST_EXPECT(jr[jss::error_message] == "Source currency is malformed.");
630 }
631
632 {
633 Json::Value jv;
634 jv[jss::books] = Json::arrayValue;
635 jv[jss::books][0u] = Json::objectValue;
636 jv[jss::books][0u][jss::taker_gets] = Json::objectValue;
637 jv[jss::books][0u][jss::taker_pays] = Json::objectValue;
638 jv[jss::books][0u][jss::taker_pays][jss::currency] = "ZZZZ";
639 auto jr = wsc->invoke(method, jv)[jss::result];
640 BEAST_EXPECT(jr[jss::error] == "srcCurMalformed");
641 BEAST_EXPECT(jr[jss::error_message] == "Source currency is malformed.");
642 }
643
644 {
645 Json::Value jv;
646 jv[jss::books] = Json::arrayValue;
647 jv[jss::books][0u] = Json::objectValue;
648 jv[jss::books][0u][jss::taker_gets] = Json::objectValue;
649 jv[jss::books][0u][jss::taker_pays] = Json::objectValue;
650 jv[jss::books][0u][jss::taker_pays][jss::currency] = "USD";
651 jv[jss::books][0u][jss::taker_pays][jss::issuer] = 1;
652 auto jr = wsc->invoke(method, jv)[jss::result];
653 BEAST_EXPECT(jr[jss::error] == "srcIsrMalformed");
654 BEAST_EXPECT(jr[jss::error_message] == "Source issuer is malformed.");
655 }
656
657 {
658 Json::Value jv;
659 jv[jss::books] = Json::arrayValue;
660 jv[jss::books][0u] = Json::objectValue;
661 jv[jss::books][0u][jss::taker_gets] = Json::objectValue;
662 jv[jss::books][0u][jss::taker_pays] = Json::objectValue;
663 jv[jss::books][0u][jss::taker_pays][jss::currency] = "USD";
664 jv[jss::books][0u][jss::taker_pays][jss::issuer] = Account{"gateway"}.human() + "DEAD";
665 auto jr = wsc->invoke(method, jv)[jss::result];
666 BEAST_EXPECT(jr[jss::error] == "srcIsrMalformed");
667 BEAST_EXPECT(jr[jss::error_message] == "Source issuer is malformed.");
668 }
669
670 {
671 Json::Value jv;
672 jv[jss::books] = Json::arrayValue;
673 jv[jss::books][0u] = Json::objectValue;
674 jv[jss::books][0u][jss::taker_pays] =
675 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::include_date);
676 jv[jss::books][0u][jss::taker_gets] = Json::objectValue;
677 auto jr = wsc->invoke(method, jv)[jss::result];
678 // NOTE: this error is slightly incongruous with the
679 // equivalent source currency error
680 BEAST_EXPECT(jr[jss::error] == "dstAmtMalformed");
681 BEAST_EXPECT(jr[jss::error_message] == "Destination amount/currency/issuer is malformed.");
682 }
683
684 {
685 Json::Value jv;
686 jv[jss::books] = Json::arrayValue;
687 jv[jss::books][0u] = Json::objectValue;
688 jv[jss::books][0u][jss::taker_pays] =
689 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::include_date);
690 jv[jss::books][0u][jss::taker_gets][jss::currency] = "ZZZZ";
691 auto jr = wsc->invoke(method, jv)[jss::result];
692 // NOTE: this error is slightly incongruous with the
693 // equivalent source currency error
694 BEAST_EXPECT(jr[jss::error] == "dstAmtMalformed");
695 BEAST_EXPECT(jr[jss::error_message] == "Destination amount/currency/issuer is malformed.");
696 }
697
698 {
699 Json::Value jv;
700 jv[jss::books] = Json::arrayValue;
701 jv[jss::books][0u] = Json::objectValue;
702 jv[jss::books][0u][jss::taker_pays] =
703 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::include_date);
704 jv[jss::books][0u][jss::taker_gets][jss::currency] = "USD";
705 jv[jss::books][0u][jss::taker_gets][jss::issuer] = 1;
706 auto jr = wsc->invoke(method, jv)[jss::result];
707 BEAST_EXPECT(jr[jss::error] == "dstIsrMalformed");
708 BEAST_EXPECT(jr[jss::error_message] == "Destination issuer is malformed.");
709 }
710
711 {
712 Json::Value jv;
713 jv[jss::books] = Json::arrayValue;
714 jv[jss::books][0u] = Json::objectValue;
715 jv[jss::books][0u][jss::taker_pays] =
716 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::include_date);
717 jv[jss::books][0u][jss::taker_gets][jss::currency] = "USD";
718 jv[jss::books][0u][jss::taker_gets][jss::issuer] = Account{"gateway"}.human() + "DEAD";
719 auto jr = wsc->invoke(method, jv)[jss::result];
720 BEAST_EXPECT(jr[jss::error] == "dstIsrMalformed");
721 BEAST_EXPECT(jr[jss::error_message] == "Destination issuer is malformed.");
722 }
723
724 {
725 Json::Value jv;
726 jv[jss::books] = Json::arrayValue;
727 jv[jss::books][0u] = Json::objectValue;
728 jv[jss::books][0u][jss::taker_pays] =
729 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::include_date);
730 jv[jss::books][0u][jss::taker_gets] =
731 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::include_date);
732 auto jr = wsc->invoke(method, jv)[jss::result];
733 BEAST_EXPECT(jr[jss::error] == "badMarket");
734 BEAST_EXPECT(jr[jss::error_message] == "No such market.");
735 }
736
737 for (auto const& nonArray : nonArrays)
738 {
739 Json::Value jv;
740 jv[jss::streams] = nonArray;
741 auto jr = wsc->invoke(method, jv)[jss::result];
742 BEAST_EXPECT(jr[jss::error] == "invalidParams");
743 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
744 }
745
746 {
747 Json::Value jv;
748 jv[jss::streams] = Json::arrayValue;
749 jv[jss::streams][0u] = 1;
750 auto jr = wsc->invoke(method, jv)[jss::result];
751 BEAST_EXPECT(jr[jss::error] == "malformedStream");
752 BEAST_EXPECT(jr[jss::error_message] == "Stream malformed.");
753 }
754
755 {
756 Json::Value jv;
757 jv[jss::streams] = Json::arrayValue;
758 jv[jss::streams][0u] = "not_a_stream";
759 auto jr = wsc->invoke(method, jv)[jss::result];
760 BEAST_EXPECT(jr[jss::error] == "malformedStream");
761 BEAST_EXPECT(jr[jss::error_message] == "Stream malformed.");
762 }
763 }
764
765 void
767 {
768 testcase("HistoryTxStream");
769
770 using namespace std::chrono_literals;
771 using namespace jtx;
773
774 Account alice("alice");
775 Account bob("bob");
776 Account carol("carol");
777 Account david("david");
779
780 /*
781 * return true if the subscribe or unsubscribe result is a success
782 */
783 auto goodSubRPC = [](Json::Value const& subReply) -> bool {
784 return subReply.isMember(jss::result) && subReply[jss::result].isMember(jss::status) &&
785 subReply[jss::result][jss::status] == jss::success;
786 };
787
788 /*
789 * try to receive txns from the tx stream subscription via the WSClient.
790 * return {true, true} if received numReplies replies and also
791 * received a tx with the account_history_tx_first == true
792 */
793 auto getTxHash = [](WSClient& wsc, IdxHashVec& v, int numReplies) -> std::pair<bool, bool> {
794 bool first_flag = false;
795
796 for (int i = 0; i < numReplies; ++i)
797 {
798 std::uint32_t idx{0};
799 auto reply = wsc.getMsg(100ms);
800 if (reply)
801 {
802 auto r = *reply;
803 if (r.isMember(jss::account_history_tx_index))
804 idx = r[jss::account_history_tx_index].asInt();
805 if (r.isMember(jss::account_history_tx_first))
806 first_flag = true;
807 bool boundary = r.isMember(jss::account_history_boundary);
808 int ledger_idx = r[jss::ledger_index].asInt();
809 if (r.isMember(jss::transaction) && r[jss::transaction].isMember(jss::hash))
810 {
811 auto t{r[jss::transaction]};
812 v.emplace_back(idx, t[jss::hash].asString(), boundary, ledger_idx);
813 continue;
814 }
815 }
816 return {false, first_flag};
817 }
818
819 return {true, first_flag};
820 };
821
822 /*
823 * send payments between the two accounts a and b,
824 * and close ledgersToClose ledgers
825 */
826 auto sendPayments = [](Env& env,
827 Account const& a,
828 Account const& b,
829 int newTxns,
830 std::uint32_t ledgersToClose,
831 int numXRP = 10) {
832 env.memoize(a);
833 env.memoize(b);
834 for (int i = 0; i < newTxns; ++i)
835 {
836 auto& from = (i % 2 == 0) ? a : b;
837 auto& to = (i % 2 == 0) ? b : a;
838 env.apply(
839 pay(from, to, jtx::XRP(numXRP)),
843 }
844 for (int i = 0; i < ledgersToClose; ++i)
845 env.close();
846 return newTxns;
847 };
848
849 /*
850 * Check if txHistoryVec has every item of accountVec,
851 * and in the same order.
852 * If sizeCompare is false, txHistoryVec is allowed to be larger.
853 */
854 auto hashCompare = [](IdxHashVec const& accountVec, IdxHashVec const& txHistoryVec, bool sizeCompare) -> bool {
855 if (accountVec.empty() || txHistoryVec.empty())
856 return false;
857 if (sizeCompare && accountVec.size() != (txHistoryVec.size()))
858 return false;
859
860 hash_map<std::string, int> txHistoryMap;
861 for (auto const& tx : txHistoryVec)
862 {
863 txHistoryMap.emplace(std::get<1>(tx), std::get<0>(tx));
864 }
865
866 auto getHistoryIndex = [&](std::size_t i) -> std::optional<int> {
867 if (i >= accountVec.size())
868 return {};
869 auto it = txHistoryMap.find(std::get<1>(accountVec[i]));
870 if (it == txHistoryMap.end())
871 return {};
872 return it->second;
873 };
874
875 auto firstHistoryIndex = getHistoryIndex(0);
876 if (!firstHistoryIndex)
877 return false;
878 for (std::size_t i = 1; i < accountVec.size(); ++i)
879 {
880 if (auto idx = getHistoryIndex(i); !idx || *idx != *firstHistoryIndex + i)
881 return false;
882 }
883 return true;
884 };
885
886 // example of vector created from the return of `subscribe` rpc
887 // with jss::accounts
888 // boundary == true on last tx of ledger
889 // ------------------------------------------------------------
890 // (0, "E5B8B...", false, 4
891 // (0, "39E1C...", false, 4
892 // (0, "14EF1...", false, 4
893 // (0, "386E6...", false, 4
894 // (0, "00F3B...", true, 4
895 // (0, "1DCDC...", false, 5
896 // (0, "BD02A...", false, 5
897 // (0, "D3E16...", false, 5
898 // (0, "CB593...", false, 5
899 // (0, "8F28B...", true, 5
900 //
901 // example of vector created from the return of `subscribe` rpc
902 // with jss::account_history_tx_stream.
903 // boundary == true on first tx of ledger
904 // ------------------------------------------------------------
905 // (-1, "8F28B...", false, 5
906 // (-2, "CB593...", false, 5
907 // (-3, "D3E16...", false, 5
908 // (-4, "BD02A...", false, 5
909 // (-5, "1DCDC...", true, 5
910 // (-6, "00F3B...", false, 4
911 // (-7, "386E6...", false, 4
912 // (-8, "14EF1...", false, 4
913 // (-9, "39E1C...", false, 4
914 // (-10, "E5B8B...", true, 4
915
916 auto checkBoundary = [](IdxHashVec const& vec, bool /* forward */) {
917 size_t num_tx = vec.size();
918 for (size_t i = 0; i < num_tx; ++i)
919 {
920 auto [idx, hash, boundary, ledger] = vec[i];
921 if ((i + 1 == num_tx || ledger != std::get<3>(vec[i + 1])) != boundary)
922 return false;
923 }
924 return true;
925 };
926
928
929 {
930 /*
931 * subscribe to an account twice with same WS client,
932 * the second should fail
933 *
934 * also test subscribe to the account before it is created
935 */
936 Env env(*this);
937 auto wscTxHistory = makeWSClient(env.app().config());
938 Json::Value request;
939 request[jss::account_history_tx_stream] = Json::objectValue;
940 request[jss::account_history_tx_stream][jss::account] = alice.human();
941 auto jv = wscTxHistory->invoke("subscribe", request);
942 if (!BEAST_EXPECT(goodSubRPC(jv)))
943 return;
944
945 jv = wscTxHistory->invoke("subscribe", request);
946 BEAST_EXPECT(!goodSubRPC(jv));
947
948 /*
949 * unsubscribe history only, future txns should still be streamed
950 */
951 request[jss::account_history_tx_stream][jss::stop_history_tx_only] = true;
952 jv = wscTxHistory->invoke("unsubscribe", request);
953 if (!BEAST_EXPECT(goodSubRPC(jv)))
954 return;
955
956 sendPayments(env, env.master, alice, 1, 1, 123456);
957
958 IdxHashVec vec;
959 auto r = getTxHash(*wscTxHistory, vec, 1);
960 if (!BEAST_EXPECT(r.first && r.second))
961 return;
962
963 /*
964 * unsubscribe, future txns should not be streamed
965 */
966 request[jss::account_history_tx_stream][jss::stop_history_tx_only] = false;
967 jv = wscTxHistory->invoke("unsubscribe", request);
968 BEAST_EXPECT(goodSubRPC(jv));
969
970 sendPayments(env, env.master, alice, 1, 1);
971 r = getTxHash(*wscTxHistory, vec, 1);
972 BEAST_EXPECT(!r.first);
973 }
974 {
975 /*
976 * subscribe genesis account tx history without txns
977 * subscribe to bob's account after it is created
978 */
979 Env env(*this);
980 auto wscTxHistory = makeWSClient(env.app().config());
981 Json::Value request;
982 request[jss::account_history_tx_stream] = Json::objectValue;
983 request[jss::account_history_tx_stream][jss::account] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
984 auto jv = wscTxHistory->invoke("subscribe", request);
985 if (!BEAST_EXPECT(goodSubRPC(jv)))
986 return;
987 IdxHashVec genesisFullHistoryVec;
988 if (!BEAST_EXPECT(!getTxHash(*wscTxHistory, genesisFullHistoryVec, 1).first))
989 return;
990
991 /*
992 * create bob's account with one tx
993 * the two subscriptions should both stream it
994 */
995 sendPayments(env, env.master, bob, 1, 1, 654321);
996
997 auto r = getTxHash(*wscTxHistory, genesisFullHistoryVec, 1);
998 if (!BEAST_EXPECT(r.first && r.second))
999 return;
1000
1001 request[jss::account_history_tx_stream][jss::account] = bob.human();
1002 jv = wscTxHistory->invoke("subscribe", request);
1003 if (!BEAST_EXPECT(goodSubRPC(jv)))
1004 return;
1005 IdxHashVec bobFullHistoryVec;
1006 r = getTxHash(*wscTxHistory, bobFullHistoryVec, 1);
1007 if (!BEAST_EXPECT(r.first && r.second))
1008 return;
1009 BEAST_EXPECT(std::get<1>(bobFullHistoryVec.back()) == std::get<1>(genesisFullHistoryVec.back()));
1010
1011 /*
1012 * unsubscribe to prepare next test
1013 */
1014 jv = wscTxHistory->invoke("unsubscribe", request);
1015 if (!BEAST_EXPECT(goodSubRPC(jv)))
1016 return;
1017 request[jss::account_history_tx_stream][jss::account] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
1018 jv = wscTxHistory->invoke("unsubscribe", request);
1019 BEAST_EXPECT(goodSubRPC(jv));
1020
1021 /*
1022 * add more txns, then subscribe bob tx history and
1023 * genesis account tx history. Their earliest txns should match.
1024 */
1025 sendPayments(env, env.master, bob, 30, 300);
1026 wscTxHistory = makeWSClient(env.app().config());
1027 request[jss::account_history_tx_stream][jss::account] = bob.human();
1028 jv = wscTxHistory->invoke("subscribe", request);
1029
1030 bobFullHistoryVec.clear();
1031 BEAST_EXPECT(getTxHash(*wscTxHistory, bobFullHistoryVec, 31).second);
1032 jv = wscTxHistory->invoke("unsubscribe", request);
1033
1034 request[jss::account_history_tx_stream][jss::account] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
1035 jv = wscTxHistory->invoke("subscribe", request);
1036 genesisFullHistoryVec.clear();
1037 BEAST_EXPECT(getTxHash(*wscTxHistory, genesisFullHistoryVec, 31).second);
1038 jv = wscTxHistory->invoke("unsubscribe", request);
1039
1040 BEAST_EXPECT(std::get<1>(bobFullHistoryVec.back()) == std::get<1>(genesisFullHistoryVec.back()));
1041 }
1042
1043 {
1044 /*
1045 * subscribe account and subscribe account tx history
1046 * and compare txns streamed
1047 */
1048 Env env(*this);
1049 auto wscAccount = makeWSClient(env.app().config());
1050 auto wscTxHistory = makeWSClient(env.app().config());
1051
1052 std::array<Account, 2> accounts = {alice, bob};
1053 env.fund(XRP(222222), accounts);
1054 env.close();
1055
1056 // subscribe account
1058 stream[jss::accounts] = Json::arrayValue;
1059 stream[jss::accounts].append(alice.human());
1060 auto jv = wscAccount->invoke("subscribe", stream);
1061
1062 sendPayments(env, alice, bob, 5, 1);
1063 sendPayments(env, alice, bob, 5, 1);
1064 IdxHashVec accountVec;
1065 if (!BEAST_EXPECT(getTxHash(*wscAccount, accountVec, 10).first))
1066 return;
1067
1068 // subscribe account tx history
1069 Json::Value request;
1070 request[jss::account_history_tx_stream] = Json::objectValue;
1071 request[jss::account_history_tx_stream][jss::account] = alice.human();
1072 jv = wscTxHistory->invoke("subscribe", request);
1073
1074 // compare historical txns
1075 IdxHashVec txHistoryVec;
1076 if (!BEAST_EXPECT(getTxHash(*wscTxHistory, txHistoryVec, 10).first))
1077 return;
1078 if (!BEAST_EXPECT(hashCompare(accountVec, txHistoryVec, true)))
1079 return;
1080
1081 // check boundary tags
1082 // only account_history_tx_stream has ledger boundary information.
1083 if (!BEAST_EXPECT(checkBoundary(txHistoryVec, false)))
1084 return;
1085
1086 {
1087 // take out all history txns from stream to prepare next test
1088 IdxHashVec initFundTxns;
1089 if (!BEAST_EXPECT(getTxHash(*wscTxHistory, initFundTxns, 10).second) ||
1090 !BEAST_EXPECT(checkBoundary(initFundTxns, false)))
1091 return;
1092 }
1093
1094 // compare future txns
1095 sendPayments(env, alice, bob, 10, 1);
1096 if (!BEAST_EXPECT(getTxHash(*wscAccount, accountVec, 10).first))
1097 return;
1098 if (!BEAST_EXPECT(getTxHash(*wscTxHistory, txHistoryVec, 10).first))
1099 return;
1100 if (!BEAST_EXPECT(hashCompare(accountVec, txHistoryVec, true)))
1101 return;
1102
1103 // check boundary tags
1104 // only account_history_tx_stream has ledger boundary information.
1105 if (!BEAST_EXPECT(checkBoundary(txHistoryVec, false)))
1106 return;
1107
1108 wscTxHistory->invoke("unsubscribe", request);
1109 wscAccount->invoke("unsubscribe", stream);
1110 }
1111
1112 {
1113 /*
1114 * alice issues USD to carol
1115 * mix USD and XRP payments
1116 */
1117 Env env(*this);
1118 auto const USD_a = alice["USD"];
1119
1120 std::array<Account, 2> accounts = {alice, carol};
1121 env.fund(XRP(333333), accounts);
1122 env.trust(USD_a(20000), carol);
1123 env.close();
1124
1125 auto mixedPayments = [&]() -> int {
1126 sendPayments(env, alice, carol, 1, 0);
1127 env(pay(alice, carol, USD_a(100)));
1128 env.close();
1129 return 2;
1130 };
1131
1132 // subscribe
1133 Json::Value request;
1134 request[jss::account_history_tx_stream] = Json::objectValue;
1135 request[jss::account_history_tx_stream][jss::account] = carol.human();
1136 auto ws = makeWSClient(env.app().config());
1137 auto jv = ws->invoke("subscribe", request);
1138 {
1139 // take out existing txns from the stream
1140 IdxHashVec tempVec;
1141 getTxHash(*ws, tempVec, 100);
1142 }
1143
1144 auto count = mixedPayments();
1145 IdxHashVec vec1;
1146 if (!BEAST_EXPECT(getTxHash(*ws, vec1, count).first))
1147 return;
1148 ws->invoke("unsubscribe", request);
1149 }
1150
1151 {
1152 /*
1153 * long transaction history
1154 */
1155 Env env(*this);
1156 std::array<Account, 2> accounts = {alice, carol};
1157 env.fund(XRP(444444), accounts);
1158 env.close();
1159
1160 // many payments, and close lots of ledgers
1161 auto oneRound = [&](int numPayments) { return sendPayments(env, alice, carol, numPayments, 300); };
1162
1163 // subscribe
1164 Json::Value request;
1165 request[jss::account_history_tx_stream] = Json::objectValue;
1166 request[jss::account_history_tx_stream][jss::account] = carol.human();
1167 auto wscLong = makeWSClient(env.app().config());
1168 auto jv = wscLong->invoke("subscribe", request);
1169 {
1170 // take out existing txns from the stream
1171 IdxHashVec tempVec;
1172 getTxHash(*wscLong, tempVec, 100);
1173 }
1174
1175 // repeat the payments many rounds
1176 for (int kk = 2; kk < 10; ++kk)
1177 {
1178 auto count = oneRound(kk);
1179 IdxHashVec vec1;
1180 if (!BEAST_EXPECT(getTxHash(*wscLong, vec1, count).first))
1181 return;
1182
1183 // another subscribe, only for this round
1184 auto wscShort = makeWSClient(env.app().config());
1185 auto jv = wscShort->invoke("subscribe", request);
1186 IdxHashVec vec2;
1187 if (!BEAST_EXPECT(getTxHash(*wscShort, vec2, count).first))
1188 return;
1189 if (!BEAST_EXPECT(hashCompare(vec1, vec2, true)))
1190 return;
1191 wscShort->invoke("unsubscribe", request);
1192 }
1193 }
1194 }
1195
1196 void
1198 {
1199 testcase("SubBookChanges");
1200 using namespace jtx;
1201 using namespace std::chrono_literals;
1202 FeatureBitset const all{
1203 jtx::testable_amendments() | featurePermissionedDomains | featureCredentials | featurePermissionedDEX};
1204
1205 Env env(*this, all);
1206 PermissionedDEX permDex(env);
1207 auto const alice = permDex.alice;
1208 auto const bob = permDex.bob;
1209 auto const carol = permDex.carol;
1210 auto const domainID = permDex.domainID;
1211 auto const gw = permDex.gw;
1212 auto const USD = permDex.USD;
1213
1214 auto wsc = makeWSClient(env.app().config());
1215
1216 Json::Value streams;
1217 streams[jss::streams] = Json::arrayValue;
1218 streams[jss::streams][0u] = "book_changes";
1219
1220 auto jv = wsc->invoke("subscribe", streams);
1221 if (!BEAST_EXPECT(jv[jss::status] == "success"))
1222 return;
1223 env(offer(alice, XRP(10), USD(10)), domain(domainID), txflags(tfHybrid));
1224 env.close();
1225
1226 env(pay(bob, carol, USD(5)), path(~USD), sendmax(XRP(5)), domain(domainID));
1227 env.close();
1228
1229 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
1230 if (jv[jss::changes].size() != 1)
1231 return false;
1232
1233 auto const jrOffer = jv[jss::changes][0u];
1234 return (jv[jss::changes][0u][jss::domain]).asString() == strHex(domainID) &&
1235 jrOffer[jss::currency_a].asString() == "XRP_drops" && jrOffer[jss::volume_a].asString() == "5000000" &&
1236 jrOffer[jss::currency_b].asString() == "rHUKYAZyUFn8PCZWbPfwHfbVQXTYrYKkHb/USD" &&
1237 jrOffer[jss::volume_b].asString() == "5";
1238 }));
1239 }
1240
1241 void
1243 {
1244 // `nftoken_id` is added for `transaction` stream in the `subscribe`
1245 // response for NFTokenMint and NFTokenAcceptOffer.
1246 //
1247 // `nftoken_ids` is added for `transaction` stream in the `subscribe`
1248 // response for NFTokenCancelOffer
1249 //
1250 // `offer_id` is added for `transaction` stream in the `subscribe`
1251 // response for NFTokenCreateOffer
1252 //
1253 // The values of these fields are dependent on the NFTokenID/OfferID
1254 // changed in its corresponding transaction. We want to validate each
1255 // response to make sure the synthetic fields hold the right values.
1256
1257 testcase("Test synthetic fields from Subscribe response");
1258
1259 using namespace test::jtx;
1260 using namespace std::chrono_literals;
1261
1262 Account const alice{"alice"};
1263 Account const bob{"bob"};
1264 Account const broker{"broker"};
1265
1266 Env env{*this, features};
1267 env.fund(XRP(10000), alice, bob, broker);
1268 env.close();
1269
1270 auto wsc = test::makeWSClient(env.app().config());
1271 Json::Value stream;
1272 stream[jss::streams] = Json::arrayValue;
1273 stream[jss::streams].append("transactions");
1274 auto jv = wsc->invoke("subscribe", stream);
1275
1276 // Verify `nftoken_id` value equals to the NFTokenID that was
1277 // changed in the most recent NFTokenMint or NFTokenAcceptOffer
1278 // transaction
1279 auto verifyNFTokenID = [&](uint256 const& actualNftID) {
1280 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
1281 uint256 nftID;
1282 BEAST_EXPECT(nftID.parseHex(jv[jss::meta][jss::nftoken_id].asString()));
1283 return nftID == actualNftID;
1284 }));
1285 };
1286
1287 // Verify `nftoken_ids` value equals to the NFTokenIDs that were
1288 // changed in the most recent NFTokenCancelOffer transaction
1289 auto verifyNFTokenIDsInCancelOffer = [&](std::vector<uint256> actualNftIDs) {
1290 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
1291 std::vector<uint256> metaIDs;
1292 std::transform(
1293 jv[jss::meta][jss::nftoken_ids].begin(),
1294 jv[jss::meta][jss::nftoken_ids].end(),
1295 std::back_inserter(metaIDs),
1296 [this](Json::Value id) {
1297 uint256 nftID;
1298 BEAST_EXPECT(nftID.parseHex(id.asString()));
1299 return nftID;
1300 });
1301 // Sort both array to prepare for comparison
1302 std::sort(metaIDs.begin(), metaIDs.end());
1303 std::sort(actualNftIDs.begin(), actualNftIDs.end());
1304
1305 // Make sure the expect number of NFTs is correct
1306 BEAST_EXPECT(metaIDs.size() == actualNftIDs.size());
1307
1308 // Check the value of NFT ID in the meta with the
1309 // actual values
1310 for (size_t i = 0; i < metaIDs.size(); ++i)
1311 BEAST_EXPECT(metaIDs[i] == actualNftIDs[i]);
1312 return true;
1313 }));
1314 };
1315
1316 // Verify `offer_id` value equals to the offerID that was
1317 // changed in the most recent NFTokenCreateOffer tx
1318 auto verifyNFTokenOfferID = [&](uint256 const& offerID) {
1319 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
1320 uint256 metaOfferID;
1321 BEAST_EXPECT(metaOfferID.parseHex(jv[jss::meta][jss::offer_id].asString()));
1322 return metaOfferID == offerID;
1323 }));
1324 };
1325
1326 // Check new fields in tx meta when for all NFTtransactions
1327 {
1328 // Alice mints 2 NFTs
1329 // Verify the NFTokenIDs are correct in the NFTokenMint tx meta
1330 uint256 const nftId1{token::getNextID(env, alice, 0u, tfTransferable)};
1331 env(token::mint(alice, 0u), txflags(tfTransferable));
1332 env.close();
1333 verifyNFTokenID(nftId1);
1334
1335 uint256 const nftId2{token::getNextID(env, alice, 0u, tfTransferable)};
1336 env(token::mint(alice, 0u), txflags(tfTransferable));
1337 env.close();
1338 verifyNFTokenID(nftId2);
1339
1340 // Alice creates one sell offer for each NFT
1341 // Verify the offer indexes are correct in the NFTokenCreateOffer tx
1342 // meta
1343 uint256 const aliceOfferIndex1 = keylet::nftoffer(alice, env.seq(alice)).key;
1344 env(token::createOffer(alice, nftId1, drops(1)), txflags(tfSellNFToken));
1345 env.close();
1346 verifyNFTokenOfferID(aliceOfferIndex1);
1347
1348 uint256 const aliceOfferIndex2 = keylet::nftoffer(alice, env.seq(alice)).key;
1349 env(token::createOffer(alice, nftId2, drops(1)), txflags(tfSellNFToken));
1350 env.close();
1351 verifyNFTokenOfferID(aliceOfferIndex2);
1352
1353 // Alice cancels two offers she created
1354 // Verify the NFTokenIDs are correct in the NFTokenCancelOffer tx
1355 // meta
1356 env(token::cancelOffer(alice, {aliceOfferIndex1, aliceOfferIndex2}));
1357 env.close();
1358 verifyNFTokenIDsInCancelOffer({nftId1, nftId2});
1359
1360 // Bobs creates a buy offer for nftId1
1361 // Verify the offer id is correct in the NFTokenCreateOffer tx meta
1362 auto const bobBuyOfferIndex = keylet::nftoffer(bob, env.seq(bob)).key;
1363 env(token::createOffer(bob, nftId1, drops(1)), token::owner(alice));
1364 env.close();
1365 verifyNFTokenOfferID(bobBuyOfferIndex);
1366
1367 // Alice accepts bob's buy offer
1368 // Verify the NFTokenID is correct in the NFTokenAcceptOffer tx meta
1369 env(token::acceptBuyOffer(alice, bobBuyOfferIndex));
1370 env.close();
1371 verifyNFTokenID(nftId1);
1372 }
1373
1374 // Check `nftoken_ids` in brokered mode
1375 {
1376 // Alice mints a NFT
1377 uint256 const nftId{token::getNextID(env, alice, 0u, tfTransferable)};
1378 env(token::mint(alice, 0u), txflags(tfTransferable));
1379 env.close();
1380 verifyNFTokenID(nftId);
1381
1382 // Alice creates sell offer and set broker as destination
1383 uint256 const offerAliceToBroker = keylet::nftoffer(alice, env.seq(alice)).key;
1384 env(token::createOffer(alice, nftId, drops(1)), token::destination(broker), txflags(tfSellNFToken));
1385 env.close();
1386 verifyNFTokenOfferID(offerAliceToBroker);
1387
1388 // Bob creates buy offer
1389 uint256 const offerBobToBroker = keylet::nftoffer(bob, env.seq(bob)).key;
1390 env(token::createOffer(bob, nftId, drops(1)), token::owner(alice));
1391 env.close();
1392 verifyNFTokenOfferID(offerBobToBroker);
1393
1394 // Check NFTokenID meta for NFTokenAcceptOffer in brokered mode
1395 env(token::brokerOffers(broker, offerBobToBroker, offerAliceToBroker));
1396 env.close();
1397 verifyNFTokenID(nftId);
1398 }
1399
1400 // Check if there are no duplicate nft id in Cancel transactions where
1401 // multiple offers are cancelled for the same NFT
1402 {
1403 // Alice mints a NFT
1404 uint256 const nftId{token::getNextID(env, alice, 0u, tfTransferable)};
1405 env(token::mint(alice, 0u), txflags(tfTransferable));
1406 env.close();
1407 verifyNFTokenID(nftId);
1408
1409 // Alice creates 2 sell offers for the same NFT
1410 uint256 const aliceOfferIndex1 = keylet::nftoffer(alice, env.seq(alice)).key;
1411 env(token::createOffer(alice, nftId, drops(1)), txflags(tfSellNFToken));
1412 env.close();
1413 verifyNFTokenOfferID(aliceOfferIndex1);
1414
1415 uint256 const aliceOfferIndex2 = keylet::nftoffer(alice, env.seq(alice)).key;
1416 env(token::createOffer(alice, nftId, drops(1)), txflags(tfSellNFToken));
1417 env.close();
1418 verifyNFTokenOfferID(aliceOfferIndex2);
1419
1420 // Make sure the metadata only has 1 nft id, since both offers are
1421 // for the same nft
1422 env(token::cancelOffer(alice, {aliceOfferIndex1, aliceOfferIndex2}));
1423 env.close();
1424 verifyNFTokenIDsInCancelOffer({nftId});
1425 }
1426
1427 if (features[featureNFTokenMintOffer])
1428 {
1429 uint256 const aliceMintWithOfferIndex1 = keylet::nftoffer(alice, env.seq(alice)).key;
1430 env(token::mint(alice), token::amount(XRP(0)));
1431 env.close();
1432 verifyNFTokenOfferID(aliceMintWithOfferIndex1);
1433 }
1434 }
1435
1436 void
1437 run() override
1438 {
1439 using namespace test::jtx;
1441 FeatureBitset const xrpFees{featureXRPFees};
1442
1443 testServer();
1444 testLedger();
1445 testTransactions_APIv1();
1446 testTransactions_APIv2();
1447 testManifests();
1448 testValidations(all - xrpFees);
1449 testValidations(all);
1450 testSubErrors(true);
1451 testSubErrors(false);
1452 testSubByUrl();
1453 testHistoryTxStream();
1454 testSubBookChanges();
1455 testNFToken(all);
1456 testNFToken(all - featureNFTokenMintOffer);
1457 }
1458};
1459
1460BEAST_DEFINE_TESTSUITE(Subscribe, rpc, xrpl);
1461
1462} // namespace test
1463} // namespace xrpl
Represents a JSON value.
Definition json_value.h:131
void clear()
Remove all object members and array elements.
A testsuite class.
Definition suite.h:52
testcase_t testcase
Memberspace for declaring test cases.
Definition suite.h:148
virtual Config & config()=0
virtual LoadFeeTrack & getFeeTrack()=0
virtual LoadManager & getLoadManager()=0
virtual NetworkOPs & getOPs()=0
uint32_t NETWORK_ID
Definition Config.h:138
virtual void reportFeeChange()=0
void run() override
Runs the suite.
void testValidations(FeatureBitset features)
void testNFToken(FeatureBitset features)
void testSubErrors(bool subscribe)
virtual std::optional< Json::Value > getMsg(std::chrono::milliseconds const &timeout=std::chrono::milliseconds{0})=0
Retrieve a message.
Immutable cryptographic account descriptor.
Definition Account.h:20
std::string const & human() const
Returns the human readable public key.
Definition Account.h:95
A transaction testing environment.
Definition Env.h:98
Application & app()
Definition Env.h:230
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:97
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:260
Account const & master
Definition Env.h:102
Env & apply(JsonValue &&jv, FN const &... fN)
Apply funclets and submit.
Definition Env.h:544
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:283
void memoize(Account const &account)
Associate AccountID with account.
Definition Env.cpp:130
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:298
Set the domain on a JTx.
Definition domain.h:11
Set the fee on a JTx.
Definition fee.h:18
Add a path.
Definition paths.h:38
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition rpc.h:16
Sets the SendMax on a JTx.
Definition sendmax.h:14
Set the regular signature on a JTx.
Definition sig.h:16
Set the flags on a JTx.
Definition txflags.h:12
T clear(T... args)
T emplace(T... args)
T end(T... args)
T find(T... args)
T is_same_v
@ booleanValue
bool value
Definition json_value.h:25
@ nullValue
'null' value
Definition json_value.h:20
@ realValue
double value
Definition json_value.h:23
@ arrayValue
array value (ordered list)
Definition json_value.h:26
@ intValue
signed integer value
Definition json_value.h:21
@ objectValue
object value (collection of name/value pairs).
Definition json_value.h:27
@ uintValue
unsigned integer value
Definition json_value.h:22
Keylet nftoffer(AccountID const &owner, std::uint32_t seq)
An offer from an account to buy or sell an NFT.
Definition Indexes.cpp:375
Json::Value mint(jtx::Account const &account, std::uint32_t nfTokenTaxon)
Mint an NFToken.
Definition token.cpp:15
Json::Value acceptBuyOffer(jtx::Account const &account, uint256 const &offerIndex)
Accept an NFToken buy offer.
Definition token.cpp:150
Json::Value brokerOffers(jtx::Account const &account, uint256 const &buyOfferIndex, uint256 const &sellOfferIndex)
Broker two NFToken offers.
Definition token.cpp:170
Json::Value cancelOffer(jtx::Account const &account, std::initializer_list< uint256 > const &nftokenOffers)
Cancel NFTokenOffers.
Definition token.cpp:132
Json::Value createOffer(jtx::Account const &account, uint256 const &nftokenID, STAmount const &amount)
Create an NFTokenOffer.
Definition token.cpp:87
uint256 getNextID(jtx::Env const &env, jtx::Account const &issuer, std::uint32_t nfTokenTaxon, std::uint16_t flags, std::uint16_t xferFee)
Get the next NFTokenID that will be issued.
Definition token.cpp:49
XRP_t const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:90
Json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:11
static autofill_t const autofill
Definition tags.h:23
FeatureBitset testable_amendments()
Definition Env.h:55
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:35
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
std::unique_ptr< Config > validator(std::unique_ptr< Config >, std::string const &)
adjust configuration with params needed to be a validator
Definition envconfig.cpp:94
std::unique_ptr< Config > no_admin(std::unique_ptr< Config >)
adjust config so no admin ports are enabled
Definition envconfig.cpp:57
Json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition offer.cpp:10
std::unique_ptr< WSClient > makeWSClient(Config const &cfg, bool v2, unsigned rpc_version, std::unordered_map< std::string, std::string > const &headers)
Returns a client operating through WebSockets/S.
Definition WSClient.cpp:285
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
constexpr std::uint32_t const tfTransferable
Definition TxFlags.h:123
PublicKey derivePublicKey(KeyType type, SecretKey const &sk)
Derive the public key from a secret key.
bool isFlagLedger(LedgerIndex seq)
Returns true if the given ledgerIndex is a flag ledgerIndex.
Definition Ledger.cpp:891
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:598
constexpr std::uint32_t tfHybrid
Definition TxFlags.h:83
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
Definition AccountID.cpp:92
SecretKey generateSecretKey(KeyType type, Seed const &seed)
Generate a new secret key deterministically.
base_uint< 256 > uint256
Definition base_uint.h:527
constexpr std::uint32_t vfFullValidation
constexpr std::uint32_t vfFullyCanonicalSig
constexpr std::uint32_t const tfSellNFToken
Definition TxFlags.h:211
T sort(T... args)
uint256 key
Definition Keylet.h:21
Set the sequence number on a JTx.
Definition seq.h:15
T to_string(T... args)