rippled
Loading...
Searching...
No Matches
AccountTx_test.cpp
1#include <test/jtx.h>
2#include <test/jtx/envconfig.h>
3
4#include <xrpl/beast/unit_test.h>
5#include <xrpl/beast/unit_test/suite.h>
6#include <xrpl/protocol/ErrorCodes.h>
7#include <xrpl/protocol/TxFlags.h>
8#include <xrpl/protocol/jss.h>
9
10#include <boost/container/flat_set.hpp>
11
12namespace xrpl {
13
14namespace test {
15
17{
18 // A data structure used to describe the basic structure of a
19 // transactions array node as returned by the account_tx RPC command.
21 {
22 int const index;
24 boost::container::flat_set<std::string> created;
25 boost::container::flat_set<std::string> deleted;
26 boost::container::flat_set<std::string> modified;
27
29 int idx,
30 Json::StaticString const& t,
34 : index(idx), txType(t)
35 {
36 auto buildSet = [](auto&& init) {
37 boost::container::flat_set<std::string> r;
38 r.reserve(init.size());
39 for (auto&& s : init)
40 r.insert(s);
41 return r;
42 };
43
44 created = buildSet(c);
45 deleted = buildSet(d);
46 modified = buildSet(m);
47 }
48 };
49
50 // A helper method tests can use to validate returned JSON vs NodeSanity.
51 void
53 {
54 BEAST_EXPECT(txNode[jss::validated].asBool() == true);
55 BEAST_EXPECT(txNode[jss::tx][sfTransactionType.jsonName].asString() == sane.txType);
56
57 // Make sure all of the expected node types are present.
58 boost::container::flat_set<std::string> createdNodes;
59 boost::container::flat_set<std::string> deletedNodes;
60 boost::container::flat_set<std::string> modifiedNodes;
61
62 for (Json::Value const& metaNode : txNode[jss::meta][sfAffectedNodes.jsonName])
63 {
64 if (metaNode.isMember(sfCreatedNode.jsonName))
65 {
66 createdNodes.insert(
67 metaNode[sfCreatedNode.jsonName][sfLedgerEntryType.jsonName].asString());
68 }
69 else if (metaNode.isMember(sfDeletedNode.jsonName))
70 {
71 deletedNodes.insert(
72 metaNode[sfDeletedNode.jsonName][sfLedgerEntryType.jsonName].asString());
73 }
74 else if (metaNode.isMember(sfModifiedNode.jsonName))
75 {
76 modifiedNodes.insert(
77 metaNode[sfModifiedNode.jsonName][sfLedgerEntryType.jsonName].asString());
78 }
79 else
80 {
81 fail("Unexpected or unlabeled node type in metadata.", __FILE__, __LINE__);
82 }
83 }
84
85 BEAST_EXPECT(createdNodes == sane.created);
86 BEAST_EXPECT(deletedNodes == sane.deleted);
87 BEAST_EXPECT(modifiedNodes == sane.modified);
88 };
89
90 void
91 testParameters(unsigned int apiVersion)
92 {
93 testcase("Parameters APIv" + std::to_string(apiVersion));
94 using namespace test::jtx;
95
96 Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
97 cfg->FEES.reference_fee = 10;
98 return cfg;
99 }));
100 Account const A1{"A1"};
101 env.fund(XRP(10000), A1);
102 env.close();
103
104 // Ledger 3 has the two txs associated with funding the account
105 // All other ledgers have no txs
106
107 auto hasTxs = [apiVersion](Json::Value const& j) {
108 switch (apiVersion)
109 {
110 case 1:
111 return j.isMember(jss::result) && (j[jss::result][jss::status] == "success") &&
112 (j[jss::result][jss::transactions].size() == 2) &&
113 (j[jss::result][jss::transactions][0u][jss::tx][jss::TransactionType] ==
114 jss::AccountSet) &&
115 (j[jss::result][jss::transactions][1u][jss::tx][jss::TransactionType] ==
116 jss::Payment) &&
117 (j[jss::result][jss::transactions][1u][jss::tx][jss::DeliverMax] ==
118 "10000000010") &&
119 (j[jss::result][jss::transactions][1u][jss::tx][jss::Amount] ==
120 j[jss::result][jss::transactions][1u][jss::tx][jss::DeliverMax]);
121 case 2:
122 case 3:
123 if (j.isMember(jss::result) && (j[jss::result][jss::status] == "success") &&
124 (j[jss::result][jss::transactions].size() == 2) &&
125 (j[jss::result][jss::transactions][0u][jss::tx_json]
126 [jss::TransactionType] == jss::AccountSet))
127 {
128 auto const& payment = j[jss::result][jss::transactions][1u];
129
130 return (payment.isMember(jss::tx_json)) &&
131 (payment[jss::tx_json][jss::TransactionType] == jss::Payment) &&
132 (payment[jss::tx_json][jss::DeliverMax] == "10000000010") &&
133 (!payment[jss::tx_json].isMember(jss::Amount)) &&
134 (!payment[jss::tx_json].isMember(jss::hash)) &&
135 (payment[jss::hash] ==
136 "9F3085D85F472D1CC29627F260DF68EDE59D42D1D0C33E345"
137 "ECF0D4CE981D0A8") &&
138 (payment[jss::validated] == true) &&
139 (payment[jss::ledger_index] == 3) &&
140 (payment[jss::ledger_hash] ==
141 "5476DCD816EA04CBBA57D47BBF1FC58A5217CC93A5ADD79CB"
142 "580A5AFDD727E33") &&
143 (payment[jss::close_time_iso] == "2000-01-01T00:00:10Z");
144 }
145 else
146 {
147 return false;
148 }
149
150 default:
151 return false;
152 }
153 };
154
155 auto noTxs = [](Json::Value const& j) {
156 return j.isMember(jss::result) && (j[jss::result][jss::status] == "success") &&
157 (j[jss::result][jss::transactions].size() == 0);
158 };
159
160 auto isErr = [](Json::Value const& j, error_code_i code) {
161 return j.isMember(jss::result) && j[jss::result].isMember(jss::error) &&
162 j[jss::result][jss::error] == RPC::get_error_info(code).token;
163 };
164
165 Json::Value jParams;
166 jParams[jss::api_version] = apiVersion;
167
168 BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(jParams)), rpcINVALID_PARAMS));
169
170 jParams[jss::account] = "0xDEADBEEF";
171
172 BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(jParams)), rpcACT_MALFORMED));
173
174 jParams[jss::account] = A1.human();
175 BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(jParams))));
176
177 // Ledger min/max index
178 {
179 Json::Value p{jParams};
180 p[jss::ledger_index_min] = -1;
181 p[jss::ledger_index_max] = -1;
182 BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p))));
183
184 p[jss::ledger_index_min] = 0;
185 p[jss::ledger_index_max] = 100;
186 if (apiVersion < 2u)
187 {
188 BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p))));
189 }
190 else
191 {
192 BEAST_EXPECT(
193 isErr(env.rpc("json", "account_tx", to_string(p)), rpcLGR_IDX_MALFORMED));
194 }
195
196 p[jss::ledger_index_min] = 1;
197 p[jss::ledger_index_max] = 2;
198 if (apiVersion < 2u)
199 {
200 BEAST_EXPECT(noTxs(env.rpc("json", "account_tx", to_string(p))));
201 }
202 else
203 {
204 BEAST_EXPECT(
205 isErr(env.rpc("json", "account_tx", to_string(p)), rpcLGR_IDX_MALFORMED));
206 }
207
208 p[jss::ledger_index_min] = 2;
209 p[jss::ledger_index_max] = 1;
210 BEAST_EXPECT(isErr(
211 env.rpc("json", "account_tx", to_string(p)),
212 (apiVersion == 1 ? rpcLGR_IDXS_INVALID : rpcINVALID_LGR_RANGE)));
213 }
214 // Ledger index min only
215 {
216 Json::Value p{jParams};
217 p[jss::ledger_index_min] = -1;
218 BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p))));
219
220 p[jss::ledger_index_min] = 1;
221 if (apiVersion < 2u)
222 {
223 BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p))));
224 }
225 else
226 {
227 BEAST_EXPECT(
228 isErr(env.rpc("json", "account_tx", to_string(p)), rpcLGR_IDX_MALFORMED));
229 }
230
231 p[jss::ledger_index_min] = env.current()->header().seq;
232 BEAST_EXPECT(isErr(
233 env.rpc("json", "account_tx", to_string(p)),
234 (apiVersion == 1 ? rpcLGR_IDXS_INVALID : rpcINVALID_LGR_RANGE)));
235 }
236
237 // Ledger index max only
238 {
239 Json::Value p{jParams};
240 p[jss::ledger_index_max] = -1;
241 BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p))));
242
243 p[jss::ledger_index_max] = env.current()->header().seq;
244 if (apiVersion < 2u)
245 {
246 BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p))));
247 }
248 else
249 {
250 BEAST_EXPECT(
251 isErr(env.rpc("json", "account_tx", to_string(p)), rpcLGR_IDX_MALFORMED));
252 }
253
254 p[jss::ledger_index_max] = 3;
255 BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p))));
256
257 p[jss::ledger_index_max] = env.closed()->header().seq;
258 BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p))));
259
260 p[jss::ledger_index_max] = env.closed()->header().seq - 1;
261 BEAST_EXPECT(noTxs(env.rpc("json", "account_tx", to_string(p))));
262 }
263
264 // Ledger Sequence
265 {
266 Json::Value p{jParams};
267
268 p[jss::ledger_index] = env.closed()->header().seq;
269 BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p))));
270
271 p[jss::ledger_index] = env.closed()->header().seq - 1;
272 BEAST_EXPECT(noTxs(env.rpc("json", "account_tx", to_string(p))));
273
274 p[jss::ledger_index] = env.current()->header().seq;
275 BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), rpcLGR_NOT_VALIDATED));
276
277 p[jss::ledger_index] = env.current()->header().seq + 1;
278 BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), rpcLGR_NOT_FOUND));
279 }
280
281 // Ledger Hash
282 {
283 Json::Value p{jParams};
284
285 p[jss::ledger_hash] = to_string(env.closed()->header().hash);
286 BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p))));
287
288 p[jss::ledger_hash] = to_string(env.closed()->header().parentHash);
289 BEAST_EXPECT(noTxs(env.rpc("json", "account_tx", to_string(p))));
290 }
291
292 // Ledger index max/min/index all specified
293 // ERRORS out with invalid Parenthesis
294 {
295 jParams[jss::account] = "0xDEADBEEF";
296 jParams[jss::account] = A1.human();
297 Json::Value p{jParams};
298
299 p[jss::ledger_index_max] = -1;
300 p[jss::ledger_index_min] = -1;
301 p[jss::ledger_index] = -1;
302
303 if (apiVersion < 2u)
304 {
305 BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p))));
306 }
307 else
308 {
309 BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), rpcINVALID_PARAMS));
310 }
311 }
312
313 // Ledger index max only
314 {
315 Json::Value p{jParams};
316 p[jss::ledger_index_max] = env.current()->header().seq;
317 if (apiVersion < 2u)
318 {
319 BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p))));
320 }
321 else
322 {
323 BEAST_EXPECT(
324 isErr(env.rpc("json", "account_tx", to_string(p)), rpcLGR_IDX_MALFORMED));
325 }
326 }
327 // test account non-string
328 {
329 auto testInvalidAccountParam = [&](auto const& param) {
330 Json::Value params;
331 params[jss::account] = param;
332 auto jrr = env.rpc("json", "account_tx", to_string(params))[jss::result];
333 BEAST_EXPECT(jrr[jss::error] == "invalidParams");
334 BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'account'.");
335 };
336
337 testInvalidAccountParam(1);
338 testInvalidAccountParam(1.1);
339 testInvalidAccountParam(true);
340 testInvalidAccountParam(Json::Value(Json::nullValue));
341 testInvalidAccountParam(Json::Value(Json::objectValue));
342 testInvalidAccountParam(Json::Value(Json::arrayValue));
343 }
344 // test binary and forward for bool/non bool values
345 {
346 Json::Value p{jParams};
347 p[jss::binary] = "asdf";
348 if (apiVersion < 2u)
349 {
350 Json::Value result{env.rpc("json", "account_tx", to_string(p))};
351 BEAST_EXPECT(result[jss::result][jss::status] == "success");
352 }
353 else
354 {
355 BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), rpcINVALID_PARAMS));
356 }
357
358 p[jss::binary] = true;
359 Json::Value result{env.rpc("json", "account_tx", to_string(p))};
360 BEAST_EXPECT(result[jss::result][jss::status] == "success");
361
362 p[jss::forward] = "true";
363 if (apiVersion < 2u)
364 {
365 BEAST_EXPECT(result[jss::result][jss::status] == "success");
366 }
367 else
368 {
369 BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), rpcINVALID_PARAMS));
370 }
371
372 p[jss::forward] = false;
373 result = env.rpc("json", "account_tx", to_string(p));
374 BEAST_EXPECT(result[jss::result][jss::status] == "success");
375 }
376 // test limit with malformed values
377 {
378 Json::Value p{jParams};
379
380 // Test case: limit = 0 should fail (below minimum)
381 p[jss::limit] = 0;
382 BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), rpcINVALID_PARAMS));
383
384 // Test case: limit = 1.2 should fail (not an integer)
385 p[jss::limit] = 1.2;
386 BEAST_EXPECT(
387 env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
388 RPC::expected_field_message(jss::limit, "unsigned integer"));
389
390 // Test case: limit = "10" should fail (string instead of integer)
391 p[jss::limit] = "10";
392 BEAST_EXPECT(
393 env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
394 RPC::expected_field_message(jss::limit, "unsigned integer"));
395
396 // Test case: limit = true should fail (boolean instead of integer)
397 p[jss::limit] = true;
398 BEAST_EXPECT(
399 env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
400 RPC::expected_field_message(jss::limit, "unsigned integer"));
401
402 // Test case: limit = false should fail (boolean instead of integer)
403 p[jss::limit] = false;
404 BEAST_EXPECT(
405 env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
406 RPC::expected_field_message(jss::limit, "unsigned integer"));
407
408 // Test case: limit = -1 should fail (negative number)
409 p[jss::limit] = -1;
410 BEAST_EXPECT(
411 env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
412 RPC::expected_field_message(jss::limit, "unsigned integer"));
413
414 // Test case: limit = [] should fail (array instead of integer)
415 p[jss::limit] = Json::Value(Json::arrayValue);
416 BEAST_EXPECT(
417 env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
418 RPC::expected_field_message(jss::limit, "unsigned integer"));
419
420 // Test case: limit = {} should fail (object instead of integer)
421 p[jss::limit] = Json::Value(Json::objectValue);
422 BEAST_EXPECT(
423 env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
424 RPC::expected_field_message(jss::limit, "unsigned integer"));
425
426 // Test case: limit = "malformed" should fail (malformed string)
427 p[jss::limit] = "malformed";
428 BEAST_EXPECT(
429 env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
430 RPC::expected_field_message(jss::limit, "unsigned integer"));
431
432 // Test case: limit = ["limit"] should fail (array with string)
433 p[jss::limit] = Json::Value(Json::arrayValue);
434 p[jss::limit].append("limit");
435 BEAST_EXPECT(
436 env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
437 RPC::expected_field_message(jss::limit, "unsigned integer"));
438
439 // Test case: limit = {"limit": 10} should fail (object with
440 // property)
441 p[jss::limit] = Json::Value(Json::objectValue);
442 p[jss::limit][jss::limit] = 10;
443 BEAST_EXPECT(
444 env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
445 RPC::expected_field_message(jss::limit, "unsigned integer"));
446
447 // Test case: limit = 10 should succeed (valid integer)
448 p[jss::limit] = 10;
449 BEAST_EXPECT(
450 env.rpc("json", "account_tx", to_string(p))[jss::result][jss::status] == "success");
451 }
452 }
453
454 void
456 {
457 testcase("Contents");
458
459 // Get results for all transaction types that can be associated
460 // with an account. Start by generating all transaction types.
461 using namespace test::jtx;
462 using namespace std::chrono_literals;
463
464 Env env(*this);
465 Account const alice{"alice"};
466 Account const alie{"alie"};
467 Account const gw{"gw"};
468 auto const USD{gw["USD"]};
469
470 env.fund(XRP(1000000), alice, gw);
471 env.close();
472
473 // AccountSet
474 env(noop(alice));
475
476 // Payment
477 env(pay(alice, gw, XRP(100)));
478
479 // Regular key set
480 env(regkey(alice, alie));
481 env.close();
482
483 // Trust and Offers
484 env(trust(alice, USD(200)), sig(alie));
485 std::uint32_t const offerSeq{env.seq(alice)};
486 env(offer(alice, USD(50), XRP(150)), sig(alie));
487 env.close();
488
489 env(offer_cancel(alice, offerSeq), sig(alie));
490 env.close();
491
492 // SignerListSet
493 env(signers(alice, 1, {{"bogie", 1}, {"demon", 1}}), sig(alie));
494
495 // Escrow
496 {
497 // Create an escrow. Requires either a CancelAfter or FinishAfter.
498 auto escrow = [](Account const& account, Account const& to, STAmount const& amount) {
499 Json::Value escrow;
500 escrow[jss::TransactionType] = jss::EscrowCreate;
501 escrow[jss::Account] = account.human();
502 escrow[jss::Destination] = to.human();
503 escrow[jss::Amount] = amount.getJson(JsonOptions::none);
504 return escrow;
505 };
506
507 NetClock::time_point const nextTime{env.now() + 2s};
508
509 Json::Value escrowWithFinish{escrow(alice, alice, XRP(500))};
510 escrowWithFinish[sfFinishAfter.jsonName] = nextTime.time_since_epoch().count();
511
512 std::uint32_t const escrowFinishSeq{env.seq(alice)};
513 env(escrowWithFinish, sig(alie));
514
515 Json::Value escrowWithCancel{escrow(alice, alice, XRP(500))};
516 escrowWithCancel[sfFinishAfter.jsonName] = nextTime.time_since_epoch().count();
517 escrowWithCancel[sfCancelAfter.jsonName] = nextTime.time_since_epoch().count() + 1;
518
519 std::uint32_t const escrowCancelSeq{env.seq(alice)};
520 env(escrowWithCancel, sig(alie));
521 env.close();
522
523 {
524 Json::Value escrowFinish;
525 escrowFinish[jss::TransactionType] = jss::EscrowFinish;
526 escrowFinish[jss::Account] = alice.human();
527 escrowFinish[sfOwner.jsonName] = alice.human();
528 escrowFinish[sfOfferSequence.jsonName] = escrowFinishSeq;
529 env(escrowFinish, sig(alie));
530 }
531 {
532 Json::Value escrowCancel;
533 escrowCancel[jss::TransactionType] = jss::EscrowCancel;
534 escrowCancel[jss::Account] = alice.human();
535 escrowCancel[sfOwner.jsonName] = alice.human();
536 escrowCancel[sfOfferSequence.jsonName] = escrowCancelSeq;
537 env(escrowCancel, sig(alie));
538 }
539 env.close();
540 }
541
542 // PayChan
543 {
544 std::uint32_t const payChanSeq{env.seq(alice)};
545 Json::Value payChanCreate;
546 payChanCreate[jss::TransactionType] = jss::PaymentChannelCreate;
547 payChanCreate[jss::Account] = alice.human();
548 payChanCreate[jss::Destination] = gw.human();
549 payChanCreate[jss::Amount] = XRP(500).value().getJson(JsonOptions::none);
550 payChanCreate[sfSettleDelay.jsonName] = NetClock::duration{100s}.count();
551 payChanCreate[sfPublicKey.jsonName] = strHex(alice.pk().slice());
552 env(payChanCreate, sig(alie));
553 env.close();
554
555 std::string const payChanIndex{strHex(keylet::payChan(alice, gw, payChanSeq).key)};
556
557 {
558 Json::Value payChanFund;
559 payChanFund[jss::TransactionType] = jss::PaymentChannelFund;
560 payChanFund[jss::Account] = alice.human();
561 payChanFund[sfChannel.jsonName] = payChanIndex;
562 payChanFund[jss::Amount] = XRP(200).value().getJson(JsonOptions::none);
563 env(payChanFund, sig(alie));
564 env.close();
565 }
566 {
567 Json::Value payChanClaim;
568 payChanClaim[jss::TransactionType] = jss::PaymentChannelClaim;
569 payChanClaim[jss::Flags] = tfClose;
570 payChanClaim[jss::Account] = gw.human();
571 payChanClaim[sfChannel.jsonName] = payChanIndex;
572 payChanClaim[sfPublicKey.jsonName] = strHex(alice.pk().slice());
573 env(payChanClaim);
574 env.close();
575 }
576 }
577
578 // Check
579 {
580 auto const aliceCheckId = keylet::check(alice, env.seq(alice)).key;
581 env(check::create(alice, gw, XRP(300)), sig(alie));
582
583 auto const gwCheckId = keylet::check(gw, env.seq(gw)).key;
584 env(check::create(gw, alice, XRP(200)));
585 env.close();
586
587 env(check::cash(alice, gwCheckId, XRP(200)), sig(alie));
588 env(check::cancel(alice, aliceCheckId), sig(alie));
589 env.close();
590 }
591 {
592 // Deposit pre-authorization with a Ticket.
593 std::uint32_t const tktSeq{env.seq(alice) + 1};
594 env(ticket::create(alice, 1), sig(alie));
595 env.close();
596
597 env(deposit::auth(alice, gw), ticket::use(tktSeq), sig(alie));
598 env.close();
599 }
600
601 // Setup is done. Look at the transactions returned by account_tx.
602 Json::Value params;
603 params[jss::account] = alice.human();
604 params[jss::ledger_index_min] = -1;
605 params[jss::ledger_index_max] = -1;
606
607 Json::Value const result{env.rpc("json", "account_tx", to_string(params))};
608
609 BEAST_EXPECT(result[jss::result][jss::status] == "success");
610 BEAST_EXPECT(result[jss::result][jss::transactions].isArray());
611
612 Json::Value const& txs{result[jss::result][jss::transactions]};
613
614 // clang-format off
615 // Do a sanity check on each returned transaction. They should
616 // be returned in the reverse order of application to the ledger.
617 static const NodeSanity sanity[]{
618 // txType, created, deleted, modified
619 {0, jss::DepositPreauth, {jss::DepositPreauth}, {jss::Ticket}, {jss::AccountRoot, jss::DirectoryNode}},
620 {1, jss::TicketCreate, {jss::Ticket}, {}, {jss::AccountRoot, jss::DirectoryNode}},
621 {2, jss::CheckCancel, {}, {jss::Check}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}},
622 {3, jss::CheckCash, {}, {jss::Check}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}},
623 {4, jss::CheckCreate, {jss::Check}, {}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}},
624 {5, jss::CheckCreate, {jss::Check}, {}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}},
625 {6, jss::PaymentChannelClaim, {}, {jss::PayChannel}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}},
626 {7, jss::PaymentChannelFund, {}, {}, {jss::AccountRoot, jss::PayChannel}},
627 {8, jss::PaymentChannelCreate, {jss::PayChannel}, {}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}},
628 {9, jss::EscrowCancel, {}, {jss::Escrow}, {jss::AccountRoot, jss::DirectoryNode}},
629 {10, jss::EscrowFinish, {}, {jss::Escrow}, {jss::AccountRoot, jss::DirectoryNode}},
630 {11, jss::EscrowCreate, {jss::Escrow}, {}, {jss::AccountRoot, jss::DirectoryNode}},
631 {12, jss::EscrowCreate, {jss::Escrow}, {}, {jss::AccountRoot, jss::DirectoryNode}},
632 {13, jss::SignerListSet, {jss::SignerList}, {}, {jss::AccountRoot, jss::DirectoryNode}},
633 {14, jss::OfferCancel, {}, {jss::Offer, jss::DirectoryNode}, {jss::AccountRoot, jss::DirectoryNode}},
634 {15, jss::OfferCreate, {jss::Offer, jss::DirectoryNode}, {}, {jss::AccountRoot, jss::DirectoryNode}},
635 {16, jss::TrustSet, {jss::RippleState, jss::DirectoryNode, jss::DirectoryNode}, {}, {jss::AccountRoot, jss::AccountRoot}},
636 {17, jss::SetRegularKey, {}, {}, {jss::AccountRoot}},
637 {18, jss::Payment, {}, {}, {jss::AccountRoot, jss::AccountRoot}},
638 {19, jss::AccountSet, {}, {}, {jss::AccountRoot}},
639 {20, jss::AccountSet, {}, {}, {jss::AccountRoot}},
640 {21, jss::Payment, {jss::AccountRoot}, {}, {jss::AccountRoot}},
641 };
642 // clang-format on
643
644 BEAST_EXPECT(std::size(sanity) == result[jss::result][jss::transactions].size());
645
646 for (unsigned int index{0}; index < std::size(sanity); ++index)
647 {
648 checkSanity(txs[index], sanity[index]);
649 }
650 }
651
652 void
654 {
655 testcase("AccountDelete");
656
657 // Verify that if an account is resurrected then the account_tx RPC
658 // command still recovers all transactions on that account before
659 // and after resurrection.
660 using namespace test::jtx;
661 using namespace std::chrono_literals;
662
663 Env env(*this);
664 Account const alice{"alice"};
665 Account const becky{"becky"};
666
667 env.fund(XRP(10000), alice, becky);
668 env.close();
669
670 // Verify that becky's account root is present.
671 Keylet const beckyAcctKey{keylet::account(becky.id())};
672 BEAST_EXPECT(env.closed()->exists(beckyAcctKey));
673
674 // becky does an AccountSet .
675 env(noop(becky));
676
677 // Close enough ledgers to be able to delete becky's account.
678 std::uint32_t const ledgerCount{env.current()->seq() + 257 - env.seq(becky)};
679
680 for (std::uint32_t i = 0; i < ledgerCount; ++i)
681 env.close();
682
683 auto const beckyPreDelBalance{env.balance(becky)};
684
685 auto const acctDelFee{drops(env.current()->fees().increment)};
686 env(acctdelete(becky, alice), fee(acctDelFee));
687 env.close();
688
689 // Verify that becky's account root is gone.
690 BEAST_EXPECT(!env.closed()->exists(beckyAcctKey));
691 env.close();
692
693 // clang-format off
694 // Do a sanity check on each returned transaction. They should
695 // be returned in the reverse order of application to the ledger.
696 //
697 // Note that the first two transactions in sanity have not occurred
698 // yet. We'll see those after becky's account is resurrected.
699 static const NodeSanity sanity[]
700 {
701 // txType, created, deleted, modified
702/* becky pays alice */ { 0, jss::Payment, {}, {}, {jss::AccountRoot, jss::AccountRoot}},
703/* alice resurrects becky's acct */ { 1, jss::Payment, {jss::AccountRoot}, {}, {jss::AccountRoot}},
704/* becky deletes her account */ { 2, jss::AccountDelete, {}, {jss::AccountRoot}, {jss::AccountRoot}},
705/* becky's noop */ { 3, jss::AccountSet, {}, {}, {jss::AccountRoot}},
706/* "fund" sets flags */ { 4, jss::AccountSet, {}, {}, {jss::AccountRoot}},
707/* "fund" creates becky's acct */ { 5, jss::Payment, {jss::AccountRoot}, {}, {jss::AccountRoot}}
708 };
709 // clang-format on
710
711 // Verify that we can recover becky's account_tx information even
712 // after the account is deleted.
713 {
714 Json::Value params;
715 params[jss::account] = becky.human();
716 params[jss::ledger_index_min] = -1;
717 params[jss::ledger_index_max] = -1;
718
719 Json::Value const result{env.rpc("json", "account_tx", to_string(params))};
720
721 BEAST_EXPECT(result[jss::result][jss::status] == "success");
722 BEAST_EXPECT(result[jss::result][jss::transactions].isArray());
723
724 // The first two transactions listed in sanity haven't happened yet.
725 constexpr unsigned int beckyDeletedOffset = 2;
726 BEAST_EXPECT(
727 std::size(sanity) ==
728 result[jss::result][jss::transactions].size() + beckyDeletedOffset);
729
730 Json::Value const& txs{result[jss::result][jss::transactions]};
731
732 for (unsigned int index = beckyDeletedOffset; index < std::size(sanity); ++index)
733 {
734 checkSanity(txs[index - beckyDeletedOffset], sanity[index]);
735 }
736 }
737
738 // All it takes is a large enough XRP payment to resurrect
739 // becky's account. Try too small a payment.
740 env(pay(alice, becky, drops(env.current()->fees().accountReserve(0)) - XRP(1)),
742 env.close();
743
744 // Actually resurrect becky's account.
745 env(pay(alice, becky, XRP(45)));
746 env.close();
747
748 // becky's account root should be back.
749 BEAST_EXPECT(env.closed()->exists(beckyAcctKey));
750 BEAST_EXPECT(env.balance(becky) == XRP(45));
751
752 // becky pays alice.
753 env(pay(becky, alice, XRP(20)));
754 env.close();
755
756 // Setup is done. Look at the transactions returned by account_tx.
757 // Verify that account_tx locates all of becky's transactions.
758 Json::Value params;
759 params[jss::account] = becky.human();
760 params[jss::ledger_index_min] = -1;
761 params[jss::ledger_index_max] = -1;
762
763 Json::Value const result{env.rpc("json", "account_tx", to_string(params))};
764
765 BEAST_EXPECT(result[jss::result][jss::status] == "success");
766 BEAST_EXPECT(result[jss::result][jss::transactions].isArray());
767
768 BEAST_EXPECT(std::size(sanity) == result[jss::result][jss::transactions].size());
769
770 Json::Value const& txs{result[jss::result][jss::transactions]};
771
772 for (unsigned int index = 0; index < std::size(sanity); ++index)
773 {
774 checkSanity(txs[index], sanity[index]);
775 }
776 }
777
778 void
780 {
781 testcase("MPT");
782
783 using namespace test::jtx;
784 using namespace std::chrono_literals;
785
786 auto cfg = makeConfig();
787 cfg->FEES.reference_fee = 10;
788 Env env(*this, std::move(cfg));
789
790 Account const alice{"alice"};
791 Account const bob{"bob"};
792 Account const carol{"carol"};
793
794 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
795
796 // check the latest mpt-related txn is in alice's account history
797 auto const checkAliceAcctTx = [&](size_t size, Json::StaticString txType) {
798 Json::Value params;
799 params[jss::account] = alice.human();
800 params[jss::limit] = 100;
801 auto const jv = env.rpc("json", "account_tx", to_string(params))[jss::result];
802
803 BEAST_EXPECT(jv[jss::transactions].size() == size);
804 auto const& tx0(jv[jss::transactions][0u][jss::tx]);
805 BEAST_EXPECT(tx0[jss::TransactionType] == txType);
806
807 std::string const txHash{env.tx()->getJson(JsonOptions::none)[jss::hash].asString()};
808 BEAST_EXPECT(tx0[jss::hash] == txHash);
809 };
810
811 // alice creates issuance
812 mptAlice.create(
813 {.ownerCount = 1,
814 .holderCount = 0,
815 .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer});
816
817 checkAliceAcctTx(3, jss::MPTokenIssuanceCreate);
818
819 // bob creates a MPToken;
820 mptAlice.authorize({.account = bob});
821 checkAliceAcctTx(4, jss::MPTokenAuthorize);
822 env.close();
823
824 // TODO: windows pipeline fails validation for the hardcoded ledger hash
825 // due to having different test config, it can be uncommented after
826 // figuring out what happened
827 //
828 // ledger hash should be fixed regardless any change to account history
829 // BEAST_EXPECT(
830 // to_string(env.closed()->header().hash) ==
831 // "0BD507BB87D3C0E73B462485E6E381798A8C82FC49BF17FE39C60E08A1AF035D");
832
833 // alice authorizes bob
834 mptAlice.authorize({.account = alice, .holder = bob});
835 checkAliceAcctTx(5, jss::MPTokenAuthorize);
836
837 // carol creates a MPToken;
838 mptAlice.authorize({.account = carol});
839 checkAliceAcctTx(6, jss::MPTokenAuthorize);
840
841 // alice authorizes carol
842 mptAlice.authorize({.account = alice, .holder = carol});
843 checkAliceAcctTx(7, jss::MPTokenAuthorize);
844
845 // alice pays bob 100 tokens
846 mptAlice.pay(alice, bob, 100);
847 checkAliceAcctTx(8, jss::Payment);
848
849 // bob pays carol 10 tokens
850 mptAlice.pay(bob, carol, 10);
851 checkAliceAcctTx(9, jss::Payment);
852 }
853
854public:
855 void
863};
864BEAST_DEFINE_TESTSUITE(AccountTx, rpc, xrpl);
865
866} // namespace test
867} // namespace xrpl
T bind_front(T... args)
Lightweight wrapper to tag static string.
Definition json_value.h:44
Represents a JSON value.
Definition json_value.h:130
bool isMember(char const *key) const
Return true if the object has a member named key.
A testsuite class.
Definition suite.h:51
testcase_t testcase
Memberspace for declaring test cases.
Definition suite.h:150
void fail(String const &reason, char const *file, int line)
Record a failure.
Definition suite.h:519
void run() override
Runs the suite.
void testParameters(unsigned int apiVersion)
void checkSanity(Json::Value const &txNode, NodeSanity const &sane)
Immutable cryptographic account descriptor.
Definition Account.h:19
A transaction testing environment.
Definition Env.h:122
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:100
std::shared_ptr< ReadView const > closed()
Returns the last closed ledger.
Definition Env.cpp:94
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:270
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:249
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:168
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:847
std::shared_ptr< STTx const > tx() const
Return the tx data for the last JTx.
Definition Env.cpp:505
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:329
NetClock::time_point now()
Returns the current network time.
Definition Env.h:282
Set the fee on a JTx.
Definition fee.h:17
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition rpc.h:15
Set the regular signature on a JTx.
Definition sig.h:15
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:15
Set a ticket sequence on a JTx.
Definition ticket.h:28
@ nullValue
'null' value
Definition json_value.h:19
@ arrayValue
array value (ordered list)
Definition json_value.h:25
@ objectValue
object value (collection of name/value pairs).
Definition json_value.h:26
std::string expected_field_message(std::string const &name, std::string const &type)
Definition ErrorCodes.h:285
ErrorInfo const & get_error_info(error_code_i code)
Returns an ErrorInfo that reflects the error code.
Keylet payChan(AccountID const &src, AccountID const &dst, std::uint32_t seq) noexcept
A PaymentChannel.
Definition Indexes.cpp:357
Keylet check(AccountID const &id, std::uint32_t seq) noexcept
A Check.
Definition Indexes.cpp:301
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:165
Json::Value cash(jtx::Account const &dest, uint256 const &checkId, STAmount const &amount)
Cash a check requiring that a specific amount be delivered.
Definition check.cpp:14
Json::Value create(A const &account, A const &dest, STAmount const &sendMax)
Create a check.
Json::Value cancel(jtx::Account const &dest, uint256 const &checkId)
Cancel a check.
Definition check.cpp:38
Json::Value auth(Account const &account, Account const &auth)
Preauthorize for deposit.
Definition deposit.cpp:13
Json::Value create(Account const &account, std::uint32_t count)
Create one of more tickets.
Definition ticket.cpp:12
Json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:13
Json::Value signers(Account const &account, std::uint32_t quorum, std::vector< signer > const &v)
Definition multisign.cpp:15
XRP_t const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:95
std::unique_ptr< Config > makeConfig(std::map< std::string, std::string > extraTxQ={}, std::map< std::string, std::string > extraVoting={})
Json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:11
Json::Value offer_cancel(Account const &account, std::uint32_t offerSeq)
Cancel an offer.
Definition offer.cpp:27
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:34
Json::Value regkey(Account const &account, disabled_t)
Disable the regular key.
Definition regkey.cpp:10
Json::Value acctdelete(Account const &account, Account const &dest)
Delete account.
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
Json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition offer.cpp:10
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:602
std::string strHex(FwdIt begin, FwdIt end)
Definition strHex.h:10
void forAllApiVersions(Fn const &fn, Args &&... args)
Definition ApiVersion.h:149
@ txNode
transaction plus metadata
@ tecNO_DST_INSUF_XRP
Definition TER.h:272
error_code_i
Definition ErrorCodes.h:20
@ rpcLGR_NOT_FOUND
Definition ErrorCodes.h:52
@ rpcLGR_NOT_VALIDATED
Definition ErrorCodes.h:53
@ rpcLGR_IDX_MALFORMED
Definition ErrorCodes.h:93
@ rpcLGR_IDXS_INVALID
Definition ErrorCodes.h:92
@ rpcINVALID_PARAMS
Definition ErrorCodes.h:64
@ rpcACT_MALFORMED
Definition ErrorCodes.h:70
@ rpcINVALID_LGR_RANGE
Definition ErrorCodes.h:116
T size(T... args)
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:19
uint256 key
Definition Keylet.h:20
Json::StaticString token
Definition ErrorCodes.h:190
boost::container::flat_set< std::string > modified
NodeSanity(int idx, Json::StaticString const &t, std::initializer_list< char const * > c, std::initializer_list< char const * > d, std::initializer_list< char const * > m)
boost::container::flat_set< std::string > created
boost::container::flat_set< std::string > deleted
T to_string(T... args)