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(pay(from, to, jtx::XRP(numXRP)),
842 }
843 for (int i = 0; i < ledgersToClose; ++i)
844 env.close();
845 return newTxns;
846 };
847
848 /*
849 * Check if txHistoryVec has every item of accountVec,
850 * and in the same order.
851 * If sizeCompare is false, txHistoryVec is allowed to be larger.
852 */
853 auto hashCompare = [](IdxHashVec const& accountVec, IdxHashVec const& txHistoryVec, bool sizeCompare) -> bool {
854 if (accountVec.empty() || txHistoryVec.empty())
855 return false;
856 if (sizeCompare && accountVec.size() != (txHistoryVec.size()))
857 return false;
858
859 hash_map<std::string, int> txHistoryMap;
860 for (auto const& tx : txHistoryVec)
861 {
862 txHistoryMap.emplace(std::get<1>(tx), std::get<0>(tx));
863 }
864
865 auto getHistoryIndex = [&](std::size_t i) -> std::optional<int> {
866 if (i >= accountVec.size())
867 return {};
868 auto it = txHistoryMap.find(std::get<1>(accountVec[i]));
869 if (it == txHistoryMap.end())
870 return {};
871 return it->second;
872 };
873
874 auto firstHistoryIndex = getHistoryIndex(0);
875 if (!firstHistoryIndex)
876 return false;
877 for (std::size_t i = 1; i < accountVec.size(); ++i)
878 {
879 if (auto idx = getHistoryIndex(i); !idx || *idx != *firstHistoryIndex + i)
880 return false;
881 }
882 return true;
883 };
884
885 // example of vector created from the return of `subscribe` rpc
886 // with jss::accounts
887 // boundary == true on last tx of ledger
888 // ------------------------------------------------------------
889 // (0, "E5B8B...", false, 4
890 // (0, "39E1C...", false, 4
891 // (0, "14EF1...", false, 4
892 // (0, "386E6...", false, 4
893 // (0, "00F3B...", true, 4
894 // (0, "1DCDC...", false, 5
895 // (0, "BD02A...", false, 5
896 // (0, "D3E16...", false, 5
897 // (0, "CB593...", false, 5
898 // (0, "8F28B...", true, 5
899 //
900 // example of vector created from the return of `subscribe` rpc
901 // with jss::account_history_tx_stream.
902 // boundary == true on first tx of ledger
903 // ------------------------------------------------------------
904 // (-1, "8F28B...", false, 5
905 // (-2, "CB593...", false, 5
906 // (-3, "D3E16...", false, 5
907 // (-4, "BD02A...", false, 5
908 // (-5, "1DCDC...", true, 5
909 // (-6, "00F3B...", false, 4
910 // (-7, "386E6...", false, 4
911 // (-8, "14EF1...", false, 4
912 // (-9, "39E1C...", false, 4
913 // (-10, "E5B8B...", true, 4
914
915 auto checkBoundary = [](IdxHashVec const& vec, bool /* forward */) {
916 size_t num_tx = vec.size();
917 for (size_t i = 0; i < num_tx; ++i)
918 {
919 auto [idx, hash, boundary, ledger] = vec[i];
920 if ((i + 1 == num_tx || ledger != std::get<3>(vec[i + 1])) != boundary)
921 return false;
922 }
923 return true;
924 };
925
927
928 {
929 /*
930 * subscribe to an account twice with same WS client,
931 * the second should fail
932 *
933 * also test subscribe to the account before it is created
934 */
935 Env env(*this);
936 auto wscTxHistory = makeWSClient(env.app().config());
937 Json::Value request;
938 request[jss::account_history_tx_stream] = Json::objectValue;
939 request[jss::account_history_tx_stream][jss::account] = alice.human();
940 auto jv = wscTxHistory->invoke("subscribe", request);
941 if (!BEAST_EXPECT(goodSubRPC(jv)))
942 return;
943
944 jv = wscTxHistory->invoke("subscribe", request);
945 BEAST_EXPECT(!goodSubRPC(jv));
946
947 /*
948 * unsubscribe history only, future txns should still be streamed
949 */
950 request[jss::account_history_tx_stream][jss::stop_history_tx_only] = true;
951 jv = wscTxHistory->invoke("unsubscribe", request);
952 if (!BEAST_EXPECT(goodSubRPC(jv)))
953 return;
954
955 sendPayments(env, env.master, alice, 1, 1, 123456);
956
957 IdxHashVec vec;
958 auto r = getTxHash(*wscTxHistory, vec, 1);
959 if (!BEAST_EXPECT(r.first && r.second))
960 return;
961
962 /*
963 * unsubscribe, future txns should not be streamed
964 */
965 request[jss::account_history_tx_stream][jss::stop_history_tx_only] = false;
966 jv = wscTxHistory->invoke("unsubscribe", request);
967 BEAST_EXPECT(goodSubRPC(jv));
968
969 sendPayments(env, env.master, alice, 1, 1);
970 r = getTxHash(*wscTxHistory, vec, 1);
971 BEAST_EXPECT(!r.first);
972 }
973 {
974 /*
975 * subscribe genesis account tx history without txns
976 * subscribe to bob's account after it is created
977 */
978 Env env(*this);
979 auto wscTxHistory = makeWSClient(env.app().config());
980 Json::Value request;
981 request[jss::account_history_tx_stream] = Json::objectValue;
982 request[jss::account_history_tx_stream][jss::account] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
983 auto jv = wscTxHistory->invoke("subscribe", request);
984 if (!BEAST_EXPECT(goodSubRPC(jv)))
985 return;
986 IdxHashVec genesisFullHistoryVec;
987 if (!BEAST_EXPECT(!getTxHash(*wscTxHistory, genesisFullHistoryVec, 1).first))
988 return;
989
990 /*
991 * create bob's account with one tx
992 * the two subscriptions should both stream it
993 */
994 sendPayments(env, env.master, bob, 1, 1, 654321);
995
996 auto r = getTxHash(*wscTxHistory, genesisFullHistoryVec, 1);
997 if (!BEAST_EXPECT(r.first && r.second))
998 return;
999
1000 request[jss::account_history_tx_stream][jss::account] = bob.human();
1001 jv = wscTxHistory->invoke("subscribe", request);
1002 if (!BEAST_EXPECT(goodSubRPC(jv)))
1003 return;
1004 IdxHashVec bobFullHistoryVec;
1005 r = getTxHash(*wscTxHistory, bobFullHistoryVec, 1);
1006 if (!BEAST_EXPECT(r.first && r.second))
1007 return;
1008 BEAST_EXPECT(std::get<1>(bobFullHistoryVec.back()) == std::get<1>(genesisFullHistoryVec.back()));
1009
1010 /*
1011 * unsubscribe to prepare next test
1012 */
1013 jv = wscTxHistory->invoke("unsubscribe", request);
1014 if (!BEAST_EXPECT(goodSubRPC(jv)))
1015 return;
1016 request[jss::account_history_tx_stream][jss::account] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
1017 jv = wscTxHistory->invoke("unsubscribe", request);
1018 BEAST_EXPECT(goodSubRPC(jv));
1019
1020 /*
1021 * add more txns, then subscribe bob tx history and
1022 * genesis account tx history. Their earliest txns should match.
1023 */
1024 sendPayments(env, env.master, bob, 30, 300);
1025 wscTxHistory = makeWSClient(env.app().config());
1026 request[jss::account_history_tx_stream][jss::account] = bob.human();
1027 jv = wscTxHistory->invoke("subscribe", request);
1028
1029 bobFullHistoryVec.clear();
1030 BEAST_EXPECT(getTxHash(*wscTxHistory, bobFullHistoryVec, 31).second);
1031 jv = wscTxHistory->invoke("unsubscribe", request);
1032
1033 request[jss::account_history_tx_stream][jss::account] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
1034 jv = wscTxHistory->invoke("subscribe", request);
1035 genesisFullHistoryVec.clear();
1036 BEAST_EXPECT(getTxHash(*wscTxHistory, genesisFullHistoryVec, 31).second);
1037 jv = wscTxHistory->invoke("unsubscribe", request);
1038
1039 BEAST_EXPECT(std::get<1>(bobFullHistoryVec.back()) == std::get<1>(genesisFullHistoryVec.back()));
1040 }
1041
1042 {
1043 /*
1044 * subscribe account and subscribe account tx history
1045 * and compare txns streamed
1046 */
1047 Env env(*this);
1048 auto wscAccount = makeWSClient(env.app().config());
1049 auto wscTxHistory = makeWSClient(env.app().config());
1050
1051 std::array<Account, 2> accounts = {alice, bob};
1052 env.fund(XRP(222222), accounts);
1053 env.close();
1054
1055 // subscribe account
1057 stream[jss::accounts] = Json::arrayValue;
1058 stream[jss::accounts].append(alice.human());
1059 auto jv = wscAccount->invoke("subscribe", stream);
1060
1061 sendPayments(env, alice, bob, 5, 1);
1062 sendPayments(env, alice, bob, 5, 1);
1063 IdxHashVec accountVec;
1064 if (!BEAST_EXPECT(getTxHash(*wscAccount, accountVec, 10).first))
1065 return;
1066
1067 // subscribe account tx history
1068 Json::Value request;
1069 request[jss::account_history_tx_stream] = Json::objectValue;
1070 request[jss::account_history_tx_stream][jss::account] = alice.human();
1071 jv = wscTxHistory->invoke("subscribe", request);
1072
1073 // compare historical txns
1074 IdxHashVec txHistoryVec;
1075 if (!BEAST_EXPECT(getTxHash(*wscTxHistory, txHistoryVec, 10).first))
1076 return;
1077 if (!BEAST_EXPECT(hashCompare(accountVec, txHistoryVec, true)))
1078 return;
1079
1080 // check boundary tags
1081 // only account_history_tx_stream has ledger boundary information.
1082 if (!BEAST_EXPECT(checkBoundary(txHistoryVec, false)))
1083 return;
1084
1085 {
1086 // take out all history txns from stream to prepare next test
1087 IdxHashVec initFundTxns;
1088 if (!BEAST_EXPECT(getTxHash(*wscTxHistory, initFundTxns, 10).second) ||
1089 !BEAST_EXPECT(checkBoundary(initFundTxns, false)))
1090 return;
1091 }
1092
1093 // compare future txns
1094 sendPayments(env, alice, bob, 10, 1);
1095 if (!BEAST_EXPECT(getTxHash(*wscAccount, accountVec, 10).first))
1096 return;
1097 if (!BEAST_EXPECT(getTxHash(*wscTxHistory, txHistoryVec, 10).first))
1098 return;
1099 if (!BEAST_EXPECT(hashCompare(accountVec, txHistoryVec, true)))
1100 return;
1101
1102 // check boundary tags
1103 // only account_history_tx_stream has ledger boundary information.
1104 if (!BEAST_EXPECT(checkBoundary(txHistoryVec, false)))
1105 return;
1106
1107 wscTxHistory->invoke("unsubscribe", request);
1108 wscAccount->invoke("unsubscribe", stream);
1109 }
1110
1111 {
1112 /*
1113 * alice issues USD to carol
1114 * mix USD and XRP payments
1115 */
1116 Env env(*this);
1117 auto const USD_a = alice["USD"];
1118
1119 std::array<Account, 2> accounts = {alice, carol};
1120 env.fund(XRP(333333), accounts);
1121 env.trust(USD_a(20000), carol);
1122 env.close();
1123
1124 auto mixedPayments = [&]() -> int {
1125 sendPayments(env, alice, carol, 1, 0);
1126 env(pay(alice, carol, USD_a(100)));
1127 env.close();
1128 return 2;
1129 };
1130
1131 // subscribe
1132 Json::Value request;
1133 request[jss::account_history_tx_stream] = Json::objectValue;
1134 request[jss::account_history_tx_stream][jss::account] = carol.human();
1135 auto ws = makeWSClient(env.app().config());
1136 auto jv = ws->invoke("subscribe", request);
1137 {
1138 // take out existing txns from the stream
1139 IdxHashVec tempVec;
1140 getTxHash(*ws, tempVec, 100);
1141 }
1142
1143 auto count = mixedPayments();
1144 IdxHashVec vec1;
1145 if (!BEAST_EXPECT(getTxHash(*ws, vec1, count).first))
1146 return;
1147 ws->invoke("unsubscribe", request);
1148 }
1149
1150 {
1151 /*
1152 * long transaction history
1153 */
1154 Env env(*this);
1155 std::array<Account, 2> accounts = {alice, carol};
1156 env.fund(XRP(444444), accounts);
1157 env.close();
1158
1159 // many payments, and close lots of ledgers
1160 auto oneRound = [&](int numPayments) { return sendPayments(env, alice, carol, numPayments, 300); };
1161
1162 // subscribe
1163 Json::Value request;
1164 request[jss::account_history_tx_stream] = Json::objectValue;
1165 request[jss::account_history_tx_stream][jss::account] = carol.human();
1166 auto wscLong = makeWSClient(env.app().config());
1167 auto jv = wscLong->invoke("subscribe", request);
1168 {
1169 // take out existing txns from the stream
1170 IdxHashVec tempVec;
1171 getTxHash(*wscLong, tempVec, 100);
1172 }
1173
1174 // repeat the payments many rounds
1175 for (int kk = 2; kk < 10; ++kk)
1176 {
1177 auto count = oneRound(kk);
1178 IdxHashVec vec1;
1179 if (!BEAST_EXPECT(getTxHash(*wscLong, vec1, count).first))
1180 return;
1181
1182 // another subscribe, only for this round
1183 auto wscShort = makeWSClient(env.app().config());
1184 auto jv = wscShort->invoke("subscribe", request);
1185 IdxHashVec vec2;
1186 if (!BEAST_EXPECT(getTxHash(*wscShort, vec2, count).first))
1187 return;
1188 if (!BEAST_EXPECT(hashCompare(vec1, vec2, true)))
1189 return;
1190 wscShort->invoke("unsubscribe", request);
1191 }
1192 }
1193 }
1194
1195 void
1197 {
1198 testcase("SubBookChanges");
1199 using namespace jtx;
1200 using namespace std::chrono_literals;
1201 FeatureBitset const all{
1202 jtx::testable_amendments() | featurePermissionedDomains | featureCredentials | featurePermissionedDEX};
1203
1204 Env env(*this, all);
1205 PermissionedDEX permDex(env);
1206 auto const alice = permDex.alice;
1207 auto const bob = permDex.bob;
1208 auto const carol = permDex.carol;
1209 auto const domainID = permDex.domainID;
1210 auto const gw = permDex.gw;
1211 auto const USD = permDex.USD;
1212
1213 auto wsc = makeWSClient(env.app().config());
1214
1215 Json::Value streams;
1216 streams[jss::streams] = Json::arrayValue;
1217 streams[jss::streams][0u] = "book_changes";
1218
1219 auto jv = wsc->invoke("subscribe", streams);
1220 if (!BEAST_EXPECT(jv[jss::status] == "success"))
1221 return;
1222 env(offer(alice, XRP(10), USD(10)), domain(domainID), txflags(tfHybrid));
1223 env.close();
1224
1225 env(pay(bob, carol, USD(5)), path(~USD), sendmax(XRP(5)), domain(domainID));
1226 env.close();
1227
1228 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
1229 if (jv[jss::changes].size() != 1)
1230 return false;
1231
1232 auto const jrOffer = jv[jss::changes][0u];
1233 return (jv[jss::changes][0u][jss::domain]).asString() == strHex(domainID) &&
1234 jrOffer[jss::currency_a].asString() == "XRP_drops" && jrOffer[jss::volume_a].asString() == "5000000" &&
1235 jrOffer[jss::currency_b].asString() == "rHUKYAZyUFn8PCZWbPfwHfbVQXTYrYKkHb/USD" &&
1236 jrOffer[jss::volume_b].asString() == "5";
1237 }));
1238 }
1239
1240 void
1242 {
1243 // `nftoken_id` is added for `transaction` stream in the `subscribe`
1244 // response for NFTokenMint and NFTokenAcceptOffer.
1245 //
1246 // `nftoken_ids` is added for `transaction` stream in the `subscribe`
1247 // response for NFTokenCancelOffer
1248 //
1249 // `offer_id` is added for `transaction` stream in the `subscribe`
1250 // response for NFTokenCreateOffer
1251 //
1252 // The values of these fields are dependent on the NFTokenID/OfferID
1253 // changed in its corresponding transaction. We want to validate each
1254 // response to make sure the synthetic fields hold the right values.
1255
1256 testcase("Test synthetic fields from Subscribe response");
1257
1258 using namespace test::jtx;
1259 using namespace std::chrono_literals;
1260
1261 Account const alice{"alice"};
1262 Account const bob{"bob"};
1263 Account const broker{"broker"};
1264
1265 Env env{*this, features};
1266 env.fund(XRP(10000), alice, bob, broker);
1267 env.close();
1268
1269 auto wsc = test::makeWSClient(env.app().config());
1270 Json::Value stream;
1271 stream[jss::streams] = Json::arrayValue;
1272 stream[jss::streams].append("transactions");
1273 auto jv = wsc->invoke("subscribe", stream);
1274
1275 // Verify `nftoken_id` value equals to the NFTokenID that was
1276 // changed in the most recent NFTokenMint or NFTokenAcceptOffer
1277 // transaction
1278 auto verifyNFTokenID = [&](uint256 const& actualNftID) {
1279 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
1280 uint256 nftID;
1281 BEAST_EXPECT(nftID.parseHex(jv[jss::meta][jss::nftoken_id].asString()));
1282 return nftID == actualNftID;
1283 }));
1284 };
1285
1286 // Verify `nftoken_ids` value equals to the NFTokenIDs that were
1287 // changed in the most recent NFTokenCancelOffer transaction
1288 auto verifyNFTokenIDsInCancelOffer = [&](std::vector<uint256> actualNftIDs) {
1289 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
1290 std::vector<uint256> metaIDs;
1291 std::transform(
1292 jv[jss::meta][jss::nftoken_ids].begin(),
1293 jv[jss::meta][jss::nftoken_ids].end(),
1294 std::back_inserter(metaIDs),
1295 [this](Json::Value id) {
1296 uint256 nftID;
1297 BEAST_EXPECT(nftID.parseHex(id.asString()));
1298 return nftID;
1299 });
1300 // Sort both array to prepare for comparison
1301 std::sort(metaIDs.begin(), metaIDs.end());
1302 std::sort(actualNftIDs.begin(), actualNftIDs.end());
1303
1304 // Make sure the expect number of NFTs is correct
1305 BEAST_EXPECT(metaIDs.size() == actualNftIDs.size());
1306
1307 // Check the value of NFT ID in the meta with the
1308 // actual values
1309 for (size_t i = 0; i < metaIDs.size(); ++i)
1310 BEAST_EXPECT(metaIDs[i] == actualNftIDs[i]);
1311 return true;
1312 }));
1313 };
1314
1315 // Verify `offer_id` value equals to the offerID that was
1316 // changed in the most recent NFTokenCreateOffer tx
1317 auto verifyNFTokenOfferID = [&](uint256 const& offerID) {
1318 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
1319 uint256 metaOfferID;
1320 BEAST_EXPECT(metaOfferID.parseHex(jv[jss::meta][jss::offer_id].asString()));
1321 return metaOfferID == offerID;
1322 }));
1323 };
1324
1325 // Check new fields in tx meta when for all NFTtransactions
1326 {
1327 // Alice mints 2 NFTs
1328 // Verify the NFTokenIDs are correct in the NFTokenMint tx meta
1329 uint256 const nftId1{token::getNextID(env, alice, 0u, tfTransferable)};
1330 env(token::mint(alice, 0u), txflags(tfTransferable));
1331 env.close();
1332 verifyNFTokenID(nftId1);
1333
1334 uint256 const nftId2{token::getNextID(env, alice, 0u, tfTransferable)};
1335 env(token::mint(alice, 0u), txflags(tfTransferable));
1336 env.close();
1337 verifyNFTokenID(nftId2);
1338
1339 // Alice creates one sell offer for each NFT
1340 // Verify the offer indexes are correct in the NFTokenCreateOffer tx
1341 // meta
1342 uint256 const aliceOfferIndex1 = keylet::nftoffer(alice, env.seq(alice)).key;
1343 env(token::createOffer(alice, nftId1, drops(1)), txflags(tfSellNFToken));
1344 env.close();
1345 verifyNFTokenOfferID(aliceOfferIndex1);
1346
1347 uint256 const aliceOfferIndex2 = keylet::nftoffer(alice, env.seq(alice)).key;
1348 env(token::createOffer(alice, nftId2, drops(1)), txflags(tfSellNFToken));
1349 env.close();
1350 verifyNFTokenOfferID(aliceOfferIndex2);
1351
1352 // Alice cancels two offers she created
1353 // Verify the NFTokenIDs are correct in the NFTokenCancelOffer tx
1354 // meta
1355 env(token::cancelOffer(alice, {aliceOfferIndex1, aliceOfferIndex2}));
1356 env.close();
1357 verifyNFTokenIDsInCancelOffer({nftId1, nftId2});
1358
1359 // Bobs creates a buy offer for nftId1
1360 // Verify the offer id is correct in the NFTokenCreateOffer tx meta
1361 auto const bobBuyOfferIndex = keylet::nftoffer(bob, env.seq(bob)).key;
1362 env(token::createOffer(bob, nftId1, drops(1)), token::owner(alice));
1363 env.close();
1364 verifyNFTokenOfferID(bobBuyOfferIndex);
1365
1366 // Alice accepts bob's buy offer
1367 // Verify the NFTokenID is correct in the NFTokenAcceptOffer tx meta
1368 env(token::acceptBuyOffer(alice, bobBuyOfferIndex));
1369 env.close();
1370 verifyNFTokenID(nftId1);
1371 }
1372
1373 // Check `nftoken_ids` in brokered mode
1374 {
1375 // Alice mints a NFT
1376 uint256 const nftId{token::getNextID(env, alice, 0u, tfTransferable)};
1377 env(token::mint(alice, 0u), txflags(tfTransferable));
1378 env.close();
1379 verifyNFTokenID(nftId);
1380
1381 // Alice creates sell offer and set broker as destination
1382 uint256 const offerAliceToBroker = keylet::nftoffer(alice, env.seq(alice)).key;
1383 env(token::createOffer(alice, nftId, drops(1)), token::destination(broker), txflags(tfSellNFToken));
1384 env.close();
1385 verifyNFTokenOfferID(offerAliceToBroker);
1386
1387 // Bob creates buy offer
1388 uint256 const offerBobToBroker = keylet::nftoffer(bob, env.seq(bob)).key;
1389 env(token::createOffer(bob, nftId, drops(1)), token::owner(alice));
1390 env.close();
1391 verifyNFTokenOfferID(offerBobToBroker);
1392
1393 // Check NFTokenID meta for NFTokenAcceptOffer in brokered mode
1394 env(token::brokerOffers(broker, offerBobToBroker, offerAliceToBroker));
1395 env.close();
1396 verifyNFTokenID(nftId);
1397 }
1398
1399 // Check if there are no duplicate nft id in Cancel transactions where
1400 // multiple offers are cancelled for the same NFT
1401 {
1402 // Alice mints a NFT
1403 uint256 const nftId{token::getNextID(env, alice, 0u, tfTransferable)};
1404 env(token::mint(alice, 0u), txflags(tfTransferable));
1405 env.close();
1406 verifyNFTokenID(nftId);
1407
1408 // Alice creates 2 sell offers for the same NFT
1409 uint256 const aliceOfferIndex1 = keylet::nftoffer(alice, env.seq(alice)).key;
1410 env(token::createOffer(alice, nftId, drops(1)), txflags(tfSellNFToken));
1411 env.close();
1412 verifyNFTokenOfferID(aliceOfferIndex1);
1413
1414 uint256 const aliceOfferIndex2 = keylet::nftoffer(alice, env.seq(alice)).key;
1415 env(token::createOffer(alice, nftId, drops(1)), txflags(tfSellNFToken));
1416 env.close();
1417 verifyNFTokenOfferID(aliceOfferIndex2);
1418
1419 // Make sure the metadata only has 1 nft id, since both offers are
1420 // for the same nft
1421 env(token::cancelOffer(alice, {aliceOfferIndex1, aliceOfferIndex2}));
1422 env.close();
1423 verifyNFTokenIDsInCancelOffer({nftId});
1424 }
1425
1426 if (features[featureNFTokenMintOffer])
1427 {
1428 uint256 const aliceMintWithOfferIndex1 = keylet::nftoffer(alice, env.seq(alice)).key;
1429 env(token::mint(alice), token::amount(XRP(0)));
1430 env.close();
1431 verifyNFTokenOfferID(aliceMintWithOfferIndex1);
1432 }
1433 }
1434
1435 void
1436 run() override
1437 {
1438 using namespace test::jtx;
1440 FeatureBitset const xrpFees{featureXRPFees};
1441
1442 testServer();
1443 testLedger();
1444 testTransactions_APIv1();
1445 testTransactions_APIv2();
1446 testManifests();
1447 testValidations(all - xrpFees);
1448 testValidations(all);
1449 testSubErrors(true);
1450 testSubErrors(false);
1451 testSubByUrl();
1452 testHistoryTxStream();
1453 testSubBookChanges();
1454 testNFToken(all);
1455 testNFToken(all - featureNFTokenMintOffer);
1456 }
1457};
1458
1459BEAST_DEFINE_TESTSUITE(Subscribe, rpc, xrpl);
1460
1461} // namespace test
1462} // namespace xrpl
Represents a JSON value.
Definition json_value.h:130
void clear()
Remove all object members and array elements.
A testsuite class.
Definition suite.h:51
testcase_t testcase
Memberspace for declaring test cases.
Definition suite.h:147
virtual Config & config()=0
uint32_t NETWORK_ID
Definition Config.h:137
virtual void reportFeeChange()=0
virtual NetworkOPs & getOPs()=0
virtual LoadManager & getLoadManager()=0
virtual LoadFeeTrack & getFeeTrack()=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:19
std::string const & human() const
Returns the human readable public key.
Definition Account.h:94
A transaction testing environment.
Definition Env.h:119
Application & app()
Definition Env.h:251
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:98
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:261
Account const & master
Definition Env.h:123
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:284
void memoize(Account const &account)
Associate AccountID with account.
Definition Env.cpp:131
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:319
Set the domain on a JTx.
Definition domain.h:11
Set the fee on a JTx.
Definition fee.h:17
Add a path.
Definition paths.h:37
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition rpc.h:15
Sets the SendMax on a JTx.
Definition sendmax.h:13
Set the regular signature on a JTx.
Definition sig.h:15
Set the flags on a JTx.
Definition txflags.h:11
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: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:22
FeatureBitset testable_amendments()
Definition Env.h:76
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:34
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:5
constexpr std::uint32_t const tfTransferable
Definition TxFlags.h:122
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:597
constexpr std::uint32_t tfHybrid
Definition TxFlags.h:82
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:526
constexpr std::uint32_t vfFullValidation
constexpr std::uint32_t vfFullyCanonicalSig
constexpr std::uint32_t const tfSellNFToken
Definition TxFlags.h:210
T sort(T... args)
uint256 key
Definition Keylet.h:20
Set the sequence number on a JTx.
Definition seq.h:14
T to_string(T... args)