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