feat: Support Dynamic NFT (#1525)

Fix #1471
Clio's changes for supporting DNFT
https://github.com/XRPLF/rippled/pull/5048/files
This commit is contained in:
cyan317
2025-01-31 13:33:20 +00:00
committed by GitHub
parent e7702e9c11
commit 1753c95910
16 changed files with 952 additions and 50 deletions

View File

@@ -28,7 +28,7 @@ class Clio(ConanFile):
'protobuf/3.21.9',
'grpc/1.50.1',
'openssl/1.1.1u',
'xrpl/2.4.0-b1',
'xrpl/2.4.0-b3',
'zlib/1.3.1',
'libbacktrace/cci.20210118'
]

View File

@@ -132,6 +132,9 @@ struct Amendments {
REGISTER(fixAMMv1_2);
REGISTER(AMMClawback);
REGISTER(Credentials);
REGISTER(DynamicNFT);
// TODO: Add PermissionedDomains related RPC changes
REGISTER(PermissionedDomains);
// Obsolete but supported by libxrpl
REGISTER(CryptoConditionsSuite);

View File

@@ -624,7 +624,6 @@ public:
return seq;
}
LOG(log_.debug()) << "Could not fetch ledger object sequence - no rows";
} else {
LOG(log_.error()) << "Could not fetch ledger object sequence: " << res.error();
}
@@ -948,28 +947,35 @@ public:
statements.reserve(data.size() * 3);
for (NFTsData const& record : data) {
statements.push_back(
schema_->insertNFT.bind(record.tokenID, record.ledgerSequence, record.owner, record.isBurned)
);
if (!record.onlyUriChanged) {
statements.push_back(
schema_->insertNFT.bind(record.tokenID, record.ledgerSequence, record.owner, record.isBurned)
);
// If `uri` is set (and it can be set to an empty uri), we know this
// is a net-new NFT. That is, this NFT has not been seen before by
// us _OR_ it is in the extreme edge case of a re-minted NFT ID with
// the same NFT ID as an already-burned token. In this case, we need
// to record the URI and link to the issuer_nf_tokens table.
if (record.uri) {
statements.push_back(schema_->insertIssuerNFT.bind(
ripple::nft::getIssuer(record.tokenID),
static_cast<uint32_t>(ripple::nft::getTaxon(record.tokenID)),
record.tokenID
));
// If `uri` is set (and it can be set to an empty uri), we know this
// is a net-new NFT. That is, this NFT has not been seen before by
// us _OR_ it is in the extreme edge case of a re-minted NFT ID with
// the same NFT ID as an already-burned token. In this case, we need
// to record the URI and link to the issuer_nf_tokens table.
if (record.uri) {
statements.push_back(schema_->insertIssuerNFT.bind(
ripple::nft::getIssuer(record.tokenID),
static_cast<uint32_t>(ripple::nft::getTaxon(record.tokenID)),
record.tokenID
));
statements.push_back(
schema_->insertNFTURI.bind(record.tokenID, record.ledgerSequence, record.uri.value())
);
}
} else {
// only uri changed, we update the uri table only
statements.push_back(
schema_->insertNFTURI.bind(record.tokenID, record.ledgerSequence, record.uri.value())
);
}
}
executor_.write(std::move(statements));
executor_.writeEach(std::move(statements));
}
void

View File

