mirror of
https://github.com/XRPLF/clio.git
synced 2025-11-20 11:45:53 +00:00
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:
@@ -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'
|
||||
]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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 {{}, {}};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
#include "util/async/AnyStrand.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user