From 42b10c71383395c04d0d3be6f906fdc730bf4c39 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Tue, 3 Feb 2026 00:53:03 -0500 Subject: [PATCH 1/3] feat: add full support for all objects in ledger_entry --- src/xrpld/rpc/handlers/LedgerEntry.cpp | 60 ++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/src/xrpld/rpc/handlers/LedgerEntry.cpp b/src/xrpld/rpc/handlers/LedgerEntry.cpp index 440ed11c189..96fb1817418 100644 --- a/src/xrpld/rpc/handlers/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/LedgerEntry.cpp @@ -142,7 +142,20 @@ parseBridge(Json::Value const& params, Json::StaticString const fieldName, [[may static Expected parseCheck(Json::Value const& params, Json::StaticString const fieldName, [[maybe_unused]] unsigned const apiVersion) { - return parseObjectID(params, fieldName, "hex string"); + if (!params.isObject()) + { + return parseObjectID(params, fieldName); + } + + auto const account = LedgerEntryHelpers::requiredAccountID(params, jss::account, "malformedAddress"); + if (!account) + return Unexpected(account.error()); + + auto const sequence = LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedRequest"); + if (!sequence) + return Unexpected(sequence.error()); + + return keylet::check(*account, *sequence).key; } static Expected @@ -491,7 +504,20 @@ parseNFTokenOffer( Json::StaticString const fieldName, [[maybe_unused]] unsigned const apiVersion) { - return parseObjectID(params, fieldName, "hex string"); + if (!params.isObject()) + { + return parseObjectID(params, fieldName); + } + + auto const owner = LedgerEntryHelpers::requiredAccountID(params, jss::owner, "malformedOwner"); + if (!owner) + return Unexpected(owner.error()); + + auto const sequence = LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedRequest"); + if (!sequence) + return Unexpected(sequence.error()); + + return keylet::nftoffer(*owner, *sequence).key; } static Expected @@ -549,7 +575,24 @@ parsePayChannel( Json::StaticString const fieldName, [[maybe_unused]] unsigned const apiVersion) { - return parseObjectID(params, fieldName, "hex string"); + if (!params.isObject()) + { + return parseObjectID(params, fieldName); + } + + auto const account = LedgerEntryHelpers::requiredAccountID(params, jss::account, "malformedAddress"); + if (!account) + return Unexpected(account.error()); + + auto const destination = LedgerEntryHelpers::requiredAccountID(params, jss::destination, "malformedDestination"); + if (!destination) + return Unexpected(destination.error()); + + auto const sequence = LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedRequest"); + if (!sequence) + return Unexpected(sequence.error()); + + return keylet::payChan(*account, *destination, *sequence).key; } static Expected @@ -628,7 +671,16 @@ parseSignerList( Json::StaticString const fieldName, [[maybe_unused]] unsigned const apiVersion) { - return parseObjectID(params, fieldName, "hex string"); + if (!params.isObject()) + { + return parseObjectID(params, fieldName); + } + + auto const id = LedgerEntryHelpers::requiredAccountID(params, jss::account, "malformedAddress"); + if (!id) + return Unexpected(id.error()); + + return keylet::signers(*id).key; } static Expected From 87514f172db7a8b447687bda9b1a0e4bad4c1234 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Tue, 3 Feb 2026 01:05:25 -0500 Subject: [PATCH 2/3] add tests --- src/test/rpc/LedgerEntry_test.cpp | 171 ++++++++++++++++++++++++++++-- 1 file changed, 162 insertions(+), 9 deletions(-) diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp index a7ae5472726..c317951c2f2 100644 --- a/src/test/rpc/LedgerEntry_test.cpp +++ b/src/test/rpc/LedgerEntry_test.cpp @@ -44,6 +44,7 @@ std::vector> mappings{ {jss::authorized, FieldType::AccountField}, {jss::credential_type, FieldType::BlobField}, {jss::currency, FieldType::CurrencyField}, + {jss::destination, FieldType::AccountField}, {jss::issuer, FieldType::AccountField}, {jss::oracle_document_id, FieldType::UInt32Field}, {jss::owner, FieldType::AccountField}, @@ -699,14 +700,15 @@ class LedgerEntry_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.close(); - auto const checkId = keylet::check(env.master, env.seq(env.master)); + std::uint32_t const checkSeq = env.seq(env.master); + auto const checkId = keylet::check(env.master, checkSeq); env(check::create(env.master, alice, XRP(100))); env.close(); std::string const ledgerHash{to_string(env.closed()->header().hash)}; { - // Request a check. + // Request a check by hash. Json::Value jvParams; jvParams[jss::check] = to_string(checkId.key); jvParams[jss::ledger_hash] = ledgerHash; @@ -714,6 +716,28 @@ class LedgerEntry_test : public beast::unit_test::suite BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Check); BEAST_EXPECT(jrr[jss::node][sfSendMax.jsonName] == "100000000"); } + { + // Request a check by account and seq. + Json::Value jvParams; + jvParams[jss::check] = Json::objectValue; + jvParams[jss::check][jss::account] = env.master.human(); + jvParams[jss::check][jss::seq] = checkSeq; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Check); + BEAST_EXPECT(jrr[jss::node][sfSendMax.jsonName] == "100000000"); + BEAST_EXPECT(jrr[jss::index] == to_string(checkId.key)); + } + { + // Request a non-existent check by account and seq. + Json::Value jvParams; + jvParams[jss::check] = Json::objectValue; + jvParams[jss::check][jss::account] = env.master.human(); + jvParams[jss::check][jss::seq] = checkSeq + 1000; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "entryNotFound", "Entry not found."); + } { // Request an index that is not a check. We'll use alice's // account root index. @@ -732,7 +756,13 @@ class LedgerEntry_test : public beast::unit_test::suite } { // Check malformed cases - runLedgerEntryTest(env, jss::check); + runLedgerEntryTest( + env, + jss::check, + { + {jss::account, "malformedAddress"}, + {jss::seq, "malformedRequest"}, + }); } } @@ -1374,10 +1404,13 @@ class LedgerEntry_test : public beast::unit_test::suite uint256 const nftokenID0 = token::getNextID(env, issuer, 0, tfTransferable); env(token::mint(issuer, 0), txflags(tfTransferable)); env.close(); - uint256 const offerID = keylet::nftoffer(issuer, env.seq(issuer)).key; + std::uint32_t const offerSeq = env.seq(issuer); + uint256 const offerID = keylet::nftoffer(issuer, offerSeq).key; env(token::createOffer(issuer, nftokenID0, drops(1)), token::destination(buyer), txflags(tfSellNFToken)); + env.close(); { + // Request by hash. Json::Value jvParams; jvParams[jss::nft_offer] = to_string(offerID); Json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; @@ -1386,9 +1419,37 @@ class LedgerEntry_test : public beast::unit_test::suite BEAST_EXPECT(jrr[jss::node][sfNFTokenID.jsonName] == to_string(nftokenID0)); BEAST_EXPECT(jrr[jss::node][sfAmount.jsonName] == "1"); } + { + // Request by owner and seq. + Json::Value jvParams; + jvParams[jss::nft_offer] = Json::objectValue; + jvParams[jss::nft_offer][jss::owner] = issuer.human(); + jvParams[jss::nft_offer][jss::seq] = offerSeq; + Json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::NFTokenOffer); + BEAST_EXPECT(jrr[jss::node][sfOwner.jsonName] == issuer.human()); + BEAST_EXPECT(jrr[jss::node][sfNFTokenID.jsonName] == to_string(nftokenID0)); + BEAST_EXPECT(jrr[jss::node][sfAmount.jsonName] == "1"); + BEAST_EXPECT(jrr[jss::index] == to_string(offerID)); + } + { + // Request a non-existent offer by owner and seq. + Json::Value jvParams; + jvParams[jss::nft_offer] = Json::objectValue; + jvParams[jss::nft_offer][jss::owner] = issuer.human(); + jvParams[jss::nft_offer][jss::seq] = offerSeq + 1000; + Json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "entryNotFound", "Entry not found."); + } // negative tests - runLedgerEntryTest(env, jss::nft_offer); + runLedgerEntryTest( + env, + jss::nft_offer, + { + {jss::owner, "malformedOwner"}, + {jss::seq, "malformedRequest"}, + }); } void @@ -1540,14 +1601,15 @@ class LedgerEntry_test : public beast::unit_test::suite return jv; }; + std::uint32_t const payChanSeq = env.seq(alice); env(payChanCreate(alice, env.master, XRP(57), 18s, alice.pk())); env.close(); std::string const ledgerHash{to_string(env.closed()->header().hash)}; - uint256 const payChanIndex{keylet::payChan(alice, env.master, env.seq(alice) - 1).key}; + uint256 const payChanIndex{keylet::payChan(alice, env.master, payChanSeq).key}; { - // Request the payment channel using its index. + // Request the payment channel using its hash. Json::Value jvParams; jvParams[jss::payment_channel] = to_string(payChanIndex); jvParams[jss::ledger_hash] = ledgerHash; @@ -1556,6 +1618,31 @@ class LedgerEntry_test : public beast::unit_test::suite BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName] == "0"); BEAST_EXPECT(jrr[jss::node][sfSettleDelay.jsonName] == 18); } + { + // Request the payment channel by account, destination, and seq. + Json::Value jvParams; + jvParams[jss::payment_channel] = Json::objectValue; + jvParams[jss::payment_channel][jss::account] = alice.human(); + jvParams[jss::payment_channel][jss::destination] = env.master.human(); + jvParams[jss::payment_channel][jss::seq] = payChanSeq; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::node][sfAmount.jsonName] == "57000000"); + BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName] == "0"); + BEAST_EXPECT(jrr[jss::node][sfSettleDelay.jsonName] == 18); + BEAST_EXPECT(jrr[jss::index] == to_string(payChanIndex)); + } + { + // Request a non-existent payment channel by account, destination, and seq. + Json::Value jvParams; + jvParams[jss::payment_channel] = Json::objectValue; + jvParams[jss::payment_channel][jss::account] = alice.human(); + jvParams[jss::payment_channel][jss::destination] = env.master.human(); + jvParams[jss::payment_channel][jss::seq] = payChanSeq + 1000; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "entryNotFound", "Entry not found."); + } { // Request an index that is not a payment channel. Json::Value jvParams; @@ -1567,7 +1654,14 @@ class LedgerEntry_test : public beast::unit_test::suite { // Malformed paychan field - runLedgerEntryTest(env, jss::payment_channel); + runLedgerEntryTest( + env, + jss::payment_channel, + { + {jss::account, "malformedAddress"}, + {jss::destination, "malformedDestination"}, + {jss::seq, "malformedRequest"}, + }); } } @@ -1706,7 +1800,66 @@ class LedgerEntry_test : public beast::unit_test::suite testcase("Signer List"); using namespace test::jtx; Env env{*this}; - runLedgerEntryTest(env, jss::signer_list); + Account const alice{"alice"}; + Account const bogie{"bogie"}; + Account const demon{"demon"}; + + env.fund(XRP(10000), alice); + env.close(); + + // Attach phantom signers to alice. + env(signers(alice, 1, {{bogie, 1}, {demon, 1}})); + env.close(); + + std::string const ledgerHash{to_string(env.closed()->header().hash)}; + auto const signerListIndex = keylet::signers(alice).key; + { + // Request by hash. + Json::Value jvParams; + jvParams[jss::signer_list] = to_string(signerListIndex); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::SignerList); + BEAST_EXPECT(jrr[jss::node][sfSignerQuorum.jsonName] == 1); + } + { + // Request by account. + Json::Value jvParams; + jvParams[jss::signer_list] = Json::objectValue; + jvParams[jss::signer_list][jss::account] = alice.human(); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::SignerList); + BEAST_EXPECT(jrr[jss::node][sfSignerQuorum.jsonName] == 1); + BEAST_EXPECT(jrr[jss::index] == to_string(signerListIndex)); + } + { + // Request a non-existent signer list by account. + Json::Value jvParams; + jvParams[jss::signer_list] = Json::objectValue; + jvParams[jss::signer_list][jss::account] = env.master.human(); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "entryNotFound", "Entry not found."); + } + { + // Request an index that is not a signer list. + Json::Value jvParams; + jvParams[jss::signer_list] = ledgerHash; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "entryNotFound", "Entry not found."); + } + + { + // Malformed signer_list fields + runLedgerEntryTest( + env, + jss::signer_list, + { + {jss::account, "malformedAddress"}, + }); + } } void From 363a9e4dfc647cc673db13a4a478b83d7b80f37f Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Tue, 3 Feb 2026 10:33:43 -0500 Subject: [PATCH 3/3] update api changelog --- API-CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/API-CHANGELOG.md b/API-CHANGELOG.md index c7a31d27fa4..3f9c00f8d0a 100644 --- a/API-CHANGELOG.md +++ b/API-CHANGELOG.md @@ -22,6 +22,12 @@ API version 2 is available in `rippled` version 2.0.0 and later. See [API-VERSIO This version is supported by all `rippled` versions. For WebSocket and HTTP JSON-RPC requests, it is currently the default API version used when no `api_version` is specified. +## Unreleased + +### Additions in unreleased + +- `ledger_entry`: Add full support for checks, NFT offers, payment channels, and signer lists. ([#6319](https://github.com/XRPLF/rippled/pull/6319)) + ## XRP Ledger server version 3.1.0 [Version 3.1.0](https://github.com/XRPLF/rippled/releases/tag/3.1.0) was released on Jan 27, 2026.