Files
rippled/src/test/rpc/Transaction_test.cpp
2026-04-21 15:32:51 +00:00

909 lines
34 KiB
C++

#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <test/jtx/envconfig.h>
#include <test/jtx/noop.h>
#include <test/jtx/pay.h>
#include <test/jtx/seq.h>
#include <test/jtx/ter.h>
#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
#include <xrpld/rpc/CTID.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/core/NetworkIDService.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/ApiVersion.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STBase.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol/serialize.h>
#include <algorithm>
#include <cctype>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <tuple>
#include <vector>
namespace xrpl {
class Transaction_test : public beast::unit_test::suite
{
static std::unique_ptr<Config>
makeNetworkConfig(uint32_t networkID)
{
using namespace test::jtx;
return envconfig([&](std::unique_ptr<Config> cfg) {
cfg->NETWORK_ID = networkID;
return cfg;
});
}
void
testRangeRequest(FeatureBitset features)
{
testcase("Test Range Request");
using namespace test::jtx;
using std::to_string;
char const* COMMAND = jss::tx.c_str();
char const* BINARY = jss::binary.c_str();
char const* NOT_FOUND = RPC::get_error_info(rpcTXN_NOT_FOUND).token;
char const* INVALID = RPC::get_error_info(rpcINVALID_LGR_RANGE).token;
char const* EXCESSIVE = RPC::get_error_info(rpcEXCESSIVE_LGR_RANGE).token;
Env env{*this, features};
auto const alice = Account("alice");
env.fund(XRP(1000), alice);
env.close();
std::vector<std::shared_ptr<STTx const>> txns;
std::vector<std::shared_ptr<STObject const>> metas;
auto const startLegSeq = env.current()->header().seq;
for (int i = 0; i < 750; ++i)
{
env(noop(alice));
txns.emplace_back(env.tx());
env.close();
metas.emplace_back(env.closed()->txRead(env.tx()->getTransactionID()).second);
}
auto const endLegSeq = env.closed()->header().seq;
// Find the existing transactions
for (size_t i = 0; i < txns.size(); ++i)
{
auto const& tx = txns[i];
auto const& meta = metas[i];
auto const result = env.rpc(
COMMAND,
to_string(tx->getTransactionID()),
BINARY,
to_string(startLegSeq),
to_string(endLegSeq));
BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
BEAST_EXPECT(result[jss::result][jss::tx] == strHex(tx->getSerializer().getData()));
BEAST_EXPECT(result[jss::result][jss::meta] == strHex(meta->getSerializer().getData()));
}
auto const tx = env.jt(noop(alice), seq(env.seq(alice))).stx;
for (int deltaEndSeq = 0; deltaEndSeq < 2; ++deltaEndSeq)
{
auto const result = env.rpc(
COMMAND,
to_string(tx->getTransactionID()),
BINARY,
to_string(startLegSeq),
to_string(endLegSeq + deltaEndSeq));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == NOT_FOUND);
if (deltaEndSeq != 0)
{
BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
}
else
{
BEAST_EXPECT(result[jss::result][jss::searched_all].asBool());
}
}
// Find transactions outside of provided range.
for (auto&& tx : txns)
{
auto const result = env.rpc(
COMMAND,
to_string(tx->getTransactionID()),
BINARY,
to_string(endLegSeq + 1),
to_string(endLegSeq + 100));
BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
}
auto const deletedLedger = (startLegSeq + endLegSeq) / 2;
{
// Remove one of the ledgers from the database directly
env.app().getRelationalDatabase().deleteTransactionByLedgerSeq(deletedLedger);
}
for (int deltaEndSeq = 0; deltaEndSeq < 2; ++deltaEndSeq)
{
auto const result = env.rpc(
COMMAND,
to_string(tx->getTransactionID()),
BINARY,
to_string(startLegSeq),
to_string(endLegSeq + deltaEndSeq));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == NOT_FOUND);
BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
}
// Provide range without providing the `binary`
// field. (Tests parameter parsing)
{
auto const result = env.rpc(
COMMAND,
to_string(tx->getTransactionID()),
to_string(startLegSeq),
to_string(endLegSeq));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == NOT_FOUND);
BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
}
// Provide range without providing the `binary`
// field. (Tests parameter parsing)
{
auto const result = env.rpc(
COMMAND,
to_string(tx->getTransactionID()),
to_string(startLegSeq),
to_string(deletedLedger - 1));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == NOT_FOUND);
BEAST_EXPECT(result[jss::result][jss::searched_all].asBool());
}
// Provide range without providing the `binary`
// field. (Tests parameter parsing)
{
auto const result = env.rpc(
COMMAND,
to_string(txns[0]->getTransactionID()),
to_string(startLegSeq),
to_string(deletedLedger - 1));
BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
// Provide an invalid range: (min > max)
{
auto const result = env.rpc(
COMMAND,
to_string(tx->getTransactionID()),
BINARY,
to_string(deletedLedger - 1),
to_string(startLegSeq));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == INVALID);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
// Provide an invalid range: (min < 0)
{
auto const result = env.rpc(
COMMAND,
to_string(tx->getTransactionID()),
BINARY,
to_string(-1),
to_string(deletedLedger - 1));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == INVALID);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
// Provide an invalid range: (min < 0, max < 0)
{
auto const result = env.rpc(
COMMAND, to_string(tx->getTransactionID()), BINARY, to_string(-20), to_string(-10));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == INVALID);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
// Provide an invalid range: (only one value)
{
auto const result =
env.rpc(COMMAND, to_string(tx->getTransactionID()), BINARY, to_string(20));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == INVALID);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
// Provide an invalid range: (only one value)
{
auto const result = env.rpc(COMMAND, to_string(tx->getTransactionID()), to_string(20));
// Since we only provided one value for the range,
// the interface parses it as a false binary flag,
// as single-value ranges are not accepted. Since
// the error this causes differs depending on the platform
// we don't call out a specific error here.
BEAST_EXPECT(result[jss::result][jss::status] == jss::error);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
// Provide an invalid range: (max - min > 1000)
{
auto const result = env.rpc(
COMMAND,
to_string(tx->getTransactionID()),
BINARY,
to_string(startLegSeq),
to_string(startLegSeq + 1001));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == EXCESSIVE);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
}
void
testRangeCTIDRequest(FeatureBitset features)
{
testcase("CTID Range Request");
using namespace test::jtx;
using std::to_string;
char const* COMMAND = jss::tx.c_str();
char const* BINARY = jss::binary.c_str();
char const* NOT_FOUND = RPC::get_error_info(rpcTXN_NOT_FOUND).token;
char const* INVALID = RPC::get_error_info(rpcINVALID_LGR_RANGE).token;
char const* EXCESSIVE = RPC::get_error_info(rpcEXCESSIVE_LGR_RANGE).token;
Env env{*this, makeNetworkConfig(11111)};
uint32_t const netID = env.app().getNetworkIDService().getNetworkID();
auto const alice = Account("alice");
env.fund(XRP(1000), alice);
env.close();
std::vector<std::shared_ptr<STTx const>> txns;
std::vector<std::shared_ptr<STObject const>> metas;
auto const startLegSeq = env.current()->header().seq;
for (int i = 0; i < 750; ++i)
{
env(noop(alice));
txns.emplace_back(env.tx());
env.close();
metas.emplace_back(env.closed()->txRead(env.tx()->getTransactionID()).second);
}
auto const endLegSeq = env.closed()->header().seq;
// Find the existing transactions
for (size_t i = 0; i < txns.size(); ++i)
{
auto const& tx = txns[i];
auto const& meta = metas[i];
uint32_t const txnIdx = meta->getFieldU32(sfTransactionIndex);
auto const result = env.rpc(
COMMAND,
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
*RPC::encodeCTID(startLegSeq + i, txnIdx, netID),
BINARY,
to_string(startLegSeq),
to_string(endLegSeq));
BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
BEAST_EXPECT(result[jss::result][jss::tx] == strHex(tx->getSerializer().getData()));
BEAST_EXPECT(result[jss::result][jss::meta] == strHex(meta->getSerializer().getData()));
}
auto const tx = env.jt(noop(alice), seq(env.seq(alice))).stx;
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
auto const ctid = *RPC::encodeCTID(endLegSeq, tx->getSeqValue(), netID);
for (int deltaEndSeq = 0; deltaEndSeq < 2; ++deltaEndSeq)
{
auto const result = env.rpc(
COMMAND, ctid, BINARY, to_string(startLegSeq), to_string(endLegSeq + deltaEndSeq));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == NOT_FOUND);
if (deltaEndSeq != 0)
{
BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
}
else
{
BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
}
}
// Find transactions outside of provided range.
for (size_t i = 0; i < txns.size(); ++i)
{
// auto const& tx = txns[i];
auto const& meta = metas[i];
uint32_t const txnIdx = meta->getFieldU32(sfTransactionIndex);
auto const result = env.rpc(
COMMAND,
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
*RPC::encodeCTID(startLegSeq + i, txnIdx, netID),
BINARY,
to_string(endLegSeq + 1),
to_string(endLegSeq + 100));
BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
}
auto const deletedLedger = (startLegSeq + endLegSeq) / 2;
{
// Remove one of the ledgers from the database directly
env.app().getRelationalDatabase().deleteTransactionByLedgerSeq(deletedLedger);
}
for (int deltaEndSeq = 0; deltaEndSeq < 2; ++deltaEndSeq)
{
auto const result = env.rpc(
COMMAND, ctid, BINARY, to_string(startLegSeq), to_string(endLegSeq + deltaEndSeq));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == NOT_FOUND);
BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
}
// Provide range without providing the `binary`
// field. (Tests parameter parsing)
{
auto const result =
env.rpc(COMMAND, ctid, to_string(startLegSeq), to_string(endLegSeq));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == NOT_FOUND);
BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
}
// Provide range without providing the `binary`
// field. (Tests parameter parsing)
{
auto const result =
env.rpc(COMMAND, ctid, to_string(startLegSeq), to_string(deletedLedger - 1));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == NOT_FOUND);
BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
}
// Provide range without providing the `binary`
// field. (Tests parameter parsing)
{
auto const& meta = metas[0];
uint32_t const txnIdx = meta->getFieldU32(sfTransactionIndex);
auto const result = env.rpc(
COMMAND,
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
*RPC::encodeCTID(endLegSeq, txnIdx, netID),
to_string(startLegSeq),
to_string(deletedLedger - 1));
BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
// Provide an invalid range: (min > max)
{
auto const result = env.rpc(
COMMAND, ctid, BINARY, to_string(deletedLedger - 1), to_string(startLegSeq));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == INVALID);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
// Provide an invalid range: (min < 0)
{
auto const result =
env.rpc(COMMAND, ctid, BINARY, to_string(-1), to_string(deletedLedger - 1));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == INVALID);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
// Provide an invalid range: (min < 0, max < 0)
{
auto const result = env.rpc(COMMAND, ctid, BINARY, to_string(-20), to_string(-10));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == INVALID);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
// Provide an invalid range: (only one value)
{
auto const result = env.rpc(COMMAND, ctid, BINARY, to_string(20));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == INVALID);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
// Provide an invalid range: (only one value)
{
auto const result = env.rpc(COMMAND, ctid, to_string(20));
// Since we only provided one value for the range,
// the interface parses it as a false binary flag,
// as single-value ranges are not accepted. Since
// the error this causes differs depending on the platform
// we don't call out a specific error here.
BEAST_EXPECT(result[jss::result][jss::status] == jss::error);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
// Provide an invalid range: (max - min > 1000)
{
auto const result = env.rpc(
COMMAND, ctid, BINARY, to_string(startLegSeq), to_string(startLegSeq + 1001));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == EXCESSIVE);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
}
void
testCTIDValidation(FeatureBitset features)
{
testcase("CTID Validation");
using namespace test::jtx;
using std::to_string;
Env const env{*this, makeNetworkConfig(11111)};
// Test case 1: Valid input values
auto const expected11 = std::optional<std::string>("CFFFFFFFFFFFFFFF");
BEAST_EXPECT(RPC::encodeCTID(0x0FFF'FFFFUL, 0xFFFFU, 0xFFFFU) == expected11);
auto const expected12 = std::optional<std::string>("C000000000000000");
BEAST_EXPECT(RPC::encodeCTID(0, 0, 0) == expected12);
auto const expected13 = std::optional<std::string>("C000000100020003");
BEAST_EXPECT(RPC::encodeCTID(1U, 2U, 3U) == expected13);
auto const expected14 = std::optional<std::string>("C0CA2AA7326FFFFF");
BEAST_EXPECT(RPC::encodeCTID(13249191UL, 12911U, 65535U) == expected14);
// Test case 2: ledger_seq greater than 0xFFFFFFF
BEAST_EXPECT(!RPC::encodeCTID(0x1000'0000UL, 0xFFFFU, 0xFFFFU));
// Test case 3: txn_index greater than 0xFFFF
BEAST_EXPECT(!RPC::encodeCTID(0x0FFF'FFFF, 0x1'0000, 0xFFFF));
// Test case 4: network_id greater than 0xFFFF
BEAST_EXPECT(!RPC::encodeCTID(0x0FFF'FFFFUL, 0xFFFFU, 0x1'0000U));
// Test case 5: Valid input values
auto const expected51 =
std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(std::make_tuple(0, 0, 0));
BEAST_EXPECT(RPC::decodeCTID("C000000000000000") == expected51);
auto const expected52 =
std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(std::make_tuple(1U, 2U, 3U));
BEAST_EXPECT(RPC::decodeCTID("C000000100020003") == expected52);
auto const expected53 = std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(
std::make_tuple(13249191UL, 12911U, 49221U));
BEAST_EXPECT(RPC::decodeCTID("C0CA2AA7326FC045") == expected53);
// Test case 6: ctid not a string or big int
BEAST_EXPECT(!RPC::decodeCTID(0xCFF));
// Test case 7: ctid not a hexadecimal string
BEAST_EXPECT(!RPC::decodeCTID("C003FFFFFFFFFFFG"));
// Test case 8: ctid not exactly 16 nibbles
BEAST_EXPECT(!RPC::decodeCTID("C003FFFFFFFFFFF"));
// Test case 9: ctid too large to be a valid CTID value
BEAST_EXPECT(!RPC::decodeCTID("CFFFFFFFFFFFFFFFF"));
// Test case 10: ctid doesn't start with a C nibble
BEAST_EXPECT(!RPC::decodeCTID("FFFFFFFFFFFFFFFF"));
// Test case 11: Valid input values
BEAST_EXPECT(
(RPC::decodeCTID(0xCFFF'FFFF'FFFF'FFFFULL) ==
std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(
std::make_tuple(0x0FFF'FFFFUL, 0xFFFFU, 0xFFFFU))));
BEAST_EXPECT(
(RPC::decodeCTID(0xC000'0000'0000'0000ULL) ==
std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(std::make_tuple(0, 0, 0))));
BEAST_EXPECT(
(RPC::decodeCTID(0xC000'0001'0002'0003ULL) ==
std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(std::make_tuple(1U, 2U, 3U))));
BEAST_EXPECT(
(RPC::decodeCTID(0xC0CA'2AA7'326F'C045ULL) ==
std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(
std::make_tuple(1324'9191UL, 12911U, 49221U))));
// Test case 12: ctid not exactly 16 nibbles
BEAST_EXPECT(!RPC::decodeCTID(0xC003'FFFF'FFFF'FFF));
// Test case 13: ctid too large to be a valid CTID value
// this test case is not possible in c++ because it would overflow the
// type, left in for completeness
// BEAST_EXPECT(!RPC::decodeCTID(0xCFFFFFFFFFFFFFFFFULL));
// Test case 14: ctid doesn't start with a C nibble
BEAST_EXPECT(!RPC::decodeCTID(0xFFFF'FFFF'FFFF'FFFFULL));
}
void
testRPCsForCTID(FeatureBitset features)
{
testcase("CTID RPC");
using namespace test::jtx;
// Use a Concise Transaction Identifier to request a transaction.
for (uint32_t const netID : {11111, 65535, 65536})
{
Env env{*this, makeNetworkConfig(netID)};
BEAST_EXPECT(netID == env.app().getNetworkIDService().getNetworkID());
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const startLegSeq = env.current()->header().seq;
env.fund(XRP(10000), alice, bob);
env(pay(alice, bob, XRP(10)));
env.close();
auto const ctid = RPC::encodeCTID(startLegSeq, 0, netID);
if (netID > 0xFFFF)
{
// Concise transaction IDs do not support a network ID > 0xFFFF.
BEAST_EXPECT(ctid == std::nullopt);
continue;
}
Json::Value jsonTx;
jsonTx[jss::binary] = false;
jsonTx[jss::ctid] = *ctid; // NOLINT(bugprone-unchecked-optional-access)
jsonTx[jss::id] = 1;
auto const jrr = env.rpc("json", "tx", to_string(jsonTx))[jss::result];
BEAST_EXPECT(jrr[jss::ctid] == ctid);
BEAST_EXPECT(jrr.isMember(jss::hash));
}
// test querying with mixed case ctid
{
Env env{*this, makeNetworkConfig(11111)};
std::uint32_t const netID = env.app().getNetworkIDService().getNetworkID();
Account const alice = Account("alice");
Account const bob = Account("bob");
std::uint32_t const startLegSeq = env.current()->header().seq;
env.fund(XRP(10000), alice, bob);
env(pay(alice, bob, XRP(10)));
env.close();
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
std::string const ctid = *RPC::encodeCTID(startLegSeq, 0, netID);
auto isUpper = [](char c) { return std::isupper(c) != 0; };
// Verify that there are at least two upper case letters in ctid and
// test a mixed case
if (BEAST_EXPECT(std::count_if(ctid.begin(), ctid.end(), isUpper) > 1))
{
// Change the first upper case letter to lower case.
std::string mixedCase = ctid;
{
auto const iter = std::ranges::find_if(mixedCase, isUpper);
*iter = std::tolower(*iter);
}
BEAST_EXPECT(ctid != mixedCase);
Json::Value jsonTx;
jsonTx[jss::binary] = false;
jsonTx[jss::ctid] = mixedCase;
jsonTx[jss::id] = 1;
Json::Value const jrr = env.rpc("json", "tx", to_string(jsonTx))[jss::result];
BEAST_EXPECT(jrr[jss::ctid] == ctid);
BEAST_EXPECT(jrr[jss::hash]);
}
}
// test that if the network is 65535 the ctid is not in the response
// Using a hash to request the transaction, test the network ID
// boundary where the CTID is (not) in the response.
for (uint32_t const netID : {2, 1024, 65535, 65536})
{
Env env{*this, makeNetworkConfig(netID)};
BEAST_EXPECT(netID == env.app().getNetworkIDService().getNetworkID());
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env(pay(alice, bob, XRP(10)));
env.close();
auto const ledgerSeq = env.current()->header().seq;
env(noop(alice), ter(tesSUCCESS));
env.close();
Json::Value params;
params[jss::id] = 1;
auto const hash = env.tx()->getJson(JsonOptions::none)[jss::hash];
params[jss::transaction] = hash;
auto const jrr = env.rpc("json", "tx", to_string(params))[jss::result];
BEAST_EXPECT(jrr[jss::hash] == hash);
BEAST_EXPECT(jrr.isMember(jss::ctid) == (netID <= 0xFFFF));
if (jrr.isMember(jss::ctid))
{
auto const ctid = RPC::encodeCTID(ledgerSeq, 0, netID);
BEAST_EXPECT(
jrr[jss::ctid] == *ctid); // NOLINT(bugprone-unchecked-optional-access)
}
}
// test the wrong network ID was submitted
{
Env env{*this, makeNetworkConfig(21337)};
uint32_t const netID = env.app().getNetworkIDService().getNetworkID();
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const startLegSeq = env.current()->header().seq;
env.fund(XRP(10000), alice, bob);
env(pay(alice, bob, XRP(10)));
env.close();
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
auto const ctid = *RPC::encodeCTID(startLegSeq, 0, netID + 1);
Json::Value jsonTx;
jsonTx[jss::binary] = false;
jsonTx[jss::ctid] = ctid;
jsonTx[jss::id] = 1;
auto const jrr = env.rpc("json", "tx", to_string(jsonTx))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "wrongNetwork");
BEAST_EXPECT(jrr[jss::error_code] == rpcWRONG_NETWORK);
BEAST_EXPECT(
jrr[jss::error_message] ==
"Wrong network. You should submit this request to a node "
"running on NetworkID: 21338");
}
}
void
testRequest(FeatureBitset features, unsigned apiVersion)
{
testcase("Test Request API version " + std::to_string(apiVersion));
using namespace test::jtx;
using std::to_string;
Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->FEES.reference_fee = 10;
return cfg;
})};
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)));
std::shared_ptr<STTx const> const txn = env.tx();
env.close();
std::shared_ptr<STObject const> const meta =
env.closed()->txRead(env.tx()->getTransactionID()).second;
Json::Value expected = txn->getJson(JsonOptions::none);
expected[jss::DeliverMax] = expected[jss::Amount];
if (apiVersion > 1)
{
expected.removeMember(jss::hash);
expected.removeMember(jss::Amount);
}
Json::Value const result = {[&env, txn, apiVersion]() {
Json::Value params{Json::objectValue};
params[jss::transaction] = to_string(txn->getTransactionID());
params[jss::binary] = false;
params[jss::api_version] = apiVersion;
return env.client().invoke("tx", params);
}()};
BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
if (apiVersion > 1)
{
BEAST_EXPECT(result[jss::result][jss::close_time_iso] == "2000-01-01T00:00:20Z");
BEAST_EXPECT(result[jss::result][jss::hash] == to_string(txn->getTransactionID()));
BEAST_EXPECT(result[jss::result][jss::validated] == true);
BEAST_EXPECT(result[jss::result][jss::ledger_index] == 4);
BEAST_EXPECT(
result[jss::result][jss::ledger_hash] ==
"B41882E20F0EC6228417D28B9AE0F33833645D35F6799DFB782AC97FC4BB51"
"D2");
}
for (auto memberIt = expected.begin(); memberIt != expected.end(); memberIt++)
{
std::string const name = memberIt.memberName();
auto const& result_transaction =
(apiVersion > 1 ? result[jss::result][jss::tx_json] : result[jss::result]);
if (BEAST_EXPECT(result_transaction.isMember(name)))
{
auto const received = result_transaction[name];
BEAST_EXPECTS(
received == *memberIt,
"Transaction contains \n\"" + name + "\": " //
+ to_string(received) //
+ " but expected " //
+ to_string(expected));
}
}
}
void
testBinaryRequest(unsigned apiVersion)
{
testcase("Test binary request API version " + std::to_string(apiVersion));
using namespace test::jtx;
using std::to_string;
Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->FEES.reference_fee = 10;
return cfg;
})};
Account const alice{"alice"};
Account const gw{"gw"};
auto const USD{gw["USD"]};
env.fund(XRP(1000000), alice, gw);
std::shared_ptr<STTx const> const txn = env.tx();
BEAST_EXPECT(
to_string(txn->getTransactionID()) ==
"3F8BDE5A5F82C4F4708E5E9255B713E303E6E1A371FD5C7A704AFD1387C23981");
env.close();
std::shared_ptr<STObject const> const meta =
env.closed()->txRead(txn->getTransactionID()).second;
std::string const expected_tx_blob = serializeHex(*txn);
std::string const expected_meta_blob = serializeHex(*meta);
Json::Value const result = [&env, txn, apiVersion]() {
Json::Value params{Json::objectValue};
params[jss::transaction] = to_string(txn->getTransactionID());
params[jss::binary] = true;
params[jss::api_version] = apiVersion;
return env.client().invoke("tx", params);
}();
if (BEAST_EXPECT(result[jss::status] == "success"))
{
BEAST_EXPECT(result[jss::result][jss::status] == "success");
BEAST_EXPECT(result[jss::result][jss::validated] == true);
BEAST_EXPECT(result[jss::result][jss::hash] == to_string(txn->getTransactionID()));
BEAST_EXPECT(result[jss::result][jss::ledger_index] == 3);
BEAST_EXPECT(result[jss::result][jss::ctid] == "C000000300030000");
if (apiVersion > 1)
{
BEAST_EXPECT(result[jss::result][jss::tx_blob] == expected_tx_blob);
BEAST_EXPECT(result[jss::result][jss::meta_blob] == expected_meta_blob);
BEAST_EXPECT(
result[jss::result][jss::ledger_hash] ==
"2D5150E5A5AA436736A732291E437ABF01BC9E206C2DF3C77C4F856915"
"7905AA");
BEAST_EXPECT(result[jss::result][jss::close_time_iso] == "2000-01-01T00:00:10Z");
}
else
{
BEAST_EXPECT(result[jss::result][jss::tx] == expected_tx_blob);
BEAST_EXPECT(result[jss::result][jss::meta] == expected_meta_blob);
BEAST_EXPECT(result[jss::result][jss::date] == 10);
}
}
}
public:
void
run() override
{
using namespace test::jtx;
forAllApiVersions(std::bind_front(&Transaction_test::testBinaryRequest, this));
FeatureBitset const all{testable_amendments()};
testWithFeats(all);
}
void
testWithFeats(FeatureBitset features)
{
testRangeRequest(features);
testRangeCTIDRequest(features);
testCTIDValidation(features);
testRPCsForCTID(features);
forAllApiVersions(std::bind_front(&Transaction_test::testRequest, this, features));
}
};
BEAST_DEFINE_TESTSUITE(Transaction, rpc, xrpl);
} // namespace xrpl