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