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