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

@@ -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: