From 0dc26c871aad802aabeb53d6270d2c6f1ed01fb6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 20:56:13 +0000 Subject: [PATCH] fix: Add CTID to ledger command expanded transactions The ledger RPC method now includes a ctid field in expanded transaction objects, consistent with tx, account_tx, and transaction stream subscriptions. The CTID is computed from the transaction metadata's TransactionIndex field, the ledger sequence, and the network ID. --- src/test/rpc/LedgerRPC_test.cpp | 88 ++++++++++++++++++++ src/xrpld/app/ledger/detail/LedgerToJson.cpp | 13 +++ 2 files changed, 101 insertions(+) diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index f35dddffb1..f7812bbc82 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -12,9 +12,11 @@ #include #include +#include #include #include +#include #include #include #include @@ -706,6 +708,91 @@ class LedgerRPC_test : public beast::unit_test::Suite } } + void + testLedgerExpandedTransactionsCTID() + { + testcase("Expanded Transactions CTID"); + using namespace test::jtx; + + Env env{*this}; + Account const alice{"alice"}; + env.fund(XRP(10000), alice); + env.close(); + + uint32_t const netID = env.app().getNetworkIDService().getNetworkID(); + + // API v2 non-binary: CTID present + { + json::Value jvParams; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::transactions] = true; + jvParams[jss::expand] = true; + jvParams[jss::api_version] = 2; + auto const jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::status] == "success"); + auto const& txns = jrr[jss::ledger][jss::transactions]; + BEAST_EXPECT(txns.isArray() && txns.size() > 0); + for (unsigned i = 0; i < txns.size(); ++i) + { + BEAST_EXPECT(txns[i].isMember(jss::ctid)); + auto const expectedCtid = + RPC::encodeCTID(jrr[jss::ledger][jss::ledger_index].asUInt(), i, netID); + BEAST_EXPECT(expectedCtid.has_value()); + BEAST_EXPECT(txns[i][jss::ctid] == *expectedCtid); + } + } + + // API v1 non-binary: CTID present + { + json::Value jvParams; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::transactions] = true; + jvParams[jss::expand] = true; + auto const jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::status] == "success"); + auto const& txns = jrr[jss::ledger][jss::transactions]; + BEAST_EXPECT(txns.isArray() && txns.size() > 0); + for (unsigned i = 0; i < txns.size(); ++i) + { + BEAST_EXPECT(txns[i].isMember(jss::ctid)); + } + } + + // Binary expanded: CTID present + { + json::Value jvParams; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::transactions] = true; + jvParams[jss::expand] = true; + jvParams[jss::binary] = true; + jvParams[jss::api_version] = 2; + auto const jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::status] == "success"); + auto const& txns = jrr[jss::ledger][jss::transactions]; + BEAST_EXPECT(txns.isArray() && txns.size() > 0); + for (unsigned i = 0; i < txns.size(); ++i) + { + BEAST_EXPECT(txns[i].isMember(jss::ctid)); + } + } + + // Non-expanded: transactions are plain hash strings, no CTID + { + json::Value jvParams; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::transactions] = true; + jvParams[jss::api_version] = 2; + auto const jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::status] == "success"); + auto const& txns = jrr[jss::ledger][jss::transactions]; + BEAST_EXPECT(txns.isArray() && txns.size() > 0); + for (unsigned i = 0; i < txns.size(); ++i) + { + BEAST_EXPECT(txns[i].isString()); + } + } + } + public: void run() override @@ -720,6 +807,7 @@ public: testNoQueue(); testQueue(); testLedgerAccountsOption(); + testLedgerExpandedTransactionsCTID(); } }; diff --git a/src/xrpld/app/ledger/detail/LedgerToJson.cpp b/src/xrpld/app/ledger/detail/LedgerToJson.cpp index 7a581e2389..a59aa1a031 100644 --- a/src/xrpld/app/ledger/detail/LedgerToJson.cpp +++ b/src/xrpld/app/ledger/detail/LedgerToJson.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -12,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -193,6 +195,17 @@ fillJsonTx( } } + // compute outgoing CTID + if (fill.context && stMeta && stMeta->isFieldPresent(sfTransactionIndex)) + { + uint32_t const lgrSeq = fill.ledger.seq(); + uint32_t const txnIdx = stMeta->getFieldU32(sfTransactionIndex); + uint32_t const netID = fill.context->app.getNetworkIDService().getNetworkID(); + + if (auto ctid = RPC::encodeCTID(lgrSeq, txnIdx, netID)) + txJson[jss::ctid] = *ctid; + } + if (((fill.options & static_cast(LedgerFill::Options::OwnerFunds)) != 0) && txn->getTxnType() == ttOFFER_CREATE) {