mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user