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