Add nftoken_id, nftoken_ids, offer_id fields for NFTokens (#4447)

Three new fields are added to the `Tx` responses for NFTs:

1. `nftoken_id`: This field is included in the `Tx` responses for
   `NFTokenMint` and `NFTokenAcceptOffer`. This field indicates the
   `NFTokenID` for the `NFToken` that was modified on the ledger by the
   transaction.
2. `nftoken_ids`: This array is included in the `Tx` response for
   `NFTokenCancelOffer`. This field provides a list of all the
   `NFTokenID`s for the `NFToken`s that were modified on the ledger by
   the transaction.
3. `offer_id`: This field is included in the `Tx` response for
   `NFTokenCreateOffer` transactions and shows the OfferID of the
   `NFTokenOffer` created.

The fields make it easier to track specific tokens and offers. The
implementation includes code (by @ledhed2222) from the Clio project to
extract NFTokenIDs from mint transactions.
This commit is contained in:
Shawn Xie
2023-05-18 19:38:18 -04:00
committed by GitHub
parent 629ed5c691
commit 3620ac287e
11 changed files with 772 additions and 0 deletions

View File

@@ -653,6 +653,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/NFTSyntheticSerializer.cpp
#[===============================[
main sources:
subdir: perflog

View File

@@ -416,6 +416,8 @@ JSS(nft_offer_index); // out nft_buy_offers, nft_sell_offers
JSS(nft_page); // in: LedgerEntry
JSS(nft_serial); // out: account_nfts
JSS(nft_taxon); // out: nft_info (clio)
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
@@ -436,6 +438,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

View File

@@ -0,0 +1,58 @@
//------------------------------------------------------------------------------
/*
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_NFTSYNTHETICSERIALIZER_H_INCLUDED
#define RIPPLE_RPC_NFTSYNTHETICSERIALIZER_H_INCLUDED
#include <ripple/protocol/Protocol.h>
#include <ripple/protocol/STBase.h>
#include <functional>
#include <memory>
namespace Json {
class Value;
}
namespace ripple {
class TxMeta;
class STTx;
namespace RPC {
struct JsonContext;
/**
Adds common synthetic fields to transaction-related JSON responses
@{
*/
void
insertNFTSyntheticInJson(
Json::Value&,
RPC::JsonContext const&,
std::shared_ptr<STTx const> const&,
TxMeta const&);
/** @} */
} // namespace RPC
} // namespace ripple
#endif

View File

@@ -0,0 +1,68 @@
//------------------------------------------------------------------------------
/*
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_NFTOKENID_H_INCLUDED
#define RIPPLE_RPC_NFTOKENID_H_INCLUDED
#include <ripple/protocol/Protocol.h>
#include <functional>
#include <memory>
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.
Helper functions are not static because they can be used by Clio.
@{
*/
bool
canHaveNFTokenID(
std::shared_ptr<STTx const> const& serializedTx,
TxMeta const& transactionMeta);
std::optional<uint256>
getNFTokenIDFromPage(TxMeta const& transactionMeta);
std::vector<uint256>
getNFTokenIDFromDeletedOffer(TxMeta const& transactionMeta);
void
insertNFTokenID(
Json::Value& response,
std::shared_ptr<STTx const> const& transaction,
TxMeta const& transactionMeta);
/** @} */
} // namespace RPC
} // namespace ripple
#endif

View File

@@ -0,0 +1,64 @@
//------------------------------------------------------------------------------
/*
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_NFTOKENOFFERID_H_INCLUDED
#define RIPPLE_RPC_NFTOKENOFFERID_H_INCLUDED
#include <ripple/protocol/Protocol.h>
#include <functional>
#include <memory>
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.
Helper functions are not static because they can be used by Clio.
@{
*/
bool
canHaveNFTokenOfferID(
std::shared_ptr<STTx const> const& serializedTx,
TxMeta const& transactionMeta);
std::optional<uint256>
getOfferIDFromCreatedOffer(TxMeta const& transactionMeta);
void
insertNFTokenOfferID(
Json::Value& response,
std::shared_ptr<STTx const> const& transaction,
TxMeta const& transactionMeta);
/** @} */
} // namespace RPC
} // namespace ripple
#endif

View File

@@ -34,6 +34,7 @@
#include <ripple/resource/Fees.h>
#include <ripple/rpc/Context.h>
#include <ripple/rpc/DeliveredAmount.h>
#include <ripple/rpc/NFTSyntheticSerializer.h>
#include <ripple/rpc/Role.h>
#include <ripple/rpc/impl/RPCHelpers.h>
@@ -307,6 +308,8 @@ populateJsonResponse(
jvObj[jss::validated] = true;
insertDeliveredAmount(
jvObj[jss::meta], context, txn, *txnMeta);
insertNFTSyntheticInJson(
jvObj, context, txn->getSTransaction(), *txnMeta);
}
}
}

View File

