#include #include #include #include #include #include #include #include namespace xrpl { namespace test { class AccountTx_test : public beast::unit_test::suite { // A data structure used to describe the basic structure of a // transactions array node as returned by the account_tx RPC command. struct NodeSanity { int const index; Json::StaticString const& txType; boost::container::flat_set created; boost::container::flat_set deleted; boost::container::flat_set modified; NodeSanity( int idx, Json::StaticString const& t, std::initializer_list c, std::initializer_list d, std::initializer_list m) : index(idx), txType(t) { auto buildSet = [](auto&& init) { boost::container::flat_set r; r.reserve(init.size()); for (auto&& s : init) r.insert(s); return r; }; created = buildSet(c); deleted = buildSet(d); modified = buildSet(m); } }; // A helper method tests can use to validate returned JSON vs NodeSanity. void checkSanity(Json::Value const& txNode, NodeSanity const& sane) { BEAST_EXPECT(txNode[jss::validated].asBool() == true); BEAST_EXPECT(txNode[jss::tx][sfTransactionType.jsonName].asString() == sane.txType); // Make sure all of the expected node types are present. boost::container::flat_set createdNodes; boost::container::flat_set deletedNodes; boost::container::flat_set modifiedNodes; for (Json::Value const& metaNode : txNode[jss::meta][sfAffectedNodes.jsonName]) { if (metaNode.isMember(sfCreatedNode.jsonName)) { createdNodes.insert( metaNode[sfCreatedNode.jsonName][sfLedgerEntryType.jsonName].asString()); } else if (metaNode.isMember(sfDeletedNode.jsonName)) { deletedNodes.insert( metaNode[sfDeletedNode.jsonName][sfLedgerEntryType.jsonName].asString()); } else if (metaNode.isMember(sfModifiedNode.jsonName)) { modifiedNodes.insert( metaNode[sfModifiedNode.jsonName][sfLedgerEntryType.jsonName].asString()); } else { fail("Unexpected or unlabeled node type in metadata.", __FILE__, __LINE__); } } BEAST_EXPECT(createdNodes == sane.created); BEAST_EXPECT(deletedNodes == sane.deleted); BEAST_EXPECT(modifiedNodes == sane.modified); }; void testParameters(unsigned int apiVersion) { testcase("Parameters APIv" + std::to_string(apiVersion)); using namespace test::jtx; Env env(*this, envconfig([](std::unique_ptr cfg) { cfg->FEES.reference_fee = 10; return cfg; })); Account const A1{"A1"}; env.fund(XRP(10000), A1); env.close(); // Ledger 3 has the two txs associated with funding the account // All other ledgers have no txs auto hasTxs = [apiVersion](Json::Value const& j) { switch (apiVersion) { case 1: return j.isMember(jss::result) && (j[jss::result][jss::status] == "success") && (j[jss::result][jss::transactions].size() == 2) && (j[jss::result][jss::transactions][0u][jss::tx][jss::TransactionType] == jss::AccountSet) && (j[jss::result][jss::transactions][1u][jss::tx][jss::TransactionType] == jss::Payment) && (j[jss::result][jss::transactions][1u][jss::tx][jss::DeliverMax] == "10000000010") && (j[jss::result][jss::transactions][1u][jss::tx][jss::Amount] == j[jss::result][jss::transactions][1u][jss::tx][jss::DeliverMax]); case 2: case 3: if (j.isMember(jss::result) && (j[jss::result][jss::status] == "success") && (j[jss::result][jss::transactions].size() == 2) && (j[jss::result][jss::transactions][0u][jss::tx_json] [jss::TransactionType] == jss::AccountSet)) { auto const& payment = j[jss::result][jss::transactions][1u]; return (payment.isMember(jss::tx_json)) && (payment[jss::tx_json][jss::TransactionType] == jss::Payment) && (payment[jss::tx_json][jss::DeliverMax] == "10000000010") && (!payment[jss::tx_json].isMember(jss::Amount)) && (!payment[jss::tx_json].isMember(jss::hash)) && (payment[jss::hash] == "9F3085D85F472D1CC29627F260DF68EDE59D42D1D0C33E345" "ECF0D4CE981D0A8") && (payment[jss::validated] == true) && (payment[jss::ledger_index] == 3) && (payment[jss::ledger_hash] == "5476DCD816EA04CBBA57D47BBF1FC58A5217CC93A5ADD79CB" "580A5AFDD727E33") && (payment[jss::close_time_iso] == "2000-01-01T00:00:10Z"); } else { return false; } default: return false; } }; auto noTxs = [](Json::Value const& j) { return j.isMember(jss::result) && (j[jss::result][jss::status] == "success") && (j[jss::result][jss::transactions].size() == 0); }; auto isErr = [](Json::Value const& j, error_code_i code) { return j.isMember(jss::result) && j[jss::result].isMember(jss::error) && j[jss::result][jss::error] == RPC::get_error_info(code).token; }; Json::Value jParams; jParams[jss::api_version] = apiVersion; BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(jParams)), rpcINVALID_PARAMS)); jParams[jss::account] = "0xDEADBEEF"; BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(jParams)), rpcACT_MALFORMED)); jParams[jss::account] = A1.human(); BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(jParams)))); // Ledger min/max index { Json::Value p{jParams}; p[jss::ledger_index_min] = -1; p[jss::ledger_index_max] = -1; BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p)))); p[jss::ledger_index_min] = 0; p[jss::ledger_index_max] = 100; if (apiVersion < 2u) { BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p)))); } else { BEAST_EXPECT( isErr(env.rpc("json", "account_tx", to_string(p)), rpcLGR_IDX_MALFORMED)); } p[jss::ledger_index_min] = 1; p[jss::ledger_index_max] = 2; if (apiVersion < 2u) { BEAST_EXPECT(noTxs(env.rpc("json", "account_tx", to_string(p)))); } else { BEAST_EXPECT( isErr(env.rpc("json", "account_tx", to_string(p)), rpcLGR_IDX_MALFORMED)); } p[jss::ledger_index_min] = 2; p[jss::ledger_index_max] = 1; BEAST_EXPECT(isErr( env.rpc("json", "account_tx", to_string(p)), (apiVersion == 1 ? rpcLGR_IDXS_INVALID : rpcINVALID_LGR_RANGE))); } // Ledger index min only { Json::Value p{jParams}; p[jss::ledger_index_min] = -1; BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p)))); p[jss::ledger_index_min] = 1; if (apiVersion < 2u) { BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p)))); } else { BEAST_EXPECT( isErr(env.rpc("json", "account_tx", to_string(p)), rpcLGR_IDX_MALFORMED)); } p[jss::ledger_index_min] = env.current()->header().seq; BEAST_EXPECT(isErr( env.rpc("json", "account_tx", to_string(p)), (apiVersion == 1 ? rpcLGR_IDXS_INVALID : rpcINVALID_LGR_RANGE))); } // Ledger index max only { Json::Value p{jParams}; p[jss::ledger_index_max] = -1; BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p)))); p[jss::ledger_index_max] = env.current()->header().seq; if (apiVersion < 2u) { BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p)))); } else { BEAST_EXPECT( isErr(env.rpc("json", "account_tx", to_string(p)), rpcLGR_IDX_MALFORMED)); } p[jss::ledger_index_max] = 3; BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p)))); p[jss::ledger_index_max] = env.closed()->header().seq; BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p)))); p[jss::ledger_index_max] = env.closed()->header().seq - 1; BEAST_EXPECT(noTxs(env.rpc("json", "account_tx", to_string(p)))); } // Ledger Sequence { Json::Value p{jParams}; p[jss::ledger_index] = env.closed()->header().seq; BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p)))); p[jss::ledger_index] = env.closed()->header().seq - 1; BEAST_EXPECT(noTxs(env.rpc("json", "account_tx", to_string(p)))); p[jss::ledger_index] = env.current()->header().seq; BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), rpcLGR_NOT_VALIDATED)); p[jss::ledger_index] = env.current()->header().seq + 1; BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), rpcLGR_NOT_FOUND)); } // Ledger Hash { Json::Value p{jParams}; p[jss::ledger_hash] = to_string(env.closed()->header().hash); BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p)))); p[jss::ledger_hash] = to_string(env.closed()->header().parentHash); BEAST_EXPECT(noTxs(env.rpc("json", "account_tx", to_string(p)))); } // Ledger index max/min/index all specified // ERRORS out with invalid Parenthesis { jParams[jss::account] = "0xDEADBEEF"; jParams[jss::account] = A1.human(); Json::Value p{jParams}; p[jss::ledger_index_max] = -1; p[jss::ledger_index_min] = -1; p[jss::ledger_index] = -1; if (apiVersion < 2u) { BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p)))); } else { BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), rpcINVALID_PARAMS)); } } // Ledger index max only { Json::Value p{jParams}; p[jss::ledger_index_max] = env.current()->header().seq; if (apiVersion < 2u) { BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p)))); } else { BEAST_EXPECT( isErr(env.rpc("json", "account_tx", to_string(p)), rpcLGR_IDX_MALFORMED)); } } // test account non-string { auto testInvalidAccountParam = [&](auto const& param) { Json::Value params; params[jss::account] = param; auto jrr = env.rpc("json", "account_tx", to_string(params))[jss::result]; BEAST_EXPECT(jrr[jss::error] == "invalidParams"); BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'account'."); }; testInvalidAccountParam(1); testInvalidAccountParam(1.1); testInvalidAccountParam(true); testInvalidAccountParam(Json::Value(Json::nullValue)); testInvalidAccountParam(Json::Value(Json::objectValue)); testInvalidAccountParam(Json::Value(Json::arrayValue)); } // test binary and forward for bool/non bool values { Json::Value p{jParams}; p[jss::binary] = "asdf"; if (apiVersion < 2u) { Json::Value result{env.rpc("json", "account_tx", to_string(p))}; BEAST_EXPECT(result[jss::result][jss::status] == "success"); } else { BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), rpcINVALID_PARAMS)); } p[jss::binary] = true; Json::Value result{env.rpc("json", "account_tx", to_string(p))}; BEAST_EXPECT(result[jss::result][jss::status] == "success"); p[jss::forward] = "true"; if (apiVersion < 2u) { BEAST_EXPECT(result[jss::result][jss::status] == "success"); } else { BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), rpcINVALID_PARAMS)); } p[jss::forward] = false; result = env.rpc("json", "account_tx", to_string(p)); BEAST_EXPECT(result[jss::result][jss::status] == "success"); } // test limit with malformed values { Json::Value p{jParams}; // Test case: limit = 0 should fail (below minimum) p[jss::limit] = 0; BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), rpcINVALID_PARAMS)); // Test case: limit = 1.2 should fail (not an integer) p[jss::limit] = 1.2; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == RPC::expected_field_message(jss::limit, "unsigned integer")); // Test case: limit = "10" should fail (string instead of integer) p[jss::limit] = "10"; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == RPC::expected_field_message(jss::limit, "unsigned integer")); // Test case: limit = true should fail (boolean instead of integer) p[jss::limit] = true; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == RPC::expected_field_message(jss::limit, "unsigned integer")); // Test case: limit = false should fail (boolean instead of integer) p[jss::limit] = false; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == RPC::expected_field_message(jss::limit, "unsigned integer")); // Test case: limit = -1 should fail (negative number) p[jss::limit] = -1; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == RPC::expected_field_message(jss::limit, "unsigned integer")); // Test case: limit = [] should fail (array instead of integer) p[jss::limit] = Json::Value(Json::arrayValue); BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == RPC::expected_field_message(jss::limit, "unsigned integer")); // Test case: limit = {} should fail (object instead of integer) p[jss::limit] = Json::Value(Json::objectValue); BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == RPC::expected_field_message(jss::limit, "unsigned integer")); // Test case: limit = "malformed" should fail (malformed string) p[jss::limit] = "malformed"; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == RPC::expected_field_message(jss::limit, "unsigned integer")); // Test case: limit = ["limit"] should fail (array with string) p[jss::limit] = Json::Value(Json::arrayValue); p[jss::limit].append("limit"); BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == RPC::expected_field_message(jss::limit, "unsigned integer")); // Test case: limit = {"limit": 10} should fail (object with // property) p[jss::limit] = Json::Value(Json::objectValue); p[jss::limit][jss::limit] = 10; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == RPC::expected_field_message(jss::limit, "unsigned integer")); // Test case: limit = 10 should succeed (valid integer) p[jss::limit] = 10; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::status] == "success"); } } void testContents() { testcase("Contents"); // Get results for all transaction types that can be associated // with an account. Start by generating all transaction types. using namespace test::jtx; using namespace std::chrono_literals; Env env(*this); Account const alice{"alice"}; Account const alie{"alie"}; Account const gw{"gw"}; auto const USD{gw["USD"]}; env.fund(XRP(1000000), alice, gw); env.close(); // AccountSet env(noop(alice)); // Payment env(pay(alice, gw, XRP(100))); // Regular key set env(regkey(alice, alie)); env.close(); // Trust and Offers env(trust(alice, USD(200)), sig(alie)); std::uint32_t const offerSeq{env.seq(alice)}; env(offer(alice, USD(50), XRP(150)), sig(alie)); env.close(); env(offer_cancel(alice, offerSeq), sig(alie)); env.close(); // SignerListSet env(signers(alice, 1, {{"bogie", 1}, {"demon", 1}}), sig(alie)); // Escrow { // Create an escrow. Requires either a CancelAfter or FinishAfter. auto escrow = [](Account const& account, Account const& to, STAmount const& amount) { Json::Value escrow; escrow[jss::TransactionType] = jss::EscrowCreate; escrow[jss::Account] = account.human(); escrow[jss::Destination] = to.human(); escrow[jss::Amount] = amount.getJson(JsonOptions::none); return escrow; }; NetClock::time_point const nextTime{env.now() + 2s}; Json::Value escrowWithFinish{escrow(alice, alice, XRP(500))}; escrowWithFinish[sfFinishAfter.jsonName] = nextTime.time_since_epoch().count(); std::uint32_t const escrowFinishSeq{env.seq(alice)}; env(escrowWithFinish, sig(alie)); Json::Value escrowWithCancel{escrow(alice, alice, XRP(500))}; escrowWithCancel[sfFinishAfter.jsonName] = nextTime.time_since_epoch().count(); escrowWithCancel[sfCancelAfter.jsonName] = nextTime.time_since_epoch().count() + 1; std::uint32_t const escrowCancelSeq{env.seq(alice)}; env(escrowWithCancel, sig(alie)); env.close(); { Json::Value escrowFinish; escrowFinish[jss::TransactionType] = jss::EscrowFinish; escrowFinish[jss::Account] = alice.human(); escrowFinish[sfOwner.jsonName] = alice.human(); escrowFinish[sfOfferSequence.jsonName] = escrowFinishSeq; env(escrowFinish, sig(alie)); } { Json::Value escrowCancel; escrowCancel[jss::TransactionType] = jss::EscrowCancel; escrowCancel[jss::Account] = alice.human(); escrowCancel[sfOwner.jsonName] = alice.human(); escrowCancel[sfOfferSequence.jsonName] = escrowCancelSeq; env(escrowCancel, sig(alie)); } env.close(); } // PayChan { std::uint32_t const payChanSeq{env.seq(alice)}; Json::Value payChanCreate; payChanCreate[jss::TransactionType] = jss::PaymentChannelCreate; payChanCreate[jss::Account] = alice.human(); payChanCreate[jss::Destination] = gw.human(); payChanCreate[jss::Amount] = XRP(500).value().getJson(JsonOptions::none); payChanCreate[sfSettleDelay.jsonName] = NetClock::duration{100s}.count(); payChanCreate[sfPublicKey.jsonName] = strHex(alice.pk().slice()); env(payChanCreate, sig(alie)); env.close(); std::string const payChanIndex{strHex(keylet::payChan(alice, gw, payChanSeq).key)}; { Json::Value payChanFund; payChanFund[jss::TransactionType] = jss::PaymentChannelFund; payChanFund[jss::Account] = alice.human(); payChanFund[sfChannel.jsonName] = payChanIndex; payChanFund[jss::Amount] = XRP(200).value().getJson(JsonOptions::none); env(payChanFund, sig(alie)); env.close(); } { Json::Value payChanClaim; payChanClaim[jss::TransactionType] = jss::PaymentChannelClaim; payChanClaim[jss::Flags] = tfClose; payChanClaim[jss::Account] = gw.human(); payChanClaim[sfChannel.jsonName] = payChanIndex; payChanClaim[sfPublicKey.jsonName] = strHex(alice.pk().slice()); env(payChanClaim); env.close(); } } // Check { auto const aliceCheckId = keylet::check(alice, env.seq(alice)).key; env(check::create(alice, gw, XRP(300)), sig(alie)); auto const gwCheckId = keylet::check(gw, env.seq(gw)).key; env(check::create(gw, alice, XRP(200))); env.close(); env(check::cash(alice, gwCheckId, XRP(200)), sig(alie)); env(check::cancel(alice, aliceCheckId), sig(alie)); env.close(); } { // Deposit pre-authorization with a Ticket. std::uint32_t const tktSeq{env.seq(alice) + 1}; env(ticket::create(alice, 1), sig(alie)); env.close(); env(deposit::auth(alice, gw), ticket::use(tktSeq), sig(alie)); env.close(); } // Setup is done. Look at the transactions returned by account_tx. Json::Value params; params[jss::account] = alice.human(); params[jss::ledger_index_min] = -1; params[jss::ledger_index_max] = -1; Json::Value const result{env.rpc("json", "account_tx", to_string(params))}; BEAST_EXPECT(result[jss::result][jss::status] == "success"); BEAST_EXPECT(result[jss::result][jss::transactions].isArray()); Json::Value const& txs{result[jss::result][jss::transactions]}; // clang-format off // Do a sanity check on each returned transaction. They should // be returned in the reverse order of application to the ledger. static const NodeSanity sanity[]{ // txType, created, deleted, modified {0, jss::DepositPreauth, {jss::DepositPreauth}, {jss::Ticket}, {jss::AccountRoot, jss::DirectoryNode}}, {1, jss::TicketCreate, {jss::Ticket}, {}, {jss::AccountRoot, jss::DirectoryNode}}, {2, jss::CheckCancel, {}, {jss::Check}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, {3, jss::CheckCash, {}, {jss::Check}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, {4, jss::CheckCreate, {jss::Check}, {}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, {5, jss::CheckCreate, {jss::Check}, {}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, {6, jss::PaymentChannelClaim, {}, {jss::PayChannel}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, {7, jss::PaymentChannelFund, {}, {}, {jss::AccountRoot, jss::PayChannel}}, {8, jss::PaymentChannelCreate, {jss::PayChannel}, {}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, {9, jss::EscrowCancel, {}, {jss::Escrow}, {jss::AccountRoot, jss::DirectoryNode}}, {10, jss::EscrowFinish, {}, {jss::Escrow}, {jss::AccountRoot, jss::DirectoryNode}}, {11, jss::EscrowCreate, {jss::Escrow}, {}, {jss::AccountRoot, jss::DirectoryNode}}, {12, jss::EscrowCreate, {jss::Escrow}, {}, {jss::AccountRoot, jss::DirectoryNode}}, {13, jss::SignerListSet, {jss::SignerList}, {}, {jss::AccountRoot, jss::DirectoryNode}}, {14, jss::OfferCancel, {}, {jss::Offer, jss::DirectoryNode}, {jss::AccountRoot, jss::DirectoryNode}}, {15, jss::OfferCreate, {jss::Offer, jss::DirectoryNode}, {}, {jss::AccountRoot, jss::DirectoryNode}}, {16, jss::TrustSet, {jss::RippleState, jss::DirectoryNode, jss::DirectoryNode}, {}, {jss::AccountRoot, jss::AccountRoot}}, {17, jss::SetRegularKey, {}, {}, {jss::AccountRoot}}, {18, jss::Payment, {}, {}, {jss::AccountRoot, jss::AccountRoot}}, {19, jss::AccountSet, {}, {}, {jss::AccountRoot}}, {20, jss::AccountSet, {}, {}, {jss::AccountRoot}}, {21, jss::Payment, {jss::AccountRoot}, {}, {jss::AccountRoot}}, }; // clang-format on BEAST_EXPECT(std::size(sanity) == result[jss::result][jss::transactions].size()); for (unsigned int index{0}; index < std::size(sanity); ++index) { checkSanity(txs[index], sanity[index]); } } void testAccountDelete() { testcase("AccountDelete"); // Verify that if an account is resurrected then the account_tx RPC // command still recovers all transactions on that account before // and after resurrection. using namespace test::jtx; using namespace std::chrono_literals; Env env(*this); Account const alice{"alice"}; Account const becky{"becky"}; env.fund(XRP(10000), alice, becky); env.close(); // Verify that becky's account root is present. Keylet const beckyAcctKey{keylet::account(becky.id())}; BEAST_EXPECT(env.closed()->exists(beckyAcctKey)); // becky does an AccountSet . env(noop(becky)); // Close enough ledgers to be able to delete becky's account. std::uint32_t const ledgerCount{env.current()->seq() + 257 - env.seq(becky)}; for (std::uint32_t i = 0; i < ledgerCount; ++i) env.close(); auto const beckyPreDelBalance{env.balance(becky)}; auto const acctDelFee{drops(env.current()->fees().increment)}; env(acctdelete(becky, alice), fee(acctDelFee)); env.close(); // Verify that becky's account root is gone. BEAST_EXPECT(!env.closed()->exists(beckyAcctKey)); env.close(); // clang-format off // Do a sanity check on each returned transaction. They should // be returned in the reverse order of application to the ledger. // // Note that the first two transactions in sanity have not occurred // yet. We'll see those after becky's account is resurrected. static const NodeSanity sanity[] { // txType, created, deleted, modified /* becky pays alice */ { 0, jss::Payment, {}, {}, {jss::AccountRoot, jss::AccountRoot}}, /* alice resurrects becky's acct */ { 1, jss::Payment, {jss::AccountRoot}, {}, {jss::AccountRoot}}, /* becky deletes her account */ { 2, jss::AccountDelete, {}, {jss::AccountRoot}, {jss::AccountRoot}}, /* becky's noop */ { 3, jss::AccountSet, {}, {}, {jss::AccountRoot}}, /* "fund" sets flags */ { 4, jss::AccountSet, {}, {}, {jss::AccountRoot}}, /* "fund" creates becky's acct */ { 5, jss::Payment, {jss::AccountRoot}, {}, {jss::AccountRoot}} }; // clang-format on // Verify that we can recover becky's account_tx information even // after the account is deleted. { Json::Value params; params[jss::account] = becky.human(); params[jss::ledger_index_min] = -1; params[jss::ledger_index_max] = -1; Json::Value const result{env.rpc("json", "account_tx", to_string(params))}; BEAST_EXPECT(result[jss::result][jss::status] == "success"); BEAST_EXPECT(result[jss::result][jss::transactions].isArray()); // The first two transactions listed in sanity haven't happened yet. constexpr unsigned int beckyDeletedOffset = 2; BEAST_EXPECT( std::size(sanity) == result[jss::result][jss::transactions].size() + beckyDeletedOffset); Json::Value const& txs{result[jss::result][jss::transactions]}; for (unsigned int index = beckyDeletedOffset; index < std::size(sanity); ++index) { checkSanity(txs[index - beckyDeletedOffset], sanity[index]); } } // All it takes is a large enough XRP payment to resurrect // becky's account. Try too small a payment. env(pay(alice, becky, drops(env.current()->fees().accountReserve(0)) - XRP(1)), ter(tecNO_DST_INSUF_XRP)); env.close(); // Actually resurrect becky's account. env(pay(alice, becky, XRP(45))); env.close(); // becky's account root should be back. BEAST_EXPECT(env.closed()->exists(beckyAcctKey)); BEAST_EXPECT(env.balance(becky) == XRP(45)); // becky pays alice. env(pay(becky, alice, XRP(20))); env.close(); // Setup is done. Look at the transactions returned by account_tx. // Verify that account_tx locates all of becky's transactions. Json::Value params; params[jss::account] = becky.human(); params[jss::ledger_index_min] = -1; params[jss::ledger_index_max] = -1; Json::Value const result{env.rpc("json", "account_tx", to_string(params))}; BEAST_EXPECT(result[jss::result][jss::status] == "success"); BEAST_EXPECT(result[jss::result][jss::transactions].isArray()); BEAST_EXPECT(std::size(sanity) == result[jss::result][jss::transactions].size()); Json::Value const& txs{result[jss::result][jss::transactions]}; for (unsigned int index = 0; index < std::size(sanity); ++index) { checkSanity(txs[index], sanity[index]); } } void testMPT() { testcase("MPT"); using namespace test::jtx; using namespace std::chrono_literals; auto cfg = makeConfig(); cfg->FEES.reference_fee = 10; Env env(*this, std::move(cfg)); Account const alice{"alice"}; Account const bob{"bob"}; Account const carol{"carol"}; MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); // check the latest mpt-related txn is in alice's account history auto const checkAliceAcctTx = [&](size_t size, Json::StaticString txType) { Json::Value params; params[jss::account] = alice.human(); params[jss::limit] = 100; auto const jv = env.rpc("json", "account_tx", to_string(params))[jss::result]; BEAST_EXPECT(jv[jss::transactions].size() == size); auto const& tx0(jv[jss::transactions][0u][jss::tx]); BEAST_EXPECT(tx0[jss::TransactionType] == txType); std::string const txHash{env.tx()->getJson(JsonOptions::none)[jss::hash].asString()}; BEAST_EXPECT(tx0[jss::hash] == txHash); }; // alice creates issuance mptAlice.create( {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer}); checkAliceAcctTx(3, jss::MPTokenIssuanceCreate); // bob creates a MPToken; mptAlice.authorize({.account = bob}); checkAliceAcctTx(4, jss::MPTokenAuthorize); env.close(); // TODO: windows pipeline fails validation for the hardcoded ledger hash // due to having different test config, it can be uncommented after // figuring out what happened // // ledger hash should be fixed regardless any change to account history // BEAST_EXPECT( // to_string(env.closed()->header().hash) == // "0BD507BB87D3C0E73B462485E6E381798A8C82FC49BF17FE39C60E08A1AF035D"); // alice authorizes bob mptAlice.authorize({.account = alice, .holder = bob}); checkAliceAcctTx(5, jss::MPTokenAuthorize); // carol creates a MPToken; mptAlice.authorize({.account = carol}); checkAliceAcctTx(6, jss::MPTokenAuthorize); // alice authorizes carol mptAlice.authorize({.account = alice, .holder = carol}); checkAliceAcctTx(7, jss::MPTokenAuthorize); // alice pays bob 100 tokens mptAlice.pay(alice, bob, 100); checkAliceAcctTx(8, jss::Payment); // bob pays carol 10 tokens mptAlice.pay(bob, carol, 10); checkAliceAcctTx(9, jss::Payment); } public: void run() override { forAllApiVersions(std::bind_front(&AccountTx_test::testParameters, this)); testContents(); testAccountDelete(); testMPT(); } }; BEAST_DEFINE_TESTSUITE(AccountTx, rpc, xrpl); } // namespace test } // namespace xrpl