mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-21 19:45:53 +00:00
Add nftoken_id, nftoken_ids, offer_id to meta for transaction stream (#5230)
This commit is contained in:
@@ -28,6 +28,8 @@
|
|||||||
|
|
||||||
namespace ripple {
|
namespace ripple {
|
||||||
|
|
||||||
|
namespace RPC {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Adds common synthetic fields to transaction-related JSON responses
|
Adds common synthetic fields to transaction-related JSON responses
|
||||||
|
|
||||||
@@ -40,6 +42,7 @@ insertNFTSyntheticInJson(
|
|||||||
TxMeta const&);
|
TxMeta const&);
|
||||||
/** @} */
|
/** @} */
|
||||||
|
|
||||||
|
} // namespace RPC
|
||||||
} // namespace ripple
|
} // namespace ripple
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace ripple {
|
namespace ripple {
|
||||||
|
namespace RPC {
|
||||||
|
|
||||||
void
|
void
|
||||||
insertNFTSyntheticInJson(
|
insertNFTSyntheticInJson(
|
||||||
@@ -39,4 +40,5 @@ insertNFTSyntheticInJson(
|
|||||||
insertNFTokenOfferID(response[jss::meta], transaction, transactionMeta);
|
insertNFTokenOfferID(response[jss::meta], transaction, transactionMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace RPC
|
||||||
} // namespace ripple
|
} // namespace ripple
|
||||||
|
|||||||
@@ -1354,6 +1354,225 @@ public:
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
testNFToken(FeatureBitset features)
|
||||||
|
{
|
||||||
|
// `nftoken_id` is added for `transaction` stream in the `subscribe`
|
||||||
|
// response for NFTokenMint and NFTokenAcceptOffer.
|
||||||
|
//
|
||||||
|
// `nftoken_ids` is added for `transaction` stream in the `subscribe`
|
||||||
|
// response for NFTokenCancelOffer
|
||||||
|
//
|
||||||
|
// `offer_id` is added for `transaction` stream in the `subscribe`
|
||||||
|
// response for NFTokenCreateOffer
|
||||||
|
//
|
||||||
|
// The values of these fields are dependent on the NFTokenID/OfferID
|
||||||
|
// changed in its corresponding transaction. We want to validate each
|
||||||
|
// response to make sure the synethic fields hold the right values.
|
||||||
|
|
||||||
|
testcase("Test synthetic fields from Subscribe response");
|
||||||
|
|
||||||
|
using namespace test::jtx;
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
auto wsc = test::makeWSClient(env.app().config());
|
||||||
|
Json::Value stream;
|
||||||
|
stream[jss::streams] = Json::arrayValue;
|
||||||
|
stream[jss::streams].append("transactions");
|
||||||
|
auto jv = wsc->invoke("subscribe", stream);
|
||||||
|
|
||||||
|
// Verify `nftoken_id` value equals to the NFTokenID that was
|
||||||
|
// changed in the most recent NFTokenMint or NFTokenAcceptOffer
|
||||||
|
// transaction
|
||||||
|
auto verifyNFTokenID = [&](uint256 const& actualNftID) {
|
||||||
|
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
|
||||||
|
uint256 nftID;
|
||||||
|
BEAST_EXPECT(
|
||||||
|
nftID.parseHex(jv[jss::meta][jss::nftoken_id].asString()));
|
||||||
|
return 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) {
|
||||||
|
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
|
||||||
|
std::vector<uint256> metaIDs;
|
||||||
|
std::transform(
|
||||||
|
jv[jss::meta][jss::nftoken_ids].begin(),
|
||||||
|
jv[jss::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]);
|
||||||
|
return true;
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verify `offer_id` value equals to the offerID that was
|
||||||
|
// changed in the most recent NFTokenCreateOffer tx
|
||||||
|
auto verifyNFTokenOfferID = [&](uint256 const& offerID) {
|
||||||
|
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
|
||||||
|
uint256 metaOfferID;
|
||||||
|
BEAST_EXPECT(metaOfferID.parseHex(
|
||||||
|
jv[jss::meta][jss::offer_id].asString()));
|
||||||
|
return 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});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (features[featureNFTokenMintOffer])
|
||||||
|
{
|
||||||
|
uint256 const aliceMintWithOfferIndex1 =
|
||||||
|
keylet::nftoffer(alice, env.seq(alice)).key;
|
||||||
|
env(token::mint(alice), token::amount(XRP(0)));
|
||||||
|
env.close();
|
||||||
|
verifyNFTokenOfferID(aliceMintWithOfferIndex1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
run() override
|
run() override
|
||||||
{
|
{
|
||||||
@@ -1373,6 +1592,8 @@ public:
|
|||||||
testSubByUrl();
|
testSubByUrl();
|
||||||
testHistoryTxStream();
|
testHistoryTxStream();
|
||||||
testSubBookChanges();
|
testSubBookChanges();
|
||||||
|
testNFToken(all);
|
||||||
|
testNFToken(all - featureNFTokenMintOffer);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
#include <xrpl/protocol/BuildInfo.h>
|
#include <xrpl/protocol/BuildInfo.h>
|
||||||
#include <xrpl/protocol/Feature.h>
|
#include <xrpl/protocol/Feature.h>
|
||||||
#include <xrpl/protocol/MultiApiJson.h>
|
#include <xrpl/protocol/MultiApiJson.h>
|
||||||
|
#include <xrpl/protocol/NFTSyntheticSerializer.h>
|
||||||
#include <xrpl/protocol/RPCErr.h>
|
#include <xrpl/protocol/RPCErr.h>
|
||||||
#include <xrpl/protocol/TxFlags.h>
|
#include <xrpl/protocol/TxFlags.h>
|
||||||
#include <xrpl/protocol/jss.h>
|
#include <xrpl/protocol/jss.h>
|
||||||
@@ -3258,6 +3259,7 @@ NetworkOPsImp::transJson(
|
|||||||
jvObj[jss::meta] = meta->get().getJson(JsonOptions::none);
|
jvObj[jss::meta] = meta->get().getJson(JsonOptions::none);
|
||||||
RPC::insertDeliveredAmount(
|
RPC::insertDeliveredAmount(
|
||||||
jvObj[jss::meta], *ledger, transaction, meta->get());
|
jvObj[jss::meta], *ledger, transaction, meta->get());
|
||||||
|
RPC::insertNFTSyntheticInJson(jvObj, transaction, meta->get());
|
||||||
RPC::insertMPTokenIssuanceID(
|
RPC::insertMPTokenIssuanceID(
|
||||||
jvObj[jss::meta], transaction, meta->get());
|
jvObj[jss::meta], transaction, meta->get());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ populateJsonResponse(
|
|||||||
txnMeta->getJson(JsonOptions::include_date);
|
txnMeta->getJson(JsonOptions::include_date);
|
||||||
insertDeliveredAmount(
|
insertDeliveredAmount(
|
||||||
jvObj[jss::meta], context, txn, *txnMeta);
|
jvObj[jss::meta], context, txn, *txnMeta);
|
||||||
insertNFTSyntheticInJson(jvObj, sttx, *txnMeta);
|
RPC::insertNFTSyntheticInJson(jvObj, sttx, *txnMeta);
|
||||||
RPC::insertMPTokenIssuanceID(
|
RPC::insertMPTokenIssuanceID(
|
||||||
jvObj[jss::meta], sttx, *txnMeta);
|
jvObj[jss::meta], sttx, *txnMeta);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ populateJsonResponse(
|
|||||||
response[jss::meta] = meta->getJson(JsonOptions::none);
|
response[jss::meta] = meta->getJson(JsonOptions::none);
|
||||||
insertDeliveredAmount(
|
insertDeliveredAmount(
|
||||||
response[jss::meta], context, result.txn, *meta);
|
response[jss::meta], context, result.txn, *meta);
|
||||||
insertNFTSyntheticInJson(response, sttx, *meta);
|
RPC::insertNFTSyntheticInJson(response, sttx, *meta);
|
||||||
RPC::insertMPTokenIssuanceID(response[jss::meta], sttx, *meta);
|
RPC::insertMPTokenIssuanceID(response[jss::meta], sttx, *meta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user