@@ -28,6 +28,7 @@
#include <ripple/rpc/Context.h>
#include <ripple/rpc/DeliveredAmount.h>
#include <ripple/rpc/GRPCHandlers.h>
#include <ripple/rpc/NFTSyntheticSerializer.h>
#include <ripple/rpc/impl/RPCHelpers.h>
namespace ripple {
@@ -295,6 +296,8 @@ populateJsonResponse(
response[jss::meta] = meta->getJson(JsonOptions::none);
insertDeliveredAmount(
response[jss::meta], context, result.txn, *meta);
insertNFTSyntheticInJson(
response, context, result.txn->getSTransaction(), *meta);
}
}
response[jss::validated] = result.validated;

View File

@@ -0,0 +1,50 @@
//------------------------------------------------------------------------------
/*
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 <ripple/rpc/NFTSyntheticSerializer.h>
#include <ripple/app/ledger/LedgerMaster.h>
#include <ripple/app/ledger/OpenLedger.h>
#include <ripple/app/misc/Transaction.h>
#include <ripple/ledger/View.h>
#include <ripple/net/RPCErr.h>
#include <ripple/protocol/AccountID.h>
#include <ripple/protocol/Feature.h>
#include <ripple/rpc/Context.h>
#include <ripple/rpc/NFTokenID.h>
#include <ripple/rpc/NFTokenOfferID.h>
#include <ripple/rpc/impl/RPCHelpers.h>
#include <boost/algorithm/string/case_conv.hpp>
namespace ripple {
namespace RPC {
void
insertNFTSyntheticInJson(
Json::Value& response,
RPC::JsonContext const& context,
std::shared_ptr<STTx const> const& transaction,
TxMeta const& transactionMeta)
{
insertNFTokenID(response[jss::meta], transaction, transactionMeta);
insertNFTokenOfferID(response[jss::meta], transaction, transactionMeta);
}
} // namespace RPC
} // namespace ripple

View File

@@ -0,0 +1,202 @@
//------------------------------------------------------------------------------
/*
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 <ripple/rpc/NFTokenID.h>
#include <ripple/app/ledger/LedgerMaster.h>
#include <ripple/app/ledger/OpenLedger.h>
#include <ripple/app/misc/Transaction.h>
#include <ripple/ledger/View.h>
#include <ripple/net/RPCErr.h>
#include <ripple/protocol/AccountID.h>
#include <ripple/protocol/Feature.h>
#include <ripple/rpc/Context.h>
#include <ripple/rpc/impl/RPCHelpers.h>
#include <boost/algorithm/string/case_conv.hpp>
namespace ripple {
namespace RPC {
bool
canHaveNFTokenID(
std::shared_ptr<STTx const> 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;
}
std::optional<uint256>
getNFTokenIDFromPage(TxMeta const& transactionMeta)
{
// The metadata does not make it obvious which NFT was added. To figure
// that out we gather up all of the previous NFT IDs and all of the final
// NFT IDs and compare them to find what changed.
std::vector<uint256> prevIDs;
std::vector<uint256> finalIDs;
for (STObject const& node : transactionMeta.getNodes())
{
if (node.getFieldU16(sfLedgerEntryType) != ltNFTOKEN_PAGE)
continue;
SField const& fName = node.getFName();
if (fName == sfCreatedNode)
{
STArray const& toAddPrevNFTs = node.peekAtField(sfNewFields)
.downcast<STObject>()
.getFieldArray(sfNFTokens);
std::transform(
toAddPrevNFTs.begin(),
toAddPrevNFTs.end(),
std::back_inserter(finalIDs),
[](STObject const& nft) {
return nft.getFieldH256(sfNFTokenID);
});
}
else if (fName == sfModifiedNode)
{
// 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<STObject>();
if (!previousFields.isFieldPresent(sfNFTokens))
continue;
STArray const& toAddPrevNFTs =
previousFields.getFieldArray(sfNFTokens);
std::transform(
toAddPrevNFTs.begin(),
toAddPrevNFTs.end(),
std::back_inserter(prevIDs),
[](STObject const& nft) {
return nft.getFieldH256(sfNFTokenID);
});
STArray const& toAddFinalNFTs = node.peekAtField(sfFinalFields)
.downcast<STObject>()
.getFieldArray(sfNFTokens);
std::transform(
toAddFinalNFTs.begin(),
toAddFinalNFTs.end(),
std::back_inserter(finalIDs),
[](STObject const& nft) {
return nft.getFieldH256(sfNFTokenID);
});
}
}
// We expect NFTs to be added one at a time. So finalIDs should be one
// longer than prevIDs. If that's not the case something is messed up.
if (finalIDs.size() != prevIDs.size() + 1)
return std::nullopt;
// Find the first NFT ID that doesn't match. We're looking for an
// added NFT, so the one we want will be the mismatch in finalIDs.
auto const diff = std::mismatch(
finalIDs.begin(), finalIDs.end(), prevIDs.begin(), prevIDs.end());
// There should always be a difference so the returned finalIDs
// iterator should never be end(). But better safe than sorry.
if (diff.first == finalIDs.end())
return std::nullopt;
return *diff.first;
}
std::vector<uint256>
getNFTokenIDFromDeletedOffer(TxMeta const& transactionMeta)
{
std::vector<uint256> tokenIDResult;
for (STObject const& node : transactionMeta.getNodes())
{
if (node.getFieldU16(sfLedgerEntryType) != ltNFTOKEN_OFFER ||
node.getFName() != sfDeletedNode)
continue;
auto const& toAddNFT = node.peekAtField(sfFinalFields)
.downcast<STObject>()
.getFieldH256(sfNFTokenID);
tokenIDResult.push_back(toAddNFT);
}
// Deduplicate the NFT IDs because multiple offers could affect the same NFT
// and hence we would get duplicate NFT IDs
sort(tokenIDResult.begin(), tokenIDResult.end());
tokenIDResult.erase(
unique(tokenIDResult.begin(), tokenIDResult.end()),
tokenIDResult.end());
return tokenIDResult;
}
void
insertNFTokenID(
Json::Value& response,
std::shared_ptr<STTx const> const& transaction,
TxMeta const& transactionMeta)
{
if (!canHaveNFTokenID(transaction, transactionMeta))
return;
// We extract the NFTokenID from metadata by comparing affected nodes
if (auto const type = transaction->getTxnType(); type == ttNFTOKEN_MINT)
{
std::optional<uint256> result = getNFTokenIDFromPage(transactionMeta);
if (result.has_value())
response[jss::nftoken_id] = to_string(result.value());
}
else if (type == ttNFTOKEN_ACCEPT_OFFER)
{
std::vector<uint256> result =
getNFTokenIDFromDeletedOffer(transactionMeta);
if (result.size() > 0)
response[jss::nftoken_id] = to_string(result.front());
}
else if (type == ttNFTOKEN_CANCEL_OFFER)
{
std::vector<uint256> result =
getNFTokenIDFromDeletedOffer(transactionMeta);
response[jss::nftoken_ids] = Json::Value(Json::arrayValue);
for (auto const& nftID : result)
response[jss::nftoken_ids].append(to_string(nftID));
}
}
} // namespace RPC
} // namespace ripple

View File

@@ -0,0 +1,85 @@
//------------------------------------------------------------------------------
/*
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 <ripple/rpc/NFTokenOfferID.h>
#include <ripple/app/ledger/LedgerMaster.h>
#include <ripple/app/ledger/OpenLedger.h>
#include <ripple/app/misc/Transaction.h>
#include <ripple/ledger/View.h>
#include <ripple/net/RPCErr.h>
#include <ripple/protocol/AccountID.h>
#include <ripple/protocol/Feature.h>
#include <ripple/rpc/Context.h>
#include <ripple/rpc/impl/RPCHelpers.h>
#include <boost/algorithm/string/case_conv.hpp>
namespace ripple {
namespace RPC {
bool
canHaveNFTokenOfferID(
std::shared_ptr<STTx const> 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;
}
std::optional<uint256>
getOfferIDFromCreatedOffer(TxMeta const& transactionMeta)
{
for (STObject const& node : transactionMeta.getNodes())
{
if (node.getFieldU16(sfLedgerEntryType) != ltNFTOKEN_OFFER ||
node.getFName() != sfCreatedNode)
continue;
return node.getFieldH256(sfLedgerIndex);
}
return std::nullopt;
}
void
insertNFTokenOfferID(
Json::Value& response,
std::shared_ptr<STTx const> const& transaction,
TxMeta const& transactionMeta)
{
if (!canHaveNFTokenOfferID(transaction, transactionMeta))
return;
std::optional<uint256> result = getOfferIDFromCreatedOffer(transactionMeta);
if (result.has_value())
response[jss::offer_id] = to_string(result.value());
}
} // namespace RPC
} // namespace ripple

View File

@@ -6566,6 +6566,238 @@ 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 verifyNFTokenIDsInCancelOffer =
[&](std::vector<uint256> 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<uint256> 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]);
};
// 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();
verifyNFTokenIDsInCancelOffer({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);
}
// Check if there are no duplicate nft id in Cancel transactions where
// multiple offers are cancelled for the same NFT
{
// 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 2 sell offers for the same NFT
uint256 const aliceOfferIndex1 =
keylet::nftoffer(alice, env.seq(alice)).key;
env(token::createOffer(alice, nftId, drops(1)),
txflags(tfSellNFToken));
env.close();
verifyNFTokenOfferID(aliceOfferIndex1);
uint256 const aliceOfferIndex2 =
keylet::nftoffer(alice, env.seq(alice)).key;
env(token::createOffer(alice, nftId, drops(1)),
txflags(tfSellNFToken));
env.close();
verifyNFTokenOfferID(aliceOfferIndex2);
// Make sure the metadata only has 1 nft id, since both offers are
// for the same nft
env(token::cancelOffer(
alice, {aliceOfferIndex1, aliceOfferIndex2}));
env.close();
verifyNFTokenIDsInCancelOffer({nftId});
}
}
void
testWithFeats(FeatureBitset features)
{
@@ -6598,6 +6830,7 @@ class NFToken_test : public beast::unit_test::suite
testIOUWithTransferFee(features);
testBrokeredSaleToSelf(features);
testFixNFTokenRemint(features);
testTxJsonMetaFields(features);
}
public: