diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index c1d1e21d4d2..7f8edcfca12 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -649,6 +649,9 @@ target_sources (rippled PRIVATE src/ripple/rpc/impl/ShardVerificationScheduler.cpp src/ripple/rpc/impl/Status.cpp src/ripple/rpc/impl/TransactionSign.cpp + src/ripple/rpc/impl/NFTokenID.cpp + src/ripple/rpc/impl/NFTokenOfferID.cpp + src/ripple/rpc/impl/TxMetaSerializer.cpp #[===============================[ main sources: subdir: perflog diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index 1c5bf8463b0..9341439c32c 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -412,6 +412,8 @@ JSS(nft_offer); // in: LedgerEntry JSS(nft_offer_index); // out nft_buy_offers, nft_sell_offers JSS(nft_page); // in: LedgerEntry JSS(nft_serial); // out: account_nfts +JSS(nftoken_id); // out: insertNFTokenID +JSS(nftoken_ids); // out: insertNFTokenID JSS(no_ripple); // out: AccountLines JSS(no_ripple_peer); // out: AccountLines JSS(node); // out: LedgerEntry @@ -432,6 +434,7 @@ JSS(node_writes_delayed); // out::GetCounts JSS(obligations); // out: GatewayBalances JSS(offer); // in: LedgerEntry JSS(offers); // out: NetworkOPs, AccountOffers, Subscribe +JSS(offer_id); // out: insertNFTokenOfferID JSS(offline); // in: TransactionSign JSS(offset); // in/out: AccountTxOld JSS(open); // out: handlers/Ledger diff --git a/src/ripple/rpc/NFTokenID.h b/src/ripple/rpc/NFTokenID.h new file mode 100644 index 00000000000..db99e4e595c --- /dev/null +++ b/src/ripple/rpc/NFTokenID.h @@ -0,0 +1,65 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 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_NFTOKENID_H_INCLUDED +#define RIPPLE_RPC_NFTOKENID_H_INCLUDED + +#include + +#include +#include + +namespace Json { +class Value; +} + +namespace ripple { + +class TxMeta; +class STTx; + +namespace RPC { + +/** + Add a `nftoken_ids` field to the `meta` output parameter. + The field is only added to successful NFTokenMint, NFTokenAcceptOffer, + and NFTokenCancelOffer transactions. + + @{ + */ +bool +canHaveNFTokenID(std::shared_ptr const&, TxMeta const&); + +void +getNFTokenIDFromPage(TxMeta const&, std::vector&); + +void +getNFTokenIDFromDeletedOffer(TxMeta const&, std::vector&); + +void +insertNFTokenID( + Json::Value&, + std::shared_ptr const&, + TxMeta const&); +/** @} */ + +} // namespace RPC +} // namespace ripple + +#endif diff --git a/src/ripple/rpc/NFTokenOfferID.h b/src/ripple/rpc/NFTokenOfferID.h new file mode 100644 index 00000000000..e9fd5e031b4 --- /dev/null +++ b/src/ripple/rpc/NFTokenOfferID.h @@ -0,0 +1,61 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 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_NFTOKENOFFERID_H_INCLUDED +#define RIPPLE_RPC_NFTOKENOFFERID_H_INCLUDED + +#include + +#include +#include + +namespace Json { +class Value; +} + +namespace ripple { + +class TxMeta; +class STTx; + +namespace RPC { + +/** + Add an `offer_id` field to the `meta` output parameter. + The field is only added to successful NFTokenCreateOffer transactions. + + @{ + */ +bool +canHaveNFTokenOfferID(std::shared_ptr const&, TxMeta const&); + +void +getOfferIDFromCreatedOffer(TxMeta const&, std::vector&); + +void +insertNFTokenOfferID( + Json::Value&, + std::shared_ptr const&, + TxMeta const&); +/** @} */ + +} // namespace RPC +} // namespace ripple + +#endif diff --git a/src/ripple/rpc/TxMetaSerializer.h b/src/ripple/rpc/TxMetaSerializer.h new file mode 100644 index 00000000000..f6b6fa7f4a6 --- /dev/null +++ b/src/ripple/rpc/TxMetaSerializer.h @@ -0,0 +1,59 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 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_TXMETASERIALIZER_H_INCLUDED +#define RIPPLE_RPC_TXMETASERIALIZER_H_INCLUDED + +#include +#include + +#include +#include + +namespace Json { +class Value; +} + +namespace ripple { + +class TxMeta; +class STTx; + +namespace RPC { + +struct JsonContext; + +/** + Adds common synthetic fields to transaction-related JSON responses + + @{ + */ +void +serializeTxMetaAsJSON( + Json::Value&, + RPC::JsonContext const&, + std::shared_ptr const&, + TxMeta const&, + JsonOptions); +/** @} */ + +} // namespace RPC +} // namespace ripple + +#endif diff --git a/src/ripple/rpc/handlers/AccountTx.cpp b/src/ripple/rpc/handlers/AccountTx.cpp index 67c80ad9bdc..976e56ba0d7 100644 --- a/src/ripple/rpc/handlers/AccountTx.cpp +++ b/src/ripple/rpc/handlers/AccountTx.cpp @@ -33,8 +33,8 @@ #include #include #include -#include #include +#include #include #include @@ -302,11 +302,13 @@ populateJsonResponse( jvObj[jss::tx] = txn->getJson(JsonOptions::include_date); if (txnMeta) { - jvObj[jss::meta] = - txnMeta->getJson(JsonOptions::include_date); jvObj[jss::validated] = true; - insertDeliveredAmount( - jvObj[jss::meta], context, txn, *txnMeta); + serializeTxMetaAsJSON( + jvObj, + context, + txn->getSTransaction(), + *txnMeta, + JsonOptions::include_date); } } } diff --git a/src/ripple/rpc/handlers/Tx.cpp b/src/ripple/rpc/handlers/Tx.cpp index 4a70f1fe061..29bfbd9f907 100644 --- a/src/ripple/rpc/handlers/Tx.cpp +++ b/src/ripple/rpc/handlers/Tx.cpp @@ -26,8 +26,8 @@ #include #include #include -#include #include +#include #include namespace ripple { @@ -292,9 +292,12 @@ populateJsonResponse( auto& meta = *m; if (meta) { - response[jss::meta] = meta->getJson(JsonOptions::none); - insertDeliveredAmount( - response[jss::meta], context, result.txn, *meta); + serializeTxMetaAsJSON( + response, + context, + result.txn->getSTransaction(), + *meta, + JsonOptions::none); } } response[jss::validated] = result.validated; diff --git a/src/ripple/rpc/impl/NFTokenID.cpp b/src/ripple/rpc/impl/NFTokenID.cpp new file mode 100644 index 00000000000..7649831005e --- /dev/null +++ b/src/ripple/rpc/impl/NFTokenID.cpp @@ -0,0 +1,192 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 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 +#include +#include +#include +#include +#include + +namespace ripple { +namespace RPC { + +bool +canHaveNFTokenID( + std::shared_ptr const& serializedTx, + TxMeta const& transactionMeta) +{ + if (!serializedTx) + return false; + + TxType const tt{serializedTx->getTxnType()}; + if (tt != ttNFTOKEN_MINT && tt != ttNFTOKEN_ACCEPT_OFFER && + tt != ttNFTOKEN_CANCEL_OFFER) + return false; + + // if the transaction failed nothing could have been delivered. + if (transactionMeta.getResultTER() != tesSUCCESS) + return false; + + return true; +} + +void +getNFTokenIDFromPage( + TxMeta const& transactionMeta, + std::vector& tokenIDResult) +{ + std::vector prevIDs; + std::vector finalIDs; + + for (STObject const& node : transactionMeta.getNodes()) + { + if (node.getFieldU16(sfLedgerEntryType) != ltNFTOKEN_PAGE) + continue; + + if (node.getFName() == sfCreatedNode) + { + STArray const& toAddNFTs = node.peekAtField(sfNewFields) + .downcast() + .getFieldArray(sfNFTokens); + std::transform( + toAddNFTs.begin(), + toAddNFTs.end(), + std::back_inserter(finalIDs), + [](STObject const& nft) { + return nft.getFieldH256(sfNFTokenID); + }); + } + // Else it's modified, as there should never be a deleted NFToken page + // as a result of a mint. + else + { + // When a mint results in splitting an existing page, + // it results in a created page and a modified node. Sometimes, + // the created node needs to be linked to a third page, resulting + // in modifying that third page's PreviousPageMin or NextPageMin + // field changing, but no NFTs within that page changing. In this + // case, there will be no previous NFTs and we need to skip. + // However, there will always be NFTs listed in the final fields, + // as rippled outputs all fields in final fields even if they were + // not changed. + STObject const& previousFields = + node.peekAtField(sfPreviousFields).downcast(); + if (!previousFields.isFieldPresent(sfNFTokens)) + continue; + + STArray const& toAddNFTs = previousFields.getFieldArray(sfNFTokens); + std::transform( + toAddNFTs.begin(), + toAddNFTs.end(), + std::back_inserter(prevIDs), + [](STObject const& nft) { + return nft.getFieldH256(sfNFTokenID); + }); + + STArray const& toAddFinalNFTs = node.peekAtField(sfFinalFields) + .downcast() + .getFieldArray(sfNFTokens); + std::transform( + toAddFinalNFTs.begin(), + toAddFinalNFTs.end(), + std::back_inserter(finalIDs), + [](STObject const& nft) { + return nft.getFieldH256(sfNFTokenID); + }); + } + } + + std::sort(finalIDs.begin(), finalIDs.end()); + std::sort(prevIDs.begin(), prevIDs.end()); + + std::set_difference( + finalIDs.begin(), + finalIDs.end(), + prevIDs.begin(), + prevIDs.end(), + std::inserter(tokenIDResult, tokenIDResult.begin())); +} + +void +getNFTokenIDFromDeletedOffer( + TxMeta const& transactionMeta, + std::vector& tokenIDResult) +{ + for (STObject const& node : transactionMeta.getNodes()) + { + if (node.getFieldU16(sfLedgerEntryType) != ltNFTOKEN_OFFER) + continue; + + if (node.getFName() == sfDeletedNode) + { + auto const& toAddNFT = node.peekAtField(sfFinalFields) + .downcast() + .getFieldH256(sfNFTokenID); + tokenIDResult.push_back(toAddNFT); + } + } +} + +void +insertNFTokenID( + Json::Value& response, + std::shared_ptr const& transaction, + TxMeta const& transactionMeta) +{ + if (!canHaveNFTokenID(transaction, transactionMeta)) + return; + + std::vector tokenIDResult; + + // We extract the NFTokenID from metadata by comparing affected nodes + if (auto const type = transaction->getTxnType(); type == ttNFTOKEN_MINT) + { + getNFTokenIDFromPage(transactionMeta, tokenIDResult); + + if (tokenIDResult.size() > 0) + response[jss::nftoken_id] = to_string(tokenIDResult.front()); + } + else if (type == ttNFTOKEN_ACCEPT_OFFER) + { + getNFTokenIDFromDeletedOffer(transactionMeta, tokenIDResult); + + // In brokered mode, there will be a duplicate NFTokenID in the + // vector, but we don't have to do anything special here. + if (tokenIDResult.size() > 0) + response[jss::nftoken_id] = to_string(tokenIDResult.front()); + } + else if (type == ttNFTOKEN_CANCEL_OFFER) + { + getNFTokenIDFromDeletedOffer(transactionMeta, tokenIDResult); + + response[jss::nftoken_ids] = Json::Value(Json::arrayValue); + for (auto const& nftID : tokenIDResult) + response[jss::nftoken_ids].append(to_string(nftID)); + } +} + +} // namespace RPC +} // namespace ripple diff --git a/src/ripple/rpc/impl/NFTokenOfferID.cpp b/src/ripple/rpc/impl/NFTokenOfferID.cpp new file mode 100644 index 00000000000..83cc9dd835b --- /dev/null +++ b/src/ripple/rpc/impl/NFTokenOfferID.cpp @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 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 +#include +#include +#include +#include +#include + +namespace ripple { +namespace RPC { + +bool +canHaveNFTokenOfferID( + std::shared_ptr const& serializedTx, + TxMeta const& transactionMeta) +{ + if (!serializedTx) + return false; + + TxType const tt{serializedTx->getTxnType()}; + if (tt != ttNFTOKEN_CREATE_OFFER) + return false; + + // if the transaction failed nothing could have been delivered. + if (transactionMeta.getResultTER() != tesSUCCESS) + return false; + + return true; +} + +void +getOfferIDFromCreatedOffer( + TxMeta const& transactionMeta, + std::vector& idResult) +{ + for (STObject const& node : transactionMeta.getNodes()) + { + if (node.getFieldU16(sfLedgerEntryType) != ltNFTOKEN_OFFER) + continue; + + if (node.getFName() == sfCreatedNode) + { + auto const& offerID = node.getFieldH256(sfLedgerIndex); + idResult.push_back(offerID); + } + } +} + +void +insertNFTokenOfferID( + Json::Value& response, + std::shared_ptr const& transaction, + TxMeta const& transactionMeta) +{ + if (!canHaveNFTokenOfferID(transaction, transactionMeta)) + return; + + std::vector offerIDResult; + getOfferIDFromCreatedOffer(transactionMeta, offerIDResult); + + if (offerIDResult.size() == 1) + response[jss::offer_id] = to_string(offerIDResult.front()); +} + +} // namespace RPC +} // namespace ripple diff --git a/src/ripple/rpc/impl/TxMetaSerializer.cpp b/src/ripple/rpc/impl/TxMetaSerializer.cpp new file mode 100644 index 00000000000..10b67eec9ab --- /dev/null +++ b/src/ripple/rpc/impl/TxMetaSerializer.cpp @@ -0,0 +1,57 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace RPC { + +void +serializeTxMetaAsJSON( + Json::Value& response, + RPC::JsonContext const& context, + std::shared_ptr const& transaction, + TxMeta const& transactionMeta, + JsonOptions options) +{ + response[jss::meta] = transactionMeta.getJson(options); + + insertDeliveredAmount( + response[jss::meta], context, transaction, transactionMeta); + insertNFTokenID(response[jss::meta], transaction, transactionMeta); + insertNFTokenOfferID(response[jss::meta], transaction, transactionMeta); +} + +} // namespace RPC +} // namespace ripple diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 81abee34b07..5deecf54e27 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -6565,6 +6565,203 @@ class NFToken_test : public beast::unit_test::suite } } + void + testTxJsonMetaFields(FeatureBitset features) + { + // `nftoken_id` is added in the `tx` response for NFTokenMint and + // NFTokenAcceptOffer. + // + // `nftoken_ids` is added in the `tx` response for NFTokenCancelOffer + // + // `offer_id` is added in the `tx` response for NFTokenCreateOffer + // + // The values of these fields are dependent on the NFTokenID/OfferID + // changed in its corresponding transaction. We want to validate each + // transaction to make sure the synethic fields hold the right values. + + testcase("Test synthetic fields from JSON response"); + + using namespace test::jtx; + + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const broker{"broker"}; + + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, broker); + env.close(); + + // Verify `nftoken_id` value equals to the NFTokenID that was + // changed in the most recent NFTokenMint or NFTokenAcceptOffer + // transaction + auto verifyNFTokenID = [&](uint256 const& actualNftID) { + // 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]; + + // Expect nftokens_id field + if (!BEAST_EXPECT(meta.isMember(jss::nftoken_id))) + return; + + // Check the value of NFT ID in the meta with the + // actual value + uint256 nftID; + BEAST_EXPECT(nftID.parseHex(meta[jss::nftoken_id].asString())); + BEAST_EXPECT(nftID == actualNftID); + }; + + // Verify `nftoken_ids` value equals to the NFTokenIDs that were + // changed in the most recent NFTokenCancelOffer transaction + auto verifyNFTokenIDs = [&](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]; + + // Expect nftokens_ids field and verify the values + if (!BEAST_EXPECT(meta.isMember(jss::nftoken_ids))) + return; + + // 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; + }); + + 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]); + }; + + // Verify `offer_id` value equals to the offerID that was + // changed in the most recent NFTokenCreateOffer tx + auto verifyNFTokenOfferID = [&](uint256 const& offerID) { + // 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]; + + // Expect offer_id field and verify the value + if (!BEAST_EXPECT(meta.isMember(jss::offer_id))) + return; + + uint256 metaOfferID; + BEAST_EXPECT(metaOfferID.parseHex(meta[jss::offer_id].asString())); + BEAST_EXPECT(metaOfferID == offerID); + }; + + // Check new fields in tx meta when for all NFTtransactions + { + // Alice mints 2 NFTs + // Verify the NFTokenIDs are correct in the NFTokenMint tx meta + uint256 const nftId1{ + token::getNextID(env, alice, 0u, tfTransferable)}; + env(token::mint(alice, 0u), txflags(tfTransferable)); + env.close(); + verifyNFTokenID(nftId1); + + uint256 const nftId2{ + token::getNextID(env, alice, 0u, tfTransferable)}; + env(token::mint(alice, 0u), txflags(tfTransferable)); + env.close(); + verifyNFTokenID(nftId2); + + // Alice creates one sell offer for each NFT + // Verify the offer indexes are correct in the NFTokenCreateOffer tx + // meta + uint256 const aliceOfferIndex1 = + keylet::nftoffer(alice, env.seq(alice)).key; + env(token::createOffer(alice, nftId1, drops(1)), + txflags(tfSellNFToken)); + env.close(); + verifyNFTokenOfferID(aliceOfferIndex1); + + uint256 const aliceOfferIndex2 = + keylet::nftoffer(alice, env.seq(alice)).key; + env(token::createOffer(alice, nftId2, drops(1)), + txflags(tfSellNFToken)); + env.close(); + verifyNFTokenOfferID(aliceOfferIndex2); + + // Alice cancels two offers she created + // Verify the NFTokenIDs are correct in the NFTokenCancelOffer tx + // meta + env(token::cancelOffer( + alice, {aliceOfferIndex1, aliceOfferIndex2})); + env.close(); + verifyNFTokenIDs({nftId1, nftId2}); + + // Bobs creates a buy offer for nftId1 + // Verify the offer id is correct in the NFTokenCreateOffer tx meta + auto const bobBuyOfferIndex = + keylet::nftoffer(bob, env.seq(bob)).key; + env(token::createOffer(bob, nftId1, drops(1)), token::owner(alice)); + env.close(); + verifyNFTokenOfferID(bobBuyOfferIndex); + + // Alice accepts bob's buy offer + // Verify the NFTokenID is correct in the NFTokenAcceptOffer tx meta + env(token::acceptBuyOffer(alice, bobBuyOfferIndex)); + env.close(); + verifyNFTokenID(nftId1); + } + + // Check `nftoken_ids` in brokered mode + { + // Alice mints a NFT + uint256 const nftId{ + token::getNextID(env, alice, 0u, tfTransferable)}; + env(token::mint(alice, 0u), txflags(tfTransferable)); + env.close(); + verifyNFTokenID(nftId); + + // Alice creates sell offer and set broker as destination + uint256 const offerAliceToBroker = + keylet::nftoffer(alice, env.seq(alice)).key; + env(token::createOffer(alice, nftId, drops(1)), + token::destination(broker), + txflags(tfSellNFToken)); + env.close(); + verifyNFTokenOfferID(offerAliceToBroker); + + // Bob creates buy offer + uint256 const offerBobToBroker = + keylet::nftoffer(bob, env.seq(bob)).key; + env(token::createOffer(bob, nftId, drops(1)), token::owner(alice)); + env.close(); + verifyNFTokenOfferID(offerBobToBroker); + + // Check NFTokenID meta for NFTokenAcceptOffer in brokered mode + env(token::brokerOffers( + broker, offerBobToBroker, offerAliceToBroker)); + env.close(); + verifyNFTokenID(nftId); + } + } + void testWithFeats(FeatureBitset features) { @@ -6597,6 +6794,7 @@ class NFToken_test : public beast::unit_test::suite testIOUWithTransferFee(features); testBrokeredSaleToSelf(features); testFixNFTokenRemint(features); + testTxJsonMetaFields(features); } public: