From 75d505a5e59bfed38b482c9b38730d8697e24e95 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Sun, 7 Jun 2026 14:45:09 -0400 Subject: [PATCH] Fix ledger(owner_funds=true) drops non-issuer MPT offers --- src/test/rpc/LedgerRPC_test.cpp | 98 ++++++++++++++++++++ src/xrpld/app/ledger/detail/LedgerToJson.cpp | 1 + 2 files changed, 99 insertions(+) diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index f35dddffb1..767ddcb0ce 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -257,6 +258,102 @@ class LedgerRPC_test : public beast::unit_test::Suite BEAST_EXPECT(jrr[jss::ledger][jss::accountState].size() == 3u); } + void + testLedgerOwnerFundsMPTOffer() + { + testcase("Ledger owner_funds with MPT offer"); + using namespace test::jtx; + + Env env{*this}; + Account const gw{"gateway"}; + Account const alice{"alice"}; + auto const usd = gw["USD"]; + + env.fund(XRP(10'000), gw, alice); + env.close(); + env.trust(usd(1'000), alice); + env(pay(gw, alice, usd(100))); + MPTTester mpt( + {.env = env, + .issuer = gw, + .holders = {alice}, + .pay = 100, + .flags = tfMPTRequireAuth | kMptDexFlags, + .authHolder = true, + .close = false}); + MPT const mptAsset = mpt; + env.close(); + + env(noop(alice)); + // These offers differ only by TakerGets asset type. Omitting + // owner_funds serializes the tx JSON without computing offer balances; + // owner_funds=true asks LedgerToJson to compute accountFunds(TakerGets) + // for both offers, which is where IOU and MPT used to diverge. + env(offer(alice, XRP(10), usd(10))); + env(offer(alice, XRP(10), mptAsset(10))); + // The MPT offer was created while authorized. Unauthorizing in the + // same ledger makes owner_funds depend on AuthHandling::IgnoreAuth. + mpt.authorize({.account = gw, .holder = alice, .flags = tfMPTUnauthorize}); + env(noop(alice)); + env.close(); + + auto const ledgerHash = to_string(env.closed()->header().hash); + + auto const getTransactions = [&](bool includeOwnerFunds) { + json::Value params; + params[jss::ledger_hash] = ledgerHash; + params[jss::transactions] = true; + params[jss::expand] = true; + // The baseline omits owner_funds, which the RPC treats as false. + // Setting it true requests the same ledger, but asks the ledger + // serializer to add owner_funds to offer transactions in that + // ledger's transaction array. + if (includeOwnerFunds) + params[jss::owner_funds] = true; + + auto const result = env.rpc("json", "ledger", to_string(params))[jss::result]; + BEAST_EXPECT(!result.isMember(jss::error)); + BEAST_EXPECT(result[jss::ledger][jss::transactions].isArray()); + return result[jss::ledger][jss::transactions]; + }; + + auto const findOffer = [](json::Value const& txs, bool mpt) -> json::Value const* { + for (auto i = 0u; i < txs.size(); ++i) + { + auto const& tx = txs[i].isMember(jss::tx_json) ? txs[i][jss::tx_json] : txs[i]; + if (tx[jss::TransactionType] == jss::OfferCreate && + tx[jss::TakerGets].isMember(jss::mpt_issuance_id) == mpt) + { + return &txs[i]; + } + } + return nullptr; + }; + + // Baseline: same ledger request without owner_funds fields. + auto const baseline = getTransactions(false); + BEAST_EXPECT(baseline.size() == 5u); + BEAST_EXPECT(findOffer(baseline, false) != nullptr); + BEAST_EXPECT(findOffer(baseline, true) != nullptr); + + // Same ledger request with owner_funds added to eligible offer txs. + auto const withOwnerFunds = getTransactions(true); + // Requesting owner_funds must not change which ledger transactions are + // returned, even when one offer's TakerGets is MPT. + BEAST_EXPECT(withOwnerFunds.size() == baseline.size()); + + // The IOU offer is the control case for expected owner_funds output. + auto const* iouOfferTx = findOffer(withOwnerFunds, false); + if (BEAST_EXPECT(iouOfferTx != nullptr)) + BEAST_EXPECT((*iouOfferTx)[jss::owner_funds] == "100"); + + // MPT owner_funds should match the IOU behavior, even though Alice is + // unauthorized by the ledger snapshot used for serialization. + auto const* mptOfferTx = findOffer(withOwnerFunds, true); + if (BEAST_EXPECT(mptOfferTx != nullptr)) + BEAST_EXPECT((*mptOfferTx)[jss::owner_funds] == "100"); + } + /// @brief ledger RPC requests as a way to drive /// input options to lookupLedger. The point of this test is /// coverage for lookupLedger, not so much the ledger @@ -716,6 +813,7 @@ public: testLedgerFull(); testLedgerFullNonAdmin(); testLedgerAccounts(); + testLedgerOwnerFundsMPTOffer(); testLookupLedger(); testNoQueue(); testQueue(); diff --git a/src/xrpld/app/ledger/detail/LedgerToJson.cpp b/src/xrpld/app/ledger/detail/LedgerToJson.cpp index 7a581e2389..655ac23125 100644 --- a/src/xrpld/app/ledger/detail/LedgerToJson.cpp +++ b/src/xrpld/app/ledger/detail/LedgerToJson.cpp @@ -208,6 +208,7 @@ fillJsonTx( account, amount, FreezeHandling::IgnoreFreeze, + AuthHandling::IgnoreAuth, beast::Journal{beast::Journal::getNullSink()}); txJson[jss::owner_funds] = ownerFunds.getText(); }