mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-02 16:26:48 +00:00
Per XLS-0095, we are taking steps to rename ripple(d) to xrpl(d). This change specifically removes all copyright notices referencing Ripple, XRPLF, and certain affiliated contributors upon mutual agreement, so the notice in the LICENSE.md file applies throughout. Copyright notices referencing external contributions remain as-is. Duplicate verbiage is also removed.
944 lines
34 KiB
C++
944 lines
34 KiB
C++
#include <test/jtx.h>
|
|
#include <test/jtx/Env.h>
|
|
#include <test/jtx/envconfig.h>
|
|
|
|
#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
|
|
#include <xrpld/rpc/CTID.h>
|
|
|
|
#include <xrpl/protocol/ErrorCodes.h>
|
|
#include <xrpl/protocol/STBase.h>
|
|
#include <xrpl/protocol/jss.h>
|
|
#include <xrpl/protocol/serialize.h>
|
|
|
|
#include <cctype>
|
|
#include <optional>
|
|
#include <tuple>
|
|
|
|
namespace ripple {
|
|
|
|
class Transaction_test : public beast::unit_test::suite
|
|
{
|
|
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()->info().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()->info().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)
|
|
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
|
|
dynamic_cast<SQLiteDatabase*>(&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 netID = env.app().config().NETWORK_ID;
|
|
|
|
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()->info().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()->info().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 txnIdx = meta->getFieldU32(sfTransactionIndex);
|
|
auto const result = env.rpc(
|
|
COMMAND,
|
|
*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;
|
|
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)
|
|
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 txnIdx = meta->getFieldU32(sfTransactionIndex);
|
|
auto const result = env.rpc(
|
|
COMMAND,
|
|
*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
|
|
dynamic_cast<SQLiteDatabase*>(&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 txnIdx = meta->getFieldU32(sfTransactionIndex);
|
|
auto const result = env.rpc(
|
|
COMMAND,
|
|
*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 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
|
|
testCTIDRPC(FeatureBitset features)
|
|
{
|
|
testcase("CTID RPC");
|
|
|
|
using namespace test::jtx;
|
|
|
|
// Use a Concise Transaction Identifier to request a transaction.
|
|
for (uint32_t netID : {11111, 65535, 65536})
|
|
{
|
|
Env env{*this, makeNetworkConfig(netID)};
|
|
BEAST_EXPECT(netID == env.app().config().NETWORK_ID);
|
|
|
|
auto const alice = Account("alice");
|
|
auto const bob = Account("bob");
|
|
|
|
auto const startLegSeq = env.current()->info().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;
|
|
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().config().NETWORK_ID;
|
|
|
|
Account const alice = Account("alice");
|
|
Account const bob = Account("bob");
|
|
|
|
std::uint32_t const startLegSeq = env.current()->info().seq;
|
|
env.fund(XRP(10000), alice, bob);
|
|
env(pay(alice, bob, XRP(10)));
|
|
env.close();
|
|
|
|
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::find_if(
|
|
mixedCase.begin(), mixedCase.end(), 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 netID : {2, 1024, 65535, 65536})
|
|
{
|
|
Env env{*this, makeNetworkConfig(netID)};
|
|
BEAST_EXPECT(netID == env.app().config().NETWORK_ID);
|
|
|
|
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()->info().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);
|
|
}
|
|
}
|
|
|
|
// test the wrong network ID was submitted
|
|
{
|
|
Env env{*this, makeNetworkConfig(21337)};
|
|
uint32_t netID = env.app().config().NETWORK_ID;
|
|
|
|
auto const alice = Account("alice");
|
|
auto const bob = Account("bob");
|
|
|
|
auto const startLegSeq = env.current()->info().seq;
|
|
env.fund(XRP(10000), alice, bob);
|
|
env(pay(alice, bob, XRP(10)));
|
|
env.close();
|
|
|
|
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> txn = env.tx();
|
|
env.close();
|
|
std::shared_ptr<STObject 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> 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);
|
|
testCTIDRPC(features);
|
|
forAllApiVersions(
|
|
std::bind_front(&Transaction_test::testRequest, this, features));
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE(Transaction, rpc, ripple);
|
|
|
|
} // namespace ripple
|