Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/xrpl/protocol/jss.h
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ JSS(index); // in: LedgerEntry
// LedgerEntry, TxHistory, LedgerData
JSS(info); // out: ServerInfo, ConsensusInfo, FetchInfo
JSS(initial_sync_duration_us);
JSS(inner_transactions); // out: tx (Batch)
JSS(internal_command); // in: Internal
JSS(invalid_API_version); // out: Many, when a request has an invalid
// version
Expand Down
111 changes: 111 additions & 0 deletions src/test/rpc/Transaction_test.cpp
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
#include <test/jtx.h>
#include <test/jtx/Env.h>
#include <test/jtx/batch.h>
#include <test/jtx/envconfig.h>

#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
#include <xrpld/rpc/CTID.h>

#include <xrpl/core/NetworkIDService.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STBase.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol/serialize.h>

Expand Down Expand Up @@ -842,6 +845,112 @@ class Transaction_test : public beast::unit_test::suite
}
}

void
testBatchInnerTransactions(FeatureBitset features, unsigned apiVersion)
{
testcase("Test Batch inner_transactions API version " + std::to_string(apiVersion));

using namespace test::jtx;
using std::to_string;

Env env{*this, features};
Account const alice{"alice"};
Account const bob{"bob"};

env.fund(XRP(10000), alice, bob);
env.close();

auto const aliceSeq = env.seq(alice);

// Create a Batch transaction with two inner Payment transactions
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto batchTxn = env.jt(
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, bob, XRP(100)), aliceSeq + 1),
batch::inner(pay(alice, bob, XRP(200)), aliceSeq + 2));
env(batchTxn, ter(tesSUCCESS));
env.close();

// Get the batch transaction ID and inner transaction IDs
auto const batchID = batchTxn.stx->getTransactionID();
auto const innerIDs = batchTxn.stx->getBatchTransactionIDs();
BEAST_EXPECT(innerIDs.size() == 2);

// Test non-binary mode
{
Json::Value params{Json::objectValue};
params[jss::transaction] = to_string(batchID);
params[jss::binary] = false;
params[jss::api_version] = apiVersion;
auto const result = env.client().invoke("tx", params);

BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
BEAST_EXPECT(result[jss::result][jss::validated] == true);

// Check inner_transactions field exists
if (BEAST_EXPECT(result[jss::result].isMember(jss::inner_transactions)))
{
auto const& innerTxns = result[jss::result][jss::inner_transactions];
if (BEAST_EXPECT(innerTxns.isArray() && innerTxns.size() == 2))
{
// Check first inner transaction
auto const& inner0 = innerTxns[0u];
BEAST_EXPECT(inner0[jss::hash] == to_string(innerIDs[0]));
BEAST_EXPECT(inner0[jss::engine_result] == "tesSUCCESS");
BEAST_EXPECT(inner0.isMember(jss::tx_json));
BEAST_EXPECT(inner0.isMember(jss::meta));
BEAST_EXPECT(inner0[jss::tx_json][jss::TransactionType] == "Payment");
// Validate ParentBatchID in metadata matches outer Batch hash
BEAST_EXPECT(inner0[jss::meta][sfParentBatchID.jsonName] == to_string(batchID));

// Check second inner transaction
auto const& inner1 = innerTxns[1u];
BEAST_EXPECT(inner1[jss::hash] == to_string(innerIDs[1]));
BEAST_EXPECT(inner1[jss::engine_result] == "tesSUCCESS");
BEAST_EXPECT(inner1.isMember(jss::tx_json));
BEAST_EXPECT(inner1.isMember(jss::meta));
BEAST_EXPECT(inner1[jss::tx_json][jss::TransactionType] == "Payment");
// Validate ParentBatchID in metadata matches outer Batch hash
BEAST_EXPECT(inner1[jss::meta][sfParentBatchID.jsonName] == to_string(batchID));
}
}
}

// Test binary mode
{
Json::Value params{Json::objectValue};
params[jss::transaction] = to_string(batchID);
params[jss::binary] = true;
params[jss::api_version] = apiVersion;
auto const result = env.client().invoke("tx", params);

BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
BEAST_EXPECT(result[jss::result][jss::validated] == true);

// Check inner_transactions field exists in binary mode
if (BEAST_EXPECT(result[jss::result].isMember(jss::inner_transactions)))
{
auto const& innerTxns = result[jss::result][jss::inner_transactions];
if (BEAST_EXPECT(innerTxns.isArray() && innerTxns.size() == 2))
{
// Check first inner transaction has binary blobs
auto const& inner0 = innerTxns[0u];
BEAST_EXPECT(inner0[jss::hash] == to_string(innerIDs[0]));
BEAST_EXPECT(inner0[jss::engine_result] == "tesSUCCESS");
BEAST_EXPECT(inner0.isMember(jss::tx_blob));
BEAST_EXPECT(inner0.isMember(jss::meta_blob));

// Check second inner transaction has binary blobs
auto const& inner1 = innerTxns[1u];
BEAST_EXPECT(inner1[jss::hash] == to_string(innerIDs[1]));
BEAST_EXPECT(inner1[jss::engine_result] == "tesSUCCESS");
BEAST_EXPECT(inner1.isMember(jss::tx_blob));
BEAST_EXPECT(inner1.isMember(jss::meta_blob));
}
}
}
}

