diff --git a/include/xrpl/protocol/NFTSyntheticSerializer.h b/include/xrpl/protocol/NFTSyntheticSerializer.h index cb33744485e..d81e8fdd6fc 100644 --- a/include/xrpl/protocol/NFTSyntheticSerializer.h +++ b/include/xrpl/protocol/NFTSyntheticSerializer.h @@ -31,13 +31,13 @@ namespace ripple { namespace RPC { /** - Adds common synthetic fields to transaction-related JSON responses + Adds common synthetic fields to transaction metadata JSON @{ */ void insertNFTSyntheticInJson( - Json::Value&, + Json::Value& metadata, std::shared_ptr const&, TxMeta const&); /** @} */ diff --git a/src/libxrpl/protocol/NFTSyntheticSerializer.cpp b/src/libxrpl/protocol/NFTSyntheticSerializer.cpp index 64fa9319de7..3d23cd0e4cf 100644 --- a/src/libxrpl/protocol/NFTSyntheticSerializer.cpp +++ b/src/libxrpl/protocol/NFTSyntheticSerializer.cpp @@ -32,12 +32,12 @@ namespace RPC { void insertNFTSyntheticInJson( - Json::Value& response, + Json::Value& metadata, std::shared_ptr const& transaction, TxMeta const& transactionMeta) { - insertNFTokenID(response[jss::meta], transaction, transactionMeta); - insertNFTokenOfferID(response[jss::meta], transaction, transactionMeta); + insertNFTokenID(metadata, transaction, transactionMeta); + insertNFTokenOfferID(metadata, transaction, transactionMeta); } } // namespace RPC diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 1c4314643cb..2083c4ab83e 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -6894,60 +6894,235 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite env.tx()->getJson(JsonOptions::none)[jss::hash].asString()}; env.close(); - Json::Value const meta = - env.rpc("tx", txHash)[jss::result][jss::meta]; - // Expect nftokens_id field - if (!BEAST_EXPECT(meta.isMember(jss::nftoken_id))) + // Test 1: Check tx RPC response + Json::Value const txResult = env.rpc("tx", txHash)[jss::result]; + Json::Value const& txMeta = txResult[jss::meta]; + + // Expect nftoken_id field + if (!BEAST_EXPECT(txMeta.isMember(jss::nftoken_id))) return; - // Check the value of NFT ID in the meta with the - // actual value + // Check the value of NFT ID matches uint256 nftID; - BEAST_EXPECT(nftID.parseHex(meta[jss::nftoken_id].asString())); + BEAST_EXPECT(nftID.parseHex(txMeta[jss::nftoken_id].asString())); BEAST_EXPECT(nftID == actualNftID); + + // Get ledger sequence from tx response + auto const ledgerSeq = txResult[jss::ledger_index].asUInt(); + + // Test 2: Check ledger RPC response with expanded transactions + Json::Value ledgerParams; + ledgerParams[jss::ledger_index] = ledgerSeq; + ledgerParams[jss::transactions] = true; + ledgerParams[jss::expand] = true; + + auto const ledgerResult = + env.rpc("json", "ledger", to_string(ledgerParams)); + auto const& tx = + ledgerResult[jss::result][jss::ledger][jss::transactions][0u]; + + // Verify transaction hash matches + BEAST_EXPECT(tx[jss::hash].asString() == txHash); + + // Check synthetic fields in ledger response (this tests our + // LedgerToJson.cpp fix) + Json::Value const* meta = nullptr; + if (tx.isMember(jss::meta)) + meta = &tx[jss::meta]; + else if (tx.isMember(jss::metaData)) + meta = &tx[jss::metaData]; + + if (BEAST_EXPECT(meta != nullptr)) + { + BEAST_EXPECT(meta->isMember(jss::nftoken_id)); + if (meta->isMember(jss::nftoken_id)) + { + uint256 ledgerNftId; + BEAST_EXPECT(ledgerNftId.parseHex( + (*meta)[jss::nftoken_id].asString())); + BEAST_EXPECT(ledgerNftId == actualNftID); + } + } + + // Test 3: Check account_tx RPC response + Json::Value accountTxParams; + accountTxParams[jss::account] = alice.human(); + accountTxParams[jss::limit] = 1; + + auto const accountTxResult = + env.rpc("json", "account_tx", to_string(accountTxParams)); + auto const& accountTx = + accountTxResult[jss::result][jss::transactions][0u]; + + // Check if the latest transaction is ours (it should be, but + // account_tx can be ordering-dependent) + bool isOurTransaction = (accountTx[jss::hash].asString() == txHash); + + // Only check synthetic fields if this is our transaction + if (isOurTransaction) + { + // Check synthetic fields in account_tx response + Json::Value const* accountMeta = nullptr; + if (accountTx.isMember(jss::meta)) + accountMeta = &accountTx[jss::meta]; + else if (accountTx.isMember(jss::metaData)) + accountMeta = &accountTx[jss::metaData]; + + if (BEAST_EXPECT(accountMeta != nullptr)) + { + BEAST_EXPECT(accountMeta->isMember(jss::nftoken_id)); + if (accountMeta->isMember(jss::nftoken_id)) + { + uint256 accountNftId; + BEAST_EXPECT(accountNftId.parseHex( + (*accountMeta)[jss::nftoken_id].asString())); + BEAST_EXPECT(accountNftId == actualNftID); + } + } + } }; // Verify `nftoken_ids` value equals to the NFTokenIDs that were // changed in the most recent NFTokenCancelOffer transaction - auto verifyNFTokenIDsInCancelOffer = - [&](std::vector actualNftIDs) { - // Get the hash for the most recent transaction. - std::string const txHash{ - env.tx()->getJson(JsonOptions::none)[jss::hash].asString()}; + auto verifyNFTokenIDsInCancelOffer = [&](std::vector + actualNftIDs) { + // Get the hash for the most recent transaction. + std::string const txHash{ + env.tx()->getJson(JsonOptions::none)[jss::hash].asString()}; - env.close(); - Json::Value const meta = - env.rpc("tx", txHash)[jss::result][jss::meta]; + env.close(); - // Expect nftokens_ids field and verify the values - if (!BEAST_EXPECT(meta.isMember(jss::nftoken_ids))) - return; + // Test 1: Check tx RPC response + Json::Value const txResult = env.rpc("tx", txHash)[jss::result]; + Json::Value const& txMeta = txResult[jss::meta]; - // Convert NFT IDs from Json::Value to uint256 - std::vector metaIDs; - std::transform( - meta[jss::nftoken_ids].begin(), - meta[jss::nftoken_ids].end(), - std::back_inserter(metaIDs), - [this](Json::Value id) { - uint256 nftID; - BEAST_EXPECT(nftID.parseHex(id.asString())); - return nftID; - }); - - // Sort both array to prepare for comparison - std::sort(metaIDs.begin(), metaIDs.end()); - std::sort(actualNftIDs.begin(), actualNftIDs.end()); - - // Make sure the expect number of NFTs is correct - BEAST_EXPECT(metaIDs.size() == actualNftIDs.size()); - - // Check the value of NFT ID in the meta with the - // actual values - for (size_t i = 0; i < metaIDs.size(); ++i) - BEAST_EXPECT(metaIDs[i] == actualNftIDs[i]); - }; + // Expect nftokens_ids field and verify the values + if (!BEAST_EXPECT(txMeta.isMember(jss::nftoken_ids))) + return; + + // Convert NFT IDs from Json::Value to uint256 + std::vector metaIDs; + std::transform( + txMeta[jss::nftoken_ids].begin(), + txMeta[jss::nftoken_ids].end(), + std::back_inserter(metaIDs), + [this](Json::Value id) { + uint256 nftID; + BEAST_EXPECT(nftID.parseHex(id.asString())); + return nftID; + }); + + // Sort both array to prepare for comparison + std::sort(metaIDs.begin(), metaIDs.end()); + std::sort(actualNftIDs.begin(), actualNftIDs.end()); + + // Make sure the expect number of NFTs is correct + BEAST_EXPECT(metaIDs.size() == actualNftIDs.size()); + + // Check the value of NFT ID in the meta with the + // actual values + for (size_t i = 0; i < metaIDs.size(); ++i) + BEAST_EXPECT(metaIDs[i] == actualNftIDs[i]); + + // Get ledger sequence from tx response + auto const ledgerSeq = txResult[jss::ledger_index].asUInt(); + + // Test 2: Check ledger RPC response with expanded transactions + Json::Value ledgerParams; + ledgerParams[jss::ledger_index] = ledgerSeq; + ledgerParams[jss::transactions] = true; + ledgerParams[jss::expand] = true; + + auto const ledgerResult = + env.rpc("json", "ledger", to_string(ledgerParams)); + auto const& tx = + ledgerResult[jss::result][jss::ledger][jss::transactions][0u]; + + // Verify transaction hash matches + BEAST_EXPECT(tx[jss::hash].asString() == txHash); + + // Check synthetic fields in ledger response + Json::Value const* meta = nullptr; + if (tx.isMember(jss::meta)) + meta = &tx[jss::meta]; + else if (tx.isMember(jss::metaData)) + meta = &tx[jss::metaData]; + + if (BEAST_EXPECT(meta != nullptr)) + { + BEAST_EXPECT(meta->isMember(jss::nftoken_ids)); + if (meta->isMember(jss::nftoken_ids)) + { + // Convert and verify NFT IDs in ledger response + std::vector ledgerMetaIDs; + std::transform( + (*meta)[jss::nftoken_ids].begin(), + (*meta)[jss::nftoken_ids].end(), + std::back_inserter(ledgerMetaIDs), + [this](Json::Value id) { + uint256 nftID; + BEAST_EXPECT(nftID.parseHex(id.asString())); + return nftID; + }); + + std::sort(ledgerMetaIDs.begin(), ledgerMetaIDs.end()); + BEAST_EXPECT(ledgerMetaIDs.size() == actualNftIDs.size()); + for (size_t i = 0; i < ledgerMetaIDs.size(); ++i) + BEAST_EXPECT(ledgerMetaIDs[i] == actualNftIDs[i]); + } + } + + // Test 3: Check account_tx RPC response + Json::Value accountTxParams; + accountTxParams[jss::account] = alice.human(); + accountTxParams[jss::limit] = 1; + + auto const accountTxResult = + env.rpc("json", "account_tx", to_string(accountTxParams)); + auto const& accountTx = + accountTxResult[jss::result][jss::transactions][0u]; + + // Check if the latest transaction is ours (it should be, but + // account_tx can be ordering-dependent) + bool isOurTransaction = (accountTx[jss::hash].asString() == txHash); + + // Only check synthetic fields if this is our transaction + if (isOurTransaction) + { + // Check synthetic fields in account_tx response + Json::Value const* accountMeta = nullptr; + if (accountTx.isMember(jss::meta)) + accountMeta = &accountTx[jss::meta]; + else if (accountTx.isMember(jss::metaData)) + accountMeta = &accountTx[jss::metaData]; + + if (BEAST_EXPECT(accountMeta != nullptr)) + { + BEAST_EXPECT(accountMeta->isMember(jss::nftoken_ids)); + if (accountMeta->isMember(jss::nftoken_ids)) + { + // Convert and verify NFT IDs in account_tx response + std::vector accountMetaIDs; + std::transform( + (*accountMeta)[jss::nftoken_ids].begin(), + (*accountMeta)[jss::nftoken_ids].end(), + std::back_inserter(accountMetaIDs), + [this](Json::Value id) { + uint256 nftID; + BEAST_EXPECT(nftID.parseHex(id.asString())); + return nftID; + }); + + std::sort(accountMetaIDs.begin(), accountMetaIDs.end()); + BEAST_EXPECT( + accountMetaIDs.size() == actualNftIDs.size()); + for (size_t i = 0; i < accountMetaIDs.size(); ++i) + BEAST_EXPECT(accountMetaIDs[i] == actualNftIDs[i]); + } + } + } + }; // Verify `offer_id` value equals to the offerID that was // changed in the most recent NFTokenCreateOffer tx diff --git a/src/xrpld/app/ledger/detail/LedgerToJson.cpp b/src/xrpld/app/ledger/detail/LedgerToJson.cpp index 0e6f81dfbc7..5e3dadc0230 100644 --- a/src/xrpld/app/ledger/detail/LedgerToJson.cpp +++ b/src/xrpld/app/ledger/detail/LedgerToJson.cpp @@ -22,11 +22,11 @@ #include #include #include -#include -#include +#include #include #include +#include #include namespace ripple { @@ -150,17 +150,10 @@ fillJsonTx( { txJson[jss::meta] = stMeta->getJson(JsonOptions::none); - // If applicable, insert delivered amount - if (txnType == ttPAYMENT || txnType == ttCHECK_CASH) - RPC::insertDeliveredAmount( - txJson[jss::meta], - fill.ledger, - txn, - {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); - - // If applicable, insert mpt issuance id - RPC::insertMPTokenIssuanceID( + // Insert all synthetic fields + RPC::insertAllSyntheticInJson( txJson[jss::meta], + fill.ledger, txn, {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); } @@ -187,17 +180,10 @@ fillJsonTx( { txJson[jss::metaData] = stMeta->getJson(JsonOptions::none); - // If applicable, insert delivered amount - if (txnType == ttPAYMENT || txnType == ttCHECK_CASH) - RPC::insertDeliveredAmount( - txJson[jss::metaData], - fill.ledger, - txn, - {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); - - // If applicable, insert mpt issuance id - RPC::insertMPTokenIssuanceID( + // Insert all synthetic fields + RPC::insertAllSyntheticInJson( txJson[jss::metaData], + fill.ledger, txn, {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); } diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index b9069442f8c..dfd26237ba7 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -49,9 +49,8 @@ #include #include #include -#include -#include #include +#include #include #include @@ -3274,11 +3273,8 @@ NetworkOPsImp::transJson( if (meta) { jvObj[jss::meta] = meta->get().getJson(JsonOptions::none); - RPC::insertDeliveredAmount( + RPC::insertAllSyntheticInJson( jvObj[jss::meta], *ledger, transaction, meta->get()); - RPC::insertNFTSyntheticInJson(jvObj, transaction, meta->get()); - RPC::insertMPTokenIssuanceID( - jvObj[jss::meta], transaction, meta->get()); } // add CTID where the needed data for it exists diff --git a/src/xrpld/rpc/detail/SyntheticFields.cpp b/src/xrpld/rpc/detail/SyntheticFields.cpp new file mode 100644 index 00000000000..21bf55679ca --- /dev/null +++ b/src/xrpld/rpc/detail/SyntheticFields.cpp @@ -0,0 +1,56 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +#include +#include +#include + +namespace ripple { +namespace RPC { + +void +insertAllSyntheticInJson( + Json::Value& metadata, + ReadView const& ledger, + std::shared_ptr const& transaction, + TxMeta const& transactionMeta) +{ + insertDeliveredAmount(metadata, ledger, transaction, transactionMeta); + insertNFTSyntheticInJson(metadata, transaction, transactionMeta); + insertMPTokenIssuanceID(metadata, transaction, transactionMeta); +} + +void +insertAllSyntheticInJson( + Json::Value& metadata, + JsonContext const& context, + std::shared_ptr const& transaction, + TxMeta const& transactionMeta) +{ + insertDeliveredAmount(metadata, context, transaction, transactionMeta); + insertNFTSyntheticInJson(metadata, transaction, transactionMeta); + insertMPTokenIssuanceID(metadata, transaction, transactionMeta); +} + +} // namespace RPC +} // namespace ripple diff --git a/src/xrpld/rpc/detail/SyntheticFields.h b/src/xrpld/rpc/detail/SyntheticFields.h new file mode 100644 index 00000000000..5c9de74961b --- /dev/null +++ b/src/xrpld/rpc/detail/SyntheticFields.h @@ -0,0 +1,62 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_RPC_DETAIL_SYNTHETICFIELDS_H_INCLUDED +#define RIPPLE_RPC_DETAIL_SYNTHETICFIELDS_H_INCLUDED + +#include +#include +#include + +#include + +namespace ripple { + +class ReadView; + +namespace RPC { + +struct JsonContext; + +/** + Adds all synthetic fields to transaction metadata JSON. + This includes delivered amount, NFT synthetic fields, and MPToken issuance + ID. + + @{ + */ +void +insertAllSyntheticInJson( + Json::Value& metadata, + ReadView const&, + std::shared_ptr const&, + TxMeta const&); + +void +insertAllSyntheticInJson( + Json::Value& metadata, + JsonContext const&, + std::shared_ptr const&, + TxMeta const&); +/** @} */ + +} // namespace RPC +} // namespace ripple + +#endif diff --git a/src/xrpld/rpc/handlers/AccountTx.cpp b/src/xrpld/rpc/handlers/AccountTx.cpp index 6b1dccdba93..fd4e1171768 100644 --- a/src/xrpld/rpc/handlers/AccountTx.cpp +++ b/src/xrpld/rpc/handlers/AccountTx.cpp @@ -23,9 +23,8 @@ #include #include #include -#include -#include #include +#include #include #include @@ -346,11 +345,8 @@ populateJsonResponse( { jvObj[jss::meta] = txnMeta->getJson(JsonOptions::include_date); - insertDeliveredAmount( - jvObj[jss::meta], context, txn, *txnMeta); - RPC::insertNFTSyntheticInJson(jvObj, sttx, *txnMeta); - RPC::insertMPTokenIssuanceID( - jvObj[jss::meta], sttx, *txnMeta); + RPC::insertAllSyntheticInJson( + jvObj[jss::meta], context, sttx, *txnMeta); } else UNREACHABLE( diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index 092b0b45627..a0ae2c8deae 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include @@ -275,17 +276,11 @@ simulateTxn(RPC::JsonContext& context, std::shared_ptr transaction) else { jvResult[jss::meta] = result.metadata->getJson(JsonOptions::none); - RPC::insertDeliveredAmount( + RPC::insertAllSyntheticInJson( jvResult[jss::meta], view, transaction->getSTransaction(), *result.metadata); - RPC::insertNFTSyntheticInJson( - jvResult, transaction->getSTransaction(), *result.metadata); - RPC::insertMPTokenIssuanceID( - jvResult[jss::meta], - transaction->getSTransaction(), - *result.metadata); } } diff --git a/src/xrpld/rpc/handlers/Tx.cpp b/src/xrpld/rpc/handlers/Tx.cpp index d43a699ab31..e7fdfcd79cd 100644 --- a/src/xrpld/rpc/handlers/Tx.cpp +++ b/src/xrpld/rpc/handlers/Tx.cpp @@ -25,10 +25,9 @@ #include #include #include -#include #include -#include #include +#include #include #include @@ -268,10 +267,8 @@ populateJsonResponse( if (meta) { response[jss::meta] = meta->getJson(JsonOptions::none); - insertDeliveredAmount( - response[jss::meta], context, result.txn, *meta); - RPC::insertNFTSyntheticInJson(response, sttx, *meta); - RPC::insertMPTokenIssuanceID(response[jss::meta], sttx, *meta); + RPC::insertAllSyntheticInJson( + response[jss::meta], context, sttx, *meta); } } response[jss::validated] = result.validated;