From 8f4e9c25d892767727ca5af09599df2b47890f9a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:19:05 +0000 Subject: [PATCH] fix: Add CTID to ledger command expanded transactions (#6401) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Mayukha Vadari Co-authored-by: Mayukha Vadari Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com> Co-authored-by: Timur Yalymov <36795566+tyalymov@users.noreply.github.com> Co-authored-by: Cursor Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Co-authored-by: Ayaz Salikhov --- src/test/rpc/LedgerRPC_test.cpp | 94 ++++++++++++++++++++ src/xrpld/app/ledger/detail/LedgerToJson.cpp | 16 ++++ 2 files changed, 110 insertions(+) diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index e7c5dd4a80..deb19e3a3f 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -13,17 +13,21 @@ #include #include +#include #include #include #include +#include #include #include #include +#include #include #include #include +#include #include #include #include @@ -807,6 +811,95 @@ 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 (auto const& txn : txns) + { + BEAST_EXPECT(txn.isMember(jss::ctid)); + auto const expectedCtid = rpc::encodeCTID( + jrr[jss::ledger][jss::ledger_index].asUInt(), + txn[jss::meta][sfTransactionIndex.jsonName].asUInt(), + netID); + // NOLINTBEGIN(bugprone-unchecked-optional-access) + if (BEAST_EXPECT(expectedCtid.has_value())) + BEAST_EXPECT(txn[jss::ctid] == expectedCtid.value()); + // NOLINTEND(bugprone-unchecked-optional-access) + } + } + + // 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 (auto const& txn : txns) + { + BEAST_EXPECT(txn.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 (auto const& txn : txns) + { + BEAST_EXPECT(txn.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 (auto const& txn : txns) + { + BEAST_EXPECT(txn.isString()); + } + } + } + public: void run() override @@ -822,6 +915,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 1d789aa604..419da2a995 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 @@ -11,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +29,7 @@ #include #include +#include #include #include #include @@ -179,6 +182,19 @@ fillJsonTx( } } + // compute outgoing CTID + if (stMeta && stMeta->isFieldPresent(sfTransactionIndex)) + { + uint32_t const lgrSeq = fill.ledger.seq(); + uint32_t const txnIdx = stMeta->getFieldU32(sfTransactionIndex); + uint32_t netID = fill.context->app.getNetworkIDService().getNetworkID(); + if (txn->isFieldPresent(sfNetworkID)) + netID = txn->getFieldU32(sfNetworkID); + + 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) {