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/batch.h>
5#include <test/jtx/domain.h>
6#include <test/jtx/envconfig.h>
7#include <test/jtx/escrow.h>
8#include <test/jtx/fee.h>
9#include <test/jtx/mpt.h>
10#include <test/jtx/offer.h>
11#include <test/jtx/owners.h> // IWYU pragma: keep
12#include <test/jtx/paths.h>
13#include <test/jtx/pay.h>
14#include <test/jtx/permissioned_dex.h>
15#include <test/jtx/sendmax.h>
16#include <test/jtx/seq.h>
17#include <test/jtx/sig.h>
18#include <test/jtx/tags.h>
19#include <test/jtx/token.h>
20#include <test/jtx/txflags.h>
21
22#include <xrpld/app/main/LoadManager.h>
23#include <xrpld/core/Config.h>
24
25#include <xrpl/basics/UnorderedContainers.h>
26#include <xrpl/basics/base_uint.h>
27#include <xrpl/basics/strHex.h>
28#include <xrpl/beast/unit_test/suite.h>
29#include <xrpl/config/Constants.h>
30#include <xrpl/core/NetworkIDService.h>
31#include <xrpl/json/json_value.h>
32#include <xrpl/json/to_string.h>
33#include <xrpl/protocol/AccountID.h>
34#include <xrpl/protocol/Feature.h>
35#include <xrpl/protocol/Indexes.h>
36#include <xrpl/protocol/KeyType.h>
37#include <xrpl/protocol/PublicKey.h>
38#include <xrpl/protocol/SField.h>
39#include <xrpl/protocol/STValidation.h>
40#include <xrpl/protocol/SecretKey.h>
41#include <xrpl/protocol/Seed.h>
42#include <xrpl/protocol/SeqProxy.h>
43#include <xrpl/protocol/TxFlags.h>
44#include <xrpl/protocol/UintTypes.h>
45#include <xrpl/protocol/XRPAmount.h>
46#include <xrpl/protocol/jss.h>
47#include <xrpl/protocol/tokens.h>
48#include <xrpl/server/LoadFeeTrack.h>
49#include <xrpl/server/NetworkOPs.h>
50
51#include <algorithm>
52#include <array>
53#include <chrono>
54#include <cstddef>
55#include <cstdint>
56#include <initializer_list>
57#include <iterator>
58#include <memory>
59#include <optional>
60#include <string>
61#include <tuple>
62#include <utility>
63#include <vector>
64
65namespace xrpl::test {
66
68{
69public:
70 void
72 {
73 using namespace std::chrono_literals;
74 using namespace jtx;
75 Env env{*this, singleThreadIo(envconfig())};
76 auto wsc = makeWSClient(env.app().config());
77 json::Value stream;
78
79 {
80 // RPC subscribe to server stream
81 stream[jss::streams] = json::ValueType::Array;
82 stream[jss::streams].append("server");
83 auto jv = wsc->invoke("subscribe", stream);
84 if (wsc->version() == 2)
85 {
86 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
87 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
88 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
89 }
90 BEAST_EXPECT(jv[jss::status] == "success");
91 }
92
93 // here we forcibly stop the load manager because it can (rarely but
94 // every-so-often) cause fees to raise or lower AFTER we've called the
95 // first findMsg but BEFORE we unsubscribe, thus causing the final
96 // findMsg check to fail since there is one unprocessed ws msg created
97 // by the loadmanager
98 env.app().getLoadManager().stop();
99 {
100 // Raise fee to cause an update
101 auto& feeTrack = env.app().getFeeTrack();
102 for (int i = 0; i < 5; ++i)
103 feeTrack.raiseLocalFee();
104 env.app().getOPs().reportFeeChange();
105
106 // Check stream update
107 BEAST_EXPECT(
108 wsc->findMsg(5s, [&](auto const& jv) { return jv[jss::type] == "serverStatus"; }));
109 }
110
111 {
112 // RPC unsubscribe
113 auto jv = wsc->invoke("unsubscribe", stream);
114 if (wsc->version() == 2)
115 {
116 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
117 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
118 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
119 }
120 BEAST_EXPECT(jv[jss::status] == "success");
121 }
122
123 {
124 // Raise fee to cause an update
125 auto& feeTrack = env.app().getFeeTrack();
126 for (int i = 0; i < 5; ++i)
127 feeTrack.raiseLocalFee();
128 env.app().getOPs().reportFeeChange();
129
130 // Check stream update
131 auto jvo = wsc->getMsg(10ms);
132 BEAST_EXPECTS(!jvo, "getMsg: " + to_string(jvo.value()));
133 }
134 }
135
136 void
138 {
139 using namespace std::chrono_literals;
140 using namespace jtx;
141 Env env{*this, singleThreadIo(envconfig())};
142 auto wsc = makeWSClient(env.app().config());
143 json::Value stream;
144
145 {
146 // RPC subscribe to ledger stream
147 stream[jss::streams] = json::ValueType::Array;
148 stream[jss::streams].append("ledger");
149 auto jv = wsc->invoke("subscribe", stream);
150 if (wsc->version() == 2)
151 {
152 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
153 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
154 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
155 }
156 BEAST_EXPECT(jv[jss::result][jss::ledger_index] == 2);
157 BEAST_EXPECT(
158 jv[jss::result][jss::network_id] == env.app().getNetworkIDService().getNetworkID());
159 }
160
161 {
162 // Accept a ledger
163 BEAST_EXPECT(env.syncClose());
164
165 // Check stream update
166 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
167 return jv[jss::ledger_index] == 3 &&
168 jv[jss::network_id] == env.app().getNetworkIDService().getNetworkID();
169 }));
170 }
171
172 {
173 // Accept another ledger
174 BEAST_EXPECT(env.syncClose());
175
176 // Check stream update
177 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
178 return jv[jss::ledger_index] == 4 &&
179 jv[jss::network_id] == env.app().getNetworkIDService().getNetworkID();
180 }));
181 }
182
183 // RPC unsubscribe
184 auto jv = wsc->invoke("unsubscribe", stream);
185 if (wsc->version() == 2)
186 {
187 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
188 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
189 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
190 }
191 BEAST_EXPECT(jv[jss::status] == "success");
192 }
193
194 void
196 {
197 using namespace std::chrono_literals;
198 using namespace jtx;
199 Env env(*this, singleThreadIo(envconfig()));
200 auto baseFee = env.current()->fees().base.drops();
201 auto wsc = makeWSClient(env.app().config());
202 json::Value stream;
203
204 {
205 // RPC subscribe to transactions stream
206 stream[jss::streams] = json::ValueType::Array;
207 stream[jss::streams].append("transactions");
208 auto jv = wsc->invoke("subscribe", stream);
209 if (wsc->version() == 2)
210 {
211 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
212 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
213 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
214 }
215 BEAST_EXPECT(jv[jss::status] == "success");
216 }
217
218 {
219 env.fund(XRP(10000), "alice");
220 BEAST_EXPECT(env.syncClose());
221
222 // Check stream update for payment transaction
223 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
224 return jv[jss::meta]["AffectedNodes"][1u]["CreatedNode"]["NewFields"]
225 [jss::Account] == Account("alice").human() &&
226 jv[jss::transaction][jss::TransactionType] == jss::Payment &&
227 jv[jss::transaction][jss::DeliverMax] ==
228 std::to_string(10000000000 + baseFee) &&
229 jv[jss::transaction][jss::Fee] == std::to_string(baseFee) &&
230 jv[jss::transaction][jss::Sequence] == 1;
231 }));
232
233 // Check stream update for accountset transaction
234 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
235 return jv[jss::meta]["AffectedNodes"][0u]["ModifiedNode"]["FinalFields"]
236 [jss::Account] == Account("alice").human();
237 }));
238
239 env.fund(XRP(10000), "bob");
240 BEAST_EXPECT(env.syncClose());
241
242 // Check stream update for payment transaction
243 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
244 return jv[jss::meta]["AffectedNodes"][1u]["CreatedNode"]["NewFields"]
245 [jss::Account] //
246 == Account("bob").human() &&
247 jv[jss::transaction][jss::TransactionType] //
248 == jss::Payment &&
249 jv[jss::transaction][jss::DeliverMax] //
250 == std::to_string(10000000000 + baseFee) &&
251 jv[jss::transaction][jss::Fee] //
252 == std::to_string(baseFee) &&
253 jv[jss::transaction][jss::Sequence] //
254 == 2;
255 }));
256
257 // Check stream update for accountset transaction
258 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
259 return jv[jss::meta]["AffectedNodes"][0u]["ModifiedNode"]["FinalFields"]
260 [jss::Account] == Account("bob").human();
261 }));
262 }
263
264 {
265 // RPC unsubscribe
266 auto jv = wsc->invoke("unsubscribe", stream);
267 if (wsc->version() == 2)
268 {
269 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
270 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
271 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
272 }
273 BEAST_EXPECT(jv[jss::status] == "success");
274 }
275
276 {
277 // RPC subscribe to accounts stream
279 stream[jss::accounts] = json::ValueType::Array;
280 stream[jss::accounts].append(Account("alice").human());
281 auto jv = wsc->invoke("subscribe", stream);
282 if (wsc->version() == 2)
283 {
284 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
285 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
286 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
287 }
288 BEAST_EXPECT(jv[jss::status] == "success");
289 }
290
291 {
292 // Transaction that does not affect stream
293 env.fund(XRP(10000), "carol");
294 BEAST_EXPECT(env.syncClose());
295 BEAST_EXPECT(!wsc->getMsg(10ms));
296
297 // Transactions concerning alice
298 env.trust(Account("bob")["USD"](100), "alice");
299 BEAST_EXPECT(env.syncClose());
300
301 // Check stream updates
302 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
303 return jv[jss::meta]["AffectedNodes"][1u]["ModifiedNode"]["FinalFields"]
304 [jss::Account] == Account("alice").human();
305 }));
306
307 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
308 return jv[jss::meta]["AffectedNodes"][1u]["CreatedNode"]["NewFields"]["LowLimit"]
309 [jss::issuer] == Account("alice").human();
310 }));
311 }
312
313 // RPC unsubscribe
314 auto jv = wsc->invoke("unsubscribe", stream);
315 if (wsc->version() == 2)
316 {
317 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
318 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
319 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
320 }
321 BEAST_EXPECT(jv[jss::status] == "success");
322 }
323
324 void
326 {
327 testcase("transactions API version 2");
328
329 using namespace std::chrono_literals;
330 using namespace jtx;
331 Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
332 cfg->fees.referenceFee = 10;
333 cfg = singleThreadIo(std::move(cfg));
334 return cfg;
335 }));
336 auto wsc = makeWSClient(env.app().config());
338
339 {
340 // RPC subscribe to transactions stream
341 stream[jss::api_version] = 2;
342 stream[jss::streams] = json::ValueType::Array;
343 stream[jss::streams].append("transactions");
344 auto jv = wsc->invoke("subscribe", stream);
345 if (wsc->version() == 2)
346 {
347 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
348 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
349 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
350 }
351 BEAST_EXPECT(jv[jss::status] == "success");
352 }
353
354 {
355 env.fund(XRP(10000), "alice");
356 BEAST_EXPECT(env.syncClose());
357
358 // Check stream update for payment transaction
359 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
360 return jv[jss::meta]["AffectedNodes"][1u]["CreatedNode"]["NewFields"]
361 [jss::Account] //
362 == Account("alice").human() &&
363 jv[jss::close_time_iso] //
364 == "2000-01-01T00:00:10Z" &&
365 jv[jss::validated] == true && //
366 jv[jss::ledger_hash] ==
367 "0F1A9E0C109ADEF6DA2BDE19217C12BBEC57174CBDBD212B0EBDC1CEDB"
368 "853185" && //
369 !jv[jss::inLedger] &&
370 jv[jss::ledger_index] == 3 && //
371 jv[jss::tx_json][jss::TransactionType] //
372 == jss::Payment &&
373 jv[jss::tx_json][jss::DeliverMax] //
374 == "10000000010" &&
375 !jv[jss::tx_json].isMember(jss::Amount) &&
376 jv[jss::tx_json][jss::Fee] //
377 == "10" &&
378 jv[jss::tx_json][jss::Sequence] //
379 == 1;
380 }));
381
382 // Check stream update for accountset transaction
383 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
384 return jv[jss::meta]["AffectedNodes"][0u]["ModifiedNode"]["FinalFields"]
385 [jss::Account] == Account("alice").human();
386 }));
387 }
388
389 {
390 // RPC unsubscribe
391 auto jv = wsc->invoke("unsubscribe", stream);
392 if (wsc->version() == 2)
393 {
394 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
395 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
396 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
397 }
398 BEAST_EXPECT(jv[jss::status] == "success");
399 }
400 }
401
402 void
404 {
405 using namespace jtx;
406 Env env(*this, singleThreadIo(envconfig()));
407 auto wsc = makeWSClient(env.app().config());
408 json::Value stream;
409
410 {
411 // RPC subscribe to manifests stream
412 stream[jss::streams] = json::ValueType::Array;
413 stream[jss::streams].append("manifests");
414 auto jv = wsc->invoke("subscribe", stream);
415 if (wsc->version() == 2)
416 {
417 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
418 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
419 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
420 }
421 BEAST_EXPECT(jv[jss::status] == "success");
422 }
423
424 // RPC unsubscribe
425 auto jv = wsc->invoke("unsubscribe", stream);
426 if (wsc->version() == 2)
427 {
428 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
429 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
430 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
431 }
432 BEAST_EXPECT(jv[jss::status] == "success");
433 }
434
435 void
437 {
438 using namespace jtx;
439
440 Env env{*this, singleThreadIo(envconfig(validator, "")), features};
441 auto& cfg = env.app().config();
442 if (!BEAST_EXPECT(cfg.section(Sections::kValidationSeed).empty()))
443 return;
444 auto const parsedseed =
445 parseBase58<Seed>(cfg.section(Sections::kValidationSeed).values()[0]);
446 if (BEAST_EXPECT(parsedseed); not parsedseed.has_value())
447 return;
448
449 std::string const valPublicKey = toBase58(
453
454 auto wsc = makeWSClient(env.app().config());
455 json::Value stream;
456
457 {
458 // RPC subscribe to validations stream
459 stream[jss::streams] = json::ValueType::Array;
460 stream[jss::streams].append("validations");
461 auto jv = wsc->invoke("subscribe", stream);
462 if (wsc->version() == 2)
463 {
464 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
465 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
466 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
467 }
468 BEAST_EXPECT(jv[jss::status] == "success");
469 }
470
471 {
472 // Lambda to check ledger validations from the stream.
473 auto validValidationFields = [&env, &valPublicKey](json::Value const& jv) {
474 if (jv[jss::type] != "validationReceived")
475 return false;
476
477 if (jv[jss::validation_public_key].asString() != valPublicKey)
478 return false;
479
480 if (jv[jss::ledger_hash] != to_string(env.closed()->header().hash))
481 return false;
482
483 if (jv[jss::ledger_index] != std::to_string(env.closed()->header().seq))
484 return false;
485
486 if (jv[jss::flags] != (kVfFullyCanonicalSig | kVfFullValidation))
487 return false;
488
489 if (jv[jss::full] != true)
490 return false;
491
492 if (jv.isMember(jss::load_fee))
493 return false;
494
495 if (!jv.isMember(jss::signature))
496 return false;
497
498 if (!jv.isMember(jss::signing_time))
499 return false;
500
501 if (!jv.isMember(jss::cookie))
502 return false;
503
504 if (!jv.isMember(jss::validated_hash))
505 return false;
506
507 uint32_t const netID = env.app().getNetworkIDService().getNetworkID();
508 if (!jv.isMember(jss::network_id) || jv[jss::network_id] != netID)
509 return false;
510
511 // Certain fields are only added on a flag ledger.
512 bool const isFlagLedger = (env.closed()->header().seq + 1) % 256 == 0;
513
514 if (jv.isMember(jss::server_version) != isFlagLedger)
515 return false;
516
517 if (jv.isMember(jss::reserve_base) != isFlagLedger)
518 return false;
519
520 if (jv.isMember(jss::reserve_inc) != isFlagLedger)
521 return false;
522
523 return true;
524 };
525
526 // Check stream update. Look at enough stream entries so we see
527 // at least one flag ledger.
528 while (env.closed()->header().seq < 300)
529 {
530 BEAST_EXPECT(env.syncClose());
531 using namespace std::chrono_literals;
532 BEAST_EXPECT(wsc->findMsg(5s, validValidationFields));
533 }
534 }
535
536 // RPC unsubscribe
537 auto jv = wsc->invoke("unsubscribe", stream);
538 if (wsc->version() == 2)
539 {
540 BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
541 BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
542 BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
543 }
544 BEAST_EXPECT(jv[jss::status] == "success");
545 }
546
547 void
549 {
550 using namespace jtx;
551 testcase("Subscribe by url");
552 Env env{*this, singleThreadIo(envconfig())};
553
554 json::Value jv;
555 jv[jss::url] = "http://localhost/events";
556 jv[jss::url_username] = "admin";
557 jv[jss::url_password] = "password";
558 jv[jss::streams] = json::ValueType::Array;
559 jv[jss::streams][0u] = "validations";
560 auto jr = env.rpc("json", "subscribe", to_string(jv))[jss::result];
561 BEAST_EXPECT(jr[jss::status] == "success");
562
563 jv[jss::streams][0u] = "ledger";
564 jr = env.rpc("json", "subscribe", to_string(jv))[jss::result];
565 BEAST_EXPECT(jr[jss::status] == "success");
566 BEAST_EXPECT(jr[jss::network_id] == env.app().getNetworkIDService().getNetworkID());
567
568 jr = env.rpc("json", "unsubscribe", to_string(jv))[jss::result];
569 BEAST_EXPECT(jr[jss::status] == "success");
570
571 jv[jss::streams][0u] = "validations";
572 jr = env.rpc("json", "unsubscribe", to_string(jv))[jss::result];
573 BEAST_EXPECT(jr[jss::status] == "success");
574 }
575
576 void
577 testSubErrors(bool subscribe)
578 {
579 using namespace jtx;
580 auto const method = subscribe ? "subscribe" : "unsubscribe";
581 testcase << "Error cases for " << method;
582
583 Env env{*this, singleThreadIo(envconfig())};
584 auto wsc = makeWSClient(env.app().config());
585
586 {
587 auto const jr = env.rpc("json", method, "{}")[jss::result];
588 BEAST_EXPECT(jr[jss::error] == "invalidParams");
589 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
590 }
591
592 {
593 json::Value jv;
594 jv[jss::url] = "not-a-url";
595 jv[jss::username] = "admin";
596 jv[jss::password] = "password";
597 auto const jr = env.rpc("json", method, to_string(jv))[jss::result];
598 if (subscribe)
599 {
600 BEAST_EXPECT(jr[jss::error] == "invalidParams");
601 BEAST_EXPECT(jr[jss::error_message] == "Failed to parse url.");
602 }
603 // else TODO: why isn't this an error for unsubscribe ?
604 // (findRpcSub returns null)
605 }
606
607 {
608 json::Value jv;
609 jv[jss::url] = "ftp://scheme.not.supported.tld";
610 auto const jr = env.rpc("json", method, to_string(jv))[jss::result];
611 if (subscribe)
612 {
613 BEAST_EXPECT(jr[jss::error] == "invalidParams");
614 BEAST_EXPECT(jr[jss::error_message] == "Only http and https is supported.");
615 }
616 }
617
618 {
619 Env envNonadmin{*this, singleThreadIo(noAdmin(envconfig()))};
620 json::Value jv;
621 jv[jss::url] = "no-url";
622 auto const jr = envNonadmin.rpc("json", method, to_string(jv))[jss::result];
623 BEAST_EXPECT(jr[jss::error] == "noPermission");
624 BEAST_EXPECT(jr[jss::error_message] == "You don't have permission for this command.");
625 }
626
632 "",
635
636 for (auto const& f : {jss::accounts_proposed, jss::accounts})
637 {
638 for (auto const& nonArray : nonArrays)
639 {
640 json::Value jv;
641 jv[f] = nonArray;
642 auto const jr = wsc->invoke(method, jv)[jss::result];
643 BEAST_EXPECT(jr[jss::error] == "invalidParams");
644 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
645 }
646
647 {
648 json::Value jv;
650 auto const jr = wsc->invoke(method, jv)[jss::result];
651 BEAST_EXPECT(jr[jss::error] == "actMalformed");
652 BEAST_EXPECT(jr[jss::error_message] == "Account malformed.");
653 }
654 }
655
656 for (auto const& nonArray : nonArrays)
657 {
658 json::Value jv;
659 jv[jss::mpt_issuances] = nonArray;
660 auto jr = wsc->invoke(method, jv)[jss::result];
661 BEAST_EXPECT(jr[jss::error] == "invalidParams");
662 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
663 }
664
665 {
666 json::Value jv;
667 jv[jss::mpt_issuances] = json::ValueType::Array;
668 jv[jss::mpt_issuances][0u] = 1;
669 auto jr = wsc->invoke(method, jv)[jss::result];
670 BEAST_EXPECT(jr[jss::error] == "invalidParams");
671 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
672 }
673
674 {
675 json::Value jv;
676 jv[jss::mpt_issuances] = json::ValueType::Array;
677 jv[jss::mpt_issuances][0u] = "not-an-mpt-issuance-id";
678 auto jr = wsc->invoke(method, jv)[jss::result];
679 BEAST_EXPECT(jr[jss::error] == "invalidParams");
680 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
681 }
682
683 {
684 json::Value jv;
685 jv[jss::mpt_issuances] = json::ValueType::Array;
686 jv[jss::mpt_issuances][0u] = "0123456789ABCDEF";
687 auto jr = wsc->invoke(method, jv)[jss::result];
688 BEAST_EXPECT(jr[jss::error] == "invalidParams");
689 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
690 }
691
692 for (auto const& nonArray : nonArrays)
693 {
694 json::Value jv;
695 jv[jss::books] = nonArray;
696 auto const jr = wsc->invoke(method, jv)[jss::result];
697 BEAST_EXPECT(jr[jss::error] == "invalidParams");
698 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
699 }
700
701 {
702 json::Value jv;
703 jv[jss::books] = json::ValueType::Array;
704 jv[jss::books][0u] = 1;
705 auto const jr = wsc->invoke(method, jv)[jss::result];
706 BEAST_EXPECT(jr[jss::error] == "invalidParams");
707 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
708 }
709
710 {
711 json::Value jv;
712 jv[jss::books] = json::ValueType::Array;
713 jv[jss::books][0u] = json::ValueType::Object;
714 jv[jss::books][0u][jss::taker_gets] = json::ValueType::Object;
715 jv[jss::books][0u][jss::taker_pays] = json::ValueType::Object;
716 auto const jr = wsc->invoke(method, jv)[jss::result];
717
718 BEAST_EXPECT(jr[jss::error] == "srcCurMalformed");
719 BEAST_EXPECT(jr[jss::error_message] == "Source currency is malformed.");
720 }
721
722 {
723 json::Value jv;
724 jv[jss::books] = json::ValueType::Array;
725 jv[jss::books][0u] = json::ValueType::Object;
726 jv[jss::books][0u][jss::taker_gets] = json::ValueType::Object;
727 jv[jss::books][0u][jss::taker_pays] = json::ValueType::Object;
728 jv[jss::books][0u][jss::taker_pays][jss::currency] = "ZZZZ";
729 auto const jr = wsc->invoke(method, jv)[jss::result];
730 BEAST_EXPECT(jr[jss::error] == "srcCurMalformed");
731 BEAST_EXPECT(jr[jss::error_message] == "Source currency is malformed.");
732 }
733
734 {
735 json::Value jv;
736 jv[jss::books] = json::ValueType::Array;
737 jv[jss::books][0u] = json::ValueType::Object;
738 jv[jss::books][0u][jss::taker_gets] = json::ValueType::Object;
739 jv[jss::books][0u][jss::taker_pays] = json::ValueType::Object;
740 jv[jss::books][0u][jss::taker_pays][jss::currency] = "USD";
741 jv[jss::books][0u][jss::taker_pays][jss::issuer] = 1;
742 auto const jr = wsc->invoke(method, jv)[jss::result];
743 BEAST_EXPECT(jr[jss::error] == "srcIsrMalformed");
744 BEAST_EXPECT(jr[jss::error_message] == "Source 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_gets] = json::ValueType::Object;
752 jv[jss::books][0u][jss::taker_pays] = json::ValueType::Object;
753 jv[jss::books][0u][jss::taker_pays][jss::currency] = "USD";
754 jv[jss::books][0u][jss::taker_pays][jss::issuer] = Account{"gateway"}.human() + "DEAD";
755 auto const jr = wsc->invoke(method, jv)[jss::result];
756 BEAST_EXPECT(jr[jss::error] == "srcIsrMalformed");
757 BEAST_EXPECT(jr[jss::error_message] == "Source 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] = json::ValueType::Object;
767 auto const jr = wsc->invoke(method, jv)[jss::result];
768 // NOTE: this error is slightly incongruous with the equivalent source currency error
769 BEAST_EXPECT(jr[jss::error] == "dstAmtMalformed");
770 BEAST_EXPECT(
771 jr[jss::error_message] == "Destination amount/currency/issuer is malformed.");
772 }
773
774 {
775 json::Value jv;
776 jv[jss::books] = json::ValueType::Array;
777 jv[jss::books][0u] = json::ValueType::Object;
778 jv[jss::books][0u][jss::taker_pays] =
779 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::Values::IncludeDate);
780 jv[jss::books][0u][jss::taker_gets][jss::currency] = "ZZZZ";
781 auto const jr = wsc->invoke(method, jv)[jss::result];
782 // NOTE: this error is slightly incongruous with the
783 // equivalent source currency error
784 BEAST_EXPECT(jr[jss::error] == "dstAmtMalformed");
785 BEAST_EXPECT(
786 jr[jss::error_message] == "Destination amount/currency/issuer is malformed.");
787 }
788
789 {
790 json::Value jv;
791 jv[jss::books] = json::ValueType::Array;
792 jv[jss::books][0u] = json::ValueType::Object;
793 jv[jss::books][0u][jss::taker_pays] =
794 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::Values::IncludeDate);
795 jv[jss::books][0u][jss::taker_gets][jss::currency] = "USD";
796 jv[jss::books][0u][jss::taker_gets][jss::issuer] = 1;
797 auto const jr = wsc->invoke(method, jv)[jss::result];
798 BEAST_EXPECT(jr[jss::error] == "dstIsrMalformed");
799 BEAST_EXPECT(jr[jss::error_message] == "Destination issuer is malformed.");
800 }
801
802 {
803 json::Value jv;
804 jv[jss::books] = json::ValueType::Array;
805 jv[jss::books][0u] = json::ValueType::Object;
806 jv[jss::books][0u][jss::taker_pays] =
807 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::Values::IncludeDate);
808 jv[jss::books][0u][jss::taker_gets][jss::currency] = "USD";
809 jv[jss::books][0u][jss::taker_gets][jss::issuer] = Account{"gateway"}.human() + "DEAD";
810 auto const jr = wsc->invoke(method, jv)[jss::result];
811 BEAST_EXPECT(jr[jss::error] == "dstIsrMalformed");
812 BEAST_EXPECT(jr[jss::error_message] == "Destination issuer is malformed.");
813 }
814
815 {
816 json::Value jv;
817 jv[jss::books] = json::ValueType::Array;
818 jv[jss::books][0u] = json::ValueType::Object;
819 jv[jss::books][0u][jss::taker_pays] =
820 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::Values::IncludeDate);
821 jv[jss::books][0u][jss::taker_gets] =
822 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::Values::IncludeDate);
823 auto const jr = wsc->invoke(method, jv)[jss::result];
824 BEAST_EXPECT(jr[jss::error] == "badMarket");
825 BEAST_EXPECT(jr[jss::error_message] == "No such market.");
826 }
827
828 for (auto const& nonArray : nonArrays)
829 {
830 json::Value jv;
831 jv[jss::streams] = nonArray;
832 auto const jr = wsc->invoke(method, jv)[jss::result];
833 BEAST_EXPECT(jr[jss::error] == "invalidParams");
834 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
835 }
836
837 {
838 json::Value jv;
839 jv[jss::streams] = json::ValueType::Array;
840 jv[jss::streams][0u] = 1;
841 auto const jr = wsc->invoke(method, jv)[jss::result];
842 BEAST_EXPECT(jr[jss::error] == "malformedStream");
843 BEAST_EXPECT(jr[jss::error_message] == "Stream malformed.");
844 }
845
846 {
847 json::Value jv;
848 jv[jss::streams] = json::ValueType::Array;
849 jv[jss::streams][0u] = "not_a_stream";
850 auto const jr = wsc->invoke(method, jv)[jss::result];
851 BEAST_EXPECT(jr[jss::error] == "malformedStream");
852 BEAST_EXPECT(jr[jss::error_message] == "Stream malformed.");
853 }
854
855 if (subscribe)
856 {
857 // invalid taker - not a string
858 {
859 json::Value jv;
860 jv[jss::books] = json::ValueType::Array;
861 jv[jss::books][0u] = json::ValueType::Object;
862 jv[jss::books][0u][jss::taker_pays] =
863 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::Values::IncludeDate);
864 jv[jss::books][0u][jss::taker_gets][jss::currency] = "XRP";
865 jv[jss::books][0u][jss::taker] = 1;
866 auto const jr = wsc->invoke(method, jv)[jss::result];
867 BEAST_EXPECTS(jr[jss::error] == "actMalformed", jr.toStyledString());
868 BEAST_EXPECT(jr[jss::error_message] == "Account malformed.");
869 }
870
871 // invalid taker - malformed account string
872 {
873 json::Value jv;
874 jv[jss::books] = json::ValueType::Array;
875 jv[jss::books][0u] = json::ValueType::Object;
876 jv[jss::books][0u][jss::taker_pays] =
877 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::Values::IncludeDate);
878 jv[jss::books][0u][jss::taker_gets][jss::currency] = "XRP";
879 jv[jss::books][0u][jss::taker] = "not_an_account";
880 auto const jr = wsc->invoke(method, jv)[jss::result];
881 BEAST_EXPECTS(jr[jss::error] == "actMalformed", jr.toStyledString());
882 BEAST_EXPECT(jr[jss::error_message] == "Account malformed.");
883 }
884
885 // invalid taker - account string with extra characters
886 {
887 json::Value jv;
888 jv[jss::books] = json::ValueType::Array;
889 jv[jss::books][0u] = json::ValueType::Object;
890 jv[jss::books][0u][jss::taker_pays] =
891 Account{"gateway"}["USD"](1).value().getJson(JsonOptions::Values::IncludeDate);
892 jv[jss::books][0u][jss::taker_gets][jss::currency] = "XRP";
893 jv[jss::books][0u][jss::taker] = Account{"alice"}.human() + "DEAD";
894 auto const jr = wsc->invoke(method, jv)[jss::result];
895 BEAST_EXPECTS(jr[jss::error] == "actMalformed", jr.toStyledString());
896 BEAST_EXPECT(jr[jss::error_message] == "Account malformed.");
897 }
898 }
899 }
900
901 void
903 {
904 testcase("HistoryTxStream");
905
906 using namespace std::chrono_literals;
907 using namespace jtx;
909
910 Account const alice("alice");
911 Account const bob("bob");
912 Account const carol("carol");
913 Account const david("david");
915
916 /*
917 * return true if the subscribe or unsubscribe result is a success
918 */
919 auto goodSubRPC = [](json::Value const& subReply) -> bool {
920 return subReply.isMember(jss::result) && subReply[jss::result].isMember(jss::status) &&
921 subReply[jss::result][jss::status] == jss::success;
922 };
923
924 /*
925 * try to receive txns from the tx stream subscription via the WSClient.
926 * return {true, true} if received numReplies replies and also
927 * received a tx with the account_history_tx_first == true
928 */
929 auto getTxHash = [](WSClient& wsc,
930 IdxHashVec& v,
931 int numReplies,
934 bool firstFlag = false;
935
936 for (int i = 0; i < numReplies; ++i)
937 {
938 std::uint32_t idx{0};
939 auto reply = wsc.getMsg(timeout);
940 if (reply)
941 {
942 auto r = *reply;
943 if (r.isMember(jss::account_history_tx_index))
944 idx = r[jss::account_history_tx_index].asInt();
945 if (r.isMember(jss::account_history_tx_first))
946 firstFlag = true;
947 bool const boundary = r.isMember(jss::account_history_boundary);
948 int const ledgerIdx = r[jss::ledger_index].asInt();
949 if (r.isMember(jss::transaction) && r[jss::transaction].isMember(jss::hash))
950 {
951 auto t{r[jss::transaction]};
952 v.emplace_back(idx, t[jss::hash].asString(), boundary, ledgerIdx);
953 continue;
954 }
955 }
956 return {false, firstFlag};
957 }
958
959 return {true, firstFlag};
960 };
961
962 /*
963 * send payments between the two accounts a and b,
964 * and close ledgersToClose ledgers
965 */
966 auto sendPayments = [this](
967 Env& env,
968 Account const& a,
969 Account const& b,
970 int newTxns,
971 std::uint32_t ledgersToClose,
972 int numXRP = 10) {
973 env.memoize(a);
974 env.memoize(b);
975 for (int i = 0; i < newTxns; ++i)
976 {
977 auto& from = (i % 2 == 0) ? a : b;
978 auto& to = (i % 2 == 0) ? b : a;
979 env(pay(from, to, jtx::XRP(numXRP)),
983 }
984 for (int i = 0; i < ledgersToClose; ++i)
985 BEAST_EXPECT(env.syncClose());
986 return newTxns;
987 };
988
989 /*
990 * Check if txHistoryVec has every item of accountVec,
991 * and in the same order.
992 * If sizeCompare is false, txHistoryVec is allowed to be larger.
993 */
994 auto hashCompare = [](IdxHashVec const& accountVec,
995 IdxHashVec const& txHistoryVec,
996 bool sizeCompare) -> bool {
997 if (accountVec.empty() || txHistoryVec.empty())
998 return false;
999 if (sizeCompare && accountVec.size() != (txHistoryVec.size()))
1000 return false;
1001
1002 HashMap<std::string, int> txHistoryMap;
1003 for (auto const& tx : txHistoryVec)
1004 {
1005 txHistoryMap.emplace(std::get<1>(tx), std::get<0>(tx));
1006 }
1007
1008 auto getHistoryIndex = [&](std::size_t i) -> std::optional<int> {
1009 if (i >= accountVec.size())
1010 return {};
1011 auto it = txHistoryMap.find(std::get<1>(accountVec[i]));
1012 if (it == txHistoryMap.end())
1013 return {};
1014 return it->second;
1015 };
1016
1017 auto firstHistoryIndex = getHistoryIndex(0);
1018 if (!firstHistoryIndex)
1019 return false;
1020 for (std::size_t i = 1; i < accountVec.size(); ++i)
1021 {
1022 if (auto idx = getHistoryIndex(i); !idx || *idx != *firstHistoryIndex + i)
1023 return false;
1024 }
1025 return true;
1026 };
1027
1028 // example of vector created from the return of `subscribe` rpc
1029 // with jss::accounts
1030 // boundary == true on last tx of ledger
1031 // ------------------------------------------------------------
1032 // (0, "E5B8B...", false, 4
1033 // (0, "39E1C...", false, 4
1034 // (0, "14EF1...", false, 4
1035 // (0, "386E6...", false, 4
1036 // (0, "00F3B...", true, 4
1037 // (0, "1DCDC...", false, 5
1038 // (0, "BD02A...", false, 5
1039 // (0, "D3E16...", false, 5
1040 // (0, "CB593...", false, 5
1041 // (0, "8F28B...", true, 5
1042 //
1043 // example of vector created from the return of `subscribe` rpc
1044 // with jss::account_history_tx_stream.
1045 // boundary == true on first tx of ledger
1046 // ------------------------------------------------------------
1047 // (-1, "8F28B...", false, 5
1048 // (-2, "CB593...", false, 5
1049 // (-3, "D3E16...", false, 5
1050 // (-4, "BD02A...", false, 5
1051 // (-5, "1DCDC...", true, 5
1052 // (-6, "00F3B...", false, 4
1053 // (-7, "386E6...", false, 4
1054 // (-8, "14EF1...", false, 4
1055 // (-9, "39E1C...", false, 4
1056 // (-10, "E5B8B...", true, 4
1057
1058 auto checkBoundary = [](IdxHashVec const& vec, bool /* forward */) {
1059 size_t const numTx = vec.size();
1060 for (size_t i = 0; i < numTx; ++i)
1061 {
1062 auto [idx, hash, boundary, ledger] = vec[i];
1063 if ((i + 1 == numTx || ledger != std::get<3>(vec[i + 1])) != boundary)
1064 return false;
1065 }
1066 return true;
1067 };
1068
1070
1071 {
1072 /*
1073 * subscribe to an account twice with same WS client,
1074 * the second should fail
1075 *
1076 * also test subscribe to the account before it is created
1077 */
1078 Env env(*this, singleThreadIo(envconfig()));
1079 auto wscTxHistory = makeWSClient(env.app().config());
1080 json::Value request;
1081 request[jss::account_history_tx_stream] = json::ValueType::Object;
1082 request[jss::account_history_tx_stream][jss::account] = alice.human();
1083 auto jv = wscTxHistory->invoke("subscribe", request);
1084 if (!BEAST_EXPECT(goodSubRPC(jv)))
1085 return;
1086
1087 jv = wscTxHistory->invoke("subscribe", request);
1088 BEAST_EXPECT(!goodSubRPC(jv));
1089
1090 /*
1091 * unsubscribe history only, future txns should still be streamed
1092 */
1093 request[jss::account_history_tx_stream][jss::stop_history_tx_only] = true;
1094 jv = wscTxHistory->invoke("unsubscribe", request);
1095 if (!BEAST_EXPECT(goodSubRPC(jv)))
1096 return;
1097
1098 sendPayments(env, env.master, alice, 1, 1, 123456);
1099
1100 IdxHashVec vec;
1101 auto r = getTxHash(*wscTxHistory, vec, 1);
1102 if (!BEAST_EXPECT(r.first && r.second))
1103 return;
1104
1105 /*
1106 * unsubscribe, future txns should not be streamed
1107 */
1108 request[jss::account_history_tx_stream][jss::stop_history_tx_only] = false;
1109 jv = wscTxHistory->invoke("unsubscribe", request);
1110 BEAST_EXPECT(goodSubRPC(jv));
1111
1112 sendPayments(env, env.master, alice, 1, 1);
1113 r = getTxHash(*wscTxHistory, vec, 1, 10ms);
1114 BEAST_EXPECT(!r.first);
1115 }
1116 {
1117 /*
1118 * subscribe genesis account tx history without txns
1119 * subscribe to bob's account after it is created
1120 */
1121 Env env(*this, singleThreadIo(envconfig()));
1122 auto wscTxHistory = makeWSClient(env.app().config());
1123 json::Value request;
1124 request[jss::account_history_tx_stream] = json::ValueType::Object;
1125 request[jss::account_history_tx_stream][jss::account] =
1126 "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
1127 auto jv = wscTxHistory->invoke("subscribe", request);
1128 if (!BEAST_EXPECT(goodSubRPC(jv)))
1129 return;
1130 IdxHashVec genesisFullHistoryVec;
1131 BEAST_EXPECT(env.syncClose());
1132 if (!BEAST_EXPECT(!getTxHash(*wscTxHistory, genesisFullHistoryVec, 1, 10ms).first))
1133 return;
1134
1135 /*
1136 * create bob's account with one tx
1137 * the two subscriptions should both stream it
1138 */
1139 sendPayments(env, env.master, bob, 1, 1, 654321);
1140
1141 auto r = getTxHash(*wscTxHistory, genesisFullHistoryVec, 1);
1142 if (!BEAST_EXPECT(r.first && r.second))
1143 return;
1144
1145 request[jss::account_history_tx_stream][jss::account] = bob.human();
1146 jv = wscTxHistory->invoke("subscribe", request);
1147 if (!BEAST_EXPECT(goodSubRPC(jv)))
1148 return;
1149 IdxHashVec bobFullHistoryVec;
1150 BEAST_EXPECT(env.syncClose());
1151 r = getTxHash(*wscTxHistory, bobFullHistoryVec, 1);
1152 if (!BEAST_EXPECT(r.first && r.second))
1153 return;
1154 BEAST_EXPECT(
1155 std::get<1>(bobFullHistoryVec.back()) == std::get<1>(genesisFullHistoryVec.back()));
1156
1157 /*
1158 * unsubscribe to prepare next test
1159 */
1160 jv = wscTxHistory->invoke("unsubscribe", request);
1161 if (!BEAST_EXPECT(goodSubRPC(jv)))
1162 return;
1163 request[jss::account_history_tx_stream][jss::account] =
1164 "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
1165 jv = wscTxHistory->invoke("unsubscribe", request);
1166 BEAST_EXPECT(goodSubRPC(jv));
1167
1168 /*
1169 * add more txns, then subscribe bob tx history and
1170 * genesis account tx history. Their earliest txns should match.
1171 */
1172 sendPayments(env, env.master, bob, 30, 300);
1173 wscTxHistory = makeWSClient(env.app().config());
1174 request[jss::account_history_tx_stream][jss::account] = bob.human();
1175 jv = wscTxHistory->invoke("subscribe", request);
1176
1177 bobFullHistoryVec.clear();
1178 BEAST_EXPECT(getTxHash(*wscTxHistory, bobFullHistoryVec, 31).second);
1179 jv = wscTxHistory->invoke("unsubscribe", request);
1180
1181 request[jss::account_history_tx_stream][jss::account] =
1182 "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
1183 jv = wscTxHistory->invoke("subscribe", request);
1184 genesisFullHistoryVec.clear();
1185 BEAST_EXPECT(env.syncClose());
1186 BEAST_EXPECT(getTxHash(*wscTxHistory, genesisFullHistoryVec, 31).second);
1187 jv = wscTxHistory->invoke("unsubscribe", request);
1188
1189 BEAST_EXPECT(
1190 std::get<1>(bobFullHistoryVec.back()) == std::get<1>(genesisFullHistoryVec.back()));
1191 }
1192
1193 {
1194 /*
1195 * subscribe account and subscribe account tx history
1196 * and compare txns streamed
1197 */
1198 Env env(*this, singleThreadIo(envconfig()));
1199 auto wscAccount = makeWSClient(env.app().config());
1200 auto wscTxHistory = makeWSClient(env.app().config());
1201
1202 std::array<Account, 2> const accounts = {alice, bob};
1203 env.fund(XRP(222222), accounts);
1204 BEAST_EXPECT(env.syncClose());
1205
1206 // subscribe account
1208 stream[jss::accounts] = json::ValueType::Array;
1209 stream[jss::accounts].append(alice.human());
1210 auto jv = wscAccount->invoke("subscribe", stream);
1211
1212 sendPayments(env, alice, bob, 5, 1);
1213 sendPayments(env, alice, bob, 5, 1);
1214 IdxHashVec accountVec;
1215 if (!BEAST_EXPECT(getTxHash(*wscAccount, accountVec, 10).first))
1216 return;
1217
1218 // subscribe account tx history
1219 json::Value request;
1220 request[jss::account_history_tx_stream] = json::ValueType::Object;
1221 request[jss::account_history_tx_stream][jss::account] = alice.human();
1222 jv = wscTxHistory->invoke("subscribe", request);
1223
1224 // compare historical txns
1225 IdxHashVec txHistoryVec;
1226 if (!BEAST_EXPECT(getTxHash(*wscTxHistory, txHistoryVec, 10).first))
1227 return;
1228 if (!BEAST_EXPECT(hashCompare(accountVec, txHistoryVec, true)))
1229 return;
1230
1231 // check boundary tags
1232 // only account_history_tx_stream has ledger boundary information.
1233 if (!BEAST_EXPECT(checkBoundary(txHistoryVec, false)))
1234 return;
1235
1236 {
1237 // take out all history txns from stream to prepare next test
1238 IdxHashVec initFundTxns;
1239 if (!BEAST_EXPECT(getTxHash(*wscTxHistory, initFundTxns, 10).second) ||
1240 !BEAST_EXPECT(checkBoundary(initFundTxns, false)))
1241 return;
1242 }
1243
1244 // compare future txns
1245 sendPayments(env, alice, bob, 10, 1);
1246 if (!BEAST_EXPECT(getTxHash(*wscAccount, accountVec, 10).first))
1247 return;
1248 if (!BEAST_EXPECT(getTxHash(*wscTxHistory, txHistoryVec, 10).first))
1249 return;
1250 if (!BEAST_EXPECT(hashCompare(accountVec, txHistoryVec, true)))
1251 return;
1252
1253 // check boundary tags
1254 // only account_history_tx_stream has ledger boundary information.
1255 if (!BEAST_EXPECT(checkBoundary(txHistoryVec, false)))
1256 return;
1257
1258 wscTxHistory->invoke("unsubscribe", request);
1259 wscAccount->invoke("unsubscribe", stream);
1260 }
1261
1262 {
1263 /*
1264 * alice issues USD to carol
1265 * mix USD and XRP payments
1266 */
1267 Env env(*this, singleThreadIo(envconfig()));
1268 auto const usdA = alice["USD"];
1269
1270 std::array<Account, 2> const accounts = {alice, carol};
1271 env.fund(XRP(333333), accounts);
1272 env.trust(usdA(20000), carol);
1273 BEAST_EXPECT(env.syncClose());
1274
1275 auto mixedPayments = [&]() -> int {
1276 sendPayments(env, alice, carol, 1, 0);
1277 env(pay(alice, carol, usdA(100)));
1278 BEAST_EXPECT(env.syncClose());
1279 return 2;
1280 };
1281
1282 // subscribe
1283 json::Value request;
1284 request[jss::account_history_tx_stream] = json::ValueType::Object;
1285 request[jss::account_history_tx_stream][jss::account] = carol.human();
1286 auto ws = makeWSClient(env.app().config());
1287 auto jv = ws->invoke("subscribe", request);
1288 BEAST_EXPECT(env.syncClose());
1289 {
1290 // take out existing txns from the stream
1291 IdxHashVec tempVec;
1292 getTxHash(*ws, tempVec, 100, 1000ms);
1293 }
1294
1295 auto count = mixedPayments();
1296 IdxHashVec vec1;
1297 if (!BEAST_EXPECT(getTxHash(*ws, vec1, count).first))
1298 return;
1299 ws->invoke("unsubscribe", request);
1300 }
1301
1302 {
1303 /*
1304 * long transaction history
1305 */
1306 Env env(*this, singleThreadIo(envconfig()));
1307 std::array<Account, 2> const accounts = {alice, carol};
1308 env.fund(XRP(444444), accounts);
1309 BEAST_EXPECT(env.syncClose());
1310
1311 // many payments, and close lots of ledgers
1312 auto oneRound = [&](int numPayments) {
1313 return sendPayments(env, alice, carol, numPayments, 300);
1314 };
1315
1316 // subscribe
1317 json::Value request;
1318 request[jss::account_history_tx_stream] = json::ValueType::Object;
1319 request[jss::account_history_tx_stream][jss::account] = carol.human();
1320 auto wscLong = makeWSClient(env.app().config());
1321 auto jv = wscLong->invoke("subscribe", request);
1322 BEAST_EXPECT(env.syncClose());
1323 {
1324 // take out existing txns from the stream
1325 IdxHashVec tempVec;
1326 getTxHash(*wscLong, tempVec, 100, 1000ms);
1327 }
1328
1329 // repeat the payments many rounds
1330 for (int kk = 2; kk < 10; ++kk)
1331 {
1332 auto count = oneRound(kk);
1333 IdxHashVec vec1;
1334 if (!BEAST_EXPECT(getTxHash(*wscLong, vec1, count).first))
1335 return;
1336
1337 // another subscribe, only for this round
1338 auto wscShort = makeWSClient(env.app().config());
1339 auto jv = wscShort->invoke("subscribe", request);
1340 IdxHashVec vec2;
1341 if (!BEAST_EXPECT(getTxHash(*wscShort, vec2, count).first))
1342 return;
1343 if (!BEAST_EXPECT(hashCompare(vec1, vec2, true)))
1344 return;
1345 wscShort->invoke("unsubscribe", request);
1346 }
1347 }
1348 }
1349
1350 void
1352 {
1353 testcase("SubBookChanges");
1354 using namespace jtx;
1355 using namespace std::chrono_literals;
1356 FeatureBitset const all{
1357 jtx::testableAmendments() | featurePermissionedDomains | featureCredentials |
1358 featurePermissionedDEX};
1359
1360 Env env(*this, singleThreadIo(envconfig()), all);
1361 PermissionedDEX const permDex(env);
1362 auto const alice = permDex.alice;
1363 auto const bob = permDex.bob;
1364 auto const carol = permDex.carol;
1365 auto const domainID = permDex.domainID;
1366 auto const gw = permDex.gw;
1367 auto const usd = permDex.usd;
1368
1369 auto wsc = makeWSClient(env.app().config());
1370
1371 json::Value streams;
1372 streams[jss::streams] = json::ValueType::Array;
1373 streams[jss::streams][0u] = "book_changes";
1374
1375 auto jv = wsc->invoke("subscribe", streams);
1376 if (!BEAST_EXPECT(jv[jss::status] == "success"))
1377 return;
1378 env(offer(alice, XRP(10), usd(10)), Domain(domainID), Txflags(tfHybrid));
1379 BEAST_EXPECT(env.syncClose());
1380
1381 env(pay(bob, carol, usd(5)), Path(~usd), Sendmax(XRP(5)), Domain(domainID));
1382 BEAST_EXPECT(env.syncClose());
1383
1384 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
1385 if (jv[jss::changes].size() != 1)
1386 return false;
1387
1388 auto const jrOffer = jv[jss::changes][0u];
1389 return (jv[jss::changes][0u][jss::domain]).asString() == strHex(domainID) &&
1390 jrOffer[jss::currency_a].asString() == "XRP_drops" &&
1391 jrOffer[jss::volume_a].asString() == "5000000" &&
1392 jrOffer[jss::currency_b].asString() == "rHUKYAZyUFn8PCZWbPfwHfbVQXTYrYKkHb/USD" &&
1393 jrOffer[jss::volume_b].asString() == "5";
1394 }));
1395 }
1396
1397 void
1399 {
1400 // `nftoken_id` is added for `transaction` stream in the `subscribe`
1401 // response for NFTokenMint and NFTokenAcceptOffer.
1402 //
1403 // `nftoken_ids` is added for `transaction` stream in the `subscribe`
1404 // response for NFTokenCancelOffer
1405 //
1406 // `offer_id` is added for `transaction` stream in the `subscribe`
1407 // response for NFTokenCreateOffer
1408 //
1409 // The values of these fields are dependent on the NFTokenID/OfferID
1410 // changed in its corresponding transaction. We want to validate each
1411 // response to make sure the synthetic fields hold the right values.
1412
1413 testcase("Test synthetic fields from Subscribe response");
1414
1415 using namespace test::jtx;
1416 using namespace std::chrono_literals;
1417
1418 Account const alice{"alice"};
1419 Account const bob{"bob"};
1420 Account const broker{"broker"};
1421
1422 Env env{*this, singleThreadIo(envconfig()), features};
1423 env.fund(XRP(10000), alice, bob, broker);
1424 BEAST_EXPECT(env.syncClose());
1425
1426 auto wsc = test::makeWSClient(env.app().config());
1427 json::Value stream;
1428 stream[jss::streams] = json::ValueType::Array;
1429 stream[jss::streams].append("transactions");
1430 auto jv = wsc->invoke("subscribe", stream);
1431
1432 // Verify `nftoken_id` value equals to the NFTokenID that was
1433 // changed in the most recent NFTokenMint or NFTokenAcceptOffer
1434 // transaction
1435 auto verifyNFTokenID = [&](UInt256 const& actualNftID) {
1436 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
1437 UInt256 nftID;
1438 BEAST_EXPECT(nftID.parseHex(jv[jss::meta][jss::nftoken_id].asString()));
1439 return nftID == actualNftID;
1440 }));
1441 };
1442
1443 // Verify `nftoken_ids` value equals to the NFTokenIDs that were
1444 // changed in the most recent NFTokenCancelOffer transaction
1445 auto verifyNFTokenIDsInCancelOffer = [&](std::vector<UInt256> actualNftIDs) {
1446 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
1447 std::vector<UInt256> metaIDs;
1448 std::transform(
1449 jv[jss::meta][jss::nftoken_ids].begin(),
1450 jv[jss::meta][jss::nftoken_ids].end(),
1451 std::back_inserter(metaIDs),
1452 [this](json::Value id) {
1453 UInt256 nftID;
1454 BEAST_EXPECT(nftID.parseHex(id.asString()));
1455 return nftID;
1456 });
1457 // Sort both array to prepare for comparison
1458 std::ranges::sort(metaIDs);
1459 std::ranges::sort(actualNftIDs);
1460
1461 // Make sure the expect number of NFTs is correct
1462 BEAST_EXPECT(metaIDs.size() == actualNftIDs.size());
1463
1464 // Check the value of NFT ID in the meta with the
1465 // actual values
1466 for (size_t i = 0; i < metaIDs.size(); ++i)
1467 BEAST_EXPECT(metaIDs[i] == actualNftIDs[i]);
1468 return true;
1469 }));
1470 };
1471
1472 // Verify `offer_id` value equals to the offerID that was
1473 // changed in the most recent NFTokenCreateOffer tx
1474 auto verifyNFTokenOfferID = [&](UInt256 const& offerID) {
1475 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
1476 UInt256 metaOfferID;
1477 BEAST_EXPECT(metaOfferID.parseHex(jv[jss::meta][jss::offer_id].asString()));
1478 return metaOfferID == offerID;
1479 }));
1480 };
1481
1482 // Check new fields in tx meta when for all NFTtransactions
1483 {
1484 // Alice mints 2 NFTs
1485 // Verify the NFTokenIDs are correct in the NFTokenMint tx meta
1486 UInt256 const nftId1{token::getNextID(env, alice, 0u, tfTransferable)};
1487 env(token::mint(alice, 0u), Txflags(tfTransferable));
1488 BEAST_EXPECT(env.syncClose());
1489 verifyNFTokenID(nftId1);
1490
1491 UInt256 const nftId2{token::getNextID(env, alice, 0u, tfTransferable)};
1492 env(token::mint(alice, 0u), Txflags(tfTransferable));
1493 BEAST_EXPECT(env.syncClose());
1494 verifyNFTokenID(nftId2);
1495
1496 // Alice creates one sell offer for each NFT
1497 // Verify the offer indexes are correct in the NFTokenCreateOffer tx
1498 // meta
1499 UInt256 const aliceOfferIndex1 =
1500 keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key;
1501 env(token::createOffer(alice, nftId1, drops(1)), Txflags(tfSellNFToken));
1502 BEAST_EXPECT(env.syncClose());
1503 verifyNFTokenOfferID(aliceOfferIndex1);
1504
1505 UInt256 const aliceOfferIndex2 =
1506 keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key;
1507 env(token::createOffer(alice, nftId2, drops(1)), Txflags(tfSellNFToken));
1508 BEAST_EXPECT(env.syncClose());
1509 verifyNFTokenOfferID(aliceOfferIndex2);
1510
1511 // Alice cancels two offers she created
1512 // Verify the NFTokenIDs are correct in the NFTokenCancelOffer tx
1513 // meta
1514 env(token::cancelOffer(alice, {aliceOfferIndex1, aliceOfferIndex2}));
1515 BEAST_EXPECT(env.syncClose());
1516 verifyNFTokenIDsInCancelOffer({nftId1, nftId2});
1517
1518 // Bobs creates a buy offer for nftId1
1519 // Verify the offer id is correct in the NFTokenCreateOffer tx meta
1520 auto const bobBuyOfferIndex =
1522 env(token::createOffer(bob, nftId1, drops(1)), token::Owner(alice));
1523 BEAST_EXPECT(env.syncClose());
1524 verifyNFTokenOfferID(bobBuyOfferIndex);
1525
1526 // Alice accepts bob's buy offer
1527 // Verify the NFTokenID is correct in the NFTokenAcceptOffer tx meta
1528 env(token::acceptBuyOffer(alice, bobBuyOfferIndex));
1529 BEAST_EXPECT(env.syncClose());
1530 verifyNFTokenID(nftId1);
1531 }
1532
1533 // Check `nftoken_ids` in brokered mode
1534 {
1535 // Alice mints a NFT
1536 UInt256 const nftId{token::getNextID(env, alice, 0u, tfTransferable)};
1537 env(token::mint(alice, 0u), Txflags(tfTransferable));
1538 BEAST_EXPECT(env.syncClose());
1539 verifyNFTokenID(nftId);
1540
1541 // Alice creates sell offer and set broker as destination
1542 UInt256 const offerAliceToBroker =
1543 keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key;
1544 env(token::createOffer(alice, nftId, drops(1)),
1545 token::Destination(broker),
1546 Txflags(tfSellNFToken));
1547 BEAST_EXPECT(env.syncClose());
1548 verifyNFTokenOfferID(offerAliceToBroker);
1549
1550 // Bob creates buy offer
1551 UInt256 const offerBobToBroker =
1553 env(token::createOffer(bob, nftId, drops(1)), token::Owner(alice));
1554 BEAST_EXPECT(env.syncClose());
1555 verifyNFTokenOfferID(offerBobToBroker);
1556
1557 // Check NFTokenID meta for NFTokenAcceptOffer in brokered mode
1558 env(token::brokerOffers(broker, offerBobToBroker, offerAliceToBroker));
1559 BEAST_EXPECT(env.syncClose());
1560 verifyNFTokenID(nftId);
1561 }
1562
1563 // Check if there are no duplicate nft id in Cancel transactions where
1564 // multiple offers are cancelled for the same NFT
1565 {
1566 // Alice mints a NFT
1567 UInt256 const nftId{token::getNextID(env, alice, 0u, tfTransferable)};
1568 env(token::mint(alice, 0u), Txflags(tfTransferable));
1569 BEAST_EXPECT(env.syncClose());
1570 verifyNFTokenID(nftId);
1571
1572 // Alice creates 2 sell offers for the same NFT
1573 UInt256 const aliceOfferIndex1 =
1574 keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key;
1575 env(token::createOffer(alice, nftId, drops(1)), Txflags(tfSellNFToken));
1576 BEAST_EXPECT(env.syncClose());
1577 verifyNFTokenOfferID(aliceOfferIndex1);
1578
1579 UInt256 const aliceOfferIndex2 =
1580 keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key;
1581 env(token::createOffer(alice, nftId, drops(1)), Txflags(tfSellNFToken));
1582 BEAST_EXPECT(env.syncClose());
1583 verifyNFTokenOfferID(aliceOfferIndex2);
1584
1585 // Make sure the metadata only has 1 nft id, since both offers are
1586 // for the same nft
1587 env(token::cancelOffer(alice, {aliceOfferIndex1, aliceOfferIndex2}));
1588 BEAST_EXPECT(env.syncClose());
1589 verifyNFTokenIDsInCancelOffer({nftId});
1590 }
1591
1592 if (features[featureNFTokenMintOffer])
1593 {
1594 UInt256 const aliceMintWithOfferIndex1 =
1595 keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key;
1596 env(token::mint(alice), token::Amount(XRP(0)));
1597 BEAST_EXPECT(env.syncClose());
1598 verifyNFTokenOfferID(aliceMintWithOfferIndex1);
1599 }
1600 }
1601
1602 // ----- Subscription limit / teardown verification ----------------------
1603 //
1604 // The helpers and tests below exercise:
1605 // * the per-connection subscription cap + proportional charge enforced
1606 // in doSubscribe (Subscribe.cpp), and
1607 // * the asynchronous, chunked teardown of a disconnecting connection's
1608 // account subscriptions (~InfoSub -> scheduleAccountCleanup -> JobQueue).
1609 //
1610 // The cap-exceeded error is rpcINVALID_PARAMS with the message "Too many
1611 // subscriptions for this connection."; the tests assert that exactly.
1612 //
1613 // There is no public accessor for the server-side per-connection count, so
1614 // the async cleanup is verified behaviorally: publishing still flows to a
1615 // live subscriber, rather than by reading a count to zero.
1616
1617 // Build `count` distinct, valid, base58-encoded account strings cheaply by
1618 // incrementing an AccountID. parseAccountIds dedups into a HashSet, so the
1619 // strings MUST be distinct for the cap arithmetic to be exact; incrementing
1620 // guarantees distinctness without deriving `count` keypairs.
1621 static std::vector<std::string>
1623 {
1625 out.reserve(count);
1626 // Start at `seed` so separate calls produce non-overlapping ranges,
1627 // letting a test subscribe disjoint batches across requests.
1628 AccountID id{static_cast<std::uint64_t>(seed)};
1629 for (std::size_t i = 0; i < count; ++i)
1630 {
1631 out.push_back(toBase58(id));
1632 ++id;
1633 }
1634 return out;
1635 }
1636
1637 // Append the given account strings as a jss::accounts array onto a fresh
1638 // subscribe request object.
1639 static json::Value
1641 {
1643 jv[jss::accounts] = json::ValueType::Array;
1644 for (auto const& a : accts)
1645 jv[jss::accounts].append(a);
1646 return jv;
1647 }
1648
1649 // Append the given account strings as a jss::accounts_proposed array onto a
1650 // fresh subscribe request object.
1651 static json::Value
1653 {
1655 jv[jss::accounts_proposed] = json::ValueType::Array;
1656 for (auto const& a : accts)
1657 jv[jss::accounts_proposed].append(a);
1658 return jv;
1659 }
1660
1661 // Build `count` distinct, valid MPT issuance id strings the same cheap way
1662 // as makeAccountStrings: by incrementing an MPTID. doSubscribe only parses
1663 // these (it does not require the issuances to exist in the ledger), and
1664 // parseMPTIssuanceIDs dedups into a HashSet, so distinctness is what makes
1665 // the cap arithmetic exact.
1668 {
1670 out.reserve(count);
1671 // Start at `seed` so separate calls produce non-overlapping ranges.
1672 MPTID id{static_cast<std::uint64_t>(seed)};
1673 for (std::size_t i = 0; i < count; ++i)
1674 {
1675 out.push_back(to_string(id));
1676 ++id;
1677 }
1678 return out;
1679 }
1680
1681 // Append the given issuance ids as a jss::mpt_issuances array onto a fresh
1682 // subscribe request object.
1683 static json::Value
1685 {
1687 jv[jss::mpt_issuances] = json::ValueType::Array;
1688 for (auto const& id : mptIDs)
1689 jv[jss::mpt_issuances].append(id);
1690 return jv;
1691 }
1692
1693 // A single, valid XRP/USD order book request, as one entry of a
1694 // jss::books array.
1695 static json::Value
1697 {
1698 using namespace jtx;
1700 jv[jss::books] = json::ValueType::Array;
1701 json::Value& book = jv[jss::books][0u];
1702 book[jss::taker_gets] = json::ValueType::Object;
1703 book[jss::taker_gets][jss::currency] = "XRP";
1704 book[jss::taker_pays] = json::ValueType::Object;
1705 book[jss::taker_pays][jss::currency] = "USD";
1706 book[jss::taker_pays][jss::issuer] = Account("alice").human();
1707 return jv;
1708 }
1709
1710 // A single account_history_tx_stream subscribe request for `acct`.
1711 static json::Value
1713 {
1715 jv[jss::account_history_tx_stream] = json::ValueType::Object;
1716 jv[jss::account_history_tx_stream][jss::account] = acct;
1717 return jv;
1718 }
1719
1720 // An envconfig modifier that lowers the per-connection subscription cap to
1721 // `cap`, so the cap logic in doSubscribe can be driven without subscribing
1722 // the production default (100'000) entries. (Env is non-movable, so this
1723 // returns the config modifier rather than a ready-made Env.)
1724 static auto
1726 {
1727 return [cap](std::unique_ptr<Config> cfg) {
1728 cfg->maxSubscriptionsPerConnection = cap;
1729 return jtx::singleThreadIo(std::move(cfg));
1730 };
1731 }
1732
1733 void
1735 {
1736 // A request that alone exceeds the cap is rejected with the exact
1737 // cap error, before any state is recorded. Baseline negative path.
1738 testcase("subscription cap rejects an over-cap request");
1739
1740 using namespace jtx;
1741 Env env{*this, envconfig(cappedConfig(5))};
1742 auto wsc = makeWSClient(env.app().config());
1743
1744 // Six accounts against a cap of five: rejected.
1745 auto const jr =
1746 wsc->invoke("subscribe", accountsRequest(makeAccountStrings(6)))[jss::result];
1747 BEAST_EXPECT(jr[jss::error] == "invalidParams");
1748 BEAST_EXPECT(jr[jss::error_message] == "Too many subscriptions for this connection.");
1749 }
1750
1751 void
1753 {
1754 // Re-subscribing accounts already held by this connection adds no new
1755 // tracked state, so it must be admitted even at the cap. The cap check
1756 // must count only NET-NEW accounts, not the raw request size.
1757 testcase("re-subscribe at the cap is not over-counted");
1758
1759 using namespace jtx;
1760 Env env{*this, envconfig(cappedConfig(5))};
1761 auto wsc = makeWSClient(env.app().config());
1762
1763 // Fill the cap exactly with five distinct accounts.
1764 auto const five = makeAccountStrings(5);
1765 {
1766 auto const r = wsc->invoke("subscribe", accountsRequest(five));
1767 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1768 }
1769
1770 // Re-subscribe the same five: net-new is zero, so it stays within the
1771 // cap and must succeed. (Pre-fix this was wrongly rejected.)
1772 {
1773 auto const r = wsc->invoke("subscribe", accountsRequest(five));
1774 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1775 }
1776 }
1777
1778 void
1780 {
1781 // Book subscriptions are tracked separately (OrderBookDB) and are not
1782 // part of totalSubscriptionCount(). An account set at the cap must not
1783 // block an unrelated book subscription.
1784 testcase("books cap is independent of account count");
1785
1786 using namespace jtx;
1787 Env env{*this, envconfig(cappedConfig(5))};
1788 Account const alice{"alice"};
1789 env.fund(XRP(10000), alice);
1790 BEAST_EXPECT(env.syncClose());
1791
1792 auto wsc = makeWSClient(env.app().config());
1793
1794 // Fill the account cap exactly.
1795 {
1796 auto const r = wsc->invoke("subscribe", accountsRequest(makeAccountStrings(5)));
1797 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1798 }
1799
1800 // A single book subscription must still be admitted: it does not count
1801 // against the account cap. (Pre-fix this was wrongly rejected.)
1802 {
1803 auto const r = wsc->invoke("subscribe", oneBookRequest());
1804 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1805 }
1806 }
1807
1808 void
1810 {
1811 // A single request mixing fields must be all-or-nothing: if a later
1812 // field trips the cap, an earlier field must NOT have subscribed. The
1813 // leak is detected through the cap arithmetic itself - a follow-up
1814 // request succeeds only if no state leaked from the rejected one.
1815 testcase("multi-field subscribe does not partially subscribe");
1816
1817 using namespace jtx;
1818 Env env{*this, envconfig(cappedConfig(5))};
1819 auto wsc = makeWSClient(env.app().config());
1820
1821 // accounts_proposed (3, evaluated first, would subscribe) +
1822 // accounts (3): combined 6 exceeds the cap of 5, so the request is
1823 // rejected. The proposed branch must not have leaked its 3 entries.
1825 for (auto const& a : makeAccountStrings(3, 100))
1826 req[jss::accounts].append(a);
1827 {
1828 auto const jr = wsc->invoke("subscribe", req)[jss::result];
1829 BEAST_EXPECT(jr[jss::error] == "invalidParams");
1830 BEAST_EXPECT(jr[jss::error_message] == "Too many subscriptions for this connection.");
1831 }
1832
1833 // If the rejected request leaked its 3 proposed subscriptions, the
1834 // connection's count is already 3 and this 3-account request would be
1835 // rejected (3 + 3 > 5). With no leak the count is 0 and it succeeds.
1836 {
1837 auto const r = wsc->invoke("subscribe", accountsRequest(makeAccountStrings(3, 200)));
1838 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1839 }
1840 }
1841
1842 void
1844 {
1845 // An account_history_tx_stream subscribe is charged against the cap only
1846 // when it is net-new, matching the account branches. Re-subscribing an
1847 // account-history already held on this connection adds no tracked entry,
1848 // so it must NOT be rejected at the cap. The two rejection causes are
1849 // told apart by their exact error_message: the cap check yields "Too
1850 // many subscriptions for this connection."; a duplicate that gets past
1851 // the cap and is rejected downstream by subAccountHistory yields the
1852 // generic "Invalid parameters.".
1853 testcase("account_history re-subscribe at the cap is not over-counted");
1854
1855 using namespace jtx;
1856 Env env{*this, envconfig(cappedConfig(1))};
1857 Account const alice{"alice"};
1858 env.fund(XRP(10000), alice);
1859 BEAST_EXPECT(env.syncClose());
1860
1861 auto wsc = makeWSClient(env.app().config());
1862
1863 // First account-history subscribe is net-new: charge 1 fills the cap of
1864 // 1 exactly, so it is admitted. Positive path.
1865 {
1866 auto const r = wsc->invoke("subscribe", accountHistoryRequest(alice.human()));
1867 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1868 }
1869
1870 // Re-subscribe the same account-history while sitting exactly at the
1871 // cap. Net-new is zero, so the cap check must pass; the request is then
1872 // rejected by subAccountHistory as a duplicate, NOT by the cap. Proven
1873 // by the exact message: it is the duplicate error, not the cap error.
1874 // (Pre-fix, the flat charge of 1 made the cap check reject this with the
1875 // cap message instead.)
1876 {
1877 auto const jr =
1878 wsc->invoke("subscribe", accountHistoryRequest(alice.human()))[jss::result];
1879 BEAST_EXPECT(jr[jss::error] == "invalidParams");
1880 BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters.");
1881 BEAST_EXPECT(jr[jss::error_message] != "Too many subscriptions for this connection.");
1882 }
1883 }
1884
1885 void
1887 {
1888 // A genuinely net-new account-history subscribe on a connection already
1889 // at the cap IS rejected, with the cap error. Negative path, and the
1890 // counterpart to testHistoryReSubscribeNotOvercounted: it confirms the
1891 // net-new charge still rejects when the entry really is new.
1892 testcase("account_history net-new subscribe is rejected at the cap");
1893
1894 using namespace jtx;
1895 Env env{*this, envconfig(cappedConfig(1))};
1896 Account const alice{"alice"};
1897 Account const bob{"bob"};
1898 env.fund(XRP(10000), alice, bob);
1899 BEAST_EXPECT(env.syncClose());
1900
1901 auto wsc = makeWSClient(env.app().config());
1902
1903 // Fill the cap of 1 with alice's account-history.
1904 {
1905 auto const r = wsc->invoke("subscribe", accountHistoryRequest(alice.human()));
1906 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1907 }
1908
1909 // A different account-history (bob) is net-new: charge 1 over a cap of 1
1910 // already full, so it is rejected with the cap error.
1911 {
1912 auto const jr =
1913 wsc->invoke("subscribe", accountHistoryRequest(bob.human()))[jss::result];
1914 BEAST_EXPECT(jr[jss::error] == "invalidParams");
1915 BEAST_EXPECT(jr[jss::error_message] == "Too many subscriptions for this connection.");
1916 }
1917 }
1918
1919 void
1921 {
1922 // MPT issuance subscriptions are per-connection tracked state, so they
1923 // are bounded by the same cap as the account branches. A request that
1924 // alone exceeds the cap is rejected with the cap error, and nothing is
1925 // recorded. (Pre-fix the mpt_issuances branch skipped the cap check
1926 // entirely, so a client could hold unbounded issuance subscriptions.)
1927 testcase("mpt_issuances cap rejects an over-cap request");
1928
1929 using namespace jtx;
1930 Env env{*this, envconfig(cappedConfig(5))};
1931 auto wsc = makeWSClient(env.app().config());
1932
1933 // Six issuances against a cap of five: rejected.
1934 {
1935 auto const jr = wsc->invoke(
1936 "subscribe", mptIssuancesRequest(makeMPTIssuanceStrings(6)))[jss::result];
1937 BEAST_EXPECT(jr[jss::error] == "invalidParams");
1938 BEAST_EXPECT(jr[jss::error_message] == "Too many subscriptions for this connection.");
1939 }
1940
1941 // The rejected request reserved nothing: a five-issuance request (the
1942 // cap exactly) still fits. If the rejected one had leaked entries this
1943 // would be rejected too.
1944 {
1945 auto const r = wsc->invoke("subscribe", mptIssuancesRequest(makeMPTIssuanceStrings(5)));
1946 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1947 }
1948 }
1949
1950 void
1952 {
1953 // Like the account branches, the MPT charge counts only NET-NEW
1954 // issuances: re-subscribing issuances already held adds no tracked
1955 // state, so it must be admitted even sitting exactly at the cap.
1956 testcase("mpt_issuances re-subscribe at the cap is not over-counted");
1957
1958 using namespace jtx;
1959 Env env{*this, envconfig(cappedConfig(5))};
1960 auto wsc = makeWSClient(env.app().config());
1961
1962 // Fill the cap exactly with five distinct issuances.
1963 auto const five = makeMPTIssuanceStrings(5);
1964 {
1965 auto const r = wsc->invoke("subscribe", mptIssuancesRequest(five));
1966 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1967 }
1968
1969 // Re-subscribe the same five: net-new is zero, so it stays within the
1970 // cap and must succeed.
1971 {
1972 auto const r = wsc->invoke("subscribe", mptIssuancesRequest(five));
1973 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
1974 }
1975
1976 // A single net-new issuance on top of a full cap IS rejected.
1977 {
1978 auto const jr = wsc->invoke(
1979 "subscribe", mptIssuancesRequest(makeMPTIssuanceStrings(1, 100)))[jss::result];
1980 BEAST_EXPECT(jr[jss::error] == "invalidParams");
1981 BEAST_EXPECT(jr[jss::error_message] == "Too many subscriptions for this connection.");
1982 }
1983 }
1984
1985 void
1987 {
1988 // The cap is one budget over all tracked kinds, so MPT issuances and
1989 // accounts consume the same allowance in either order.
1990 testcase("mpt_issuances and accounts share one cap");
1991
1992 using namespace jtx;
1993
1994 // Accounts first, then MPTs: 3 + 3 over a cap of 5 is rejected.
1995 {
1996 Env env{*this, envconfig(cappedConfig(5))};
1997 auto wsc = makeWSClient(env.app().config());
1998
1999 auto const r = wsc->invoke("subscribe", accountsRequest(makeAccountStrings(3)));
2000 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
2001
2002 auto const jr = wsc->invoke(
2003 "subscribe", mptIssuancesRequest(makeMPTIssuanceStrings(3)))[jss::result];
2004 BEAST_EXPECT(jr[jss::error] == "invalidParams");
2005 BEAST_EXPECT(jr[jss::error_message] == "Too many subscriptions for this connection.");
2006
2007 // Two more issuances do fit the remaining allowance.
2008 auto const r2 =
2009 wsc->invoke("subscribe", mptIssuancesRequest(makeMPTIssuanceStrings(2)));
2010 BEAST_EXPECTS(r2[jss::status] == "success", to_string(r2));
2011 }
2012
2013 // MPTs first, then accounts: the tally MPTs contribute is what rejects
2014 // the accounts, proving mptSubscriptions_ is part of the count and not
2015 // just checked against it.
2016 {
2017 Env env{*this, envconfig(cappedConfig(5))};
2018 auto wsc = makeWSClient(env.app().config());
2019
2020 auto const r = wsc->invoke("subscribe", mptIssuancesRequest(makeMPTIssuanceStrings(5)));
2021 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
2022
2023 auto const jr =
2024 wsc->invoke("subscribe", accountsRequest(makeAccountStrings(1)))[jss::result];
2025 BEAST_EXPECT(jr[jss::error] == "invalidParams");
2026 BEAST_EXPECT(jr[jss::error_message] == "Too many subscriptions for this connection.");
2027 }
2028 }
2029
2030 void
2032 {
2033 // Unsubscribing releases the allowance: after dropping the issuances,
2034 // the same connection can subscribe a fresh batch. Guards against a
2035 // reserve that is never given back (which would turn the cap into a
2036 // permanent per-connection budget).
2037 testcase("mpt_issuances unsubscribe frees cap allowance");
2038
2039 using namespace jtx;
2040 Env env{*this, envconfig(cappedConfig(5))};
2041 auto wsc = makeWSClient(env.app().config());
2042
2043 auto const five = makeMPTIssuanceStrings(5);
2044 {
2045 auto const r = wsc->invoke("subscribe", mptIssuancesRequest(five));
2046 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
2047 }
2048
2049 {
2050 auto const r = wsc->invoke("unsubscribe", mptIssuancesRequest(five));
2051 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
2052 }
2053
2054 // The cap is free again, so a disjoint batch of five is admitted.
2055 {
2056 auto const r =
2057 wsc->invoke("subscribe", mptIssuancesRequest(makeMPTIssuanceStrings(5, 100)));
2058 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
2059 }
2060 }
2061
2062 void
2064 {
2065 // Test C (core regression): disconnecting a connection with many
2066 // account subscriptions must NOT block subsequent operations or
2067 // publishing. The teardown is now posted to a JobQueue job
2068 // (scheduleAccountCleanup), so it runs off the disconnect thread.
2069 testcase("async teardown does not stall publishing");
2070
2071 using namespace std::chrono_literals;
2072 using namespace jtx;
2073 Env env{*this, singleThreadIo(envconfig())};
2074
2075 Account const alice{"alice"};
2076 env.fund(XRP(10000), alice);
2077 BEAST_EXPECT(env.syncClose());
2078
2079 // A second, long-lived subscriber to alice that must keep receiving
2080 // publishes after the first connection disconnects.
2081 auto wscLive = makeWSClient(env.app().config());
2082 {
2084 jv[jss::accounts] = json::ValueType::Array;
2085 jv[jss::accounts].append(alice.human());
2086 auto const r = wscLive->invoke("subscribe", jv);
2087 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
2088 }
2089
2090 // A connection that subscribes to many accounts, then disconnects. A
2091 // few thousand entries is enough to be a real teardown while still
2092 // running fast in CI.
2093 constexpr std::size_t kBulk = 3000;
2094 {
2095 auto wscBulk = makeWSClient(env.app().config());
2096 auto const r =
2097 wscBulk->invoke("subscribe", accountsRequest(makeAccountStrings(kBulk, 10)));
2098 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
2099 // Destroying the client closes the WS connection, which destroys
2100 // the server-side InfoSub and posts the chunked async cleanup job.
2101 // WSClient exposes no explicit close(); resetting the owning
2102 // unique_ptr is the disconnect path.
2103 wscBulk.reset();
2104 }
2105
2106 // Immediately after the disconnect, an unrelated operation completes
2107 // promptly (it would block for seconds with inline teardown). This is a
2108 // cheap liveness check; the publish assertion below is the real proof.
2109 {
2110 auto const info = env.app().getOPs().getServerInfo(false, true, false);
2111 BEAST_EXPECT(info.isMember(jss::server_state));
2112 }
2113
2114 // The live subscriber still receives a published transaction for alice
2115 // within a short timeout, proving account-publishing was not stalled by
2116 // the concurrent teardown.
2117 {
2118 env(pay(env.master, alice, XRP(100)));
2119 BEAST_EXPECT(env.syncClose());
2120 BEAST_EXPECT(wscLive->findMsg(5s, [&](auto const& jv) {
2121 return jv.isMember(jss::transaction) &&
2122 jv[jss::transaction][jss::TransactionType] == jss::Payment &&
2123 jv[jss::transaction][jss::Destination] == alice.human();
2124 }));
2125 }
2126
2127 wscLive->invoke("unsubscribe", accountsRequest({alice.human()}));
2128 }
2129
2130 void
2132 {
2133 // Test D (Phase 3 correctness): connection A subscribes to account X
2134 // and disconnects (async cleanup pending, keyed on A's seq). A new
2135 // connection B subscribes to X and MUST still receive publishes for X -
2136 // A's deferred, seq-keyed cleanup must not remove B's subscription.
2137 testcase("re-subscribe after disconnect still delivers");
2138
2139 using namespace std::chrono_literals;
2140 using namespace jtx;
2141 Env env{*this, singleThreadIo(envconfig())};
2142
2143 Account const alice{"alice"};
2144 env.fund(XRP(10000), alice);
2145 BEAST_EXPECT(env.syncClose());
2146
2147 // Connection A subscribes to alice, then disconnects. A also subscribes
2148 // to a bulk set so its deferred cleanup is non-trivial and races with B.
2149 {
2150 auto wscA = makeWSClient(env.app().config());
2151 auto bulk = makeAccountStrings(2000, 10);
2152 bulk.push_back(alice.human());
2153 auto const r = wscA->invoke("subscribe", accountsRequest(bulk));
2154 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
2155 // Disconnect A by destroying its client (no explicit close()).
2156 wscA.reset();
2157 }
2158
2159 // Connection B (a new InfoSub with a distinct seq) subscribes to alice.
2160 auto wscB = makeWSClient(env.app().config());
2161 {
2163 jv[jss::accounts] = json::ValueType::Array;
2164 jv[jss::accounts].append(alice.human());
2165 auto const r = wscB->invoke("subscribe", jv);
2166 BEAST_EXPECTS(r[jss::status] == "success", to_string(r));
2167 }
2168
2169 // A publish for alice must reach B. If A's seq-keyed cleanup had wrongly
2170 // removed the shared alice entry, B would receive nothing.
2171 {
2172 env(pay(env.master, alice, XRP(100)));
2173 BEAST_EXPECT(env.syncClose());
2174 BEAST_EXPECT(wscB->findMsg(5s, [&](auto const& jv) {
2175 return jv.isMember(jss::transaction) &&
2176 jv[jss::transaction][jss::TransactionType] == jss::Payment &&
2177 jv[jss::transaction][jss::Destination] == alice.human();
2178 }));
2179 }
2180
2181 wscB->invoke("unsubscribe", accountsRequest({alice.human()}));
2182 }
2183
2184 void
2186 {
2187 // subscribe to multiple MPTs
2188 testcase("SubMPT");
2189 using namespace jtx;
2190 using namespace std::chrono_literals;
2191
2192 Env env(*this);
2193
2194 Account const alice{"alice"};
2195 Account const bob{"bob"};
2196 Account const carol{"carol"};
2197 Account const dan{"dan"};
2198
2199 auto wsc = makeWSClient(env.app().config());
2200
2201 MPTTester mptAlice(env, alice, {.holders = {bob}});
2202 MPTTester mptCarol(env, carol, {.holders = {dan}});
2203
2204 // Transfer fee is 10%
2205 mptAlice.create(
2206 {.transferFee = 10'000, .ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanLock});
2207 mptCarol.create({.ownerCount = 1, .flags = tfMPTCanTransfer});
2208
2209 json::Value stream;
2210 stream = json::ValueType::Object;
2211 stream[jss::mpt_issuances] = json::ValueType::Array;
2212 stream[jss::mpt_issuances].append(to_string(mptAlice.issuanceID()));
2213 stream[jss::mpt_issuances].append(to_string(mptCarol.issuanceID()));
2214 auto jv = wsc->invoke("subscribe", stream);
2215 BEAST_EXPECT(jv[jss::status] == "success");
2216
2217 // bob create MPToken
2218 mptAlice.authorize({.account = bob});
2219 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
2220 return jv[jss::engine_result] == "tesSUCCESS" &&
2221 jv[jss::transaction][jss::Account] == bob.human() &&
2222 jv[jss::transaction][jss::Flags] == 0 &&
2223 jv[jss::transaction][sfMPTokenIssuanceID.jsonName] ==
2224 to_string(mptAlice.issuanceID()) &&
2225 jv[jss::transaction][jss::TransactionType] == "MPTokenAuthorize" &&
2226 jv[jss::type] == "mptTransaction";
2227 }));
2228
2229 // dan create MPToken
2230 mptCarol.authorize({.account = dan});
2231 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
2232 return jv[jss::engine_result] == "tesSUCCESS" &&
2233 jv[jss::transaction][jss::Account] == dan.human() &&
2234 jv[jss::transaction][jss::Flags] == 0 &&
2235 jv[jss::transaction][sfMPTokenIssuanceID.jsonName] ==
2236 to_string(mptCarol.issuanceID()) &&
2237 jv[jss::transaction][jss::Sequence] == 5 &&
2238 jv[jss::transaction][jss::TransactionType] == "MPTokenAuthorize" &&
2239 jv[jss::type] == "mptTransaction";
2240 }));
2241
2242 // subscribe stream sees alice's MPT
2243 mptAlice.pay(alice, bob, 2000);
2244 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
2245 return jv[jss::engine_result] == "tesSUCCESS" &&
2246 jv[jss::transaction][jss::Account] == alice.human() &&
2247 jv[jss::transaction][jss::Amount][jss::mpt_issuance_id] ==
2248 to_string(mptAlice.issuanceID()) &&
2249 jv[jss::transaction][jss::Amount][jss::value] == "2000" &&
2250 jv[jss::transaction][jss::DeliverMax][jss::mpt_issuance_id] ==
2251 to_string(mptAlice.issuanceID()) &&
2252 jv[jss::transaction][jss::DeliverMax][jss::value] == "2000" &&
2253 jv[jss::transaction][jss::Destination] == bob.human() &&
2254 jv[jss::transaction][jss::Flags] == tfFullyCanonicalSig &&
2255 jv[jss::transaction][jss::TransactionType] == "Payment" &&
2256 jv[jss::type] == "mptTransaction";
2257 }));
2258
2259 // subscribe stream sees carol's MPT
2260 mptCarol.pay(carol, dan, 1000);
2261 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
2262 return jv[jss::engine_result] == "tesSUCCESS" &&
2263 jv[jss::transaction][jss::Account] == carol.human() &&
2264 jv[jss::transaction][jss::Amount][jss::mpt_issuance_id] ==
2265 to_string(mptCarol.issuanceID()) &&
2266 jv[jss::transaction][jss::Amount][jss::value] == "1000" &&
2267 jv[jss::transaction][jss::DeliverMax][jss::mpt_issuance_id] ==
2268 to_string(mptCarol.issuanceID()) &&
2269 jv[jss::transaction][jss::DeliverMax][jss::value] == "1000" &&
2270 jv[jss::transaction][jss::Destination] == dan.human() &&
2271 jv[jss::transaction][jss::Flags] == tfFullyCanonicalSig &&
2272 jv[jss::transaction][jss::Sequence] == 6 &&
2273 jv[jss::transaction][jss::TransactionType] == "Payment" &&
2274 jv[jss::type] == "mptTransaction";
2275 }));
2276
2277 // subscribe stream sees alice's MPT lock
2278 mptAlice.set({.account = alice, .flags = tfMPTLock});
2279 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
2280 return jv[jss::engine_result] == "tesSUCCESS" &&
2281 jv[jss::transaction][jss::Account] == alice.human() &&
2282 jv[jss::transaction][jss::Flags] == tfMPTLock &&
2283 jv[jss::transaction][sfMPTokenIssuanceID.jsonName] ==
2284 to_string(mptAlice.issuanceID()) &&
2285 jv[jss::transaction][jss::TransactionType] == "MPTokenIssuanceSet" &&
2286 jv[jss::type] == "mptTransaction";
2287 }));
2288
2289 // subscribe stream sees alice's MPT unlock
2290 mptAlice.set({.account = alice, .flags = tfMPTUnlock});
2291 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
2292 return jv[jss::engine_result] == "tesSUCCESS" &&
2293 jv[jss::transaction][jss::Account] == alice.human() &&
2294 jv[jss::transaction][jss::Flags] == tfMPTUnlock &&
2295 jv[jss::transaction][sfMPTokenIssuanceID.jsonName] ==
2296 to_string(mptAlice.issuanceID()) &&
2297 jv[jss::transaction][jss::TransactionType] == "MPTokenIssuanceSet" &&
2298 jv[jss::type] == "mptTransaction";
2299 }));
2300
2301 // unsub alice's MPT from the stream
2302 json::Value unsubStream;
2303 unsubStream = json::ValueType::Object;
2304 unsubStream[jss::mpt_issuances] = json::ValueType::Array;
2305 unsubStream[jss::mpt_issuances].append(to_string(mptAlice.issuanceID()));
2306 auto unsubJv = wsc->invoke("unsubscribe", unsubStream);
2307 BEAST_EXPECT(unsubJv[jss::status] == "success");
2308
2309 // bob pays alice, this txn should no longer be sent by the stream
2310 mptAlice.pay(bob, alice, 500);
2311
2312 // we don't expect to find alice's mpt in the stream
2313 BEAST_EXPECT(!wsc->findMsg(5s, [&](auto const& jv) {
2314 return jv[jss::transaction][jss::Amount][jss::mpt_issuance_id] ==
2315 to_string(mptAlice.issuanceID()) &&
2316 jv[jss::transaction][jss::Amount][jss::value] == "500";
2317 }));
2318
2319 // this txn should be seen
2320 mptCarol.pay(dan, carol, 100);
2321
2322 // only carol's MPT txn will be seen
2323 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
2324 return jv[jss::engine_result] == "tesSUCCESS" &&
2325 jv[jss::transaction][jss::Account] == dan.human() &&
2326 jv[jss::transaction][jss::Amount][jss::mpt_issuance_id] ==
2327 to_string(mptCarol.issuanceID()) &&
2328 jv[jss::transaction][jss::Amount][jss::value] == "100" &&
2329 jv[jss::transaction][jss::DeliverMax][jss::mpt_issuance_id] ==
2330 to_string(mptCarol.issuanceID()) &&
2331 jv[jss::transaction][jss::DeliverMax][jss::value] == "100" &&
2332 jv[jss::transaction][jss::Destination] == carol.human() &&
2333 jv[jss::transaction][jss::Flags] == tfFullyCanonicalSig &&
2334 jv[jss::transaction][jss::TransactionType] == "Payment" &&
2335 jv[jss::type] == "mptTransaction";
2336 }));
2337 }
2338
2339 void
2341 {
2342 // MPTs destroyed by the inner transactions of a Batch are still seen by
2343 // the stream. An MPTokenIssuance entry does not carry its own issuance
2344 // id, so this exercises deriving the id from the metadata.
2345 testcase("SubMPT batch");
2346 using namespace jtx;
2347 using namespace std::chrono_literals;
2348
2349 Env env(*this);
2350
2351 Account const alice{"alice"};
2352
2353 auto wsc = makeWSClient(env.app().config());
2354
2355 // A Batch requires at least two inner transactions, so destroy two
2356 // issuances at once.
2357 MPTTester mptAlice1(env, alice, MPTInit{});
2358 mptAlice1.create({.ownerCount = 1});
2359 auto const mptID1 = mptAlice1.issuanceID();
2360
2361 MPTTester mptAlice2(env, alice, MPTInit{.fund = false});
2362 mptAlice2.create({.ownerCount = 2});
2363 auto const mptID2 = mptAlice2.issuanceID();
2364
2365 json::Value stream;
2366 stream = json::ValueType::Object;
2367 stream[jss::mpt_issuances] = json::ValueType::Array;
2368 stream[jss::mpt_issuances].append(to_string(mptID1));
2369 stream[jss::mpt_issuances].append(to_string(mptID2));
2370 auto const jv = wsc->invoke("subscribe", stream);
2371 BEAST_EXPECT(jv[jss::status] == "success");
2372
2373 // destroy both issuances from inside a Batch
2374 auto const seq = env.seq(alice);
2375 auto const batchFee = batch::calcBatchFee(env, 0, 2);
2376 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
2377 batch::Inner(mptAlice1.destroyJV({.issuer = alice, .id = mptID1}), seq + 1),
2378 batch::Inner(mptAlice2.destroyJV({.issuer = alice, .id = mptID2}), seq + 2));
2379 env.close();
2380
2381 // each inner transaction is published on the stream for its own issuance
2382 for (auto const& mptID : {mptID1, mptID2})
2383 {
2384 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
2385 return jv[jss::engine_result] == "tesSUCCESS" &&
2386 jv[jss::transaction][jss::Account] == alice.human() &&
2387 jv[jss::transaction][sfMPTokenIssuanceID.jsonName] == to_string(mptID) &&
2388 jv[jss::transaction][jss::TransactionType] == "MPTokenIssuanceDestroy" &&
2389 jv[jss::type] == "mptTransaction";
2390 }));
2391 }
2392
2393 // the outer Batch transaction is not itself published on the stream;
2394 // only the inner transactions touch the issuances
2395 BEAST_EXPECT(!wsc->findMsg(1s, [](auto const& jv) {
2396 return jv[jss::transaction][jss::TransactionType] == "Batch";
2397 }));
2398
2399 json::Value unsubStream;
2400 unsubStream = json::ValueType::Object;
2401 unsubStream[jss::mpt_issuances] = json::ValueType::Array;
2402 unsubStream[jss::mpt_issuances].append(to_string(mptID1));
2403 unsubStream[jss::mpt_issuances].append(to_string(mptID2));
2404 BEAST_EXPECT(wsc->invoke("unsubscribe", unsubStream)[jss::status] == "success");
2405 }
2406
2407 void
2409 {
2410 testcase("SubMPT amount fields");
2411 using namespace jtx;
2412 using namespace std::chrono_literals;
2413
2414 Env env(*this);
2415 Account const alice{"alice"};
2416 Account const bob{"bob"};
2417 Account const carol{"carol"};
2418
2419 MPTTester mpt(env, alice, {.holders = {bob, carol}});
2420 mpt.create({.pay = {{{bob}, 100}}, .flags = kMptDexFlags | tfMPTCanEscrow});
2421
2422 auto wsc = makeWSClient(env.app().config());
2423 auto const subscribed =
2424 wsc->invoke("subscribe", mptIssuancesRequest({to_string(mpt.issuanceID())}));
2425 BEAST_EXPECT(subscribed[jss::status] == "success");
2426
2427 auto expectTransaction = [&](std::string const& type) {
2428 BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
2429 return jv[jss::engine_result] == "tesSUCCESS" &&
2430 jv[jss::transaction][jss::TransactionType] == type &&
2431 jv[jss::type] == "mptTransaction";
2432 }));
2433 };
2434
2435 // Created offers carry the issuance only inside their amount fields.
2436 env(offer(bob, XRP(10), mpt(10)));
2437 env.close();
2438 expectTransaction("OfferCreate");
2439
2440 env(offer(carol, mpt(5), XRP(10)));
2441 env.close();
2442 expectTransaction("OfferCreate");
2443
2444 // Escrow entries similarly carry the issuance in sfAmount.
2445 auto const baseFee = env.current()->fees().base;
2446 env(escrow::create(bob, carol, mpt(10)),
2447 escrow::kFinishTime(env.now() + 1s),
2448 Fee(baseFee * 150));
2449 env.close();
2450 expectTransaction("EscrowCreate");
2451 }
2452
2453 void
2490};
2491
2493
2494} // 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 std::vector< std::string > makeMPTIssuanceStrings(std::size_t count, std::uint32_t seed=1)
static json::Value accountsProposedRequest(std::vector< std::string > const &accts)
void testSubErrors(bool subscribe)
static json::Value mptIssuancesRequest(std::vector< std::string > const &mptIDs)
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
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
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
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:302
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:1058
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
NetClock::time_point now()
Returns the current network time.
Definition Env.h:326
Set the fee on a JTx.
Definition fee.h:20
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:512
void create(MPTCreate const &arg=MPTCreate{}, std::source_location const &loc=std::source_location::current())
Definition mpt.cpp:343
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
Adds an inner Batch transaction to a JTx and autofills it.
Definition batch.h:91
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:453
API version numbers used in later API versions.
Definition ApiVersion.h:36
json::Value outer(jtx::Account const &account, uint32_t seq, STAmount const &fee, std::uint32_t flags)
Build an outer Batch transaction JSON object.
Definition batch.cpp:55
XRPAmount calcBatchFee(jtx::Env const &env, uint32_t const &numSigners, uint32_t const &txns=0)
Calculate the expected outer Batch transaction fee.
Definition batch.cpp:37
json::Value create(AccountID const &account, AccountID const &to, STAmount const &amount)
Definition escrow.cpp:24
auto const kFinishTime
Set the "FinishAfter" time tag on a JTx.
Definition escrow.h:78
json::Value mint(jtx::Account const &account, std::uint32_t nfTokenTaxon)
Mint an NFToken.
Definition token.cpp:23
json::Value brokerOffers(jtx::Account const &account, UInt256 const &buyOfferIndex, UInt256 const &sellOfferIndex)
Broker two NFToken offers.
Definition token.cpp:179
json::Value createOffer(jtx::Account const &account, UInt256 const &nftokenID, STAmount const &amount)
Create an NFTokenOffer.
Definition token.cpp:96
json::Value cancelOffer(jtx::Account const &account, std::initializer_list< UInt256 > const &nftokenOffers)
Cancel NFTokenOffers.
Definition token.cpp:141
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 acceptBuyOffer(jtx::Account const &account, UInt256 const &offerIndex)
Accept an NFToken buy offer.
Definition token.cpp:159
auto const kMptDexFlags
Definition mpt.h:47
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 >)
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:38
std::unique_ptr< Config > noAdmin(std::unique_ptr< Config >)
adjust config so no admin ports are enabled
Definition envconfig.cpp:75
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:657
BaseUInt< 256 > UInt256
Definition base_uint.h:580
BaseUInt< 192 > MPTID
MPTID is a 192-bit value representing MPT Issuance ID, which is a concatenation of a 32-bit sequence ...
Definition UintTypes.h:54
constexpr std::uint32_t kVfFullValidation
constexpr std::uint32_t kVfFullyCanonicalSig
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
std::unordered_map< Key, Value, Hash, Pred, Allocator > HashMap
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
Arguments for initializing funded MPT test accounts and issuance.
Definition mpt.h:189
Set the sequence number on a JTx.
Definition seq.h:16
T to_string(T... args)