@@ -107,6 +107,7 @@ struct NFTsData {
ripple::AccountID owner;
std::optional<ripple::Blob> uri;
bool isBurned = false;
bool onlyUriChanged = false; // Whether only the URI was changed
/**
* @brief Construct a new NFTsData object
@@ -170,6 +171,23 @@ struct NFTsData {
: tokenID(tokenID), ledgerSequence(ledgerSequence), owner(owner), uri(uri)
{
}
/**
* @brief Construct a new NFTsData object with only the URI changed
*
* @param tokenID The token ID
* @param meta The transaction metadata
* @param uri The new URI
*
*/
NFTsData(ripple::uint256 const& tokenID, ripple::TxMeta const& meta, ripple::Blob const& uri)
: tokenID(tokenID)
, ledgerSequence(meta.getLgrSeq())
, transactionIndex(meta.getIndex())
, uri(uri)
, onlyUriChanged(true)
{
}
};
/**

View File

@@ -35,6 +35,7 @@
#include <boost/asio/spawn.hpp>
#include <boost/json/object.hpp>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
@@ -192,10 +193,24 @@ public:
template <typename... Args>
void
write(PreparedStatementType const& preparedStatement, Args&&... args)
{
auto statement = preparedStatement.bind(std::forward<Args>(args)...);
write(std::move(statement));
}
/**
* @brief Non-blocking query execution used for writing data.
*
* Retries forever with retry policy specified by @ref AsyncExecutor
*
* @param statement Statement to execute
* @throw DatabaseTimeout on timeout
*/
void
write(StatementType&& statement)
{
auto const startTime = std::chrono::steady_clock::now();
auto statement = preparedStatement.bind(std::forward<Args>(args)...);
incrementOutstandingRequestCount();
counters_->registerWriteStarted();
@@ -251,6 +266,21 @@ public:
});
}
/**
* @brief Non-blocking query execution used for writing data. Constrast with write, this method does not execute
* the statements in a batch.
*
* Retries forever with retry policy specified by @ref AsyncExecutor.
*
* @param statements Vector of statements to execute
* @throw DatabaseTimeout on timeout
*/
void
writeEach(std::vector<StatementType>&& statements)
{
std::ranges::for_each(std::move(statements), [this](auto& statement) { this->write(std::move(statement)); });
}
/**
* @brief Coroutine-based query execution used for reading data.
*

View File

@@ -47,6 +47,17 @@
namespace etl {
std::pair<std::vector<NFTTransactionsData>, std::optional<NFTsData>>
getNftokenModifyData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
{
auto const tokenID = sttx.getFieldH256(ripple::sfNFTokenID);
// note: sfURI is optional, if it is absent, we will update the uri as empty string
return {
{NFTTransactionsData(sttx.getFieldH256(ripple::sfNFTokenID), txMeta, sttx.getTransactionID())},
NFTsData(tokenID, txMeta, sttx.getFieldVL(ripple::sfURI))
};
}
std::pair<std::vector<NFTTransactionsData>, std::optional<NFTsData>>
getNFTokenMintData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
{
@@ -166,7 +177,7 @@ getNFTokenBurnData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
node.peekAtField(ripple::sfPreviousFields).downcast<ripple::STObject>();
if (previousFields.isFieldPresent(ripple::sfNFTokens))
prevNFTs = previousFields.getFieldArray(ripple::sfNFTokens);
} else if (!prevNFTs && node.getFName() == ripple::sfDeletedNode) {
} else if (node.getFName() == ripple::sfDeletedNode) {
prevNFTs =
node.peekAtField(ripple::sfFinalFields).downcast<ripple::STObject>().getFieldArray(ripple::sfNFTokens);
}
@@ -336,6 +347,9 @@ getNFTDataFromTx(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
case ripple::TxType::ttNFTOKEN_CREATE_OFFER:
return getNFTokenCreateOfferData(txMeta, sttx);
case ripple::TxType::ttNFTOKEN_MODIFY:
return getNftokenModifyData(txMeta, sttx);
default:
return {{}, {}};
}

View File

@@ -33,6 +33,16 @@
namespace etl {
/**
* @brief Get the NFT URI change data from a NFToken Modify transaction
*
* @param txMeta Transaction metadata
* @param sttx The transaction
* @return NFT URI change data as a pair of transactions and optional NFTsData
*/
std::pair<std::vector<NFTTransactionsData>, std::optional<NFTsData>>
getNftokenModifyData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx);
/**
* @brief Get the NFT Token mint data from a transaction
*

View File

@@ -57,6 +57,7 @@ struct FormattedTransactionsData {
std::vector<NFTTransactionsData> nfTokenTxData;
std::vector<NFTsData> nfTokensData;
std::vector<MPTHolderData> mptHoldersData;
std::vector<NFTsData> nfTokenURIChanges;
};
namespace etl::impl {
@@ -111,6 +112,7 @@ public:
{
FormattedTransactionsData result;
std::vector<NFTsData> nfTokenURIChanges;
for (auto& txn : *(data.mutable_transactions_list()->mutable_transactions())) {
std::string* raw = txn.mutable_transaction_blob();
@@ -123,8 +125,15 @@ public:
auto const [nftTxs, maybeNFT] = getNFTDataFromTx(txMeta, sttx);
result.nfTokenTxData.insert(result.nfTokenTxData.end(), nftTxs.begin(), nftTxs.end());
if (maybeNFT)
result.nfTokensData.push_back(*maybeNFT);
// We need to unique the URI changes separately, in case the URI changes are discarded
if (maybeNFT) {
if (maybeNFT->onlyUriChanged) {
nfTokenURIChanges.push_back(*maybeNFT);
} else {
result.nfTokensData.push_back(*maybeNFT);
}
}
auto const maybeMPTHolder = getMPTHolderFromTx(txMeta, sttx);
if (maybeMPTHolder)
@@ -143,6 +152,10 @@ public:
}
result.nfTokensData = getUniqueNFTsDatas(result.nfTokensData);
nfTokenURIChanges = getUniqueNFTsDatas(nfTokenURIChanges);
// Put uri change at the end to ensure the uri not overwritten
result.nfTokensData.insert(result.nfTokensData.end(), nfTokenURIChanges.begin(), nfTokenURIChanges.end());
return result;
}

View File

@@ -28,7 +28,6 @@
#include "util/async/AnyStrand.hpp"
#include "util/log/Logger.hpp"
#include <chrono>
#include <cstddef>
#include <functional>

View File

@@ -728,7 +728,230 @@ createMintNftTxWithMetadata(
}
data::TransactionAndMetadata
createAcceptNftOfferTxWithMetadata(std::string_view accountId, uint32_t seq, uint32_t fee, std::string_view nftId)
createMintNftTxWithMetadataOfCreatedNode(
std::string_view accountId,
uint32_t seq,
uint32_t fee,
uint32_t nfTokenTaxon,
std::optional<std::string_view> nftID,
std::optional<std::string_view> uri,
std::optional<std::string_view> pageIndex
)
{
// tx
ripple::STObject tx(ripple::sfTransaction);
tx.setFieldU16(ripple::sfTransactionType, ripple::ttNFTOKEN_MINT);
auto account = util::parseBase58Wrapper<ripple::AccountID>(std::string(accountId));
tx.setAccountID(ripple::sfAccount, account.value());
auto amount = ripple::STAmount(fee, false);
tx.setFieldAmount(ripple::sfFee, amount);
// required field for ttNFTOKEN_MINT
tx.setFieldU32(ripple::sfNFTokenTaxon, nfTokenTaxon);
tx.setFieldU32(ripple::sfSequence, seq);
char const* key = "test";
ripple::Slice const slice(key, 4);
tx.setFieldVL(ripple::sfSigningPubKey, slice);
if (uri)
tx.setFieldVL(ripple::sfURI, ripple::Slice(uri->data(), uri->size()));
// meta
ripple::STObject metaObj(ripple::sfTransactionMetaData);
ripple::STArray metaArray{1};
ripple::STObject node(ripple::sfCreatedNode);
node.setFieldU16(ripple::sfLedgerEntryType, ripple::ltNFTOKEN_PAGE);
ripple::STObject newFields(ripple::sfNewFields);
ripple::STArray NFTArray1{1};
if (nftID) {
// finalFields contain new NFT while previousFields does not
auto entry = ripple::STObject(ripple::sfNFToken);
entry.setFieldH256(ripple::sfNFTokenID, ripple::uint256{*nftID});
if (uri)
entry.setFieldVL(ripple::sfURI, ripple::Slice(uri->data(), uri->size()));
NFTArray1.push_back(entry);
}
newFields.setFieldArray(ripple::sfNFTokens, NFTArray1);
node.emplace_back(std::move(newFields));
if (pageIndex)
node.setFieldH256(ripple::sfLedgerIndex, ripple::uint256{*pageIndex});
// add a ledger object ahead of nft page
ripple::STObject node2(ripple::sfCreatedNode);
node2.setFieldU16(ripple::sfLedgerEntryType, ripple::ltACCOUNT_ROOT);
metaArray.push_back(node2);
metaArray.push_back(node);
metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray);
metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS);
metaObj.setFieldU32(ripple::sfTransactionIndex, 0);
data::TransactionAndMetadata ret;
ret.transaction = tx.getSerializer().peekData();
ret.metadata = metaObj.getSerializer().peekData();
return ret;
}
data::TransactionAndMetadata
createNftModifyTxWithMetadata(std::string_view accountId, std::string_view nftID, ripple::Blob uri)
{
// tx
ripple::STObject tx(ripple::sfTransaction);
tx.setFieldU16(ripple::sfTransactionType, ripple::ttNFTOKEN_MODIFY);
auto account = ripple::parseBase58<ripple::AccountID>(std::string(accountId));
tx.setAccountID(ripple::sfAccount, account.value());
auto amount = ripple::STAmount(10, false);
tx.setFieldAmount(ripple::sfFee, amount);
tx.setFieldH256(ripple::sfNFTokenID, ripple::uint256{nftID});
tx.setFieldU32(ripple::sfSequence, 100);
char const* key = "test";
ripple::Slice const slice(key, 4);
tx.setFieldVL(ripple::sfSigningPubKey, slice);
if (!uri.empty()) // sfURI should be absent if empty
tx.setFieldVL(ripple::sfURI, uri);
// meta
ripple::STObject metaObj(ripple::sfTransactionMetaData);
ripple::STArray metaArray{1};
ripple::STObject node(ripple::sfModifiedNode);
node.setFieldU16(ripple::sfLedgerEntryType, ripple::ltNFTOKEN_PAGE);
ripple::STObject finalFields(ripple::sfFinalFields);
ripple::STArray NFTArray1{1};
ripple::STArray NFTArray2{1};
// finalFields contain new NFT while previousFields does not
auto entry = ripple::STObject(ripple::sfNFToken);
entry.setFieldH256(ripple::sfNFTokenID, ripple::uint256{nftID});
if (!uri.empty())
entry.setFieldVL(ripple::sfURI, uri);
NFTArray1.push_back(entry);
auto entry2 = ripple::STObject(ripple::sfNFToken);
entry2.setFieldH256(ripple::sfNFTokenID, ripple::uint256{nftID});
char const* url = "previous";
entry2.setFieldVL(ripple::sfURI, ripple::Slice(url, 7));
NFTArray2.push_back(entry2);
finalFields.setFieldArray(ripple::sfNFTokens, NFTArray1);
ripple::STObject previousFields(ripple::sfPreviousFields);
previousFields.setFieldArray(ripple::sfNFTokens, NFTArray2);
node.emplace_back(std::move(finalFields));
node.emplace_back(std::move(previousFields));
metaArray.push_back(node);
metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray);
metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS);
metaObj.setFieldU32(ripple::sfTransactionIndex, 0);
data::TransactionAndMetadata ret;
ret.transaction = tx.getSerializer().peekData();
ret.metadata = metaObj.getSerializer().peekData();
return ret;
}
data::TransactionAndMetadata
createNftBurnTxWithMetadataOfDeletedNode(std::string_view accountId, std::string_view nftID)
{
// tx
ripple::STObject tx(ripple::sfTransaction);
tx.setFieldU16(ripple::sfTransactionType, ripple::ttNFTOKEN_BURN);
auto account = getAccountIdWithString(accountId);
tx.setAccountID(ripple::sfAccount, account);
auto amount = ripple::STAmount(10, false);
tx.setFieldAmount(ripple::sfFee, amount);
tx.setFieldH256(ripple::sfNFTokenID, ripple::uint256{nftID});
tx.setFieldU32(ripple::sfSequence, 100);
char const* key = "test";
ripple::Slice const slice(key, 4);
tx.setFieldVL(ripple::sfSigningPubKey, slice);
// meta
ripple::STObject metaObj(ripple::sfTransactionMetaData);
ripple::STArray metaArray{1};
ripple::STObject node(ripple::sfDeletedNode);
node.setFieldU16(ripple::sfLedgerEntryType, ripple::ltNFTOKEN_PAGE);
// deleted node should contain finalFields
ripple::STObject finalFields(ripple::sfFinalFields);
ripple::STArray NFTArray{1};
auto entry = ripple::STObject(ripple::sfNFToken);
entry.setFieldH256(ripple::sfNFTokenID, ripple::uint256{nftID});
NFTArray.push_back(entry);
finalFields.setFieldArray(ripple::sfNFTokens, NFTArray);
node.emplace_back(std::move(finalFields));
// add a ledger object ahead of nft page
ripple::STObject node2(ripple::sfCreatedNode);
node2.setFieldU16(ripple::sfLedgerEntryType, ripple::ltACCOUNT_ROOT);
metaArray.push_back(node2);
metaArray.push_back(node);
metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray);
metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS);
metaObj.setFieldU32(ripple::sfTransactionIndex, 0);
data::TransactionAndMetadata ret;
ret.transaction = tx.getSerializer().peekData();
ret.metadata = metaObj.getSerializer().peekData();
return ret;
}
data::TransactionAndMetadata
createNftBurnTxWithMetadataOfModifiedNode(std::string_view accountId, std::string_view nftID)
{
// tx
ripple::STObject tx(ripple::sfTransaction);
tx.setFieldU16(ripple::sfTransactionType, ripple::ttNFTOKEN_BURN);
auto account = getAccountIdWithString(accountId);
tx.setAccountID(ripple::sfAccount, account);
auto amount = ripple::STAmount(10, false);
tx.setFieldAmount(ripple::sfFee, amount);
tx.setFieldH256(ripple::sfNFTokenID, ripple::uint256{nftID});
tx.setFieldU32(ripple::sfSequence, 100);
char const* key = "test";
ripple::Slice const slice(key, 4);
tx.setFieldVL(ripple::sfSigningPubKey, slice);
// meta
ripple::STObject metaObj(ripple::sfTransactionMetaData);
ripple::STArray metaArray{1};
ripple::STObject node(ripple::sfModifiedNode);
node.setFieldU16(ripple::sfLedgerEntryType, ripple::ltNFTOKEN_PAGE);
ripple::STObject finalFields(ripple::sfFinalFields);
ripple::STArray NFTArray{1};
ripple::STObject previousFields(ripple::sfPreviousFields);
auto entry = ripple::STObject(ripple::sfNFToken);
entry.setFieldH256(ripple::sfNFTokenID, ripple::uint256{nftID});
NFTArray.push_back(entry);
previousFields.setFieldArray(ripple::sfNFTokens, NFTArray);
node.emplace_back(std::move(previousFields));
node.emplace_back(std::move(finalFields));
metaArray.push_back(node);
metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray);
metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS);
metaObj.setFieldU32(ripple::sfTransactionIndex, 0);
data::TransactionAndMetadata ret;
ret.transaction = tx.getSerializer().peekData();
ret.metadata = metaObj.getSerializer().peekData();
return ret;
}
data::TransactionAndMetadata
createAcceptNftBuyerOfferTxWithMetadata(
std::string_view accountId,
uint32_t seq,
uint32_t fee,
std::string_view nftId,
std::string_view offerId
)
{
// tx
ripple::STObject tx(ripple::sfTransaction);
@@ -738,7 +961,7 @@ createAcceptNftOfferTxWithMetadata(std::string_view accountId, uint32_t seq, uin
auto amount = ripple::STAmount(fee, false);
tx.setFieldAmount(ripple::sfFee, amount);
tx.setFieldU32(ripple::sfSequence, seq);
tx.setFieldH256(ripple::sfNFTokenBuyOffer, ripple::uint256{kINDEX1});
tx.setFieldH256(ripple::sfNFTokenBuyOffer, ripple::uint256{offerId});
char const* key = "test";
ripple::Slice const slice(key, 4);
tx.setFieldVL(ripple::sfSigningPubKey, slice);
@@ -752,8 +975,11 @@ createAcceptNftOfferTxWithMetadata(std::string_view accountId, uint32_t seq, uin
ripple::STObject finalFields(ripple::sfFinalFields);
finalFields.setFieldH256(ripple::sfNFTokenID, ripple::uint256{nftId});
// for buyer offer, the offer owner is the nft's new owner
finalFields.setAccountID(ripple::sfOwner, account.value());
node.emplace_back(std::move(finalFields));
node.setFieldH256(ripple::sfLedgerIndex, ripple::uint256{offerId});
metaArray.push_back(node);
metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray);
metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS);
@@ -765,6 +991,99 @@ createAcceptNftOfferTxWithMetadata(std::string_view accountId, uint32_t seq, uin
return ret;
}
data::TransactionAndMetadata
createAcceptNftSellerOfferTxWithMetadata(
std::string_view accountId,
uint32_t seq,
uint32_t fee,
std::string_view nftId,
std::string_view offerId,
std::string_view pageIndex,
bool isNewPageCreated
)
{
// tx
ripple::STObject tx(ripple::sfTransaction);
tx.setFieldU16(ripple::sfTransactionType, ripple::ttNFTOKEN_ACCEPT_OFFER);
auto account = util::parseBase58Wrapper<ripple::AccountID>(std::string(accountId));
tx.setAccountID(ripple::sfAccount, account.value());
auto amount = ripple::STAmount(fee, false);
tx.setFieldAmount(ripple::sfFee, amount);
tx.setFieldU32(ripple::sfSequence, seq);
tx.setFieldH256(ripple::sfNFTokenSellOffer, ripple::uint256{offerId});
char const* key = "test";
ripple::Slice const slice(key, 4);
tx.setFieldVL(ripple::sfSigningPubKey, slice);
// meta
// create deletedNode with ltNFTOKEN_OFFER
ripple::STObject metaObj(ripple::sfTransactionMetaData);
ripple::STArray metaArray{1};
ripple::STObject node(ripple::sfDeletedNode);
node.setFieldU16(ripple::sfLedgerEntryType, ripple::ltNFTOKEN_OFFER);
ripple::STObject finalFields(ripple::sfFinalFields);
finalFields.setFieldH256(ripple::sfNFTokenID, ripple::uint256{nftId});
// offer owner is not the nft's new owner for seller offer, we need to create other nodes for processing new owner
finalFields.setAccountID(ripple::sfOwner, account.value());
node.emplace_back(std::move(finalFields));
node.setFieldH256(ripple::sfLedgerIndex, ripple::uint256{offerId});
metaArray.push_back(node);
// new owner's nft page node changed: 1 new nft page node added 2 old nft page node modified
if (isNewPageCreated) {
ripple::STObject node2(ripple::sfCreatedNode);
node2.setFieldU16(ripple::sfLedgerEntryType, ripple::ltNFTOKEN_PAGE);
ripple::STObject newFields(ripple::sfNewFields);
ripple::STArray nftArray1{1};
auto entry = ripple::STObject(ripple::sfNFToken);
entry.setFieldH256(ripple::sfNFTokenID, ripple::uint256{nftId});
nftArray1.push_back(entry);
newFields.setFieldArray(ripple::sfNFTokens, nftArray1);
node2.emplace_back(std::move(newFields));
node2.setFieldH256(ripple::sfLedgerIndex, ripple::uint256{pageIndex});
metaArray.push_back(node2);
} else {
ripple::STObject node2(ripple::sfModifiedNode);
node2.setFieldU16(ripple::sfLedgerEntryType, ripple::ltNFTOKEN_PAGE);
ripple::STArray nftArray1{2};
// finalFields contain new NFT while previousFields does not
auto entry = ripple::STObject(ripple::sfNFToken);
entry.setFieldH256(ripple::sfNFTokenID, ripple::uint256{nftId});
nftArray1.push_back(entry);
auto entry2 = ripple::STObject(ripple::sfNFToken);
entry2.setFieldH256(ripple::sfNFTokenID, ripple::uint256{kINDEX1});
nftArray1.push_back(entry2);
finalFields.setFieldArray(ripple::sfNFTokens, nftArray1);
nftArray1.erase(nftArray1.begin());
ripple::STObject previousFields(ripple::sfPreviousFields);
previousFields.setFieldArray(ripple::sfNFTokens, nftArray1);
node2.emplace_back(std::move(finalFields));
node2.emplace_back(std::move(previousFields));
node2.setFieldH256(ripple::sfLedgerIndex, ripple::uint256{pageIndex});
metaArray.push_back(node2);
}
metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray);
metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS);
metaObj.setFieldU32(ripple::sfTransactionIndex, 0);
data::TransactionAndMetadata ret;
ret.transaction = tx.getSerializer().peekData();
ret.metadata = metaObj.getSerializer().peekData();
return ret;
}
// NFTokenCancelOffer can be used to cancel multiple offers
data::TransactionAndMetadata
createCancelNftOffersTxWithMetadata(

View File

@@ -302,6 +302,9 @@ createNftTokenPage(
std::optional<ripple::uint256> previousPage
);
/**
* Create NFToken mint tx, the metadata contained a changed node
*/
[[nodiscard]] data::TransactionAndMetadata
createMintNftTxWithMetadata(
std::string_view accountId,
@@ -311,8 +314,54 @@ createMintNftTxWithMetadata(
std::string_view nftID
);
/**
* Create NFToken mint tx, the metadata contained a created node
*/
[[nodiscard]] data::TransactionAndMetadata
createAcceptNftOfferTxWithMetadata(std::string_view accountId, uint32_t seq, uint32_t fee, std::string_view nftId);
createMintNftTxWithMetadataOfCreatedNode(
std::string_view accountId,
uint32_t seq,
uint32_t fee,
uint32_t nfTokenTaxon,
std::optional<std::string_view> nftID,
std::optional<std::string_view> uri,
std::optional<std::string_view> pageIndex
);
[[nodiscard]] data::TransactionAndMetadata
createNftModifyTxWithMetadata(std::string_view accountId, std::string_view nftID, ripple::Blob uri);
/**
* Create NFToken burn tx, tx causes a nft page node deleted
*/
[[nodiscard]] data::TransactionAndMetadata
createNftBurnTxWithMetadataOfDeletedNode(std::string_view accountId, std::string_view nftID);
/**
* Create NFToken mint tx, tx causes a nft page node changed
*/
[[nodiscard]] data::TransactionAndMetadata
createNftBurnTxWithMetadataOfModifiedNode(std::string_view accountId, std::string_view nftID);
[[nodiscard]] data::TransactionAndMetadata
createAcceptNftBuyerOfferTxWithMetadata(
std::string_view accountId,
uint32_t seq,
uint32_t fee,
std::string_view nftId,
std::string_view offerId
);
[[nodiscard]] data::TransactionAndMetadata
createAcceptNftSellerOfferTxWithMetadata(
std::string_view accountId,
uint32_t seq,
uint32_t fee,
std::string_view nftId,
std::string_view offerId,
std::string_view pageIndex,
bool isNewPageCreated
);
[[nodiscard]] data::TransactionAndMetadata
createCancelNftOffersTxWithMetadata(
@@ -322,9 +371,6 @@ createCancelNftOffersTxWithMetadata(
std::vector<std::string> const& nftOffers
);
[[nodiscard]] data::TransactionAndMetadata
createCreateNftOfferTxWithMetadata(std::string_view accountId, uint32_t seq, uint32_t fee, std::string_view offerId);
[[nodiscard]] data::TransactionAndMetadata
createCreateNftOfferTxWithMetadata(
std::string_view accountId,

View File

@@ -421,3 +421,37 @@ TEST_F(BackendCassandraExecutionStrategyTest, StatsCallsCountersReport)
EXPECT_CALL(*counters_, report());
strat.stats();
}
TEST_F(BackendCassandraExecutionStrategyTest, WriteEachAndCallSyncSucceeds)
{
auto strat = makeStrategy();
auto const totalRequests = 1024u;
auto const numStatements = 16u;
auto callCount = std::atomic_uint{0u};
auto work = std::optional<boost::asio::io_context::work>{ctx_};
auto thread = std::thread{[this]() { ctx_.run(); }};
EXPECT_CALL(handle_, asyncExecute(A<FakeStatement const&>(), A<std::function<void(FakeResultOrError)>&&>()))
.Times(totalRequests * numStatements)
.WillRepeatedly([this, &callCount](auto const&, auto&& cb) {
// run on thread to emulate concurrency model of real asyncExecute
boost::asio::post(ctx_, [&callCount, cb = std::forward<decltype(cb)>(cb)] {
++callCount;
cb({}); // pretend we got data
});
return FakeFutureWithCallback{};
}); // numStatements per write call
EXPECT_CALL(*counters_, registerWriteStarted()).Times(totalRequests * numStatements);
EXPECT_CALL(*counters_, registerWriteFinished(testing::_)).Times(totalRequests * numStatements);
auto makeStatements = [] { return std::vector<FakeStatement>(16); };
for (auto i = 0u; i < totalRequests; ++i)
strat.writeEach(makeStatements());
strat.sync(); // make sure all above writes are finished
EXPECT_EQ(callCount, totalRequests * numStatements); // all requests should finish
work.reset();
thread.join();
}

View File

@@ -22,56 +22,306 @@
#include "util/LoggerFixtures.hpp"
#include "util/TestObject.hpp"
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/json/array.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/value.hpp>
#include <gmock/gmock.h>
#include <grpcpp/support/status.h>
#include <gtest/gtest.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/TxMeta.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/tokens.h>
#include <cstdint>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
namespace {
constexpr auto kACCOUNT = "rM2AGCCCRb373FRuD8wHyUwUsh2dV4BW5Q";
constexpr auto kACCOUNT2 = "rnd1nHuzceyQDqnLH8urWNr4QBKt4v7WVk";
constexpr auto kNFT_ID = "0008013AE1CD8B79A8BCB52335CD40DE97401B2D60A828720000099B00000000";
constexpr auto kNFT_ID2 = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA";
constexpr auto kOFFER1 = "23F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8";
constexpr auto kTX = "13F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8";
// Page index is a valid nft page for ACCOUNT
constexpr auto kPAGE_INDEX = "E1CD8B79A8BCB52335CD40DE97401B2D60A82872FFFFFFFFFFFFFFFFFFFFFFFF";
constexpr auto kOFFER_ID = "AA86CBF29770F72FA3FF4A5D9A9FA54D6F399A8E038F72393EF782224865E27F";
} // namespace
struct NFTHelpersTests : public NoLoggerFixture {};
struct NFTHelpersTest : NoLoggerFixture {
protected:
static void
verifyNFTTransactionsData(
NFTTransactionsData const& data,
ripple::STTx const& sttx,
ripple::TxMeta const& txMeta,
std::string_view nftId
)
{
EXPECT_EQ(data.tokenID, ripple::uint256(nftId));
EXPECT_EQ(data.ledgerSequence, txMeta.getLgrSeq());
EXPECT_EQ(data.transactionIndex, txMeta.getIndex());
EXPECT_EQ(data.txHash, sttx.getTransactionID());
}
TEST_F(NFTHelpersTests, ConvertDataFromNFTCancelOfferTx)
static void
verifyNFTsData(
NFTsData const& data,
ripple::STTx const& sttx,
ripple::TxMeta const& txMeta,
std::string_view nftId,
std::optional<std::string> const& owner
)
{
EXPECT_EQ(data.tokenID, ripple::uint256(nftId));
EXPECT_EQ(data.ledgerSequence, txMeta.getLgrSeq());
EXPECT_EQ(data.transactionIndex, txMeta.getIndex());
if (owner)
EXPECT_EQ(data.owner, getAccountIdWithString(*owner));
if (sttx.getTxnType() == ripple::ttNFTOKEN_MINT || sttx.getTxnType() == ripple::ttNFTOKEN_MODIFY) {
EXPECT_TRUE(data.uri.has_value());
EXPECT_EQ(*data.uri, sttx.getFieldVL(ripple::sfURI));
} else {
EXPECT_FALSE(data.uri.has_value());
}
if (sttx.getTxnType() == ripple::ttNFTOKEN_BURN) {
EXPECT_TRUE(data.isBurned);
} else {
EXPECT_FALSE(data.isBurned);
}
if (sttx.getTxnType() == ripple::ttNFTOKEN_MODIFY) {
EXPECT_TRUE(data.onlyUriChanged);
} else {
EXPECT_FALSE(data.onlyUriChanged);
}
}
};
TEST_F(NFTHelpersTest, NFTDataFromFailedTx)
{
auto const tx = createCancelNftOffersTxWithMetadata(kACCOUNT, 1, 2, std::vector<std::string>{kNFT_ID2, kNFT_ID});
ripple::TxMeta const txMeta(ripple::uint256(kTX), 1, tx.metadata);
auto const tx = createNftModifyTxWithMetadata(kACCOUNT, kNFT_ID, ripple::Blob{});
// Inject a failed result
ripple::SerialIter sitMeta(ripple::makeSlice(tx.metadata));
ripple::STObject objMeta(sitMeta, ripple::sfMetadata);
objMeta.setFieldU8(ripple::sfTransactionResult, ripple::tecINCOMPLETE);
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, objMeta.getSerializer().peekData());
auto const [nftTxs, nftDatas] =
etl::getNFTDataFromTx(txMeta, ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()}));
EXPECT_EQ(nftTxs.size(), 2);
EXPECT_EQ(nftTxs.size(), 0);
EXPECT_FALSE(nftDatas);
}
TEST_F(NFTHelpersTests, ConvertDataFromNFTCancelOfferTxContainingDuplicateNFT)
TEST_F(NFTHelpersTest, NotNFTTx)
{
auto const tx = createOracleSetTxWithMetadata(
kACCOUNT,
1,
123,
1,
4321u,
createPriceDataSeries({createOraclePriceData(1e3, ripple::to_currency("EUR"), ripple::to_currency("XRP"), 2)}),
kPAGE_INDEX,
false,
kTX
);
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
auto const [nftTxs, nftDatas] =
etl::getNFTDataFromTx(txMeta, ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()}));
EXPECT_EQ(nftTxs.size(), 0);
EXPECT_FALSE(nftDatas);
}
TEST_F(NFTHelpersTest, NFTModifyWithURI)
{
std::string const uri("1234567890A");
ripple::Blob const uriBlob(uri.begin(), uri.end());
auto const tx = createNftModifyTxWithMetadata(kACCOUNT, kNFT_ID, uriBlob);
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
auto const sttx = ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()});
auto const [nftTxs, nftDatas] =
etl::getNFTDataFromTx(txMeta, ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()}));
EXPECT_EQ(nftTxs.size(), 1);
verifyNFTTransactionsData(nftTxs[0], sttx, txMeta, kNFT_ID);
verifyNFTsData(*nftDatas, sttx, txMeta, kNFT_ID, std::nullopt);
}
TEST_F(NFTHelpersTest, NFTModifyWithoutURI)
{
auto const tx = createNftModifyTxWithMetadata(kACCOUNT, kNFT_ID, ripple::Blob{});
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
auto const sttx = ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()});
auto const [nftTxs, nftDatas] = etl::getNFTDataFromTx(txMeta, sttx);
EXPECT_EQ(nftTxs.size(), 1);
verifyNFTTransactionsData(nftTxs[0], sttx, txMeta, kNFT_ID);
verifyNFTsData(*nftDatas, sttx, txMeta, kNFT_ID, std::nullopt);
}
TEST_F(NFTHelpersTest, NFTMintFromModifedNode)
{
auto const tx = createMintNftTxWithMetadata(kACCOUNT, 1, 20, 1, kNFT_ID);
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
txMeta.getNodes()[0].setFieldH256(ripple::sfLedgerIndex, ripple::uint256(kPAGE_INDEX));
auto const sttx = ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()});
auto const [nftTxs, nftDatas] = etl::getNFTDataFromTx(txMeta, sttx);
EXPECT_EQ(nftTxs.size(), 1);
verifyNFTTransactionsData(nftTxs[0], sttx, txMeta, kNFT_ID);
verifyNFTsData(*nftDatas, sttx, txMeta, kNFT_ID, kACCOUNT);
}
TEST_F(NFTHelpersTest, NFTMintCantFindNewNFT)
{
// No NFT added to the page
auto const tx =
createMintNftTxWithMetadataOfCreatedNode(kACCOUNT, 1, 20, 1, std::nullopt, std::nullopt, kPAGE_INDEX);
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
EXPECT_THROW(
etl::getNFTDataFromTx(txMeta, ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()})),
std::runtime_error
);
}
TEST_F(NFTHelpersTest, NFTMintFromCreatedNode)
{
std::string const uri("1234567890A");
ripple::Blob const uriBlob(uri.begin(), uri.end());
auto const tx = createMintNftTxWithMetadataOfCreatedNode(kACCOUNT, 1, 20, 1, kNFT_ID, uri, kPAGE_INDEX);
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
auto const sttx = ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()});
auto const [nftTxs, nftDatas] = etl::getNFTDataFromTx(txMeta, sttx);
EXPECT_EQ(nftTxs.size(), 1);
verifyNFTTransactionsData(nftTxs[0], sttx, txMeta, kNFT_ID);
verifyNFTsData(*nftDatas, sttx, txMeta, kNFT_ID, kACCOUNT);
}
TEST_F(NFTHelpersTest, NFTMintWithoutUriField)
{
auto const tx = createMintNftTxWithMetadataOfCreatedNode(kACCOUNT, 1, 20, 1, kNFT_ID, std::nullopt, kPAGE_INDEX);
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
auto const sttx = ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()});
auto const [nftTxs, nftDatas] = etl::getNFTDataFromTx(txMeta, sttx);
EXPECT_EQ(nftTxs.size(), 1);
verifyNFTTransactionsData(nftTxs[0], sttx, txMeta, kNFT_ID);
verifyNFTsData(*nftDatas, sttx, txMeta, kNFT_ID, kACCOUNT);
}
TEST_F(NFTHelpersTest, NFTMintZeroMetaNode)
{
auto const tx = createMintNftTxWithMetadataOfCreatedNode(kACCOUNT, 1, 20, 1, kNFT_ID, std::nullopt, kPAGE_INDEX);
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
txMeta.getNodes().clear();
EXPECT_THROW(
etl::getNFTDataFromTx(txMeta, ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()})),
std::runtime_error
);
}
TEST_F(NFTHelpersTest, NFTBurnFromDeletedNode)
{
auto const tx = createNftBurnTxWithMetadataOfDeletedNode(kACCOUNT, kNFT_ID);
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
txMeta.getNodes()[1].setFieldH256(ripple::sfLedgerIndex, ripple::uint256(kPAGE_INDEX));
auto const sttx = ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()});
auto const [nftTxs, nftDatas] = etl::getNFTDataFromTx(txMeta, sttx);
EXPECT_EQ(nftTxs.size(), 1);
verifyNFTTransactionsData(nftTxs[0], sttx, txMeta, kNFT_ID);
verifyNFTsData(*nftDatas, sttx, txMeta, kNFT_ID, kACCOUNT);
}
TEST_F(NFTHelpersTest, NFTBurnZeroMetaNode)
{
auto const tx = createNftBurnTxWithMetadataOfDeletedNode(kACCOUNT, kNFT_ID);
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
txMeta.getNodes().clear();
EXPECT_THROW(
etl::getNFTDataFromTx(txMeta, ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()})),
std::runtime_error
);
}
TEST_F(NFTHelpersTest, NFTBurnFromModifiedNode)
{
auto const tx = createNftBurnTxWithMetadataOfModifiedNode(kACCOUNT, kNFT_ID);
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
txMeta.getNodes()[0].setFieldH256(ripple::sfLedgerIndex, ripple::uint256(kPAGE_INDEX));
auto const sttx = ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()});
auto const [nftTxs, nftDatas] = etl::getNFTDataFromTx(txMeta, sttx);
EXPECT_EQ(nftTxs.size(), 1);
verifyNFTTransactionsData(nftTxs[0], sttx, txMeta, kNFT_ID);
verifyNFTsData(*nftDatas, sttx, txMeta, kNFT_ID, kACCOUNT);
}
TEST_F(NFTHelpersTest, NFTCancelOffer)
{
auto const tx = createCancelNftOffersTxWithMetadata(kACCOUNT, 1, 2, std::vector<std::string>{kNFT_ID, kNFT_ID2});
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
txMeta.getNodes()[0].setFieldH256(ripple::sfLedgerIndex, ripple::uint256(kPAGE_INDEX));
auto const sttx = ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()});
auto const [nftTxs, nftDatas] = etl::getNFTDataFromTx(txMeta, sttx);
EXPECT_EQ(nftTxs.size(), 2);
EXPECT_FALSE(nftDatas);
verifyNFTTransactionsData(nftTxs[0], sttx, txMeta, kNFT_ID);
verifyNFTTransactionsData(nftTxs[1], sttx, txMeta, kNFT_ID2);
}
TEST_F(NFTHelpersTest, NFTCancelOfferContainsDuplicateNFTs)
{
auto const tx = createCancelNftOffersTxWithMetadata(
kACCOUNT, 1, 2, std::vector<std::string>{kNFT_ID2, kNFT_ID, kNFT_ID2, kNFT_ID}
);
ripple::TxMeta const txMeta(ripple::uint256(kTX), 1, tx.metadata);
auto const [nftTxs, nftDatas] =
etl::getNFTDataFromTx(txMeta, ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()}));
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
auto const sttx = ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()});
auto const [nftTxs, nftDatas] = etl::getNFTDataFromTx(txMeta, sttx);
EXPECT_EQ(nftTxs.size(), 2);
EXPECT_FALSE(nftDatas);
verifyNFTTransactionsData(nftTxs[0], sttx, txMeta, kNFT_ID);
verifyNFTTransactionsData(nftTxs[1], sttx, txMeta, kNFT_ID2);
}
TEST_F(NFTHelpersTests, UniqueNFTDatas)
TEST_F(NFTHelpersTest, UniqueNFTDatas)
{
std::vector<NFTsData> nftDatas;
@@ -104,3 +354,156 @@ TEST_F(NFTHelpersTests, UniqueNFTDatas)
EXPECT_EQ(uniqueNFTDatas[0].tokenID, ripple::uint256(kNFT_ID2));
EXPECT_EQ(uniqueNFTDatas[1].tokenID, ripple::uint256(kNFT_ID));
}
TEST_F(NFTHelpersTest, NFTAcceptBuyerOffer)
{
auto const tx = createAcceptNftBuyerOfferTxWithMetadata(kACCOUNT, 1, 2, kNFT_ID, kOFFER_ID);
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
auto const sttx = ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()});
auto const [nftTxs, nftDatas] = etl::getNFTDataFromTx(txMeta, sttx);
EXPECT_EQ(nftTxs.size(), 1);
EXPECT_TRUE(nftDatas);
verifyNFTTransactionsData(nftTxs[0], sttx, txMeta, kNFT_ID);
verifyNFTsData(*nftDatas, sttx, txMeta, kNFT_ID, kACCOUNT);
}
// The offer id in tx is different from the offer id in deleted node in metadata
TEST_F(NFTHelpersTest, NFTAcceptBuyerOfferCheckOfferIDFail)
{
auto const tx = createAcceptNftBuyerOfferTxWithMetadata(kACCOUNT, 1, 2, kNFT_ID, kOFFER_ID);
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
// inject a different offer id
txMeta.getNodes()[0].setFieldH256(ripple::sfLedgerIndex, ripple::uint256(kPAGE_INDEX));
EXPECT_THROW(
etl::getNFTDataFromTx(txMeta, ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()})),
std::runtime_error
);
}
TEST_F(NFTHelpersTest, NFTAcceptSellerOfferFromCreatedNode)
{
auto const tx = createAcceptNftSellerOfferTxWithMetadata(kACCOUNT2, 1, 2, kNFT_ID, kOFFER_ID, kPAGE_INDEX, true);
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
auto const sttx = ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()});
auto const [nftTxs, nftDatas] = etl::getNFTDataFromTx(txMeta, sttx);
EXPECT_EQ(nftTxs.size(), 1);
EXPECT_TRUE(nftDatas);
verifyNFTTransactionsData(nftTxs[0], sttx, txMeta, kNFT_ID);
verifyNFTsData(*nftDatas, sttx, txMeta, kNFT_ID, kACCOUNT);
}
TEST_F(NFTHelpersTest, NFTAcceptSellerOfferFromModifiedNode)
{
auto const tx = createAcceptNftSellerOfferTxWithMetadata(kACCOUNT2, 1, 2, kNFT_ID, kOFFER_ID, kPAGE_INDEX, false);
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
auto const sttx = ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()});
auto const [nftTxs, nftDatas] = etl::getNFTDataFromTx(txMeta, sttx);
EXPECT_EQ(nftTxs.size(), 1);
EXPECT_TRUE(nftDatas);
verifyNFTTransactionsData(nftTxs[0], sttx, txMeta, kNFT_ID);
verifyNFTsData(*nftDatas, sttx, txMeta, kNFT_ID, kACCOUNT);
}
TEST_F(NFTHelpersTest, NFTAcceptSellerOfferCheckFail)
{
// The only changed nft page is owned by ACCOUNT, thus can't find the new owner
auto const tx = createAcceptNftSellerOfferTxWithMetadata(kACCOUNT, 1, 2, kNFT_ID, kOFFER_ID, kPAGE_INDEX, true);
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
EXPECT_THROW(
etl::getNFTDataFromTx(txMeta, ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()})),
std::runtime_error
);
}
TEST_F(NFTHelpersTest, NFTAcceptSellerOfferNotInMeta)
{
auto const tx = createAcceptNftSellerOfferTxWithMetadata(kACCOUNT, 1, 2, kNFT_ID, kOFFER_ID, kPAGE_INDEX, true);
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
// inject a different offer id
txMeta.getNodes()[0].setFieldH256(ripple::sfLedgerIndex, ripple::uint256(kPAGE_INDEX));
EXPECT_THROW(
etl::getNFTDataFromTx(txMeta, ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()})),
std::runtime_error
);
}
TEST_F(NFTHelpersTest, NFTAcceptSellerOfferZeroMetaNode)
{
auto const tx = createAcceptNftSellerOfferTxWithMetadata(kACCOUNT2, 1, 2, kNFT_ID, kOFFER_ID, kPAGE_INDEX, true);
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
txMeta.getNodes().clear();
EXPECT_THROW(
etl::getNFTDataFromTx(txMeta, ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()})),
std::runtime_error
);
}
TEST_F(NFTHelpersTest, NFTAcceptSellerOfferIDNotInMetaData)
{
auto const tx = createAcceptNftSellerOfferTxWithMetadata(kACCOUNT2, 1, 2, kNFT_ID, kOFFER_ID, kPAGE_INDEX, true);
ripple::TxMeta txMeta(ripple::uint256(kTX), 1, tx.metadata);
// The first node is offer, the second is nft page. Change the offer id to something else
txMeta.getNodes()[0]
.getField(ripple::sfFinalFields)
.downcast<ripple::STObject>()
.setFieldH256(ripple::sfNFTokenID, ripple::uint256(kNFT_ID2));
EXPECT_THROW(
etl::getNFTDataFromTx(txMeta, ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()})),
std::runtime_error
);
}
TEST_F(NFTHelpersTest, NFTCreateOffer)
{
auto const tx = createCreateNftOfferTxWithMetadata(kACCOUNT, 1, 2, kNFT_ID, 1, kOFFER_ID);
ripple::TxMeta txMeta(ripple::uint256(kTX), 5, tx.metadata);
auto const sttx = ripple::STTx(ripple::SerialIter{tx.transaction.data(), tx.transaction.size()});
auto const [nftTxs, nftDatas] = etl::getNFTDataFromTx(txMeta, sttx);
EXPECT_EQ(nftTxs.size(), 1);
EXPECT_FALSE(nftDatas);
verifyNFTTransactionsData(nftTxs[0], sttx, txMeta, kNFT_ID);
}
TEST_F(NFTHelpersTest, NFTDataFromLedgerObject)
{
std::string const url1 = "abcd1";
std::string const url2 = "abcd2";
ripple::Blob const uri1Blob(url1.begin(), url1.end());
ripple::Blob const uri2Blob(url2.begin(), url2.end());
auto const nftPage = createNftTokenPage({{kNFT_ID, url1}, {kNFT_ID2, url2}}, std::nullopt);
auto const serializerNftPage = nftPage.getSerializer();
int constexpr kSEQ{5};
auto const account = getAccountIdWithString(kACCOUNT);
auto const nftDatas = etl::getNFTDataFromObj(
kSEQ,
std::string(static_cast<char const*>(static_cast<void const*>(account.data())), ripple::AccountID::size()),
std::string(static_cast<char const*>(serializerNftPage.getDataPtr()), serializerNftPage.getDataLength())
);
EXPECT_EQ(nftDatas.size(), 2);
EXPECT_EQ(nftDatas[0].tokenID, ripple::uint256(kNFT_ID));
EXPECT_EQ(*(nftDatas[0].uri), uri1Blob);
EXPECT_FALSE(nftDatas[0].onlyUriChanged);
EXPECT_EQ(nftDatas[0].owner, account);
EXPECT_EQ(nftDatas[0].ledgerSequence, kSEQ);
EXPECT_FALSE(nftDatas[0].isBurned);
EXPECT_EQ(nftDatas[1].tokenID, ripple::uint256(kNFT_ID2));
EXPECT_EQ(*(nftDatas[1].uri), uri2Blob);
EXPECT_FALSE(nftDatas[1].onlyUriChanged);
EXPECT_EQ(nftDatas[1].owner, account);
EXPECT_EQ(nftDatas[1].ledgerSequence, kSEQ);
EXPECT_FALSE(nftDatas[1].isBurned);
}

