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