public:
void
run() override
Expand All @@ -861,6 +970,8 @@ class Transaction_test : public beast::unit_test::suite
testCTIDValidation(features);
testRPCsForCTID(features);
forAllApiVersions(std::bind_front(&Transaction_test::testRequest, this, features));
forAllApiVersions(
std::bind_front(&Transaction_test::testBatchInnerTransactions, this, features));
}
};

Expand Down
88 changes: 88 additions & 0 deletions src/xrpld/rpc/handlers/Tx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,90 @@
return {result, rpcSUCCESS};
}

// Helper function to fetch and format inner transaction results for Batch
// transactions
static void
insertBatchInnerTransactions(
Json::Value& response,
std::shared_ptr<STTx const> const& sttx,
TxArgs const& args,
RPC::JsonContext const& context)
{
auto const& innerTxnIds = sttx->getBatchTransactionIDs();
if (innerTxnIds.empty())
return;

Check warning on line 174 in src/xrpld/rpc/handlers/Tx.cpp

View check run for this annotation

Codecov / codecov/patch

src/xrpld/rpc/handlers/Tx.cpp#L174

Added line #L174 was not covered by tests

Json::Value innerTxns(Json::arrayValue);

for (auto const& innerTxnId : innerTxnIds)
{
Json::Value innerResult(Json::objectValue);
innerResult[jss::hash] = to_string(innerTxnId);

// Fetch the inner transaction
auto ec = rpcSUCCESS;
auto v = context.app.getMasterTransaction().fetch(innerTxnId, ec);

if (auto txPair =
std::get_if<std::pair<std::shared_ptr<Transaction>, std::shared_ptr<TxMeta>>>(&v))
{
auto const& [innerTxn, innerMeta] = *txPair;
if (innerTxn && innerMeta)
{
// Add transaction result code
auto const ter = innerMeta->getResultTER();
innerResult[jss::engine_result] = transToken(ter);

if (!args.binary)
{
// Add the full transaction JSON
auto const& innerSttx = innerTxn->getSTransaction();
if (context.apiVersion > 1)
{
constexpr auto optionsJson =
JsonOptions::include_date | JsonOptions::disable_API_prior_V2;
innerResult[jss::tx_json] = innerTxn->getJson(optionsJson);
RPC::insertDeliverMax(
innerResult[jss::tx_json], innerSttx->getTxnType(), context.apiVersion);
}
else
{
innerResult[jss::tx_json] = innerTxn->getJson(JsonOptions::include_date);
RPC::insertDeliverMax(
innerResult[jss::tx_json], innerSttx->getTxnType(), context.apiVersion);
}

// Add metadata
innerResult[jss::meta] = innerMeta->getJson(JsonOptions::none);
insertDeliveredAmount(innerResult[jss::meta], context, innerTxn, *innerMeta);
RPC::insertNFTSyntheticInJson(innerResult, innerSttx, *innerMeta);
RPC::insertMPTokenIssuanceID(innerResult[jss::meta], innerSttx, *innerMeta);
}
else
{
// Binary mode
if (context.apiVersion > 1)
{
innerResult[jss::tx_blob] = innerTxn->getJson(
JsonOptions::include_date | JsonOptions::disable_API_prior_V2, true);
}
else
{
innerResult[jss::tx_blob] =
innerTxn->getJson(JsonOptions::include_date, true);
}
innerResult[jss::meta_blob] =
strHex(innerMeta->getAsObject().getSerializer().getData());
}
}
}

innerTxns.append(innerResult);
}

response[jss::inner_transactions] = innerTxns;
}

Json::Value
populateJsonResponse(
std::pair<TxResult, RPC::Status> const& res,
Expand Down Expand Up @@ -243,6 +327,10 @@

if (result.ctid)
response[jss::ctid] = *(result.ctid);

// For Batch transactions, include inner transaction results
if (result.validated && sttx->getTxnType() == ttBATCH)
insertBatchInnerTransactions(response, sttx, args, context);
}
return response;
}
Expand Down
Loading