From 9edc26a2a37108eea77f43e1933c4d9d359967bf Mon Sep 17 00:00:00 2001 From: Peter Chen <34582813+PeterChen13579@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:15:47 -0700 Subject: [PATCH] fix: Add MPT to txn JSON (#2392) fixes #2332 --- src/feed/impl/TransactionFeed.cpp | 6 +- src/rpc/RPCHelpers.cpp | 20 ++-- src/rpc/RPCHelpers.hpp | 9 +- tests/common/util/TestObject.cpp | 95 +++++++++++------ tests/common/util/TestObject.hpp | 6 ++ tests/unit/rpc/handlers/AccountTxTests.cpp | 112 ++++++++++++++++++--- 6 files changed, 183 insertions(+), 65 deletions(-) diff --git a/src/feed/impl/TransactionFeed.cpp b/src/feed/impl/TransactionFeed.cpp index 38a9d1ff..503721b5 100644 --- a/src/feed/impl/TransactionFeed.cpp +++ b/src/feed/impl/TransactionFeed.cpp @@ -206,8 +206,10 @@ TransactionFeed::pub( pubObj[txKey] = rpc::toJson(*tx); pubObj[JS(meta)] = rpc::toJson(*meta); rpc::insertDeliveredAmount(pubObj[JS(meta)].as_object(), tx, meta, txMeta.date); - rpc::insertDeliverMaxAlias(pubObj[txKey].as_object(), version); - rpc::insertMPTIssuanceID(pubObj[JS(meta)].as_object(), tx, meta); + + auto& txnPubobj = pubObj[txKey].as_object(); + rpc::insertDeliverMaxAlias(txnPubobj, version); + rpc::insertMPTIssuanceID(txnPubobj, meta); Json::Value nftJson; ripple::RPC::insertNFTSyntheticInJson(nftJson, tx, *meta); diff --git a/src/rpc/RPCHelpers.cpp b/src/rpc/RPCHelpers.cpp index 33b1c587..2f7eb09c 100644 --- a/src/rpc/RPCHelpers.cpp +++ b/src/rpc/RPCHelpers.cpp @@ -33,6 +33,7 @@ #include "web/Context.hpp" #include +#include #include #include #include @@ -262,7 +263,7 @@ toExpandedJson( auto metaJson = toJson(*meta); insertDeliveredAmount(metaJson, txn, meta, blobs.date); insertDeliverMaxAlias(txnJson, apiVersion); - insertMPTIssuanceID(metaJson, txn, meta); + insertMPTIssuanceID(txnJson, meta); if (nftEnabled == NFTokenjson::ENABLE) { Json::Value nftJson; @@ -347,14 +348,15 @@ getMPTIssuanceID(std::shared_ptr const& meta) /** * @brief Check if transaction has a new MPToken created * - * @param txn The transaction + * @param txnJson The transaction Json * @param meta The metadata * @return true if the transaction can have a mpt_issuance_id */ static bool -canHaveMPTIssuanceID(std::shared_ptr const& txn, std::shared_ptr const& meta) +canHaveMPTIssuanceID(boost::json::object const& txnJson, std::shared_ptr const& meta) { - if (txn->getTxnType() != ripple::ttMPTOKEN_ISSUANCE_CREATE) + if (txnJson.at(JS(TransactionType)).is_string() and + not boost::iequals(txnJson.at(JS(TransactionType)).as_string(), JS(MPTokenIssuanceCreate))) return false; if (meta->getResultTER() != ripple::tesSUCCESS) @@ -364,17 +366,13 @@ canHaveMPTIssuanceID(std::shared_ptr const& txn, std::shared } bool -insertMPTIssuanceID( - boost::json::object& metaJson, - std::shared_ptr const& txn, - std::shared_ptr const& meta -) +insertMPTIssuanceID(boost::json::object& txnJson, std::shared_ptr const& meta) { - if (!canHaveMPTIssuanceID(txn, meta)) + if (!canHaveMPTIssuanceID(txnJson, meta)) return false; if (auto const id = getMPTIssuanceID(meta)) { - metaJson[JS(mpt_issuance_id)] = ripple::to_string(*id); + txnJson[JS(mpt_issuance_id)] = ripple::to_string(*id); return true; } diff --git a/src/rpc/RPCHelpers.hpp b/src/rpc/RPCHelpers.hpp index e3173bf9..8f298b86 100644 --- a/src/rpc/RPCHelpers.hpp +++ b/src/rpc/RPCHelpers.hpp @@ -202,17 +202,12 @@ insertDeliveredAmount( /** * @brief Add "mpt_issuance_id" into MPTokenIssuanceCreate transaction json. * - * @param metaJson The metadata json object to add "MPTokenIssuanceID" - * @param txn The transaction object + * @param txnJson The transaction Json object * @param meta The metadata object * @return true if the "mpt_issuance_id" is added to the metadata json object */ bool -insertMPTIssuanceID( - boost::json::object& metaJson, - std::shared_ptr const& txn, - std::shared_ptr const& meta -); +insertMPTIssuanceID(boost::json::object& txnJson, std::shared_ptr const& meta); /** * @brief Convert STBase object to JSON diff --git a/tests/common/util/TestObject.cpp b/tests/common/util/TestObject.cpp index 459da15a..0dddd9d5 100644 --- a/tests/common/util/TestObject.cpp +++ b/tests/common/util/TestObject.cpp @@ -60,6 +60,7 @@ namespace { constexpr auto kINDEX1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"; +ripple::Slice const kSLICE("test", 4); } // namespace ripple::AccountID @@ -183,9 +184,7 @@ createPaymentTransactionObject( auto account2 = util::parseBase58Wrapper(std::string(accountId2)); obj.setAccountID(ripple::sfDestination, account2.value()); obj.setFieldU32(ripple::sfSequence, seq); - char const* key = "test"; - ripple::Slice const slice(key, 4); - obj.setFieldVL(ripple::sfSigningPubKey, slice); + obj.setFieldVL(ripple::sfSigningPubKey, kSLICE); return obj; } @@ -697,9 +696,7 @@ createMintNftTxWithMetadata( // required field for ttNFTOKEN_MINT tx.setFieldU32(ripple::sfNFTokenTaxon, nfTokenTaxon); tx.setFieldU32(ripple::sfSequence, seq); - char const* key = "test"; - ripple::Slice const slice(key, 4); - tx.setFieldVL(ripple::sfSigningPubKey, slice); + tx.setFieldVL(ripple::sfSigningPubKey, kSLICE); // meta ripple::STObject metaObj(ripple::sfTransactionMetaData); @@ -762,9 +759,7 @@ createMintNftTxWithMetadataOfCreatedNode( // required field for ttNFTOKEN_MINT tx.setFieldU32(ripple::sfNFTokenTaxon, nfTokenTaxon); tx.setFieldU32(ripple::sfSequence, seq); - char const* key = "test"; - ripple::Slice const slice(key, 4); - tx.setFieldVL(ripple::sfSigningPubKey, slice); + tx.setFieldVL(ripple::sfSigningPubKey, kSLICE); if (uri) tx.setFieldVL(ripple::sfURI, ripple::Slice(uri->data(), uri->size())); @@ -820,9 +815,7 @@ createNftModifyTxWithMetadata(std::string_view accountId, std::string_view nftID tx.setFieldAmount(ripple::sfFee, amount); tx.setFieldH256(ripple::sfNFTokenID, ripple::uint256{nftID}); tx.setFieldU32(ripple::sfSequence, 100); - char const* key = "test"; - ripple::Slice const slice(key, 4); - tx.setFieldVL(ripple::sfSigningPubKey, slice); + tx.setFieldVL(ripple::sfSigningPubKey, kSLICE); if (!uri.empty()) // sfURI should be absent if empty tx.setFieldVL(ripple::sfURI, uri); @@ -880,9 +873,7 @@ createNftBurnTxWithMetadataOfDeletedNode(std::string_view accountId, std::string tx.setFieldAmount(ripple::sfFee, amount); tx.setFieldH256(ripple::sfNFTokenID, ripple::uint256{nftID}); tx.setFieldU32(ripple::sfSequence, 100); - char const* key = "test"; - ripple::Slice const slice(key, 4); - tx.setFieldVL(ripple::sfSigningPubKey, slice); + tx.setFieldVL(ripple::sfSigningPubKey, kSLICE); // meta ripple::STObject metaObj(ripple::sfTransactionMetaData); @@ -927,9 +918,7 @@ createNftBurnTxWithMetadataOfModifiedNode(std::string_view accountId, std::strin tx.setFieldAmount(ripple::sfFee, amount); tx.setFieldH256(ripple::sfNFTokenID, ripple::uint256{nftID}); tx.setFieldU32(ripple::sfSequence, 100); - char const* key = "test"; - ripple::Slice const slice(key, 4); - tx.setFieldVL(ripple::sfSigningPubKey, slice); + tx.setFieldVL(ripple::sfSigningPubKey, kSLICE); // meta ripple::STObject metaObj(ripple::sfTransactionMetaData); @@ -976,9 +965,7 @@ createAcceptNftBuyerOfferTxWithMetadata( tx.setFieldAmount(ripple::sfFee, amount); tx.setFieldU32(ripple::sfSequence, seq); tx.setFieldH256(ripple::sfNFTokenBuyOffer, ripple::uint256{offerId}); - char const* key = "test"; - ripple::Slice const slice(key, 4); - tx.setFieldVL(ripple::sfSigningPubKey, slice); + tx.setFieldVL(ripple::sfSigningPubKey, kSLICE); // meta // create deletedNode with ltNFTOKEN_OFFER @@ -1025,9 +1012,7 @@ createAcceptNftSellerOfferTxWithMetadata( tx.setFieldAmount(ripple::sfFee, amount); tx.setFieldU32(ripple::sfSequence, seq); tx.setFieldH256(ripple::sfNFTokenSellOffer, ripple::uint256{offerId}); - char const* key = "test"; - ripple::Slice const slice(key, 4); - tx.setFieldVL(ripple::sfSigningPubKey, slice); + tx.setFieldVL(ripple::sfSigningPubKey, kSLICE); // meta // create deletedNode with ltNFTOKEN_OFFER @@ -1121,9 +1106,7 @@ createCancelNftOffersTxWithMetadata( return ripple::uint256{nftId.c_str()}; }); tx.setFieldV256(ripple::sfNFTokenOffers, offers); - char const* key = "test"; - ripple::Slice const slice(key, 4); - tx.setFieldVL(ripple::sfSigningPubKey, slice); + tx.setFieldVL(ripple::sfSigningPubKey, kSLICE); // meta // create deletedNode with ltNFTOKEN_OFFER @@ -1172,9 +1155,7 @@ createCreateNftOfferTxWithMetadata( tx.setFieldAmount(ripple::sfAmount, price); tx.setFieldU32(ripple::sfSequence, seq); tx.setFieldH256(ripple::sfNFTokenID, ripple::uint256{nftId}); - char const* key = "test"; - ripple::Slice const slice(key, 4); - tx.setFieldVL(ripple::sfSigningPubKey, slice); + tx.setFieldVL(ripple::sfSigningPubKey, kSLICE); // meta // create createdNode with LedgerIndex @@ -1219,9 +1200,7 @@ createOracleSetTxWithMetadata( tx.setFieldU32(ripple::sfLastUpdateTime, lastUpdateTime); tx.setFieldU32(ripple::sfOracleDocumentID, docId); tx.setFieldU32(ripple::sfSequence, seq); - char const* key = "test"; - ripple::Slice const slice(key, 4); - tx.setFieldVL(ripple::sfSigningPubKey, slice); + tx.setFieldVL(ripple::sfSigningPubKey, kSLICE); tx.setFieldArray(ripple::sfPriceDataSeries, priceDataSeries); // meta @@ -1499,6 +1478,56 @@ createMpTokenObject(std::string_view accountId, ripple::uint192 issuanceID, std: return mptoken; } +ripple::STObject +createMPTIssuanceCreateTx(std::string_view accountId, uint32_t fee, uint32_t seq) +{ + ripple::STObject tx(ripple::sfTransaction); + tx.setFieldU16(ripple::sfTransactionType, ripple::ttMPTOKEN_ISSUANCE_CREATE); + tx.setAccountID(ripple::sfAccount, getAccountIdWithString(accountId)); + tx.setFieldAmount(ripple::sfFee, ripple::STAmount(fee, false)); + tx.setFieldU32(ripple::sfSequence, seq); + tx.setFieldVL(ripple::sfSigningPubKey, kSLICE); + return tx; +} + +data::TransactionAndMetadata +createMPTIssuanceCreateTxWithMetadata(std::string_view accountId, uint32_t fee, uint32_t seq) +{ + ripple::STObject tx = createMPTIssuanceCreateTx(accountId, fee, seq); + + ripple::STObject metaObj(ripple::sfTransactionMetaData); + metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS); + metaObj.setFieldU32(ripple::sfTransactionIndex, 0); + + ripple::STObject newFields(ripple::sfNewFields); + newFields.setAccountID(ripple::sfIssuer, getAccountIdWithString(accountId)); + newFields.setFieldU16(ripple::sfLedgerEntryType, ripple::ltMPTOKEN_ISSUANCE); + newFields.setFieldU32(ripple::sfFlags, 0); + newFields.setFieldU32(ripple::sfSequence, seq); + newFields.setFieldU64(ripple::sfOwnerNode, 0); + newFields.setFieldU64(ripple::sfMaximumAmount, 0); + newFields.setFieldU64(ripple::sfOutstandingAmount, 0); + newFields.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{}); + newFields.setFieldU32(ripple::sfPreviousTxnLgrSeq, 0); + std::string_view metadata = "test-meta"; + ripple::Slice const sliceMetadata(metadata.data(), metadata.size()); + newFields.setFieldVL(ripple::sfMPTokenMetadata, sliceMetadata); + + ripple::STObject createdNode(ripple::sfCreatedNode); + createdNode.setFieldU16(ripple::sfLedgerEntryType, ripple::ltMPTOKEN_ISSUANCE); + createdNode.setFieldH256(ripple::sfLedgerIndex, ripple::uint256{}); + createdNode.emplace_back(std::move(newFields)); + + ripple::STArray affectedNodes(ripple::sfAffectedNodes); + affectedNodes.push_back(std::move(createdNode)); + metaObj.setFieldArray(ripple::sfAffectedNodes, affectedNodes); + + data::TransactionAndMetadata ret; + ret.transaction = tx.getSerializer().peekData(); + ret.metadata = metaObj.getSerializer().peekData(); + return ret; +} + ripple::STObject createPermissionedDomainObject( std::string_view accountId, diff --git a/tests/common/util/TestObject.hpp b/tests/common/util/TestObject.hpp index d0e6755e..9fbd0214 100644 --- a/tests/common/util/TestObject.hpp +++ b/tests/common/util/TestObject.hpp @@ -456,6 +456,12 @@ createMptIssuanceObject(std::string_view accountId, std::uint32_t seq, std::stri [[nodiscard]] ripple::STObject createMpTokenObject(std::string_view accountId, ripple::uint192 issuanceID, std::uint64_t mptAmount = 1); +[[nodiscard]] ripple::STObject +createMPTIssuanceCreateTx(std::string_view accountId, uint32_t fee, uint32_t seq); + +[[nodiscard]] data::TransactionAndMetadata +createMPTIssuanceCreateTxWithMetadata(std::string_view accountId, uint32_t fee, uint32_t seq); + [[nodiscard]] ripple::STObject createPermissionedDomainObject( std::string_view accountId, diff --git a/tests/unit/rpc/handlers/AccountTxTests.cpp b/tests/unit/rpc/handlers/AccountTxTests.cpp index 5da4c270..0158cc5d 100644 --- a/tests/unit/rpc/handlers/AccountTxTests.cpp +++ b/tests/unit/rpc/handlers/AccountTxTests.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include using namespace rpc; @@ -55,6 +56,7 @@ constexpr auto kNFT_ID = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76 constexpr auto kNFT_ID2 = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"; constexpr auto kNFT_ID3 = "15FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF"; constexpr auto kINDEX = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC322"; +constexpr auto kMPT_ISSUANCE_ID = "000000014B4E9C06F24296074F7BC48F92A97916C6DC5EA9"; } // namespace @@ -907,9 +909,7 @@ TEST_F(RPCAccountTxHandlerTest, SpecificLedgerIndex) ); auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kMAX_SEQ - 1); - EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); - ON_CALL(*backend_, fetchLedgerBySequence(kMAX_SEQ - 1, _)).WillByDefault(Return(ledgerHeader)); - + EXPECT_CALL(*backend_, fetchLedgerBySequence(kMAX_SEQ - 1, _)).WillOnce(Return(ledgerHeader)); ON_CALL(*mockETLServicePtr_, getETLState).WillByDefault(Return(etl::ETLState{})); runSpawn([&, this](auto yield) { @@ -937,8 +937,7 @@ TEST_F(RPCAccountTxHandlerTest, SpecificLedgerIndex) TEST_F(RPCAccountTxHandlerTest, SpecificNonexistLedgerIntIndex) { - EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); - ON_CALL(*backend_, fetchLedgerBySequence(kMAX_SEQ - 1, _)).WillByDefault(Return(std::nullopt)); + EXPECT_CALL(*backend_, fetchLedgerBySequence(kMAX_SEQ - 1, _)).WillOnce(Return(std::nullopt)); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{AccountTxHandler{backend_, mockETLServicePtr_}}; @@ -962,8 +961,7 @@ TEST_F(RPCAccountTxHandlerTest, SpecificNonexistLedgerIntIndex) TEST_F(RPCAccountTxHandlerTest, SpecificNonexistLedgerStringIndex) { - EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); - ON_CALL(*backend_, fetchLedgerBySequence(kMAX_SEQ - 1, _)).WillByDefault(Return(std::nullopt)); + EXPECT_CALL(*backend_, fetchLedgerBySequence(kMAX_SEQ - 1, _)).WillOnce(Return(std::nullopt)); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{AccountTxHandler{backend_, mockETLServicePtr_}}; @@ -1000,11 +998,10 @@ TEST_F(RPCAccountTxHandlerTest, SpecificLedgerHash) testing::Optional(testing::Eq(TransactionsCursor{kMAX_SEQ - 1, INT32_MAX})), testing::_ ) - ) - .Times(1); + ); auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kMAX_SEQ - 1); - EXPECT_CALL(*backend_, fetchLedgerByHash).Times(1); + EXPECT_CALL(*backend_, fetchLedgerByHash); ON_CALL(*backend_, fetchLedgerByHash(ripple::uint256{kLEDGER_HASH}, _)).WillByDefault(Return(ledgerHeader)); ON_CALL(*mockETLServicePtr_, getETLState).WillByDefault(Return(etl::ETLState{})); @@ -1050,8 +1047,7 @@ TEST_F(RPCAccountTxHandlerTest, SpecificLedgerIndexValidated) ); auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kMAX_SEQ); - EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); - ON_CALL(*backend_, fetchLedgerBySequence(kMAX_SEQ, _)).WillByDefault(Return(ledgerHeader)); + EXPECT_CALL(*backend_, fetchLedgerBySequence(kMAX_SEQ, _)).WillOnce(Return(ledgerHeader)); ON_CALL(*mockETLServicePtr_, getETLState).WillByDefault(Return(etl::ETLState{})); @@ -1599,6 +1595,98 @@ TEST_F(RPCAccountTxHandlerTest, NFTTxs_API_v2) }); } +TEST_F(RPCAccountTxHandlerTest, MPTTxs_API_v2) +{ + auto const out = fmt::format( + R"JSON({{ + "account": "{}", + "ledger_index_min": 10, + "ledger_index_max": 30, + "transactions": [ + {{ + "meta": {{ + "AffectedNodes": [ + {{ + "CreatedNode": {{ + "LedgerEntryType": "MPTokenIssuance", + "LedgerIndex": "0000000000000000000000000000000000000000000000000000000000000000", + "NewFields": {{ + "Flags": 0, + "Issuer": "{}", + "LedgerEntryType": "MPTokenIssuance", + "MPTokenMetadata": "746573742D6D657461", + "MaximumAmount": "0", + "OutstandingAmount": "0", + "OwnerNode": "0", + "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000", + "PreviousTxnLgrSeq": 0, + "Sequence": 1 + }} + }} + }} + ], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS" + }}, + "hash": "A52221F4003C281D3C83F501F418B55A1F9DC1C6A129EF13E1A8F0E5C008DAE3", + "ledger_index": 11, + "ledger_hash": "{}", + "close_time_iso": "2000-01-01T00:00:00Z", + "tx_json": {{ + "Account": "{}", + "Fee": "50", + "Sequence": 1, + "SigningPubKey": "74657374", + "TransactionType": "MPTokenIssuanceCreate", + "mpt_issuance_id": "{}", + "ledger_index": 11, + "ctid": "C000000B00000000", + "date": 1 + }}, + "validated": true + }} + ], + "validated": true + }})JSON", + kACCOUNT, + kACCOUNT, + kLEDGER_HASH, + kACCOUNT, + kMPT_ISSUANCE_ID + ); + + auto mptTx = createMPTIssuanceCreateTxWithMetadata(kACCOUNT, 50, 1); + mptTx.ledgerSequence = kMIN_SEQ + 1; + mptTx.date = 1; + + auto transactions = std::vector{std::move(mptTx)}; + auto const transCursor = TransactionsAndCursor{.txns = std::move(transactions), .cursor = std::nullopt}; + + EXPECT_CALL(*backend_, fetchAccountTransactions).WillOnce(Return(transCursor)); + + auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kMIN_SEQ + 1); + EXPECT_CALL(*backend_, fetchLedgerBySequence(kMIN_SEQ + 1, _)).WillOnce(Return(ledgerHeader)); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{AccountTxHandler{backend_, mockETLServicePtr_}}; + static auto const kINPUT = json::parse( + fmt::format( + R"JSON({{ + "account": "{}", + "ledger_index_min": {}, + "ledger_index_max": {} + }})JSON", + kACCOUNT, + kMIN_SEQ, + kMAX_SEQ + ) + ); + auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2u}); + ASSERT_TRUE(output); + EXPECT_EQ(*output.result, json::parse(out)); + }); +} + struct AccountTxTransactionBundle { std::string testName; std::string testJson;