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/AccountID.h>
31#include <xrpl/protocol/Feature.h>
32#include <xrpl/protocol/Indexes.h>
33#include <xrpl/protocol/KeyType.h>
34#include <xrpl/protocol/PublicKey.h>
35#include <xrpl/protocol/STValidation.h>
36#include <xrpl/protocol/SecretKey.h>
37#include <xrpl/protocol/Seed.h>
38#include <xrpl/protocol/SeqProxy.h>
39#include <xrpl/protocol/TxFlags.h>
40#include <xrpl/protocol/jss.h>
41#include <xrpl/protocol/tokens.h>
42#include <xrpl/server/LoadFeeTrack.h>
43#include <xrpl/server/NetworkOPs.h>
44
45#include <algorithm>
46#include <array>
47#include <chrono>
48#include <cstddef>
49#include <cstdint>
50#include <initializer_list>
51#include <iterator>
52#include <memory>
53#include <optional>
54#include <string>
55#include <tuple>
56#include <utility>
57#include <vector>
58
59namespace xrpl::test {
60
62{
63public:
64 void
66 {
67 using namespace std::chrono_literals;
68 using namespace jtx;
69 Env env{*this, singleThreadIo(envconfig())};
70 auto wsc = makeWSClient(env.app().config());
71 json::Value stream;
72
73 {
74 // RPC subscribe to server stream
75 stream[jss::streams] = json::ValueType::Array;
76 stream[jss::streams].append("server");
77 auto jv = wsc->invoke("subscribe", stream);
78 if (wsc->version() == 2)
79 {
80 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
81 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
82 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
83 }
84 BEAST_EXPECT(jv[jss::status] == "success");
85 }
86
87 // here we forcibly stop the load manager because it can (rarely but
88 // every-so-often) cause fees to raise or lower AFTER we've called the
89 // first findMsg but BEFORE we unsubscribe, thus causing the final
90 // findMsg check to fail since there is one unprocessed ws msg created
91 // by the loadmanager
92 env.app().getLoadManager().stop();
93 {
94 // Raise fee to cause an update
95 auto& feeTrack = env.app().getFeeTrack();
96 for (int i = 0; i < 5; ++i)
97 feeTrack.raiseLocalFee();
98 env.app().getOPs().reportFeeChange();
99
100 // Check stream update
101 BEAST_EXPECT(
102 wsc->findMsg(5s, [&](auto const& jv) { return jv[jss::type] == "serverStatus"; }));
103 }
104
105 {
106 // RPC unsubscribe
107 auto jv = wsc->invoke("unsubscribe", stream);
108 if (wsc->version() == 2)
109 {
110 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
111 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
112 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
113 }
114 BEAST_EXPECT(jv[jss::status] == "success");
115 }
116
117 {
118 // Raise fee to cause an update
119 auto& feeTrack = env.app().getFeeTrack();
120 for (int i = 0; i < 5; ++i)
121 feeTrack.raiseLocalFee();
122 env.app().getOPs().reportFeeChange();
123
124 // Check stream update
125 auto jvo = wsc->getMsg(10ms);
126 BEAST_EXPECTS(!jvo, "getMsg: " + to_string(jvo.value()));
127 }
128 }
129
130 void
132 {
133 using namespace std::chrono_literals;
134 using namespace jtx;
135 Env env{*this, singleThreadIo(envconfig())};
136 auto wsc = makeWSClient(env.app().config());
137 json::Value stream;
138
139 {
140 // RPC subscribe to ledger stream
141 stream[jss::streams] = json::ValueType::Array;
142 stream[jss::streams].append("ledger");
143 auto jv = wsc->invoke("subscribe", stream);
144 if (wsc->version() == 2)
145 {
146 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
147 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
148 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
149 }
150 BEAST_EXPECT(jv[jss::result][jss::ledger_index] == 2);
151 BEAST_EXPECT(
152 jv[jss::result][jss::network_id] == env.app().getNetworkIDService().getNetworkID());
153 }
154
155 {
156 // Accept a ledger
157 BEAST_EXPECT(env.syncClose());
158
159 // Check stream update
160 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
161 return jv[jss::ledger_index] == 3 &&
162 jv[jss::network_id] == env.app().getNetworkIDService().getNetworkID();
163 }));
164 }
165
166 {
167 // Accept another ledger
168 BEAST_EXPECT(env.syncClose());
169
170 // Check stream update
171 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
172 return jv[jss::ledger_index] == 4 &&
173 jv[jss::network_id] == env.app().getNetworkIDService().getNetworkID();
174 }));
175 }
176
177 // RPC unsubscribe
178 auto jv = wsc->invoke("unsubscribe", stream);
179 if (wsc->version() == 2)
180 {
181 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
182 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
183 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
184 }
185 BEAST_EXPECT(jv[jss::status] == "success");
186 }
187
188 void
190 {
191 using namespace std::chrono_literals;
192 using namespace jtx;
193 Env env(*this, singleThreadIo(envconfig()));
194 auto baseFee = env.current()->fees().base.drops();
195 auto wsc = makeWSClient(env.app().config());
196 json::Value stream;
197
198 {
199 // RPC subscribe to transactions stream
200 stream[jss::streams] = json::ValueType::Array;
201 stream[jss::streams].append("transactions");
202 auto jv = wsc->invoke("subscribe", stream);
203 if (wsc->version() == 2)
204 {
205 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
206 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
207 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
208 }
209 BEAST_EXPECT(jv[jss::status] == "success");
210 }
211
212 {
213 env.fund(XRP(10000), "alice");
214 BEAST_EXPECT(env.syncClose());
215
216 // Check stream update for payment transaction
217 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
218 return jv[jss::meta]["AffectedNodes"][1u]["CreatedNode"]["NewFields"]
219 [jss::Account] == Account("alice").human() &&
220 jv[jss::transaction][jss::TransactionType] == jss::Payment &&
221 jv[jss::transaction][jss::DeliverMax] ==
222 std::to_string(10000000000 + baseFee) &&
223 jv[jss::transaction][jss::Fee] == std::to_string(baseFee) &&
224 jv[jss::transaction][jss::Sequence] == 1;
225 }));
226
227 // Check stream update for accountset transaction
228 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
229 return jv[jss::meta]["AffectedNodes"][0u]["ModifiedNode"]["FinalFields"]
230 [jss::Account] == Account("alice").human();
231 }));
232
233 env.fund(XRP(10000), "bob");
234 BEAST_EXPECT(env.syncClose());
235
236 // Check stream update for payment transaction
237 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
238 return jv[jss::meta]["AffectedNodes"][1u]["CreatedNode"]["NewFields"]
239 [jss::Account] //
240 == Account("bob").human() &&
241 jv[jss::transaction][jss::TransactionType] //
242 == jss::Payment &&
243 jv[jss::transaction][jss::DeliverMax] //
244 == std::to_string(10000000000 + baseFee) &&
245 jv[jss::transaction][jss::Fee] //
246 == std::to_string(baseFee) &&
247 jv[jss::transaction][jss::Sequence] //
248 == 2;
249 }));
250
251 // Check stream update for accountset transaction
252 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
253 return jv[jss::meta]["AffectedNodes"][0u]["ModifiedNode"]["FinalFields"]
254 [jss::Account] == Account("bob").human();
255 }));
256 }
257
258 {
259 // RPC unsubscribe
260 auto jv = wsc->invoke("unsubscribe", stream);
261 if (wsc->version() == 2)
262 {
263 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
264 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
265 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
266 }
267 BEAST_EXPECT(jv[jss::status] == "success");
268 }
269
270 {
271 // RPC subscribe to accounts stream
273 stream[jss::accounts] = json::ValueType::Array;
274 stream[jss::accounts].append(Account("alice").human());
275 auto jv = wsc->invoke("subscribe", stream);
276 if (wsc->version() == 2)
277 {
278 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
279 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
280 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
281 }
282 BEAST_EXPECT(jv[jss::status] == "success");
283 }
284
285 {
286 // Transaction that does not affect stream
287 env.fund(XRP(10000), "carol");
288 BEAST_EXPECT(env.syncClose());
289 BEAST_EXPECT(!wsc->getMsg(10ms));
290
291 // Transactions concerning alice
292 env.trust(Account("bob")["USD"](100), "alice");
293 BEAST_EXPECT(env.syncClose());
294
295 // Check stream updates
296 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
297 return jv[jss::meta]["AffectedNodes"][1u]["ModifiedNode"]["FinalFields"]
298 [jss::Account] == Account("alice").human();
299 }));
300
301 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
302 return jv[jss::meta]["AffectedNodes"][1u]["CreatedNode"]["NewFields"]["LowLimit"]
303 [jss::issuer] == Account("alice").human();
304 }));
305 }
306
307 // RPC unsubscribe
308 auto jv = wsc->invoke("unsubscribe", stream);
309 if (wsc->version() == 2)
310 {
311 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
312 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
313 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
314 }
315 BEAST_EXPECT(jv[jss::status] == "success");
316 }
317
318 void
320 {
321 testcase("transactions API version 2");
322
323 using namespace std::chrono_literals;
324 using namespace jtx;
325 Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
326 cfg->fees.referenceFee = 10;
327 cfg = singleThreadIo(std::move(cfg));
328 return cfg;
329 }));
330 auto wsc = makeWSClient(env.app().config());
332
333 {
334 // RPC subscribe to transactions stream
335 stream[jss::api_version] = 2;
336 stream[jss::streams] = json::ValueType::Array;
337 stream[jss::streams].append("transactions");
338 auto jv = wsc->invoke("subscribe", stream);
339 if (wsc->version() == 2)
340 {
341 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
342 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
343 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
344 }
345 BEAST_EXPECT(jv[jss::status] == "success");
346 }
347
348 {
349 env.fund(XRP(10000), "alice");
350 BEAST_EXPECT(env.syncClose());
351
352 // Check stream update for payment transaction
353 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
354 return jv[jss::meta]["AffectedNodes"][1u]["CreatedNode"]["NewFields"]
355 [jss::Account] //
356 == Account("alice").human() &&
357 jv[jss::close_time_iso] //
358 == "2000-01-01T00:00:10Z" &&
359 jv[jss::validated] == true && //
360 jv[jss::ledger_hash] ==
361 "0F1A9E0C109ADEF6DA2BDE19217C12BBEC57174CBDBD212B0EBDC1CEDB"
362 "853185" && //
363 !jv[jss::inLedger] &&
364 jv[jss::ledger_index] == 3 && //
365 jv[jss::tx_json][jss::TransactionType] //
366 == jss::Payment &&
367 jv[jss::tx_json][jss::DeliverMax] //
368 == "10000000010" &&
369 !jv[jss::tx_json].isMember(jss::Amount) &&
370 jv[jss::tx_json][jss::Fee] //
371 == "10" &&
372 jv[jss::tx_json][jss::Sequence] //
373 == 1;
374 }));
375
376 // Check stream update for accountset transaction
377 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
378 return jv[jss::meta]["AffectedNodes"][0u]["ModifiedNode"]["FinalFields"]
379 [jss::Account] == Account("alice").human();
380 }));
381 }
382
383 {
384 // RPC unsubscribe
385 auto jv = wsc->invoke("unsubscribe", stream);
386 if (wsc->version() == 2)
387 {
388 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
389 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
390 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
391 }
392 BEAST_EXPECT(jv[jss::status] == "success");
393 }
394 }
395
396 void
398 {
399 using namespace jtx;
400 Env env(*this, singleThreadIo(envconfig()));
401 auto wsc = makeWSClient(env.app().config());
402 json::Value stream;
403
404 {
405 // RPC subscribe to manifests stream
406 stream[jss::streams] = json::ValueType::Array;
407 stream[jss::streams].append("manifests");
408 auto jv = wsc->invoke("subscribe", stream);
409 if (wsc->version() == 2)
410 {
411 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
412 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
413 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
414 }
415 BEAST_EXPECT(jv[jss::status] == "success");
416 }
417
418 // RPC unsubscribe
419 auto jv = wsc->invoke("unsubscribe", stream);
420 if (wsc->version() == 2)
421 {
422 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
423 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
424 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
425 }
426 BEAST_EXPECT(jv[jss::status] == "success");
427 }
428
429 void
431 {
432 using namespace jtx;
433
434 Env env{*this, singleThreadIo(envconfig(validator, "")), features};
435 auto& cfg = env.app().config();
436 if (!BEAST_EXPECT(cfg.section(Sections::kValidationSeed).empty()))
437 return;
438 auto const parsedseed =
439 parseBase58<Seed>(cfg.section(Sections::kValidationSeed).values()[0]);
440 if (BEAST_EXPECT(parsedseed); not parsedseed.has_value())
441 return;
442
443 std::string const valPublicKey = toBase58(
447
448 auto wsc = makeWSClient(env.app().config());
449 json::Value stream;
450
451 {
452 // RPC subscribe to validations stream
453 stream[jss::streams] = json::ValueType::Array;
454 stream[jss::streams].append("validations");
455 auto jv = wsc->invoke("subscribe", stream);
456 if (wsc->version() == 2)
457 {
458 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
459 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
460 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
461 }
462 BEAST_EXPECT(jv[jss::status] == "success");
463 }
464
465 {
466 // Lambda to check ledger validations from the stream.
467 auto validValidationFields = [&env, &valPublicKey](json::Value const& jv) {
468 if (jv[jss::type] != "validationReceived")
469 return false;
470
471 if (jv[jss::validation_public_key].asString() != valPublicKey)
472 return false;
473
474 if (jv[jss::ledger_hash] != to_string(env.closed()->header().hash))
475 return false;
476
477 if (jv[jss::ledger_index] != std::to_string(env.closed()->header().seq))
478 return false;
479
480 if (jv[jss::flags] != (kVfFullyCanonicalSig | kVfFullValidation))
481 return false;
482
483 if (jv[jss::full] != true)
484 return false;
485
486 if (jv.isMember(jss::load_fee))
487 return false;
488
489 if (!jv.isMember(jss::signature))
490 return false;
491
492 if (!jv.isMember(jss::signing_time))
493 return false;
494
495 if (!jv.isMember(jss::cookie))
496 return false;
497
498 if (!jv.isMember(jss::validated_hash))
499 return false;
500
501 uint32_t const netID = env.app().getNetworkIDService().getNetworkID();
502 if (!jv.isMember(jss::network_id) || jv[jss::network_id] != netID)
503 return false;
504
505 // Certain fields are only added on a flag ledger.
506 bool const isFlagLedger = (env.closed()->header().seq + 1) % 256 == 0;
507
508 if (jv.isMember(jss::server_version) != isFlagLedger)
509 return false;
510
511 if (jv.isMember(jss::reserve_base) != isFlagLedger)
512 return false;
513
514 if (jv.isMember(jss::reserve_inc) != isFlagLedger)
515 return false;
516
517 return true;
518 };
519
520 // Check stream update. Look at enough stream entries so we see
521 // at least one flag ledger.
522 while (env.closed()->header().seq < 300)
523 {
524 BEAST_EXPECT(env.syncClose());
525 using namespace std::chrono_literals;
526 BEAST_EXPECT(wsc->findMsg(5s, validValidationFields));
527 }
528 }
529
530 // RPC unsubscribe
531 auto jv = wsc->invoke("unsubscribe", stream);
532 if (wsc->version() == 2)
533 {
534 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
535 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
536 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
537 }
538 BEAST_EXPECT(jv[jss::status] == "success");
539 }
540
541 void
543 {
544 using namespace jtx;
545 testcase("Subscribe by url");
546 Env env{*this, singleThreadIo(envconfig())};
547
548 json::Value jv;
549 jv[jss::url] = "http://localhost/events";
550 jv[jss::url_username] = "admin";
551 jv[jss::url_password] = "password";
552 jv[jss::streams] = json::ValueType::Array;
553 jv[jss::streams][0u] = "validations";
554 auto jr = env.rpc("json", "subscribe", to_string(jv))[jss::result];
555 BEAST_EXPECT(jr[jss::status] == "success");
556
557 jv[jss::streams][0u] = "ledger";
558 jr = env.rpc("json", "subscribe", to_string(jv))[jss::result];
559 BEAST_EXPECT(jr[jss::status] == "success");
560 BEAST_EXPECT(jr[jss::network_id] == env.app().getNetworkIDService().getNetworkID());
561
562 jr = env.rpc("json", "unsubscribe", to_string(jv))[jss::result];
563 BEAST_EXPECT(jr[jss::status] == "success");
564
565 jv[jss::streams][0u] = "validations";
566 jr = env.rpc("json", "unsubscribe", to_string(jv))[jss::result];
567 BEAST_EXPECT(jr[jss::status] == "success");
568 }
569
570 void
571 testSubErrors(bool subscribe)
572 {
573 using namespace jtx;
574 auto const method = subscribe ? "subscribe" : "unsubscribe";
575 testcase << "Error cases for " << method;
576
577 Env env{*this, singleThreadIo(envconfig())};
578 auto wsc = makeWSClient(env.app().config());
579
580 {
581 auto const jr = env.rpc("json", method, "{}")[jss::result];
582 BEAST_EXPECT(jr[jss::error] == "invalidParams");
583 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
584 }
585
586 {
587 json::Value jv;
588 jv[jss::url] = "not-a-url";
589 jv[jss::username] = "admin";
590 jv[jss::password] = "password";
591 auto const jr = env.rpc("json", method, to_string(jv))[jss::result];
592 if (subscribe)
593 {
594 BEAST_EXPECT(jr[jss::error] == "invalidParams");
595 BEAST_EXPECT(jr[jss::error_message] == "Failed to parse url.");
596 }
597 // else TODO: why isn't this an error for unsubscribe ?
598 // (findRpcSub returns null)
599 }
600
601 {
602 json::Value jv;
603 jv[jss::url] = "ftp://scheme.not.supported.tld";
604 auto const jr = env.rpc("json", method, to_string(jv))[jss::result];
605 if (subscribe)
606 {
607 BEAST_EXPECT(jr[jss::error] == "invalidParams");
608 BEAST_EXPECT(jr[jss::error_message] == "Only http and https is supported.");
609 }
610 }
611
612 {
613 Env envNonadmin{*this, singleThreadIo(noAdmin(envconfig()))};
614 json::Value jv;
615 jv[jss::url] = "no-url";
616 auto const jr = envNonadmin.rpc("json", method, to_string(jv))[jss::result];
617 BEAST_EXPECT(jr[jss::error] == "noPermission");
618 BEAST_EXPECT(jr[jss::error_message] == "You don't have permission for this command.");
619 }
620
626 "",
629
630 for (auto const& f : {jss::accounts_proposed, jss::accounts})
631 {
632 for (auto const& nonArray : nonArrays)
633 {
634 json::Value jv;
635 jv[f] = nonArray;
636 auto const jr = wsc->invoke(method, jv)[jss::result];
637 BEAST_EXPECT(jr[jss::error] == "invalidParams");
638 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
639 }
640
641 {
642 json::Value jv;
644 auto const jr = wsc->invoke(method, jv)[jss::result];
645 BEAST_EXPECT(jr[jss::error] == "actMalformed");
646 BEAST_EXPECT(jr[jss::error_message] == "Account malformed.");
647 }
648 }
649
650 for (auto const& nonArray : nonArrays)
651 {
652 json::Value jv;
653 jv[jss::books] = nonArray;
654 auto const jr = wsc->invoke(method, jv)[jss::result];
655 BEAST_EXPECT(jr[jss::error] == "invalidParams");
656 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
657 }
658
659 {
660 json::Value jv;
661 jv[jss::books] = json::ValueType::Array;
662 jv[jss::books][0u] = 1;
663 auto const jr = wsc->invoke(method, jv)[jss::result];
664 BEAST_EXPECT(jr[jss::error] == "invalidParams");
665 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
666 }
667
668 {
669 json::Value jv;
670 jv[jss::books] = json::ValueType::Array;
671 jv[jss::books][0u] = json::ValueType::Object;
672 jv[jss::books][0u][jss::taker_gets] = json::ValueType::Object;
673 jv[jss::books][0u][jss::taker_pays] = json::ValueType::Object;
674 auto const jr = wsc->invoke(method, jv)[jss::result];
675
676 BEAST_EXPECT(jr[jss::error] == "srcCurMalformed");
677 BEAST_EXPECT(jr[jss::error_message] == "Source currency is malformed.");
678 }
679
680 {
681 json::Value jv;
682 jv[jss::books] = json::ValueType::Array;
683 jv[jss::books][0u] = json::ValueType::Object;
684 jv[jss::books][0u][jss::taker_gets] = json::ValueType::Object;
685 jv[jss::books][0u][jss::taker_pays] = json::ValueType::Object;
686 jv[jss::books][0u][jss::taker_pays][jss::currency] = "ZZZZ";
687 auto const jr = wsc->invoke(method, jv)[jss::result];
688 BEAST_EXPECT(jr[jss::error] == "srcCurMalformed");
689 BEAST_EXPECT(jr[jss::error_message] == "Source currency is malformed.");
690 }
691
692 {
693 json::Value jv;
694 jv[jss::books] = json::ValueType::Array;
695 jv[jss::books][0u] = json::ValueType::Object;
696 jv[jss::books][0u][jss::taker_gets] = json::ValueType::Object;
697 jv[jss::books][0u][jss::taker_pays] = json::ValueType::Object;
698 jv[jss::books][0u][jss::taker_pays][jss::currency] = "USD";
699 jv[jss::books][0u][jss::taker_pays][jss::issuer] = 1;
700 auto const jr = wsc->invoke(method, jv)[jss::result];
701 BEAST_EXPECT(jr[jss::error] == "srcIsrMalformed");
702 BEAST_EXPECT(jr[jss::error_message] == "Source issuer is malformed.");
703 }
704
705 {
706 json::Value jv;
707 jv[jss::books] = json::ValueType::Array;
708 jv[jss::books][0u] = json::ValueType::Object;
709 jv[jss::books][0u][jss::taker_gets] = json::ValueType::Object;
710 jv[jss::books][0u][jss::taker_pays] = json::ValueType::Object;
711 jv[jss::books][0u][jss::taker_pays][jss::currency] = "USD";
712 jv[jss::books][0u][jss::taker_pays][jss::issuer] = Account{"gateway"}.human() + "DEAD";
713 auto const jr = wsc->invoke(method, jv)[jss::result];
714 BEAST_EXPECT(jr[jss::error] == "srcIsrMalformed");
715 BEAST_EXPECT(jr[jss::error_message] == "Source issuer is malformed.");
716 }
717
718 {
719 json::Value jv;
720 jv[jss::books] = json::ValueType::Array;
721 jv[jss::books][0u] = json::ValueType::Object;
722 jv[jss::books][0u][jss::taker_pays] =
723 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::Values::IncludeDate);
724 jv[jss::books][0u][jss::taker_gets] = json::ValueType::Object;
725 auto const jr = wsc->invoke(method, jv)[jss::result];
726 // NOTE: this error is slightly incongruous with the equivalent source currency error
727 BEAST_EXPECT(jr[jss::error] == "dstAmtMalformed");
728 BEAST_EXPECT(
729 jr[jss::error_message] == "Destination amount/currency/issuer is malformed.");
730 }
731
732 {
733 json::Value jv;
734 jv[jss::books] = json::ValueType::Array;
735 jv[jss::books][0u] = json::ValueType::Object;
736 jv[jss::books][0u][jss::taker_pays] =
737 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::Values::IncludeDate);
738 jv[jss::books][0u][jss::taker_gets][jss::currency] = "ZZZZ";
739 auto const jr = wsc->invoke(method, jv)[jss::result];
740 // NOTE: this error is slightly incongruous with the
741 // equivalent source currency error
742 BEAST_EXPECT(jr[jss::error] == "dstAmtMalformed");
743 BEAST_EXPECT(
744 jr[jss::error_message] == "Destination amount/currency/issuer is malformed.");
745 }
746
747 {
748 json::Value jv;
749 jv[jss::books] = json::ValueType::Array;
750 jv[jss::books][0u] = json::ValueType::Object;
751 jv[jss::books][0u][jss::taker_pays] =
752 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::Values::IncludeDate);
753 jv[jss::books][0u][jss::taker_gets][jss::currency] = "USD";
754 jv[jss::books][0u][jss::taker_gets][jss::issuer] = 1;
755 auto const jr = wsc->invoke(method, jv)[jss::result];
756 BEAST_EXPECT(jr[jss::error] == "dstIsrMalformed");
757 BEAST_EXPECT(jr[jss::error_message] == "Destination issuer is malformed.");
758 }
759
760 {
761 json::Value jv;
762 jv[jss::books] = json::ValueType::Array;
763 jv[jss::books][0u] = json::ValueType::Object;
764 jv[jss::books][0u][jss::taker_pays] =
765 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::Values::IncludeDate);
766 jv[jss::books][0u][jss::taker_gets][jss::currency] = "USD";
767 jv[jss::books][0u][jss::taker_gets][jss::issuer] = Account{"gateway"}.human() + "DEAD";
768 auto const jr = wsc->invoke(method, jv)[jss::result];
769 BEAST_EXPECT(jr[jss::error] == "dstIsrMalformed");
770 BEAST_EXPECT(jr[jss::error_message] == "Destination issuer is malformed.");
771 }
772
773 {
774 json::Value jv;
775 jv[jss::books] = json::ValueType::Array;
776 jv[jss::books][0u] = json::ValueType::Object;
777 jv[jss::books][0u][jss::taker_pays] =
778 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::Values::IncludeDate);
779 jv[jss::books][0u][jss::taker_gets] =
780 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::Values::IncludeDate);
781 auto const jr = wsc->invoke(method, jv)[jss::result];
782 BEAST_EXPECT(jr[jss::error] == "badMarket");
783 BEAST_EXPECT(jr[jss::error_message] == "No such market.");
784 }
785
786 for (auto const& nonArray : nonArrays)
787 {
788 json::Value jv;
789 jv[jss::streams] = nonArray;
790 auto const jr = wsc->invoke(method, jv)[jss::result];
791 BEAST_EXPECT(jr[jss::error] == "invalidParams");
792 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
793 }
794
795 {
796 json::Value jv;
797 jv[jss::streams] = json::ValueType::Array;
798 jv[jss::streams][0u] = 1;
799 auto const jr = wsc->invoke(method, jv)[jss::result];
800 BEAST_EXPECT(jr[jss::error] == "malformedStream");
801 BEAST_EXPECT(jr[jss::error_message] == "Stream malformed.");
802 }
803
804 {
805 json::Value jv;
806 jv[jss::streams] = json::ValueType::Array;
807 jv[jss::streams][0u] = "not_a_stream";
808 auto const jr = wsc->invoke(method, jv)[jss::result];
809 BEAST_EXPECT(jr[jss::error] == "malformedStream");
810 BEAST_EXPECT(jr[jss::error_message] == "Stream malformed.");
811 }
812
813 if (subscribe)
814 {
815 // invalid taker - not a string
816 {
817 json::Value jv;
818 jv[jss::books] = json::ValueType::Array;
819 jv[jss::books][0u] = json::ValueType::Object;
820 jv[jss::books][0u][jss::taker_pays] =
821 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::Values::IncludeDate);
822 jv[jss::books][0u][jss::taker_gets][jss::currency] = "XRP";
823 jv[jss::books][0u][jss::taker] = 1;
824 auto const jr = wsc->invoke(method, jv)[jss::result];
825 BEAST_EXPECTS(jr[jss::error] == "actMalformed", jr.toStyledString());
826 BEAST_EXPECT(jr[jss::error_message] == "Account malformed.");
827 }
828
829 // invalid taker - malformed account string
830 {
831 json::Value jv;
832 jv[jss::books] = json::ValueType::Array;
833 jv[jss::books][0u] = json::ValueType::Object;
834 jv[jss::books][0u][jss::taker_pays] =
835 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::Values::IncludeDate);
836 jv[jss::books][0u][jss::taker_gets][jss::currency] = "XRP";
837 jv[jss::books][0u][jss::taker] = "not_an_account";
838 auto const jr = wsc->invoke(method, jv)[jss::result];
839 BEAST_EXPECTS(jr[jss::error] == "actMalformed", jr.toStyledString());
840 BEAST_EXPECT(jr[jss::error_message] == "Account malformed.");
841 }
842
843 // invalid taker - account string with extra characters
844 {
845 json::Value jv;
846 jv[jss::books] = json::ValueType::Array;
847 jv[jss::books][0u] = json::ValueType::Object;
848 jv[jss::books][0u][jss::taker_pays] =
849 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::Values::IncludeDate);
850 jv[jss::books][0u][jss::taker_gets][jss::currency] = "XRP";
851 jv[jss::books][0u][jss::taker] = Account{"alice"}.human() + "DEAD";
852 auto const jr = wsc->invoke(method, jv)[jss::result];
853 BEAST_EXPECTS(jr[jss::error] == "actMalformed", jr.toStyledString());
854 BEAST_EXPECT(jr[jss::error_message] == "Account malformed.");
855 }
856 }
857 }
858
859 void
861 {
862 testcase("HistoryTxStream");
863
864 using namespace std::chrono_literals;
865 using namespace jtx;
867
868 Account const alice("alice");
869 Account const bob("bob");
870 Account const carol("carol");
871 Account const david("david");
873
874 /*
875 * return true if the subscribe or unsubscribe result is a success
876 */
877 auto goodSubRPC = [](json::Value const& subReply) -> bool {
878 return subReply.isMember(jss::result) && subReply[jss::result].isMember(jss::status) &&
879 subReply[jss::result][jss::status] == jss::success;
880 };
881
882 /*
883 * try to receive txns from the tx stream subscription via the WSClient.
884 * return {true, true} if received numReplies replies and also
885 * received a tx with the account_history_tx_first == true
886 */
887 auto getTxHash = [](WSClient& wsc,
888 IdxHashVec& v,
889 int numReplies,
892 bool firstFlag = false;
893
894 for (int i = 0; i < numReplies; ++i)
895 {
896 std::uint32_t idx{0};
897 auto reply = wsc.getMsg(timeout);
898 if (reply)
899 {
900 auto r = *reply;
901 if (r.isMember(jss::account_history_tx_index))
902 idx = r[jss::account_history_tx_index].asInt();
903 if (r.isMember(jss::account_history_tx_first))
904 firstFlag = true;
905 bool const boundary = r.isMember(jss::account_history_boundary);
906 int const ledgerIdx = r[jss::ledger_index].asInt();
907 if (r.isMember(jss::transaction) && r[jss::transaction].isMember(jss::hash))
908 {
909 auto t{r[jss::transaction]};
910 v.emplace_back(idx, t[jss::hash].asString(), boundary, ledgerIdx);
911 continue;
912 }
913 }
914 return {false, firstFlag};
915 }
916
917 return {true, firstFlag};
918 };
919
920 /*
921 * send payments between the two accounts a and b,
922 * and close ledgersToClose ledgers
923 */
924 auto sendPayments = [this](
925 Env& env,
926 Account const& a,
927 Account const& b,
928 int newTxns,
929 std::uint32_t ledgersToClose,
930 int numXRP = 10) {
931 env.memoize(a);
932 env.memoize(b);
933 for (int i = 0; i < newTxns; ++i)
934 {
935 auto& from = (i % 2 == 0) ? a : b;
936 auto& to = (i % 2 == 0) ? b : a;
937 env(pay(from, to, jtx::XRP(numXRP)),
941 }
942 for (int i = 0; i < ledgersToClose; ++i)
943 BEAST_EXPECT(env.syncClose());
944 return newTxns;
945 };
946
947 /*
948 * Check if txHistoryVec has every item of accountVec,
949 * and in the same order.
950 * If sizeCompare is false, txHistoryVec is allowed to be larger.
951 */
952 auto hashCompare = [](IdxHashVec const& accountVec,
953 IdxHashVec const& txHistoryVec,
954 bool sizeCompare) -> bool {
955 if (accountVec.empty() || txHistoryVec.empty())
956 return false;
957 if (sizeCompare && accountVec.size() != (txHistoryVec.size()))
958 return false;
959
960 hash_map<std::string, int> txHistoryMap;
961 for (auto const& tx : txHistoryVec)
962 {
963 txHistoryMap.emplace(std::get<1>(tx), std::get<0>(tx));
964 }
965
966 auto getHistoryIndex = [&](std::size_t i) -> std::optional<int> {
967 if (i >= accountVec.size())
968 return {};
969 auto it = txHistoryMap.find(std::get<1>(accountVec[i]));
970 if (it == txHistoryMap.end())
971 return {};
972 return it->second;
973 };
974
975 auto firstHistoryIndex = getHistoryIndex(0);
976 if (!firstHistoryIndex)
977 return false;
978 for (std::size_t i = 1; i < accountVec.size(); ++i)
979 {
980 if (auto idx = getHistoryIndex(i); !idx || *idx != *firstHistoryIndex + i)
981 return false;
982 }
983 return true;
984 };
985
986 // example of vector created from the return of `subscribe` rpc
987 // with jss::accounts
988 // boundary == true on last tx of ledger
989 // ------------------------------------------------------------
990 // (0, "E5B8B...", false, 4
991 // (0, "39E1C...", false, 4
992 // (0, "14EF1...", false, 4
993 // (0, "386E6...", false, 4
994 // (0, "00F3B...", true, 4
995 // (0, "1DCDC...", false, 5
996 // (0, "BD02A...", false, 5
997 // (0, "D3E16...", false, 5
998 // (0, "CB593...", false, 5
999 // (0, "8F28B...", true, 5
1000 //
1001 // example of vector created from the return of `subscribe` rpc
1002 // with jss::account_history_tx_stream.
1003 // boundary == true on first tx of ledger
1004 // ------------------------------------------------------------
1005 // (-1, "8F28B...", false, 5
1006 // (-2, "CB593...", false, 5
1007 // (-3, "D3E16...", false, 5
1008 // (-4, "BD02A...", false, 5
1009 // (-5, "1DCDC...", true, 5
1010 // (-6, "00F3B...", false, 4
1011 // (-7, "386E6...", false, 4
1012 // (-8, "14EF1...", false, 4
1013 // (-9, "39E1C...", false, 4
1014 // (-10, "E5B8B...", true, 4
1015
1016 auto checkBoundary = [](IdxHashVec const& vec, bool /* forward */) {
1017 size_t const numTx = vec.size();
1018 for (size_t i = 0; i < numTx; ++i)
1019 {
1020 auto [idx, hash, boundary, ledger] = vec[i];
1021 if ((i + 1 == numTx || ledger != std::get<3>(vec[i + 1])) != boundary)
1022 return false;
1023 }
1024 return true;
1025 };
1026
1028
1029 {
1030 /*
1031 * subscribe to an account twice with same WS client,
1032 * the second should fail
1033 *
1034 * also test subscribe to the account before it is created
1035 */
1036 Env env(*this, singleThreadIo(envconfig()));
1037 auto wscTxHistory = makeWSClient(env.app().config());
1038 json::Value request;
1039 request[jss::account_history_tx_stream] = json::ValueType::Object;
1040 request[jss::account_history_tx_stream][jss::account] = alice.human();
1041 auto jv = wscTxHistory->invoke("subscribe", request);
1042 if (!BEAST_EXPECT(goodSubRPC(jv)))
1043 return;
1044
1045 jv = wscTxHistory->invoke("subscribe", request);
1046 BEAST_EXPECT(!goodSubRPC(jv));
1047
1048 /*
1049 * unsubscribe history only, future txns should still be streamed
1050 */
1051 request[jss::account_history_tx_stream][jss::stop_history_tx_only] = true;
1052 jv = wscTxHistory->invoke("unsubscribe", request);
1053 if (!BEAST_EXPECT(goodSubRPC(jv)))
1054 return;
1055
1056 sendPayments(env, env.master, alice, 1, 1, 123456);
1057
1058 IdxHashVec vec;
1059 auto r = getTxHash(*wscTxHistory, vec, 1);
1060 if (!BEAST_EXPECT(r.first && r.second))
1061 return;
1062
1063 /*
1064 * unsubscribe, future txns should not be streamed
1065 */
1066 request[jss::account_history_tx_stream][jss::stop_history_tx_only] = false;
1067 jv = wscTxHistory->invoke("unsubscribe", request);
1068 BEAST_EXPECT(goodSubRPC(jv));
1069
1070 sendPayments(env, env.master, alice, 1, 1);
1071 r = getTxHash(*wscTxHistory, vec, 1, 10ms);
1072 BEAST_EXPECT(!r.first);
1073 }
1074 {
1075 /*
1076 * subscribe genesis account tx history without txns
1077 * subscribe to bob's account after it is created
1078 */
1079 Env env(*this, singleThreadIo(envconfig()));
1080 auto wscTxHistory = makeWSClient(env.app().config());
1081 json::Value request;
1082 request[jss::account_history_tx_stream] = json::ValueType::Object;
1083 request[jss::account_history_tx_stream][jss::account] =
1084 "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
1085 auto jv = wscTxHistory->invoke("subscribe", request);
1086 if (!BEAST_EXPECT(goodSubRPC(jv)))
1087 return;
1088 IdxHashVec genesisFullHistoryVec;
1089 BEAST_EXPECT(env.syncClose());
1090 if (!BEAST_EXPECT(!getTxHash(*wscTxHistory, genesisFullHistoryVec, 1, 10ms).first))
1091 return;
1092
1093 /*
1094 * create bob's account with one tx
1095 * the two subscriptions should both stream it
1096 */
1097 sendPayments(env, env.master, bob, 1, 1, 654321);
1098
1099 auto r = getTxHash(*wscTxHistory, genesisFullHistoryVec, 1);
1100 if (!BEAST_EXPECT(r.first && r.second))
1101 return;
1102
1103 request[jss::account_history_tx_stream][jss::account] = bob.human();
1104 jv = wscTxHistory->invoke("subscribe", request);
1105 if (!BEAST_EXPECT(goodSubRPC(jv)))
1106 return;
1107 IdxHashVec bobFullHistoryVec;
1108 BEAST_EXPECT(env.syncClose());
1109 r = getTxHash(*wscTxHistory, bobFullHistoryVec, 1);
1110 if (!BEAST_EXPECT(r.first && r.second))
1111 return;
1112 BEAST_EXPECT(
1113 std::get<1>(bobFullHistoryVec.back()) == std::get<1>(genesisFullHistoryVec.back()));
1114
1115 /*
1116 * unsubscribe to prepare next test
1117 */
1118 jv = wscTxHistory->invoke("unsubscribe", request);
1119 if (!BEAST_EXPECT(goodSubRPC(jv)))
1120 return;
1121 request[jss::account_history_tx_stream][jss::account] =
1122 "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
1123 jv = wscTxHistory->invoke("unsubscribe", request);
1124 BEAST_EXPECT(goodSubRPC(jv));
1125
1126 /*
1127 * add more txns, then subscribe bob tx history and
1128 * genesis account tx history. Their earliest txns should match.
1129 */
1130 sendPayments(env, env.master, bob, 30, 300);
1131 wscTxHistory = makeWSClient(env.app().config());
1132 request[jss::account_history_tx_stream][jss::account] = bob.human();
1133 jv = wscTxHistory->invoke("subscribe", request);
1134
1135 bobFullHistoryVec.clear();
1136 BEAST_EXPECT(getTxHash(*wscTxHistory, bobFullHistoryVec, 31).second);
1137 jv = wscTxHistory->invoke("unsubscribe", request);
1138
1139 request[jss::account_history_tx_stream][jss::account] =
1140 "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
1141 jv = wscTxHistory->invoke("subscribe", request);
1142 genesisFullHistoryVec.clear();
1143 BEAST_EXPECT(env.syncClose());
1144 BEAST_EXPECT(getTxHash(*wscTxHistory, genesisFullHistoryVec, 31).second);
1145 jv = wscTxHistory->invoke("unsubscribe", request);
1146
1147 BEAST_EXPECT(
1148 std::get<1>(bobFullHistoryVec.back()) == std::get<1>(genesisFullHistoryVec.back()));
1149 }
1150
1151 {
1152 /*
1153 * subscribe account and subscribe account tx history
1154 * and compare txns streamed
1155 */
1156 Env env(*this, singleThreadIo(envconfig()));
1157 auto wscAccount = makeWSClient(env.app().config());
1158 auto wscTxHistory = makeWSClient(env.app().config());
1159
1160 std::array<Account, 2> const accounts = {alice, bob};
1161 env.fund(XRP(222222), accounts);
1162 BEAST_EXPECT(env.syncClose());
1163
1164 // subscribe account
1166 stream[jss::accounts] = json::ValueType::Array;
1167 stream[jss::accounts].append(alice.human());
1168 auto jv = wscAccount->invoke("subscribe", stream);
1169
1170 sendPayments(env, alice, bob, 5, 1);
1171 sendPayments(env, alice, bob, 5, 1);
1172 IdxHashVec accountVec;
1173 if (!BEAST_EXPECT(getTxHash(*wscAccount, accountVec, 10).first))
1174 return;
1175
1176 // subscribe account tx history
1177 json::Value request;
1178 request[jss::account_history_tx_stream] = json::ValueType::Object;
1179 request[jss::account_history_tx_stream][jss::account] = alice.human();
1180 jv = wscTxHistory->invoke("subscribe", request);
1181
1182 // compare historical txns
1183 IdxHashVec txHistoryVec;
1184 if (!BEAST_EXPECT(getTxHash(*wscTxHistory, txHistoryVec, 10).first))
1185 return;
1186 if (!BEAST_EXPECT(hashCompare(accountVec, txHistoryVec, true)))
1187 return;
1188
1189 // check boundary tags
1190 // only account_history_tx_stream has ledger boundary information.
1191 if (!BEAST_EXPECT(checkBoundary(txHistoryVec, false)))
1192 return;
1193
1194 {
1195 // take out all history txns from stream to prepare next test
1196 IdxHashVec initFundTxns;
1197 if (!BEAST_EXPECT(getTxHash(*wscTxHistory, initFundTxns, 10).second) ||
1198 !BEAST_EXPECT(checkBoundary(initFundTxns, false)))
1199 return;
1200 }
1201
1202 // compare future txns
1203 sendPayments(env, alice, bob, 10, 1);
1204 if (!BEAST_EXPECT(getTxHash(*wscAccount, accountVec, 10).first))
1205 return;
1206 if (!BEAST_EXPECT(getTxHash(*wscTxHistory, txHistoryVec, 10).first))
1207 return;
1208 if (!BEAST_EXPECT(hashCompare(accountVec, txHistoryVec, true)))
1209 return;
1210
1211 // check boundary tags
1212 // only account_history_tx_stream has ledger boundary information.
1213 if (!BEAST_EXPECT(checkBoundary(txHistoryVec, false)))
1214 return;
1215
1216 wscTxHistory->invoke("unsubscribe", request);
1217 wscAccount->invoke("unsubscribe", stream);
1218 }
1219
1220 {
1221 /*
1222 * alice issues USD to carol
1223 * mix USD and XRP payments
1224 */
1225 Env env(*this, singleThreadIo(envconfig()));
1226 auto const usdA = alice["USD"];
1227
1228 std::array<Account, 2> const accounts = {alice, carol};
1229 env.fund(XRP(333333), accounts);
1230 env.trust(usdA(20000), carol);
1231 BEAST_EXPECT(env.syncClose());
1232
1233 auto mixedPayments = [&]() -> int {
1234 sendPayments(env, alice, carol, 1, 0);
1235 env(pay(alice, carol, usdA(100)));
1236 BEAST_EXPECT(env.syncClose());
1237 return 2;
1238 };
1239
1240 // subscribe
1241 json::Value request;
1242 request[jss::account_history_tx_stream] = json::ValueType::Object;
1243 request[jss::account_history_tx_stream][jss::account] = carol.human();
1244 auto ws = makeWSClient(env.app().config());
1245 auto jv = ws->invoke("subscribe", request);
1246 BEAST_EXPECT(env.syncClose());
1247 {
1248 // take out existing txns from the stream
1249 IdxHashVec tempVec;
1250 getTxHash(*ws, tempVec, 100, 1000ms);
1251 }
1252
1253 auto count = mixedPayments();
1254 IdxHashVec vec1;
1255 if (!BEAST_EXPECT(getTxHash(*ws, vec1, count).first))
1256 return;
1257 ws->invoke("unsubscribe", request);
1258 }
1259
1260 {
1261 /*
1262 * long transaction history
1263 */
1264 Env env(*this, singleThreadIo(envconfig()));
1265 std::array<Account, 2> const accounts = {alice, carol};
1266 env.fund(XRP(444444), accounts);
1267 BEAST_EXPECT(env.syncClose());
1268
1269 // many payments, and close lots of ledgers
1270 auto oneRound = [&](int numPayments) {
1271 return sendPayments(env, alice, carol, numPayments, 300);
1272 };
1273
1274 // subscribe
1275 json::Value request;
1276 request[jss::account_history_tx_stream] = json::ValueType::Object;
1277 request[jss::account_history_tx_stream][jss::account] = carol.human();
1278 auto wscLong = makeWSClient(env.app().config());
1279 auto jv = wscLong->invoke("subscribe", request);
1280 BEAST_EXPECT(env.syncClose());
1281 {
1282 // take out existing txns from the stream
1283 IdxHashVec tempVec;
1284 getTxHash(*wscLong, tempVec, 100, 1000ms);
1285 }
1286
1287 // repeat the payments many rounds
1288 for (int kk = 2; kk < 10; ++kk)
1289 {
1290 auto count = oneRound(kk);
1291 IdxHashVec vec1;
1292 if (!BEAST_EXPECT(getTxHash(*wscLong, vec1, count).first))
1293 return;
1294
1295 // another subscribe, only for this round
1296 auto wscShort = makeWSClient(env.app().config());
1297 auto jv = wscShort->invoke("subscribe", request);
1298 IdxHashVec vec2;
1299 if (!BEAST_EXPECT(getTxHash(*wscShort, vec2, count).first))
1300 return;
1301 if (!BEAST_EXPECT(hashCompare(vec1, vec2, true)))
1302 return;
1303 wscShort->invoke("unsubscribe", request);
1304 }
1305 }
1306 }
1307
1308 void
1310 {
1311 testcase("SubBookChanges");
1312 using namespace jtx;
1313 using namespace std::chrono_literals;
1314 FeatureBitset const all{
1315 jtx::testableAmendments() | featurePermissionedDomains | featureCredentials |
1316 featurePermissionedDEX};
1317
1318 Env env(*this, singleThreadIo(envconfig()), all);
1319 PermissionedDEX const permDex(env);
1320 auto const alice = permDex.alice;
1321 auto const bob = permDex.bob;
1322 auto const carol = permDex.carol;
1323 auto const domainID = permDex.domainID;
1324 auto const gw = permDex.gw;
1325 auto const usd = permDex.usd;
1326
1327 auto wsc = makeWSClient(env.app().config());
1328
1329 json::Value streams;
1330 streams[jss::streams] = json::ValueType::Array;
1331 streams[jss::streams][0u] = "book_changes";
1332
1333 auto jv = wsc->invoke("subscribe", streams);
1334 if (!BEAST_EXPECT(jv[jss::status] == "success"))
1335 return;
1336 env(offer(alice, XRP(10), usd(10)), Domain(domainID), Txflags(tfHybrid));
1337 BEAST_EXPECT(env.syncClose());
1338
1339 env(pay(bob, carol, usd(5)), Path(~usd), Sendmax(XRP(5)), Domain(domainID));
1340 BEAST_EXPECT(env.syncClose());
1341
1342 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
1343 if (jv[jss::changes].size() != 1)
1344 return false;
1345
1346 auto const jrOffer = jv[jss::changes][0u];
1347 return (jv[jss::changes][0u][jss::domain]).asString() == strHex(domainID) &&
1348 jrOffer[jss::currency_a].asString() == "XRP_drops" &&
1349 jrOffer[jss::volume_a].asString() == "5000000" &&
1350 jrOffer[jss::currency_b].asString() == "rHUKYAZyUFn8PCZWbPfwHfbVQXTYrYKkHb/USD" &&
1351 jrOffer[jss::volume_b].asString() == "5";
1352 }));
1353 }
1354
1355 void
1357 {
1358 // `nftoken_id` is added for `transaction` stream in the `subscribe`
1359 // response for NFTokenMint and NFTokenAcceptOffer.
1360 //
1361 // `nftoken_ids` is added for `transaction` stream in the `subscribe`
1362 // response for NFTokenCancelOffer
1363 //
1364 // `offer_id` is added for `transaction` stream in the `subscribe`
1365 // response for NFTokenCreateOffer
1366 //
1367 // The values of these fields are dependent on the NFTokenID/OfferID
1368 // changed in its corresponding transaction. We want to validate each
1369 // response to make sure the synthetic fields hold the right values.
1370
1371 testcase("Test synthetic fields from Subscribe response");
1372
1373 using namespace test::jtx;
1374 using namespace std::chrono_literals;
1375
1376 Account const alice{"alice"};
1377 Account const bob{"bob"};
1378 Account const broker{"broker"};
1379
1380 Env env{*this, singleThreadIo(envconfig()), features};
1381 env.fund(XRP(10000), alice, bob, broker);
1382 BEAST_EXPECT(env.syncClose());
1383
1384 auto wsc = test::makeWSClient(env.app().config());
1385 json::Value stream;
1386 stream[jss::streams] = json::ValueType::Array;
1387 stream[jss::streams].append("transactions");
1388 auto jv = wsc->invoke("subscribe", stream);
1389
1390 // Verify `nftoken_id` value equals to the NFTokenID that was
1391 // changed in the most recent NFTokenMint or NFTokenAcceptOffer
1392 // transaction
1393 auto verifyNFTokenID = [&](uint256 const& actualNftID) {
1394 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
1395 uint256 nftID;
1396 BEAST_EXPECT(nftID.parseHex(jv[jss::meta][jss::nftoken_id].asString()));
1397 return nftID == actualNftID;
1398 }));
1399 };
1400
1401 // Verify `nftoken_ids` value equals to the NFTokenIDs that were
1402 // changed in the most recent NFTokenCancelOffer transaction
1403 auto verifyNFTokenIDsInCancelOffer = [&](std::vector<uint256> actualNftIDs) {
1404 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
1405 std::vector<uint256> metaIDs;
1406 std::transform(
1407 jv[jss::meta][jss::nftoken_ids].begin(),
1408 jv[jss::meta][jss::nftoken_ids].end(),
1409 std::back_inserter(metaIDs),
1410 [this](json::Value id) {
1411 uint256 nftID;
1412 BEAST_EXPECT(nftID.parseHex(id.asString()));
1413 return nftID;
1414 });
1415 // Sort both array to prepare for comparison
1416 std::ranges::sort(metaIDs);
1417 std::ranges::sort(actualNftIDs);
1418
1419 // Make sure the expect number of NFTs is correct
1420 BEAST_EXPECT(metaIDs.size() == actualNftIDs.size());
1421
1422 // Check the value of NFT ID in the meta with the
1423 // actual values
1424 for (size_t i = 0; i < metaIDs.size(); ++i)
1425 BEAST_EXPECT(metaIDs[i] == actualNftIDs[i]);
1426 return true;
1427 }));
1428 };
1429
1430 // Verify `offer_id` value equals to the offerID that was
1431 // changed in the most recent NFTokenCreateOffer tx
1432 auto verifyNFTokenOfferID = [&](uint256 const& offerID) {
1433 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
1434 uint256 metaOfferID;
1435 BEAST_EXPECT(metaOfferID.parseHex(jv[jss::meta][jss::offer_id].asString()));
1436 return metaOfferID == offerID;
1437 }));
1438 };
1439
1440 // Check new fields in tx meta when for all NFTtransactions
1441 {
1442 // Alice mints 2 NFTs
1443 // Verify the NFTokenIDs are correct in the NFTokenMint tx meta
1444 uint256 const nftId1{token::getNextID(env, alice, 0u, tfTransferable)};
1445 env(token::mint(alice, 0u), Txflags(tfTransferable));
1446 BEAST_EXPECT(env.syncClose());
1447 verifyNFTokenID(nftId1);
1448
1449 uint256 const nftId2{token::getNextID(env, alice, 0u, tfTransferable)};
1450 env(token::mint(alice, 0u), Txflags(tfTransferable));
1451 BEAST_EXPECT(env.syncClose());
1452 verifyNFTokenID(nftId2);
1453
1454 // Alice creates one sell offer for each NFT
1455 // Verify the offer indexes are correct in the NFTokenCreateOffer tx
1456 // meta
1457 uint256 const aliceOfferIndex1 =
1458 keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key;
1459 env(token::createOffer(alice, nftId1, drops(1)), Txflags(tfSellNFToken));
1460 BEAST_EXPECT(env.syncClose());
1461 verifyNFTokenOfferID(aliceOfferIndex1);
1462
1463 uint256 const aliceOfferIndex2 =
1464 keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key;
1465 env(token::createOffer(alice, nftId2, drops(1)), Txflags(tfSellNFToken));
1466 BEAST_EXPECT(env.syncClose());
1467 verifyNFTokenOfferID(aliceOfferIndex2);
1468
1469 // Alice cancels two offers she created
1470 // Verify the NFTokenIDs are correct in the NFTokenCancelOffer tx
1471 // meta
1472 env(token::cancelOffer(alice, {aliceOfferIndex1, aliceOfferIndex2}));
1473 BEAST_EXPECT(env.syncClose());
1474 verifyNFTokenIDsInCancelOffer({nftId1, nftId2});
1475
1476 // Bobs creates a buy offer for nftId1
1477 // Verify the offer id is correct in the NFTokenCreateOffer tx meta
1478 auto const bobBuyOfferIndex =
1480 env(token::createOffer(bob, nftId1, drops(1)), token::Owner(alice));
1481 BEAST_EXPECT(env.syncClose());
1482 verifyNFTokenOfferID(bobBuyOfferIndex);
1483
1484 // Alice accepts bob's buy offer
1485 // Verify the NFTokenID is correct in the NFTokenAcceptOffer tx meta
1486 env(token::acceptBuyOffer(alice, bobBuyOfferIndex));
1487 BEAST_EXPECT(env.syncClose());
1488 verifyNFTokenID(nftId1);
1489 }
1490
1491 // Check `nftoken_ids` in brokered mode
1492 {
1493 // Alice mints a NFT
1494 uint256 const nftId{token::getNextID(env, alice, 0u, tfTransferable)};
1495 env(token::mint(alice, 0u), Txflags(tfTransferable));
1496 BEAST_EXPECT(env.syncClose());
1497 verifyNFTokenID(nftId);
1498
1499 // Alice creates sell offer and set broker as destination
1500 uint256 const offerAliceToBroker =
1501 keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key;
1502 env(token::createOffer(alice, nftId, drops(1)),
1503 token::Destination(broker),
1504 Txflags(tfSellNFToken));
1505 BEAST_EXPECT(env.syncClose());
1506 verifyNFTokenOfferID(offerAliceToBroker);
1507
1508 // Bob creates buy offer
1509 uint256 const offerBobToBroker =
1511 env(token::createOffer(bob, nftId, drops(1)), token::Owner(alice));
1512 BEAST_EXPECT(env.syncClose());
1513 verifyNFTokenOfferID(offerBobToBroker);
1514
1515 // Check NFTokenID meta for NFTokenAcceptOffer in brokered mode
1516 env(token::brokerOffers(broker, offerBobToBroker, offerAliceToBroker));
1517 BEAST_EXPECT(env.syncClose());
1518 verifyNFTokenID(nftId);
1519 }
1520
1521 // Check if there are no duplicate nft id in Cancel transactions where
1522 // multiple offers are cancelled for the same NFT
1523 {
1524 // Alice mints a NFT
1525 uint256 const nftId{token::getNextID(env, alice, 0u, tfTransferable)};
1526 env(token::mint(alice, 0u), Txflags(tfTransferable));
1527 BEAST_EXPECT(env.syncClose());
1528 verifyNFTokenID(nftId);
1529
1530 // Alice creates 2 sell offers for the same NFT
1531 uint256 const aliceOfferIndex1 =
1532 keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key;
1533 env(token::createOffer(alice, nftId, drops(1)), Txflags(tfSellNFToken));
1534 BEAST_EXPECT(env.syncClose());
1535 verifyNFTokenOfferID(aliceOfferIndex1);
1536
1537 uint256 const aliceOfferIndex2 =
1538 keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key;
1539 env(token::createOffer(alice, nftId, drops(1)), Txflags(tfSellNFToken));
1540 BEAST_EXPECT(env.syncClose());
1541 verifyNFTokenOfferID(aliceOfferIndex2);
1542
1543 // Make sure the metadata only has 1 nft id, since both offers are
1544 // for the same nft
1545 env(token::cancelOffer(alice, {aliceOfferIndex1, aliceOfferIndex2}));
1546 BEAST_EXPECT(env.syncClose());
1547 verifyNFTokenIDsInCancelOffer({nftId});
1548 }
1549
1550 if (features[featureNFTokenMintOffer])
1551 {
1552 uint256 const aliceMintWithOfferIndex1 =
1553 keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key;
1554 env(token::mint(alice), token::Amount(XRP(0)));
1555 BEAST_EXPECT(env.syncClose());
1556 verifyNFTokenOfferID(aliceMintWithOfferIndex1);
1557 }
1558 }
1559
1560 // ----- Subscription limit / teardown verification ----------------------
1561 //
1562 // The helpers and tests below exercise:
1563 // * the per-connection subscription cap + proportional charge enforced
1564 // in doSubscribe (Subscribe.cpp), and
1565 // * the asynchronous, chunked teardown of a disconnecting connection's
1566 // account subscriptions (~InfoSub -> scheduleAccountCleanup -> JobQueue).
1567 //
1568 // The cap-exceeded error is rpcINVALID_PARAMS with the message "Too many
1569 // subscriptions for this connection."; the tests assert that exactly.
1570 //
1571 // There is no public accessor for the server-side per-connection count, so
1572 // the async cleanup is verified behaviorally: publishing still flows to a
1573 // live subscriber, rather than by reading a count to zero.
1574
1575 // Build `count` distinct, valid, base58-encoded account strings cheaply by
1576 // incrementing an AccountID. parseAccountIds dedups into a hash_set, so the
1577 // strings MUST be distinct for the cap arithmetic to be exact; incrementing
1578 // guarantees distinctness without deriving `count` keypairs.
1579 static std::vector<std::string>
1581 {
1583 out.reserve(count);
1584 // Start at `seed` so separate calls produce non-overlapping ranges,
1585 // letting a test subscribe disjoint batches across requests.
1586 AccountID id{static_cast<std::uint64_t>(seed)};
1587 for (std::size_t i = 0; i < count; ++i)
1588 {
1589 out.push_back(toBase58(id));
1590 ++id;
1591 }
1592 return out;
1593 }
1594
1595 // Append the given account strings as a jss::accounts array onto a fresh
1596 // subscribe request object.
1597 static json::Value
1599 {
1601 jv[jss::accounts] = json::ValueType::Array;
1602 for (auto const& a : accts)
1603 jv[jss::accounts].append(a);
1604 return jv;
1605 }
1606
1607 // Append the given account strings as a jss::accounts_proposed array onto a
1608 // fresh subscribe request object.
1609 static json::Value
1611 {
1613 jv[jss::accounts_proposed] = json::ValueType::Array;
1614 for (auto const& a : accts)
1615 jv[jss::accounts_proposed].append(a);
1616 return jv;
1617 }
1618
1619 // A single, valid XRP/USD order book request, as one entry of a
1620 // jss::books array.
1621 static json::Value
1623 {
1624 using namespace jtx;
1626 jv[jss::books] = json::ValueType::Array;
1627 json::Value& book = jv[jss::books][0u];
1628 book[jss::taker_gets] = json::ValueType::Object;
1629 book[jss::taker_gets][jss::currency] = "XRP";
1630 book[jss::taker_pays] = json::ValueType::Object;
1631 book[jss::taker_pays][jss::currency] = "USD";
1632 book[jss::taker_pays][jss::issuer] = Account("alice").human();
1633 return jv;
1634 }
1635
1636 // A single account_history_tx_stream subscribe request for `acct`.
1637 static json::Value
1639 {
1641 jv[jss::account_history_tx_stream] = json::ValueType::Object;
1642 jv[jss::account_history_tx_stream][jss::account] = acct;
1643 return jv;
1644 }
1645
1646 // An envconfig modifier that lowers the per-connection subscription cap to
1647 // `cap`, so the cap logic in doSubscribe can be driven without subscribing
1648 // the production default (100'000) entries. (Env is non-movable, so this
1649 // returns the config modifier rather than a ready-made Env.)
1650 static auto
1652 {
1653 return [cap](std::unique_ptr<Config> cfg) {
1654 cfg->maxSubscriptionsPerConnection = cap;
1655 return jtx::singleThreadIo(std::move(cfg));
1656 };
1657 }
1658
1659 void
1661 {
1662 // A request that alone exceeds the cap is rejected with the exact
1663 // cap error, before any state is recorded. Baseline negative path.
1664 testcase("subscription cap rejects an over-cap request");
1665
1666 using namespace jtx;
1667 Env env{*this, envconfig(cappedConfig(5))};
1668 auto wsc = makeWSClient(env.app().config());
1669
1670 // Six accounts against a cap of five: rejected.
1671 auto const jr =
1672 wsc->invoke("subscribe", accountsRequest(makeAccountStrings(6)))[jss::result];
1673 BEAST_EXPECT(jr[jss::error] == "invalidParams");
1674 BEAST_EXPECT(jr[jss::error_message] == "Too many subscriptions for this connection.");
1675 }
1676
1677 void
1679 {
1680 // Re-subscribing accounts already held by this connection adds no new
1681 // tracked state, so it must be admitted even at the cap. The cap check
1682 // must count only NET-NEW accounts, not the raw request size.
1683 testcase("re-subscribe at the cap is not over-counted");
1684
1685 using namespace jtx;
1686 Env env{*this, envconfig(cappedConfig(5))};
1687 auto wsc = makeWSClient(env.app().config());
1688
1689 // Fill the cap exactly with five distinct accounts.
1690 auto const five = makeAccountStrings(5);
1691 {
1692 auto const r = wsc->invoke("subscribe", accountsRequest(five));
1693 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1694 }
1695
1696 // Re-subscribe the same five: net-new is zero, so it stays within the
1697 // cap and must succeed. (Pre-fix this was wrongly rejected.)
1698 {
1699 auto const r = wsc->invoke("subscribe", accountsRequest(five));
1700 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1701 }
1702 }
1703
1704 void
1706 {
1707 // Book subscriptions are tracked separately (OrderBookDB) and are not
1708 // part of totalSubscriptionCount(). An account set at the cap must not
1709 // block an unrelated book subscription.
1710 testcase("books cap is independent of account count");
1711
1712 using namespace jtx;
1713 Env env{*this, envconfig(cappedConfig(5))};
1714 Account const alice{"alice"};
1715 env.fund(XRP(10000), alice);
1716 BEAST_EXPECT(env.syncClose());
1717
1718 auto wsc = makeWSClient(env.app().config());
1719
1720 // Fill the account cap exactly.
1721 {
1722 auto const r = wsc->invoke("subscribe", accountsRequest(makeAccountStrings(5)));
1723 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1724 }
1725
1726 // A single book subscription must still be admitted: it does not count
1727 // against the account cap. (Pre-fix this was wrongly rejected.)
1728 {
1729 auto const r = wsc->invoke("subscribe", oneBookRequest());
1730 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1731 }
1732 }
1733
1734 void
1736 {
1737 // A single request mixing fields must be all-or-nothing: if a later
1738 // field trips the cap, an earlier field must NOT have subscribed. The
1739 // leak is detected through the cap arithmetic itself - a follow-up
1740 // request succeeds only if no state leaked from the rejected one.
1741 testcase("multi-field subscribe does not partially subscribe");
1742
1743 using namespace jtx;
1744 Env env{*this, envconfig(cappedConfig(5))};
1745 auto wsc = makeWSClient(env.app().config());
1746
1747 // accounts_proposed (3, evaluated first, would subscribe) +
1748 // accounts (3): combined 6 exceeds the cap of 5, so the request is
1749 // rejected. The proposed branch must not have leaked its 3 entries.
1751 for (auto const& a : makeAccountStrings(3, 100))
1752 req[jss::accounts].append(a);
1753 {
1754 auto const jr = wsc->invoke("subscribe", req)[jss::result];
1755 BEAST_EXPECT(jr[jss::error] == "invalidParams");
1756 BEAST_EXPECT(jr[jss::error_message] == "Too many subscriptions for this connection.");
1757 }
1758
1759 // If the rejected request leaked its 3 proposed subscriptions, the
1760 // connection's count is already 3 and this 3-account request would be
1761 // rejected (3 + 3 > 5). With no leak the count is 0 and it succeeds.
1762 {
1763 auto const r = wsc->invoke("subscribe", accountsRequest(makeAccountStrings(3, 200)));
1764 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1765 }
1766 }
1767
1768 void
1770 {
1771 // An account_history_tx_stream subscribe is charged against the cap only
1772 // when it is net-new, matching the account branches. Re-subscribing an
1773 // account-history already held on this connection adds no tracked entry,
1774 // so it must NOT be rejected at the cap. The two rejection causes are
1775 // told apart by their exact error_message: the cap check yields "Too
1776 // many subscriptions for this connection."; a duplicate that gets past
1777 // the cap and is rejected downstream by subAccountHistory yields the
1778 // generic "Invalid parameters.".
1779 testcase("account_history re-subscribe at the cap is not over-counted");
1780
1781 using namespace jtx;
1782 Env env{*this, envconfig(cappedConfig(1))};
1783 Account const alice{"alice"};
1784 env.fund(XRP(10000), alice);
1785 BEAST_EXPECT(env.syncClose());
1786
1787 auto wsc = makeWSClient(env.app().config());
1788
1789 // First account-history subscribe is net-new: charge 1 fills the cap of
1790 // 1 exactly, so it is admitted. Positive path.
1791 {
1792 auto const r = wsc->invoke("subscribe", accountHistoryRequest(alice.human()));
1793 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1794 }
1795
1796 // Re-subscribe the same account-history while sitting exactly at the
1797 // cap. Net-new is zero, so the cap check must pass; the request is then
1798 // rejected by subAccountHistory as a duplicate, NOT by the cap. Proven
1799 // by the exact message: it is the duplicate error, not the cap error.
1800 // (Pre-fix, the flat charge of 1 made the cap check reject this with the
1801 // cap message instead.)
1802 {
1803 auto const jr =
1804 wsc->invoke("subscribe", accountHistoryRequest(alice.human()))[jss::result];
1805 BEAST_EXPECT(jr[jss::error] == "invalidParams");
1806 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
1807 BEAST_EXPECT(jr[jss::error_message] != "Too many subscriptions for this connection.");
1808 }
1809 }
1810
1811 void
1813 {
1814 // A genuinely net-new account-history subscribe on a connection already
1815 // at the cap IS rejected, with the cap error. Negative path, and the
1816 // counterpart to testHistoryReSubscribeNotOvercounted: it confirms the
1817 // net-new charge still rejects when the entry really is new.
1818 testcase("account_history net-new subscribe is rejected at the cap");
1819
1820 using namespace jtx;
1821 Env env{*this, envconfig(cappedConfig(1))};
1822 Account const alice{"alice"};
1823 Account const bob{"bob"};
1824 env.fund(XRP(10000), alice, bob);
1825 BEAST_EXPECT(env.syncClose());
1826
1827 auto wsc = makeWSClient(env.app().config());
1828
1829 // Fill the cap of 1 with alice's account-history.
1830 {
1831 auto const r = wsc->invoke("subscribe", accountHistoryRequest(alice.human()));
1832 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1833 }
1834
1835 // A different account-history (bob) is net-new: charge 1 over a cap of 1
1836 // already full, so it is rejected with the cap error.
1837 {
1838 auto const jr =
1839 wsc->invoke("subscribe", accountHistoryRequest(bob.human()))[jss::result];
1840 BEAST_EXPECT(jr[jss::error] == "invalidParams");
1841 BEAST_EXPECT(jr[jss::error_message] == "Too many subscriptions for this connection.");
1842 }
1843 }
1844
1845 void
1847 {
1848 // Test C (core regression): disconnecting a connection with many
1849 // account subscriptions must NOT block subsequent operations or
1850 // publishing. The teardown is now posted to a JobQueue job
1851 // (scheduleAccountCleanup), so it runs off the disconnect thread.
1852 testcase("async teardown does not stall publishing");
1853
1854 using namespace std::chrono_literals;
1855 using namespace jtx;
1856 Env env{*this, singleThreadIo(envconfig())};
1857
1858 Account const alice{"alice"};
1859 env.fund(XRP(10000), alice);
1860 BEAST_EXPECT(env.syncClose());
1861
1862 // A second, long-lived subscriber to alice that must keep receiving
1863 // publishes after the first connection disconnects.
1864 auto wscLive = makeWSClient(env.app().config());
1865 {
1867 jv[jss::accounts] = json::ValueType::Array;
1868 jv[jss::accounts].append(alice.human());
1869 auto const r = wscLive->invoke("subscribe", jv);
1870 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1871 }
1872
1873 // A connection that subscribes to many accounts, then disconnects. A
1874 // few thousand entries is enough to be a real teardown while still
1875 // running fast in CI.
1876 constexpr std::size_t kBulk = 3000;
1877 {
1878 auto wscBulk = makeWSClient(env.app().config());
1879 auto const r =
1880 wscBulk->invoke("subscribe", accountsRequest(makeAccountStrings(kBulk, 10)));
1881 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1882 // Destroying the client closes the WS connection, which destroys
1883 // the server-side InfoSub and posts the chunked async cleanup job.
1884 // WSClient exposes no explicit close(); resetting the owning
1885 // unique_ptr is the disconnect path.
1886 wscBulk.reset();
1887 }
1888
1889 // Immediately after the disconnect, an unrelated operation completes
1890 // promptly (it would block for seconds with inline teardown). This is a
1891 // cheap liveness check; the publish assertion below is the real proof.
1892 {
1893 auto const info = env.app().getOPs().getServerInfo(false, true, false);
1894 BEAST_EXPECT(info.isMember(jss::server_state));
1895 }
1896
1897 // The live subscriber still receives a published transaction for alice
1898 // within a short timeout, proving account-publishing was not stalled by
1899 // the concurrent teardown.
1900 {
1901 env(pay(env.master, alice, XRP(100)));
1902 BEAST_EXPECT(env.syncClose());
1903 BEAST_EXPECT(wscLive->findMsg(5s, [&](auto const& jv) {
1904 return jv.isMember(jss::transaction) &&
1905 jv[jss::transaction][jss::TransactionType] == jss::Payment &&
1906 jv[jss::transaction][jss::Destination] == alice.human();
1907 }));
1908 }
1909
1910 wscLive->invoke("unsubscribe", accountsRequest({alice.human()}));
1911 }
1912
1913 void
1915 {
1916 // Test D (Phase 3 correctness): connection A subscribes to account X
1917 // and disconnects (async cleanup pending, keyed on A's seq). A new
1918 // connection B subscribes to X and MUST still receive publishes for X -
1919 // A's deferred, seq-keyed cleanup must not remove B's subscription.
1920 testcase("re-subscribe after disconnect still delivers");
1921
1922 using namespace std::chrono_literals;
1923 using namespace jtx;
1924 Env env{*this, singleThreadIo(envconfig())};
1925
1926 Account const alice{"alice"};
1927 env.fund(XRP(10000), alice);
1928 BEAST_EXPECT(env.syncClose());
1929
1930 // Connection A subscribes to alice, then disconnects. A also subscribes
1931 // to a bulk set so its deferred cleanup is non-trivial and races with B.
1932 {
1933 auto wscA = makeWSClient(env.app().config());
1934 auto bulk = makeAccountStrings(2000, 10);
1935 bulk.push_back(alice.human());
1936 auto const r = wscA->invoke("subscribe", accountsRequest(bulk));
1937 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1938 // Disconnect A by destroying its client (no explicit close()).
1939 wscA.reset();
1940 }
1941
1942 // Connection B (a new InfoSub with a distinct seq) subscribes to alice.
1943 auto wscB = makeWSClient(env.app().config());
1944 {
1946 jv[jss::accounts] = json::ValueType::Array;
1947 jv[jss::accounts].append(alice.human());
1948 auto const r = wscB->invoke("subscribe", jv);
1949 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1950 }
1951
1952 // A publish for alice must reach B. If A's seq-keyed cleanup had wrongly
1953 // removed the shared alice entry, B would receive nothing.
1954 {
1955 env(pay(env.master, alice, XRP(100)));
1956 BEAST_EXPECT(env.syncClose());
1957 BEAST_EXPECT(wscB->findMsg(5s, [&](auto const& jv) {
1958 return jv.isMember(jss::transaction) &&
1959 jv[jss::transaction][jss::TransactionType] == jss::Payment &&
1960 jv[jss::transaction][jss::Destination] == alice.human();
1961 }));
1962 }
1963
1964 wscB->invoke("unsubscribe", accountsRequest({alice.human()}));
1965 }
1966
1967 void
1968 run() override
1969 {
1970 using namespace test::jtx;
1971 FeatureBitset const all{testableAmendments()};
1972 FeatureBitset const xrpFees{featureXRPFees};
1973
1974 testServer();
1975 testLedger();
1978 testManifests();
1979 testValidations(all - xrpFees);
1980 testValidations(all);
1981 testSubErrors(true);
1982 testSubErrors(false);
1983 testSubByUrl();
1986 testNFToken(all);
1987 testNFToken(all - featureNFTokenMintOffer);
1996 }
1997};
1998
2000
2001} // namespace xrpl::test
A testsuite class.
Definition suite.h:52
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Represents a JSON value.
Definition json_value.h:117
Value & append(Value const &value)
Append value to array at the end.
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 json::Value getServerInfo(bool human, bool admin, bool counters)=0
virtual void reportFeeChange()=0
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
virtual NetworkOPs & getOPs()=0
virtual LoadManager & getLoadManager()=0
virtual NetworkIDService & getNetworkIDService()=0
virtual LoadFeeTrack & getFeeTrack()=0
static json::Value oneBookRequest()
void run() override
Runs the suite.
static std::vector< std::string > makeAccountStrings(std::size_t count, std::uint32_t seed=1)
static json::Value accountsRequest(std::vector< std::string > const &accts)
void testValidations(FeatureBitset features)
static json::Value accountHistoryRequest(std::string const &acct)
static auto cappedConfig(std::size_t cap)
void testNFToken(FeatureBitset features)
static json::Value accountsProposedRequest(std::vector< std::string > const &accts)
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:21
std::string const & human() const
Returns the human readable public key.
A transaction testing environment.
Definition Env.h:161
Application & app()
Definition Env.h:300
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:479
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:323
Account const & master
Definition Env.h:165
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:1056
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:354
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:377
Set the fee on a JTx.
Definition fee.h:20
Add a path.
Definition paths.h:47
Sets the SendMax on a JTx.
Definition sendmax.h:16
Set the regular signature on a JTx.
Definition sig.h:19
Set the flags on a JTx.
Definition txflags.h:14
T clear(T... args)
T emplace(T... args)
T end(T... args)
T find(T... args)
@ UInt
unsigned integer value
Definition json_value.h:24
@ Int
signed integer value
Definition json_value.h:23
@ Boolean
bool value
Definition json_value.h:27
@ Array
array value (ordered list)
Definition json_value.h:28
@ Object
object value (collection of name/value pairs).
Definition json_value.h:29
@ Real
double value
Definition json_value.h:25
@ Null
'null' value
Definition json_value.h:22
Keylet nftokenOffer(AccountID const &owner, SeqProxy const &seq)
An offer from an account to buy or sell an NFT.
Definition Indexes.cpp:423
API version numbers used in later API versions.
Definition ApiVersion.h:36
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:92
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:37
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:371
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:59
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
Definition AccountID.cpp:95
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:651
constexpr std::uint32_t kVfFullValidation
constexpr std::uint32_t kVfFullyCanonicalSig
std::unordered_map< Key, Value, Hash, Pred, Allocator > hash_map
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
BaseUInt< 256 > uint256
Definition base_uint.h:580
T push_back(T... args)
T reserve(T... args)
T sort(T... args)
uint256 key
Definition Keylet.h:21
static constexpr auto kValidationSeed
Definition Constants.h:67
Set the sequence number on a JTx.
Definition seq.h:16
T to_string(T... args)