diff --git a/conanfile.py b/conanfile.py index 997dee1c..ee1d0259 100644 --- a/conanfile.py +++ b/conanfile.py @@ -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' ] diff --git a/src/data/AmendmentCenter.hpp b/src/data/AmendmentCenter.hpp index 0413f009..698f5797 100644 --- a/src/data/AmendmentCenter.hpp +++ b/src/data/AmendmentCenter.hpp @@ -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); diff --git a/src/data/CassandraBackend.hpp b/src/data/CassandraBackend.hpp index 71e9ecf5..65309a5f 100644 --- a/src/data/CassandraBackend.hpp +++ b/src/data/CassandraBackend.hpp @@ -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(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(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 diff --git a/src/data/DBHelpers.hpp b/src/data/DBHelpers.hpp index 520228e3..cba1ec1a 100644 --- a/src/data/DBHelpers.hpp +++ b/src/data/DBHelpers.hpp @@ -107,6 +107,7 @@ struct NFTsData { ripple::AccountID owner; std::optional 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) + { + } }; /** diff --git a/src/data/cassandra/impl/ExecutionStrategy.hpp b/src/data/cassandra/impl/ExecutionStrategy.hpp index 91ee8336..0b95d3f8 100644 --- a/src/data/cassandra/impl/ExecutionStrategy.hpp +++ b/src/data/cassandra/impl/ExecutionStrategy.hpp @@ -35,6 +35,7 @@ #include #include +#include #include #include #include @@ -192,10 +193,24 @@ public: template void write(PreparedStatementType const& preparedStatement, Args&&... args) + { + auto statement = preparedStatement.bind(std::forward(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)...); 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&& 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. * diff --git a/src/etl/NFTHelpers.cpp b/src/etl/NFTHelpers.cpp index be3d739d..f7e90e25 100644 --- a/src/etl/NFTHelpers.cpp +++ b/src/etl/NFTHelpers.cpp @@ -47,6 +47,17 @@ namespace etl { +std::pair, std::optional> +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::optional> 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(); 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().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 {{}, {}}; } diff --git a/src/etl/NFTHelpers.hpp b/src/etl/NFTHelpers.hpp index c60eb9bf..ef8cdb74 100644 --- a/src/etl/NFTHelpers.hpp +++ b/src/etl/NFTHelpers.hpp @@ -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::optional> +getNftokenModifyData(ripple::TxMeta const& txMeta, ripple::STTx const& sttx); + /** * @brief Get the NFT Token mint data from a transaction * diff --git a/src/etl/impl/LedgerLoader.hpp b/src/etl/impl/LedgerLoader.hpp index 753b211b..e6d88ee8 100644 --- a/src/etl/impl/LedgerLoader.hpp +++ b/src/etl/impl/LedgerLoader.hpp @@ -57,6 +57,7 @@ struct FormattedTransactionsData { std::vector nfTokenTxData; std::vector nfTokensData; std::vector mptHoldersData; + std::vector nfTokenURIChanges; }; namespace etl::impl { @@ -111,6 +112,7 @@ public: { FormattedTransactionsData result; + std::vector 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; } diff --git a/src/etlng/impl/TaskManager.cpp b/src/etlng/impl/TaskManager.cpp index a5910242..a5fc20ce 100644 --- a/src/etlng/impl/TaskManager.cpp +++ b/src/etlng/impl/TaskManager.cpp @@ -28,7 +28,6 @@ #include "util/async/AnyStrand.hpp" #include "util/log/Logger.hpp" - #include #include #include diff --git a/tests/common/util/TestObject.cpp b/tests/common/util/TestObject.cpp index ac1badd6..b8ad157c 100644 --- a/tests/common/util/TestObject.cpp +++ b/tests/common/util/TestObject.cpp @@ -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 nftID, + std::optional uri, + std::optional pageIndex +) +{ + // tx + ripple::STObject tx(ripple::sfTransaction); + tx.setFieldU16(ripple::sfTransactionType, ripple::ttNFTOKEN_MINT); + auto account = util::parseBase58Wrapper(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(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(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( diff --git a/tests/common/util/TestObject.hpp b/tests/common/util/TestObject.hpp index b27d772d..7ad745d7 100644 --- a/tests/common/util/TestObject.hpp +++ b/tests/common/util/TestObject.hpp @@ -302,6 +302,9 @@ createNftTokenPage( std::optional 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 nftID, + std::optional uri, + std::optional 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 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, diff --git a/tests/unit/data/cassandra/ExecutionStrategyTests.cpp b/tests/unit/data/cassandra/ExecutionStrategyTests.cpp index c0d8f327..b12ece67 100644 --- a/tests/unit/data/cassandra/ExecutionStrategyTests.cpp +++ b/tests/unit/data/cassandra/ExecutionStrategyTests.cpp @@ -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{ctx_}; + auto thread = std::thread{[this]() { ctx_.run(); }}; + + EXPECT_CALL(handle_, asyncExecute(A(), A&&>())) + .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(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(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(); +} diff --git a/tests/unit/etl/NFTHelpersTests.cpp b/tests/unit/etl/NFTHelpersTests.cpp index 7579de72..b4ae6b50 100644 --- a/tests/unit/etl/NFTHelpersTests.cpp +++ b/tests/unit/etl/NFTHelpersTests.cpp @@ -22,56 +22,306 @@ #include "util/LoggerFixtures.hpp" #include "util/TestObject.hpp" +#include +#include +#include +#include +#include +#include +#include +#include #include #include +#include #include +#include #include #include #include #include +#include +#include #include +#include +#include #include +#include +#include #include +#include #include 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 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{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{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{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 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() + .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(static_cast(account.data())), ripple::AccountID::size()), + std::string(static_cast(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); +} diff --git a/tests/unit/rpc/RPCHelpersTests.cpp b/tests/unit/rpc/RPCHelpersTests.cpp index 61e94b8a..7c3e7dfd 100644 --- a/tests/unit/rpc/RPCHelpersTests.cpp +++ b/tests/unit/rpc/RPCHelpersTests.cpp @@ -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))); diff --git a/tests/unit/rpc/handlers/AccountTxTests.cpp b/tests/unit/rpc/handlers/AccountTxTests.cpp index 049c5174..dc1cb8b2 100644 --- a/tests/unit/rpc/handlers/AccountTxTests.cpp +++ b/tests/unit/rpc/handlers/AccountTxTests.cpp @@ -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", diff --git a/tests/unit/rpc/handlers/TxTests.cpp b/tests/unit/rpc/handlers/TxTests.cpp index ffa2baa1..7b8df41d 100644 --- a/tests/unit/rpc/handlers/TxTests.cpp +++ b/tests/unit/rpc/handlers/TxTests.cpp @@ -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;