Skip to content

Commit c02ed4f

Browse files
committed
add inner transaction info (if present) to outer transaction tx return
1 parent 3a8a18c commit c02ed4f

File tree

3 files changed

+200
-0
lines changed

3 files changed

+200
-0
lines changed

include/xrpl/protocol/jss.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ JSS(index); // in: LedgerEntry
305305
// LedgerEntry, TxHistory, LedgerData
306306
JSS(info); // out: ServerInfo, ConsensusInfo, FetchInfo
307307
JSS(initial_sync_duration_us);
308+
JSS(inner_transactions); // out: tx (Batch)
308309
JSS(internal_command); // in: Internal
309310
JSS(invalid_API_version); // out: Many, when a request has an invalid
310311
// version

src/test/rpc/Transaction_test.cpp

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
#include <test/jtx.h>
22
#include <test/jtx/Env.h>
3+
#include <test/jtx/batch.h>
34
#include <test/jtx/envconfig.h>
45

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

89
#include <xrpl/core/NetworkIDService.h>
910
#include <xrpl/protocol/ErrorCodes.h>
11+
#include <xrpl/protocol/SField.h>
1012
#include <xrpl/protocol/STBase.h>
13+
#include <xrpl/protocol/TxFlags.h>
1114
#include <xrpl/protocol/jss.h>
1215
#include <xrpl/protocol/serialize.h>
1316

@@ -842,6 +845,112 @@ class Transaction_test : public beast::unit_test::suite
842845
}
843846
}
844847

848+
void
849+
testBatchInnerTransactions(FeatureBitset features, unsigned apiVersion)
850+
{
851+
testcase("Test Batch inner_transactions API version " + std::to_string(apiVersion));
852+
853+
using namespace test::jtx;
854+
using std::to_string;
855+
856+
Env env{*this, features};
857+
Account const alice{"alice"};
858+
Account const bob{"bob"};
859+
860+
env.fund(XRP(10000), alice, bob);
861+
env.close();
862+
863+
auto const aliceSeq = env.seq(alice);
864+
865+
// Create a Batch transaction with two inner Payment transactions
866+
auto const batchFee = batch::calcBatchFee(env, 0, 2);
867+
auto batchTxn = env.jt(
868+
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
869+
batch::inner(pay(alice, bob, XRP(100)), aliceSeq + 1),
870+
batch::inner(pay(alice, bob, XRP(200)), aliceSeq + 2));
871+
env(batchTxn, ter(tesSUCCESS));
872+
env.close();
873+
874+
// Get the batch transaction ID and inner transaction IDs
875+
auto const batchID = batchTxn.stx->getTransactionID();
876+
auto const innerIDs = batchTxn.stx->getBatchTransactionIDs();
877+
BEAST_EXPECT(innerIDs.size() == 2);
878+
879+
// Test non-binary mode
880+
{
881+
Json::Value params{Json::objectValue};
882+
params[jss::transaction] = to_string(batchID);
883+
params[jss::binary] = false;
884+
params[jss::api_version] = apiVersion;
885+
auto const result = env.client().invoke("tx", params);
886+
887+
BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
888+
BEAST_EXPECT(result[jss::result][jss::validated] == true);
889+
890+
// Check inner_transactions field exists
891+
if (BEAST_EXPECT(result[jss::result].isMember(jss::inner_transactions)))
892+
{
893+
auto const& innerTxns = result[jss::result][jss::inner_transactions];
894+
if (BEAST_EXPECT(innerTxns.isArray() && innerTxns.size() == 2))
895+
{
896+
// Check first inner transaction
897+
auto const& inner0 = innerTxns[0u];
898+
BEAST_EXPECT(inner0[jss::hash] == to_string(innerIDs[0]));
899+
BEAST_EXPECT(inner0[jss::engine_result] == "tesSUCCESS");
900+
BEAST_EXPECT(inner0.isMember(jss::tx_json));
901+
BEAST_EXPECT(inner0.isMember(jss::meta));
902+
BEAST_EXPECT(inner0[jss::tx_json][jss::TransactionType] == "Payment");
903+
// Validate ParentBatchID in metadata matches outer Batch hash
904+
BEAST_EXPECT(inner0[jss::meta][sfParentBatchID.jsonName] == to_string(batchID));
905+
906+
// Check second inner transaction
907+
auto const& inner1 = innerTxns[1u];
908+
BEAST_EXPECT(inner1[jss::hash] == to_string(innerIDs[1]));
909+
BEAST_EXPECT(inner1[jss::engine_result] == "tesSUCCESS");
910+
BEAST_EXPECT(inner1.isMember(jss::tx_json));
911+
BEAST_EXPECT(inner1.isMember(jss::meta));
912+
BEAST_EXPECT(inner1[jss::tx_json][jss::TransactionType] == "Payment");
913+
// Validate ParentBatchID in metadata matches outer Batch hash
914+
BEAST_EXPECT(inner1[jss::meta][sfParentBatchID.jsonName] == to_string(batchID));
915+
}
916+
}
917+
}
918+
919+
// Test binary mode
920+
{
921+
Json::Value params{Json::objectValue};
922+
params[jss::transaction] = to_string(batchID);
923+
params[jss::binary] = true;
924+
params[jss::api_version] = apiVersion;
925+
auto const result = env.client().invoke("tx", params);
926+
927+
BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
928+
BEAST_EXPECT(result[jss::result][jss::validated] == true);
929+
930+
// Check inner_transactions field exists in binary mode
931+
if (BEAST_EXPECT(result[jss::result].isMember(jss::inner_transactions)))
932+
{
933+
auto const& innerTxns = result[jss::result][jss::inner_transactions];
934+
if (BEAST_EXPECT(innerTxns.isArray() && innerTxns.size() == 2))
935+
{
936+
// Check first inner transaction has binary blobs
937+
auto const& inner0 = innerTxns[0u];
938+
BEAST_EXPECT(inner0[jss::hash] == to_string(innerIDs[0]));
939+
BEAST_EXPECT(inner0[jss::engine_result] == "tesSUCCESS");
940+
BEAST_EXPECT(inner0.isMember(jss::tx_blob));
941+
BEAST_EXPECT(inner0.isMember(jss::meta_blob));
942+
943+
// Check second inner transaction has binary blobs
944+
auto const& inner1 = innerTxns[1u];
945+
BEAST_EXPECT(inner1[jss::hash] == to_string(innerIDs[1]));
946+
BEAST_EXPECT(inner1[jss::engine_result] == "tesSUCCESS");
947+
BEAST_EXPECT(inner1.isMember(jss::tx_blob));
948+
BEAST_EXPECT(inner1.isMember(jss::meta_blob));
949+
}
950+
}
951+
}
952+
}
953+
845954
public:
846955
void
847956
run() override
@@ -861,6 +970,8 @@ class Transaction_test : public beast::unit_test::suite
861970
testCTIDValidation(features);
862971
testRPCsForCTID(features);
863972
forAllApiVersions(std::bind_front(&Transaction_test::testRequest, this, features));
973+
forAllApiVersions(
974+
std::bind_front(&Transaction_test::testBatchInnerTransactions, this, features));
864975
}
865976
};
866977

