Files
rippled/src/test/rpc/AccountTx_test.cpp
2026-02-19 23:30:00 +00:00

831 lines
36 KiB
C++

#include <test/jtx.h>
#include <test/jtx/envconfig.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <boost/container/flat_set.hpp>
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<std::string> created;
boost::container::flat_set<std::string> deleted;
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)
: index(idx), txType(t)
{
auto buildSet = [](auto&& init) {
boost::container::flat_set<std::string> 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<std::string> createdNodes;
boost::container::flat_set<std::string> deletedNodes;
boost::container::flat_set<std::string> 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<Config> cfg) {
cfg->FEES.reference_fee = 10;
return cfg;
}));
Account 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 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