View File

@@ -498,7 +498,7 @@ TEST_F(RPCHelpersTest, LedgerHeaderJsonV2)
TEST_F(RPCHelpersTest, TransactionAndMetadataBinaryJsonV1)
{
auto const txMeta = createAcceptNftOfferTxWithMetadata(kACCOUNT, 30, 1, kINDEX1);
auto const txMeta = createAcceptNftBuyerOfferTxWithMetadata(kACCOUNT, 30, 1, kINDEX1, kINDEX2);
auto const json = toJsonWithBinaryTx(txMeta, 1);
EXPECT_TRUE(json.contains(JS(tx_blob)));
EXPECT_TRUE(json.contains(JS(meta)));
@@ -506,7 +506,7 @@ TEST_F(RPCHelpersTest, TransactionAndMetadataBinaryJsonV1)
TEST_F(RPCHelpersTest, TransactionAndMetadataBinaryJsonV2)
{
auto const txMeta = createAcceptNftOfferTxWithMetadata(kACCOUNT, 30, 1, kINDEX1);
auto const txMeta = createAcceptNftBuyerOfferTxWithMetadata(kACCOUNT, 30, 1, kINDEX1, kINDEX2);
auto const json = toJsonWithBinaryTx(txMeta, 2);
EXPECT_TRUE(json.contains(JS(tx_blob)));
EXPECT_TRUE(json.contains(JS(meta_blob)));

View File

@@ -52,6 +52,7 @@ constexpr auto kLEDGER_HASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF2
constexpr auto kNFT_ID = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF";
constexpr auto kNFT_ID2 = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA";
constexpr auto kNFT_ID3 = "15FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF";
constexpr auto kINDEX = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC322";
} // namespace
@@ -471,7 +472,7 @@ genNFTTransactions(uint32_t seq)
trans1.date = 1;
transactions.push_back(trans1);
auto trans2 = createAcceptNftOfferTxWithMetadata(kACCOUNT, 1, 50, kNFT_ID2);
auto trans2 = createAcceptNftBuyerOfferTxWithMetadata(kACCOUNT, 1, 50, kNFT_ID2, kINDEX);
trans2.ledgerSequence = seq;
trans2.date = 2;
transactions.push_back(trans2);
@@ -1199,9 +1200,12 @@ TEST_F(RPCAccountTxHandlerTest, NFTTxs_API_v1)
{
"FinalFields":
{
"NFTokenID": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"
"NFTokenID": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
},
"LedgerEntryType": "NFTokenOffer"
"LedgerEntryType": "NFTokenOffer",
"LedgerIndex": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC322"
}
}
],
@@ -1213,11 +1217,11 @@ TEST_F(RPCAccountTxHandlerTest, NFTTxs_API_v1)
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "50",
"NFTokenBuyOffer": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC",
"NFTokenBuyOffer": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC322",
"Sequence": 1,
"SigningPubKey": "74657374",
"TransactionType": "NFTokenAcceptOffer",
"hash": "7682BE6BCDE62F8142915DD852936623B68FC3839A8A424A6064B898702B0CDF",
"hash": "C85E486EE308C68D7E601FCEB4FC961BFA914C80ABBF7ECC7E6277B06692B490",
"ledger_index": 11,
"inLedger": 11,
"date": 2
@@ -1431,9 +1435,11 @@ TEST_F(RPCAccountTxHandlerTest, NFTTxs_API_v2)
{
"FinalFields":
{
"NFTokenID": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"
"NFTokenID": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
},
"LedgerEntryType": "NFTokenOffer"
"LedgerEntryType": "NFTokenOffer",
"LedgerIndex": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC322"
}
}
],
@@ -1441,7 +1447,7 @@ TEST_F(RPCAccountTxHandlerTest, NFTTxs_API_v2)
"TransactionResult": "tesSUCCESS",
"nftoken_id": "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DA"
},
"hash": "7682BE6BCDE62F8142915DD852936623B68FC3839A8A424A6064B898702B0CDF",
"hash": "C85E486EE308C68D7E601FCEB4FC961BFA914C80ABBF7ECC7E6277B06692B490",
"ledger_index": 11,
"ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652",
"close_time_iso": "2000-01-01T00:00:00Z",
@@ -1449,7 +1455,7 @@ TEST_F(RPCAccountTxHandlerTest, NFTTxs_API_v2)
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "50",
"NFTokenBuyOffer": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC",
"NFTokenBuyOffer": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC322",
"Sequence": 1,
"SigningPubKey": "74657374",
"TransactionType": "NFTokenAcceptOffer",

View File

@@ -136,6 +136,7 @@ constexpr auto kDEFAULT_OUT2 = R"({
"close_time_iso": "2000-01-01T00:00:00Z",
"validated": true
})";
constexpr auto kINDEX = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC322";
} // namespace
@@ -652,7 +653,7 @@ TEST_F(RPCTxTest, MintNFT)
TEST_F(RPCTxTest, NFTAcceptOffer)
{
TransactionAndMetadata tx = createAcceptNftOfferTxWithMetadata(kACCOUNT, 1, 50, kNFT_ID);
TransactionAndMetadata tx = createAcceptNftBuyerOfferTxWithMetadata(kACCOUNT, 1, 50, kNFT_ID, kINDEX);
tx.date = 123456;
tx.ledgerSequence = 100;