src/xrpld/rpc/handlers/Tx.cpp

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,90 @@ doTxHelp(RPC::Context& context, TxArgs args)
160160
return {result, rpcSUCCESS};
161161
}
162162

163+
// Helper function to fetch and format inner transaction results for Batch
164+
// transactions
165+
static void
166+
insertBatchInnerTransactions(
167+
Json::Value& response,
168+
std::shared_ptr<STTx const> const& sttx,
169+
TxArgs const& args,
170+
RPC::JsonContext const& context)
171+
{
172+
auto const& innerTxnIds = sttx->getBatchTransactionIDs();
173+
if (innerTxnIds.empty())
174+
return;
175+
176+
Json::Value innerTxns(Json::arrayValue);
177+
178+
for (auto const& innerTxnId : innerTxnIds)
179+
{
180+
Json::Value innerResult(Json::objectValue);
181+
innerResult[jss::hash] = to_string(innerTxnId);
182+
183+
// Fetch the inner transaction
184+
auto ec = rpcSUCCESS;
185+
auto v = context.app.getMasterTransaction().fetch(innerTxnId, ec);
186+
187+
if (auto txPair =
188+
std::get_if<std::pair<std::shared_ptr<Transaction>, std::shared_ptr<TxMeta>>>(&v))
189+
{
190+
auto const& [innerTxn, innerMeta] = *txPair;
191+
if (innerTxn && innerMeta)
192+
{
193+
// Add transaction result code
194+
auto const ter = innerMeta->getResultTER();
195+
innerResult[jss::engine_result] = transToken(ter);
196+
197+
if (!args.binary)
198+
{
199+
// Add the full transaction JSON
200+
auto const& innerSttx = innerTxn->getSTransaction();
201+
if (context.apiVersion > 1)
202+
{
203+
constexpr auto optionsJson =
204+
JsonOptions::include_date | JsonOptions::disable_API_prior_V2;
205+
innerResult[jss::tx_json] = innerTxn->getJson(optionsJson);
206+
RPC::insertDeliverMax(
207+
innerResult[jss::tx_json], innerSttx->getTxnType(), context.apiVersion);
208+
}
209+
else
210+
{
211+
innerResult[jss::tx_json] = innerTxn->getJson(JsonOptions::include_date);
212+
RPC::insertDeliverMax(
213+
innerResult[jss::tx_json], innerSttx->getTxnType(), context.apiVersion);
214+
}
215+
216+
// Add metadata
217+
innerResult[jss::meta] = innerMeta->getJson(JsonOptions::none);
218+
insertDeliveredAmount(innerResult[jss::meta], context, innerTxn, *innerMeta);
219+
RPC::insertNFTSyntheticInJson(innerResult, innerSttx, *innerMeta);
220+
RPC::insertMPTokenIssuanceID(innerResult[jss::meta], innerSttx, *innerMeta);
221+
}
222+
else
223+
{
224+
// Binary mode
225+
if (context.apiVersion > 1)
226+
{
227+
innerResult[jss::tx_blob] = innerTxn->getJson(
228+
JsonOptions::include_date | JsonOptions::disable_API_prior_V2, true);
229+
}
230+
else
231+
{
232+
innerResult[jss::tx_blob] =
233+
innerTxn->getJson(JsonOptions::include_date, true);
234+
}
235+
innerResult[jss::meta_blob] =
236+
strHex(innerMeta->getAsObject().getSerializer().getData());
237+
}
238+
}
239+
}
240+
241+
innerTxns.append(innerResult);
242+
}
243+
244+
response[jss::inner_transactions] = innerTxns;
245+
}
246+
163247
Json::Value
164248
populateJsonResponse(
165249
std::pair<TxResult, RPC::Status> const& res,
@@ -243,6 +327,10 @@ populateJsonResponse(
243327

244328
if (result.ctid)
245329
response[jss::ctid] = *(result.ctid);
330+
331+
// For Batch transactions, include inner transaction results
332+
if (result.validated && sttx->getTxnType() == ttBATCH)
333+
insertBatchInnerTransactions(response, sttx, args, context);
246334
}
247335
return response;
248336
}

0 commit comments

Comments
 (0)