diff --git a/CMakeLists.txt b/CMakeLists.txt index 90061ac8..af20e7a9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,21 +73,21 @@ target_sources(clio PRIVATE src/rpc/ngHandlers/AccountCurrencies.cpp src/rpc/ngHandlers/AccountLines.cpp src/rpc/ngHandlers/AccountTx.cpp - src/rpc/ngHandlers/AccountTx.cpp src/rpc/ngHandlers/AccountOffers.cpp src/rpc/ngHandlers/AccountInfo.cpp - src/rpc/ngHandlers/Tx.cpp + src/rpc/ngHandlers/BookOffers.cpp src/rpc/ngHandlers/GatewayBalances.cpp src/rpc/ngHandlers/LedgerEntry.cpp src/rpc/ngHandlers/LedgerRange.cpp - src/rpc/ngHandlers/BookOffers.cpp src/rpc/ngHandlers/TransactionEntry.cpp + src/rpc/ngHandlers/Tx.cpp + src/rpc/ngHandlers/Random.cpp src/rpc/ngHandlers/NoRippleCheck.cpp src/rpc/ngHandlers/NFTInfo.cpp src/rpc/ngHandlers/NFTOffersCommon.cpp src/rpc/ngHandlers/NFTBuyOffers.cpp src/rpc/ngHandlers/NFTSellOffers.cpp - src/rpc/ngHandlers/Random.cpp + src/rpc/ngHandlers/NFTHistory.cpp ## RPC Methods # Account src/rpc/handlers/AccountChannels.cpp @@ -147,28 +147,27 @@ if(BUILD_TESTS) unittests/rpc/BaseTests.cpp unittests/rpc/RPCHelpersTest.cpp ## RPC handlers + unittests/rpc/handlers/DefaultProcessorTests.cpp unittests/rpc/handlers/TestHandlerTests.cpp unittests/rpc/handlers/AccountCurrenciesTest.cpp unittests/rpc/handlers/AccountLinesTest.cpp unittests/rpc/handlers/AccountTxTest.cpp - unittests/rpc/handlers/AccountTxTest.cpp unittests/rpc/handlers/AccountOffersTest.cpp unittests/rpc/handlers/AccountInfoTest.cpp - unittests/rpc/handlers/DefaultProcessorTests.cpp - unittests/rpc/handlers/PingTest.cpp unittests/rpc/handlers/AccountChannelsTest.cpp + unittests/rpc/handlers/BookOffersTest.cpp + unittests/rpc/handlers/GatewayBalancesTest.cpp unittests/rpc/handlers/TxTest.cpp unittests/rpc/handlers/TransactionEntryTest.cpp - unittests/rpc/handlers/GatewayBalancesTest.cpp unittests/rpc/handlers/LedgerEntryTest.cpp unittests/rpc/handlers/LedgerRangeTest.cpp - unittests/rpc/handlers/BookOffersTest.cpp unittests/rpc/handlers/NoRippleCheckTest.cpp - unittests/rpc/handlers/NFTInfoTest.cpp + unittests/rpc/handlers/PingTest.cpp + unittests/rpc/handlers/RandomTest.cpp unittests/rpc/handlers/NFTInfoTest.cpp unittests/rpc/handlers/NFTBuyOffersTest.cpp unittests/rpc/handlers/NFTSellOffersTest.cpp - unittests/rpc/handlers/RandomTest.cpp + unittests/rpc/handlers/NFTHistoryTest.cpp # Backend unittests/backend/cassandra/BaseTests.cpp unittests/backend/cassandra/BackendTests.cpp diff --git a/src/rpc/ngHandlers/AccountChannels.h b/src/rpc/ngHandlers/AccountChannels.h index 605f0677..1f990b31 100644 --- a/src/rpc/ngHandlers/AccountChannels.h +++ b/src/rpc/ngHandlers/AccountChannels.h @@ -24,8 +24,6 @@ #include #include -#include - #include namespace RPCng { @@ -106,22 +104,20 @@ private: void addChannel(std::vector& jsonLines, ripple::SLE const& line) const; + + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + Output const& output); + + friend Input + tag_invoke(boost::json::value_to_tag, boost::json::value const& jv); + + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + ChannelResponse const& channel); }; - -AccountChannelsHandler::Input -tag_invoke( - boost::json::value_to_tag, - boost::json::value const& jv); - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - AccountChannelsHandler::Output const& output); - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - AccountChannelsHandler::ChannelResponse const& channel); } // namespace RPCng diff --git a/src/rpc/ngHandlers/AccountCurrencies.h b/src/rpc/ngHandlers/AccountCurrencies.h index 5a29daa4..533f8846 100644 --- a/src/rpc/ngHandlers/AccountCurrencies.h +++ b/src/rpc/ngHandlers/AccountCurrencies.h @@ -24,8 +24,6 @@ #include #include -#include - #include namespace RPCng { @@ -73,16 +71,15 @@ public: Result process(Input input, Context const& ctx) const; + +private: + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + Output const& output); + + friend Input + tag_invoke(boost::json::value_to_tag, boost::json::value const& jv); }; - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - AccountCurrenciesHandler::Output const& output); - -AccountCurrenciesHandler::Input -tag_invoke( - boost::json::value_to_tag, - boost::json::value const& jv); } // namespace RPCng diff --git a/src/rpc/ngHandlers/AccountInfo.h b/src/rpc/ngHandlers/AccountInfo.h index c9631bef..51217d64 100644 --- a/src/rpc/ngHandlers/AccountInfo.h +++ b/src/rpc/ngHandlers/AccountInfo.h @@ -24,8 +24,6 @@ #include #include -#include - namespace RPCng { class AccountInfoHandler { @@ -97,16 +95,15 @@ public: Result process(Input input, Context const& ctx) const; + +private: + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + Output const& output); + + friend Input + tag_invoke(boost::json::value_to_tag, boost::json::value const& jv); }; - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - AccountInfoHandler::Output const& output); - -AccountInfoHandler::Input -tag_invoke( - boost::json::value_to_tag, - boost::json::value const& jv); } // namespace RPCng diff --git a/src/rpc/ngHandlers/AccountLines.h b/src/rpc/ngHandlers/AccountLines.h index 7bdac3c5..f8ef170e 100644 --- a/src/rpc/ngHandlers/AccountLines.h +++ b/src/rpc/ngHandlers/AccountLines.h @@ -24,8 +24,6 @@ #include #include -#include - #include namespace RPCng { @@ -113,23 +111,21 @@ private: ripple::SLE const& lineSle, ripple::AccountID const& account, std::optional const& peerAccount) const; + +private: + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + Output const& output); + + friend Input + tag_invoke(boost::json::value_to_tag, boost::json::value const& jv); + + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + LineResponse const& line); }; - -AccountLinesHandler::Input -tag_invoke( - boost::json::value_to_tag, - boost::json::value const& jv); - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - AccountLinesHandler::Output const& output); - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - AccountLinesHandler::LineResponse const& line); - } // namespace RPCng diff --git a/src/rpc/ngHandlers/AccountOffers.h b/src/rpc/ngHandlers/AccountOffers.h index bae59882..2f8f574d 100644 --- a/src/rpc/ngHandlers/AccountOffers.h +++ b/src/rpc/ngHandlers/AccountOffers.h @@ -24,8 +24,6 @@ #include #include -#include - namespace RPCng { class AccountOffersHandler { @@ -91,22 +89,20 @@ public: private: void addOffer(std::vector& offers, ripple::SLE const& offerSle) const; + + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + Output const& output); + + friend Input + tag_invoke(boost::json::value_to_tag, boost::json::value const& jv); + + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + Offer const& offer); }; - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - AccountOffersHandler::Output const& output); - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - AccountOffersHandler::Offer const& offer); - -AccountOffersHandler::Input -tag_invoke( - boost::json::value_to_tag, - boost::json::value const& jv); } // namespace RPCng diff --git a/src/rpc/ngHandlers/AccountTx.cpp b/src/rpc/ngHandlers/AccountTx.cpp index b5d2685d..66737c9a 100644 --- a/src/rpc/ngHandlers/AccountTx.cpp +++ b/src/rpc/ngHandlers/AccountTx.cpp @@ -18,12 +18,12 @@ //============================================================================== #include - -namespace { -clio::Logger gLog{"RPC-AccountTxHandler"}; -} +#include namespace RPCng { + +// TODO: this is currently very similar to nft_history but its own copy for time +// being. we should aim to reuse common logic in some way in the future. AccountTxHandler::Result AccountTxHandler::process(AccountTxHandler::Input input, Context const& ctx) const @@ -76,7 +76,9 @@ AccountTxHandler::process(AccountTxHandler::Input input, Context const& ctx) maxIndex = minIndex = std::get(lgrInfoOrStatus).seq; } + std::optional cursor; + // if marker exists if (input.marker) { @@ -89,11 +91,17 @@ AccountTxHandler::process(AccountTxHandler::Input input, Context const& ctx) else cursor = {maxIndex, INT32_MAX}; } - auto constexpr limitDefault = 50; + + static auto constexpr limitDefault = 50; auto const limit = input.limit.value_or(limitDefault); auto const accountID = RPC::accountFromStringStrict(input.account); - auto const [blobs, retCursor] = sharedPtrBackend_->fetchAccountTransactions( - *accountID, limit, input.forward, cursor, ctx.yield); + auto const [txnsAndCursor, timeDiff] = util::timed([&]() { + return sharedPtrBackend_->fetchAccountTransactions( + *accountID, limit, input.forward, cursor, ctx.yield); + }); + log_.info() << "db fetch took " << timeDiff + << " milliseconds - num blobs = " << txnsAndCursor.txns.size(); + auto const [blobs, retCursor] = txnsAndCursor; Output response; if (retCursor) @@ -111,7 +119,7 @@ AccountTxHandler::process(AccountTxHandler::Input input, Context const& ctx) } else if (txnPlusMeta.ledgerSequence > maxIndex && !input.forward) { - gLog.debug() << "Skipping over transactions from incomplete ledger"; + log_.debug() << "Skipping over transactions from incomplete ledger"; continue; } @@ -134,13 +142,15 @@ AccountTxHandler::process(AccountTxHandler::Input input, Context const& ctx) obj[JS(date)] = txnPlusMeta.date; } obj[JS(validated)] = true; + response.transactions.push_back(obj); } response.limit = input.limit; - response.account = input.account; + response.account = ripple::to_string(*accountID); response.ledgerIndexMin = minIndex; response.ledgerIndexMax = maxIndex; + return response; } diff --git a/src/rpc/ngHandlers/AccountTx.h b/src/rpc/ngHandlers/AccountTx.h index 95c7d590..f2a294d9 100644 --- a/src/rpc/ngHandlers/AccountTx.h +++ b/src/rpc/ngHandlers/AccountTx.h @@ -20,15 +20,15 @@ #pragma once #include +#include #include #include #include -#include - namespace RPCng { class AccountTxHandler { + clio::Logger log_{"RPC"}; std::shared_ptr sharedPtrBackend_; public: @@ -106,22 +106,21 @@ public: Result process(Input input, Context const& ctx) const; + +private: + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + Output const& output); + + friend Input + tag_invoke(boost::json::value_to_tag, boost::json::value const& jv); + + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + Marker const& marker); }; - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - AccountTxHandler::Output const& output); - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - AccountTxHandler::Marker const& marker); - -AccountTxHandler::Input -tag_invoke( - boost::json::value_to_tag, - boost::json::value const& jv); } // namespace RPCng diff --git a/src/rpc/ngHandlers/BookOffers.h b/src/rpc/ngHandlers/BookOffers.h index 51166d5a..be5bda14 100644 --- a/src/rpc/ngHandlers/BookOffers.h +++ b/src/rpc/ngHandlers/BookOffers.h @@ -23,8 +23,6 @@ #include #include -#include - namespace RPCng { class BookOffersHandler { @@ -103,16 +101,15 @@ public: Result process(Input input, Context const& ctx) const; + +private: + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + Output const& output); + + friend Input + tag_invoke(boost::json::value_to_tag, boost::json::value const& jv); }; - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - BookOffersHandler::Output const& output); - -BookOffersHandler::Input -tag_invoke( - boost::json::value_to_tag, - boost::json::value const& jv); } // namespace RPCng diff --git a/src/rpc/ngHandlers/GatewayBalances.h b/src/rpc/ngHandlers/GatewayBalances.h index 8b3b6722..fad3ae19 100644 --- a/src/rpc/ngHandlers/GatewayBalances.h +++ b/src/rpc/ngHandlers/GatewayBalances.h @@ -24,8 +24,6 @@ #include #include -#include - namespace RPCng { class GatewayBalancesHandler { @@ -114,16 +112,15 @@ public: Result process(Input input, Context const& ctx) const; + +private: + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + Output const& output); + + friend Input + tag_invoke(boost::json::value_to_tag, boost::json::value const& jv); }; - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - GatewayBalancesHandler::Output const& output); - -GatewayBalancesHandler::Input -tag_invoke( - boost::json::value_to_tag, - boost::json::value const& jv); } // namespace RPCng diff --git a/src/rpc/ngHandlers/LedgerEntry.h b/src/rpc/ngHandlers/LedgerEntry.h index bb46c074..ebb81ff8 100644 --- a/src/rpc/ngHandlers/LedgerEntry.h +++ b/src/rpc/ngHandlers/LedgerEntry.h @@ -24,8 +24,6 @@ #include #include -#include - namespace RPCng { class LedgerEntryHandler @@ -197,16 +195,14 @@ private: std::variant composeKeyFromDirectory( boost::json::object const& directory) const noexcept; + + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + Output const& output); + + friend Input + tag_invoke(boost::json::value_to_tag, boost::json::value const& jv); }; - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - LedgerEntryHandler::Output const& output); - -LedgerEntryHandler::Input -tag_invoke( - boost::json::value_to_tag, - boost::json::value const& jv); } // namespace RPCng diff --git a/src/rpc/ngHandlers/LedgerRange.h b/src/rpc/ngHandlers/LedgerRange.h index 3a893e6c..142e6f2e 100644 --- a/src/rpc/ngHandlers/LedgerRange.h +++ b/src/rpc/ngHandlers/LedgerRange.h @@ -48,12 +48,12 @@ public: Result process() const; + +private: + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + Output const& output); }; - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - LedgerRangeHandler::Output const& output); - } // namespace RPCng diff --git a/src/rpc/ngHandlers/NFTBuyOffers.h b/src/rpc/ngHandlers/NFTBuyOffers.h index ef9d9487..783dc691 100644 --- a/src/rpc/ngHandlers/NFTBuyOffers.h +++ b/src/rpc/ngHandlers/NFTBuyOffers.h @@ -22,8 +22,6 @@ #include #include -#include - namespace RPCng { class NFTBuyOffersHandler : public NFTOffersHandlerBase { diff --git a/src/rpc/ngHandlers/NFTHistory.cpp b/src/rpc/ngHandlers/NFTHistory.cpp new file mode 100644 index 00000000..fdd0816e --- /dev/null +++ b/src/rpc/ngHandlers/NFTHistory.cpp @@ -0,0 +1,239 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include + +namespace RPCng { + +// TODO: this is currently very similar to account_tx but its own copy for time +// being. we should aim to reuse common logic in some way in the future. +NFTHistoryHandler::Result +NFTHistoryHandler::process(NFTHistoryHandler::Input input, Context const& ctx) + const +{ + auto const range = sharedPtrBackend_->fetchLedgerRange(); + auto [minIndex, maxIndex] = *range; + if (input.ledgerIndexMin) + { + if (range->maxSequence < input.ledgerIndexMin || + range->minSequence > input.ledgerIndexMin) + { + return Error{RPC::Status{ + RPC::RippledError::rpcLGR_IDX_MALFORMED, + "ledgerSeqMinOutOfRange"}}; + } + minIndex = *input.ledgerIndexMin; + } + + if (input.ledgerIndexMax) + { + if (range->maxSequence < input.ledgerIndexMax || + range->minSequence > input.ledgerIndexMax) + return Error{RPC::Status{ + RPC::RippledError::rpcLGR_IDX_MALFORMED, + "ledgerSeqMaxOutOfRange"}}; + maxIndex = *input.ledgerIndexMax; + } + + if (minIndex > maxIndex) + return Error{ + RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, "invalidIndex"}}; + + if (input.ledgerHash || input.ledgerIndex) + { + // rippled does not have this check + if (input.ledgerIndexMax || input.ledgerIndexMin) + return Error{RPC::Status{ + RPC::RippledError::rpcINVALID_PARAMS, + "containsLedgerSpecifierAndRange"}}; + + auto const lgrInfoOrStatus = RPC::getLedgerInfoFromHashOrSeq( + *sharedPtrBackend_, + ctx.yield, + input.ledgerHash, + input.ledgerIndex, + range->maxSequence); + + if (auto status = std::get_if(&lgrInfoOrStatus)) + return Error{*status}; + + maxIndex = minIndex = std::get(lgrInfoOrStatus).seq; + } + + std::optional cursor; + + // if marker exists + if (input.marker) + { + cursor = {input.marker->ledger, input.marker->seq}; + } + else + { + if (input.forward) + cursor = {minIndex, 0}; + else + cursor = {maxIndex, INT32_MAX}; + } + + static auto constexpr limitDefault = 50; + auto const limit = input.limit.value_or(limitDefault); + auto const tokenID = ripple::uint256{input.nftID.c_str()}; + auto const [txnsAndCursor, timeDiff] = util::timed([&]() { + return sharedPtrBackend_->fetchNFTTransactions( + tokenID, limit, input.forward, cursor, ctx.yield); + }); + log_.info() << "db fetch took " << timeDiff + << " milliseconds - num blobs = " << txnsAndCursor.txns.size(); + auto const [blobs, retCursor] = txnsAndCursor; + + Output response; + if (retCursor) + response.marker = { + retCursor->ledgerSequence, retCursor->transactionIndex}; + + for (auto const& txnPlusMeta : blobs) + { + // over the range + if ((txnPlusMeta.ledgerSequence < minIndex && !input.forward) || + (txnPlusMeta.ledgerSequence > maxIndex && input.forward)) + { + response.marker = std::nullopt; + break; + } + else if (txnPlusMeta.ledgerSequence > maxIndex && !input.forward) + { + log_.debug() << "Skipping over transactions from incomplete ledger"; + continue; + } + + boost::json::object obj; + if (!input.binary) + { + auto [txn, meta] = RPC::toExpandedJson(txnPlusMeta); + obj[JS(meta)] = std::move(meta); + obj[JS(tx)] = std::move(txn); + obj[JS(tx)].as_object()[JS(ledger_index)] = + txnPlusMeta.ledgerSequence; + obj[JS(tx)].as_object()[JS(date)] = txnPlusMeta.date; + } + else + { + obj[JS(meta)] = ripple::strHex(txnPlusMeta.metadata); + obj[JS(tx_blob)] = ripple::strHex(txnPlusMeta.transaction); + obj[JS(ledger_index)] = txnPlusMeta.ledgerSequence; + // only clio has this field + obj[JS(date)] = txnPlusMeta.date; + } + obj[JS(validated)] = true; + + response.transactions.push_back(obj); + } + + response.limit = input.limit; + response.nftID = ripple::to_string(tokenID); + response.ledgerIndexMin = minIndex; + response.ledgerIndexMax = maxIndex; + + return response; +} + +void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + NFTHistoryHandler::Output const& output) +{ + jv = { + {JS(nft_id), output.nftID}, + {JS(ledger_index_min), output.ledgerIndexMin}, + {JS(ledger_index_max), output.ledgerIndexMax}, + {JS(transactions), output.transactions}, + {JS(validated), output.validated}}; + if (output.marker) + jv.as_object()[JS(marker)] = boost::json::value_from(*(output.marker)); + if (output.limit) + jv.as_object()[JS(limit)] = *(output.limit); +} + +void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + NFTHistoryHandler::Marker const& marker) +{ + jv = {{JS(ledger), marker.ledger}, {JS(seq), marker.seq}}; +} + +NFTHistoryHandler::Input +tag_invoke( + boost::json::value_to_tag, + boost::json::value const& jv) +{ + auto const& jsonObject = jv.as_object(); + NFTHistoryHandler::Input input; + input.nftID = jsonObject.at(JS(nft_id)).as_string().c_str(); + if (jsonObject.contains(JS(ledger_index_min)) && + jsonObject.at(JS(ledger_index_min)).as_int64() != -1) + { + input.ledgerIndexMin = jsonObject.at(JS(ledger_index_min)).as_int64(); + } + if (jsonObject.contains(JS(ledger_index_max)) && + jsonObject.at(JS(ledger_index_max)).as_int64() != -1) + { + input.ledgerIndexMax = jsonObject.at(JS(ledger_index_max)).as_int64(); + } + if (jsonObject.contains(JS(ledger_hash))) + { + input.ledgerHash = jsonObject.at(JS(ledger_hash)).as_string().c_str(); + } + if (jsonObject.contains(JS(ledger_index))) + { + if (!jsonObject.at(JS(ledger_index)).is_string()) + { + input.ledgerIndex = jsonObject.at(JS(ledger_index)).as_int64(); + } + else if (jsonObject.at(JS(ledger_index)).as_string() != "validated") + { + input.ledgerIndex = + std::stoi(jsonObject.at(JS(ledger_index)).as_string().c_str()); + } + } + if (jsonObject.contains(JS(binary))) + { + input.binary = jsonObject.at(JS(binary)).as_bool(); + } + if (jsonObject.contains(JS(forward))) + { + input.forward = jsonObject.at(JS(forward)).as_bool(); + } + if (jsonObject.contains(JS(limit))) + { + input.limit = jsonObject.at(JS(limit)).as_int64(); + } + if (jsonObject.contains(JS(marker))) + { + input.marker = NFTHistoryHandler::Marker{ + jsonObject.at(JS(marker)).as_object().at(JS(ledger)).as_int64(), + jsonObject.at(JS(marker)).as_object().at(JS(seq)).as_int64()}; + } + return input; +} + +} // namespace RPCng diff --git a/src/rpc/ngHandlers/NFTHistory.h b/src/rpc/ngHandlers/NFTHistory.h new file mode 100644 index 00000000..21336a5f --- /dev/null +++ b/src/rpc/ngHandlers/NFTHistory.h @@ -0,0 +1,129 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include +#include +#include +#include +#include + +namespace RPCng { +class NFTHistoryHandler +{ + clio::Logger log_{"RPC"}; + std::shared_ptr sharedPtrBackend_; + +public: + // TODO: this marker is same as account_tx, reuse in future + struct Marker + { + uint32_t ledger; + uint32_t seq; + }; + + struct Output + { + std::string nftID; + uint32_t ledgerIndexMin; + uint32_t ledgerIndexMax; + std::optional limit; + std::optional marker; + // TODO: use a better type than json + boost::json::array transactions; + // validated should be sent via framework + bool validated = true; + }; + + // TODO: we did not implement the "strict" field + struct Input + { + std::string nftID; + // You must use at least one of the following fields in your request: + // ledger_index, ledger_hash, ledger_index_min, or ledger_index_max. + std::optional ledgerHash; + std::optional ledgerIndex; + std::optional ledgerIndexMin; + std::optional ledgerIndexMax; + bool binary = false; + bool forward = false; + std::optional limit; + std::optional marker; + }; + + using Result = RPCng::HandlerReturnType; + + NFTHistoryHandler(std::shared_ptr const& sharedPtrBackend) + : sharedPtrBackend_(sharedPtrBackend) + { + } + + RpcSpecConstRef + spec() const + { + static auto const rpcSpec = RpcSpec{ + {JS(nft_id), + validation::Required{}, + validation::Uint256HexStringValidator}, + {JS(ledger_hash), validation::Uint256HexStringValidator}, + {JS(ledger_index), validation::LedgerIndexValidator}, + {JS(ledger_index_min), validation::Type{}}, + {JS(ledger_index_max), validation::Type{}}, + {JS(binary), validation::Type{}}, + {JS(forward), validation::Type{}}, + {JS(limit), + validation::Type{}, + validation::Between{1, 100}}, + {JS(marker), + validation::WithCustomError{ + validation::Type{}, + RPC::Status{ + RPC::RippledError::rpcINVALID_PARAMS, "invalidMarker"}}, + validation::Section{ + {JS(ledger), + validation::Required{}, + validation::Type{}}, + {JS(seq), + validation::Required{}, + validation::Type{}}, + }}}; + return rpcSpec; + } + + Result + process(Input input, Context const& ctx) const; + +private: + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + Output const& output); + + friend Input + tag_invoke(boost::json::value_to_tag, boost::json::value const& jv); + + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + Marker const& marker); +}; +} // namespace RPCng diff --git a/src/rpc/ngHandlers/NFTInfo.h b/src/rpc/ngHandlers/NFTInfo.h index 0ca15f0f..f94ad6e0 100644 --- a/src/rpc/ngHandlers/NFTInfo.h +++ b/src/rpc/ngHandlers/NFTInfo.h @@ -24,8 +24,6 @@ #include #include -#include - namespace RPCng { class NFTInfoHandler { @@ -82,16 +80,15 @@ public: Result process(Input input, Context const& ctx) const; + +private: + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + Output const& output); + + friend Input + tag_invoke(boost::json::value_to_tag, boost::json::value const& jv); }; - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - NFTInfoHandler::Output const& output); - -NFTInfoHandler::Input -tag_invoke( - boost::json::value_to_tag, - boost::json::value const& jv); } // namespace RPCng diff --git a/src/rpc/ngHandlers/NFTOffersCommon.h b/src/rpc/ngHandlers/NFTOffersCommon.h index 6cd50cca..c4b95da8 100644 --- a/src/rpc/ngHandlers/NFTOffersCommon.h +++ b/src/rpc/ngHandlers/NFTOffersCommon.h @@ -24,8 +24,6 @@ #include #include -#include - namespace RPCng { class NFTOffersHandlerBase diff --git a/src/rpc/ngHandlers/NFTSellOffers.h b/src/rpc/ngHandlers/NFTSellOffers.h index c99b2b69..0eef66a3 100644 --- a/src/rpc/ngHandlers/NFTSellOffers.h +++ b/src/rpc/ngHandlers/NFTSellOffers.h @@ -22,8 +22,6 @@ #include #include -#include - namespace RPCng { class NFTSellOffersHandler : public NFTOffersHandlerBase { diff --git a/src/rpc/ngHandlers/NoRippleCheck.h b/src/rpc/ngHandlers/NoRippleCheck.h index 6b24cb88..d94b11b0 100644 --- a/src/rpc/ngHandlers/NoRippleCheck.h +++ b/src/rpc/ngHandlers/NoRippleCheck.h @@ -24,8 +24,6 @@ #include #include -#include - #include namespace RPCng { @@ -85,16 +83,15 @@ public: Result process(Input input, Context const& ctx) const; + +private: + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + Output const& output); + + friend Input + tag_invoke(boost::json::value_to_tag, boost::json::value const& jv); }; - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - NoRippleCheckHandler::Output const& output); - -NoRippleCheckHandler::Input -tag_invoke( - boost::json::value_to_tag, - boost::json::value const& jv); } // namespace RPCng diff --git a/src/rpc/ngHandlers/TransactionEntry.h b/src/rpc/ngHandlers/TransactionEntry.h index b643df8f..2bb329b5 100644 --- a/src/rpc/ngHandlers/TransactionEntry.h +++ b/src/rpc/ngHandlers/TransactionEntry.h @@ -24,8 +24,6 @@ #include #include -#include - namespace RPCng { class TransactionEntryHandler { @@ -72,16 +70,15 @@ public: Result process(Input input, Context const& ctx) const; + +private: + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + Output const& output); + + friend Input + tag_invoke(boost::json::value_to_tag, boost::json::value const& jv); }; - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - TransactionEntryHandler::Output const& output); - -TransactionEntryHandler::Input -tag_invoke( - boost::json::value_to_tag, - boost::json::value const& jv); } // namespace RPCng diff --git a/src/rpc/ngHandlers/Tx.h b/src/rpc/ngHandlers/Tx.h index 2555f609..0b0dc26f 100644 --- a/src/rpc/ngHandlers/Tx.h +++ b/src/rpc/ngHandlers/Tx.h @@ -23,8 +23,6 @@ #include #include -#include - namespace RPCng { class TxHandler { @@ -76,16 +74,15 @@ public: Result process(Input input, Context const& ctx) const; + +private: + friend void + tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + Output const& output); + + friend Input + tag_invoke(boost::json::value_to_tag, boost::json::value const& jv); }; - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - TxHandler::Output const& output); - -TxHandler::Input -tag_invoke( - boost::json::value_to_tag, - boost::json::value const& jv); } // namespace RPCng diff --git a/unittests/rpc/handlers/NFTHistoryTest.cpp b/unittests/rpc/handlers/NFTHistoryTest.cpp new file mode 100644 index 00000000..6c3eabb3 --- /dev/null +++ b/unittests/rpc/handlers/NFTHistoryTest.cpp @@ -0,0 +1,809 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include + +#include + +using namespace RPCng; +namespace json = boost::json; +using namespace testing; + +constexpr static auto MINSEQ = 10; +constexpr static auto MAXSEQ = 30; +constexpr static auto ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr static auto LEDGERHASH = + "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; +constexpr static auto NFTID = + "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004"; + +class RPCNFTHistoryHandlerTest : public HandlerBaseTest +{ +}; + +struct NFTHistoryParamTestCaseBundle +{ + std::string testName; + std::string testJson; + std::string expectedError; + std::string expectedErrorMessage; +}; + +// parameterized test cases for parameters check +struct NFTHistoryParameterTest + : public RPCNFTHistoryHandlerTest, + public WithParamInterface +{ + struct NameGenerator + { + template + std::string + operator()(const testing::TestParamInfo& info) const + { + auto bundle = + static_cast(info.param); + return bundle.testName; + } + }; +}; + +static auto +generateTestValuesForParametersTest() +{ + return std::vector{ + NFTHistoryParamTestCaseBundle{ + "MissingNFTID", + R"({})", + "invalidParams", + "Required field 'nft_id' missing"}, + NFTHistoryParamTestCaseBundle{ + "BinaryNotBool", + R"({"nft_id":"00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "binary": 1})", + "invalidParams", + "Invalid parameters."}, + NFTHistoryParamTestCaseBundle{ + "ForwardNotBool", + R"({"nft_id":"00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "forward": 1})", + "invalidParams", + "Invalid parameters."}, + NFTHistoryParamTestCaseBundle{ + "ledger_index_minNotInt", + R"({"nft_id":"00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "ledger_index_min": "x"})", + "invalidParams", + "Invalid parameters."}, + NFTHistoryParamTestCaseBundle{ + "ledger_index_maxNotInt", + R"({"nft_id":"00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "ledger_index_max": "x"})", + "invalidParams", + "Invalid parameters."}, + NFTHistoryParamTestCaseBundle{ + "ledger_indexInvalid", + R"({"nft_id":"00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "ledger_index": "x"})", + "invalidParams", + "ledgerIndexMalformed"}, + NFTHistoryParamTestCaseBundle{ + "ledger_hashInvalid", + R"({"nft_id":"00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "ledger_hash": "x"})", + "invalidParams", + "ledger_hashMalformed"}, + NFTHistoryParamTestCaseBundle{ + "ledger_hashNotString", + R"({"nft_id":"00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "ledger_hash": 123})", + "invalidParams", + "ledger_hashNotString"}, + NFTHistoryParamTestCaseBundle{ + "limitNotInt", + R"({"nft_id":"00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "limit": "123"})", + "invalidParams", + "Invalid parameters."}, + NFTHistoryParamTestCaseBundle{ + "limitOverRange", + R"({"nft_id":"00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "limit": 101})", + "invalidParams", + "Invalid parameters."}, + NFTHistoryParamTestCaseBundle{ + "MarkerNotObject", + R"({"nft_id":"00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "marker": 101})", + "invalidParams", + "invalidMarker"}, + NFTHistoryParamTestCaseBundle{ + "MarkerMissingSeq", + R"({ + "nft_id":"00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", + "marker": {"ledger": 123} + })", + "invalidParams", + "Required field 'seq' missing"}, + NFTHistoryParamTestCaseBundle{ + "MarkerMissingLedger", + R"({ + "nft_id":"00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", + "marker":{"seq": 123} + })", + "invalidParams", + "Required field 'ledger' missing"}, + NFTHistoryParamTestCaseBundle{ + "MarkerLedgerNotInt", + R"({ + "nft_id":"00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", + "marker": + { + "seq": "string", + "ledger": 1 + } + })", + "invalidParams", + "Invalid parameters."}, + NFTHistoryParamTestCaseBundle{ + "MarkerSeqNotInt", + R"({ + "nft_id":"00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", + "marker": + { + "ledger": "string", + "seq": 1 + } + })", + "invalidParams", + "Invalid parameters."}, + NFTHistoryParamTestCaseBundle{ + "LedgerIndexMinLessThanMinSeq", + R"({ + "nft_id":"00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", + "ledger_index_min": 9 + })", + "lgrIdxMalformed", + "ledgerSeqMinOutOfRange"}, + NFTHistoryParamTestCaseBundle{ + "LedgerIndexMaxLargeThanMaxSeq", + R"({ + "nft_id":"00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", + "ledger_index_max": 31 + })", + "lgrIdxMalformed", + "ledgerSeqMaxOutOfRange"}, + NFTHistoryParamTestCaseBundle{ + "LedgerIndexMaxLessThanLedgerIndexMin", + R"({ + "nft_id":"00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", + "ledger_index_max": 11, + "ledger_index_min": 20 + })", + "invalidParams", + "invalidIndex"}, + NFTHistoryParamTestCaseBundle{ + "LedgerIndexMaxMinAndLedgerIndex", + R"({ + "nft_id":"00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", + "ledger_index_max": 20, + "ledger_index_min": 11, + "ledger_index": 10 + })", + "invalidParams", + "containsLedgerSpecifierAndRange"}, + }; +} + +INSTANTIATE_TEST_CASE_P( + RPCNFTHistoryGroup1, + NFTHistoryParameterTest, + ValuesIn(generateTestValuesForParametersTest()), + NFTHistoryParameterTest::NameGenerator{}); + +TEST_P(NFTHistoryParameterTest, InvalidParams) +{ + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + auto const testBundle = GetParam(); + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NFTHistoryHandler{mockBackendPtr}}; + auto const req = json::parse(testBundle.testJson); + auto const output = handler.process(req, Context{std::ref(yield)}); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), testBundle.expectedError); + EXPECT_EQ( + err.at("error_message").as_string(), + testBundle.expectedErrorMessage); + }); +} + +static std::vector +genTransactions(uint32_t seq1, uint32_t seq2) +{ + auto transactions = std::vector{}; + auto trans1 = TransactionAndMetadata(); + ripple::STObject obj = + CreatePaymentTransactionObject(ACCOUNT, ACCOUNT2, 1, 1, 32); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = seq1; + ripple::STObject metaObj = + CreatePaymentTransactionMetaObject(ACCOUNT, ACCOUNT2, 22, 23); + trans1.metadata = metaObj.getSerializer().peekData(); + trans1.date = 1; + transactions.push_back(trans1); + + auto trans2 = TransactionAndMetadata(); + ripple::STObject obj2 = + CreatePaymentTransactionObject(ACCOUNT, ACCOUNT2, 1, 1, 32); + trans2.transaction = obj.getSerializer().peekData(); + trans2.ledgerSequence = seq2; + ripple::STObject metaObj2 = + CreatePaymentTransactionMetaObject(ACCOUNT, ACCOUNT2, 22, 23); + trans2.metadata = metaObj2.getSerializer().peekData(); + trans2.date = 2; + transactions.push_back(trans2); + return transactions; +} + +TEST_F(RPCNFTHistoryHandlerTest, IndexSpecificForwardTrue) +{ + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + auto const transactions = genTransactions(MINSEQ + 1, MAXSEQ - 1); + auto const transCursor = + TransactionsAndCursor{transactions, TransactionsCursor{12, 34}}; + ON_CALL(*rawBackendPtr, fetchNFTTransactions) + .WillByDefault(Return(transCursor)); + EXPECT_CALL( + *rawBackendPtr, + fetchNFTTransactions( + testing::_, + testing::_, + true, + testing::Optional(testing::Eq(TransactionsCursor{MINSEQ + 1, 0})), + testing::_)) + .Times(1); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NFTHistoryHandler{mockBackendPtr}}; + auto const static input = boost::json::parse(fmt::format( + R"({{ + "nft_id":"{}", + "ledger_index_min": {}, + "ledger_index_max": {}, + "forward": true + }})", + NFTID, + MINSEQ + 1, + MAXSEQ - 1)); + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_EQ(output->at("nft_id").as_string(), NFTID); + EXPECT_EQ(output->at("ledger_index_min").as_uint64(), MINSEQ + 1); + EXPECT_EQ(output->at("ledger_index_max").as_uint64(), MAXSEQ - 1); + EXPECT_EQ( + output->at("marker").as_object(), + json::parse(R"({"ledger":12,"seq":34})")); + EXPECT_EQ(output->at("transactions").as_array().size(), 2); + EXPECT_FALSE(output->as_object().contains("limit")); + }); +} + +TEST_F(RPCNFTHistoryHandlerTest, IndexSpecificForwardFalse) +{ + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + auto const transactions = genTransactions(MINSEQ + 1, MAXSEQ - 1); + auto const transCursor = + TransactionsAndCursor{transactions, TransactionsCursor{12, 34}}; + ON_CALL(*rawBackendPtr, fetchNFTTransactions) + .WillByDefault(Return(transCursor)); + EXPECT_CALL( + *rawBackendPtr, + fetchNFTTransactions( + testing::_, + testing::_, + false, + testing::Optional( + testing::Eq(TransactionsCursor{MAXSEQ - 1, INT32_MAX})), + testing::_)) + .Times(1); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NFTHistoryHandler{mockBackendPtr}}; + auto const static input = boost::json::parse(fmt::format( + R"({{ + "nft_id":"{}", + "ledger_index_min": {}, + "ledger_index_max": {}, + "forward": false + }})", + NFTID, + MINSEQ + 1, + MAXSEQ - 1)); + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_EQ(output->at("nft_id").as_string(), NFTID); + EXPECT_EQ(output->at("ledger_index_min").as_uint64(), MINSEQ + 1); + EXPECT_EQ(output->at("ledger_index_max").as_uint64(), MAXSEQ - 1); + EXPECT_EQ( + output->at("marker").as_object(), + json::parse(R"({"ledger":12,"seq":34})")); + EXPECT_EQ(output->at("transactions").as_array().size(), 2); + EXPECT_FALSE(output->as_object().contains("limit")); + }); +} + +TEST_F(RPCNFTHistoryHandlerTest, IndexNotSpecificForwardTrue) +{ + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + auto const transactions = genTransactions(MINSEQ + 1, MAXSEQ - 1); + auto const transCursor = + TransactionsAndCursor{transactions, TransactionsCursor{12, 34}}; + ON_CALL(*rawBackendPtr, fetchNFTTransactions) + .WillByDefault(Return(transCursor)); + EXPECT_CALL( + *rawBackendPtr, + fetchNFTTransactions( + testing::_, + testing::_, + true, + testing::Optional(testing::Eq(TransactionsCursor{MINSEQ, 0})), + testing::_)) + .Times(1); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NFTHistoryHandler{mockBackendPtr}}; + auto const static input = boost::json::parse(fmt::format( + R"({{ + "nft_id":"{}", + "ledger_index_min": {}, + "ledger_index_max": {}, + "forward": true + }})", + NFTID, + -1, + -1)); + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_EQ(output->at("nft_id").as_string(), NFTID); + EXPECT_EQ(output->at("ledger_index_min").as_uint64(), MINSEQ); + EXPECT_EQ(output->at("ledger_index_max").as_uint64(), MAXSEQ); + EXPECT_EQ( + output->at("marker").as_object(), + json::parse(R"({"ledger":12,"seq":34})")); + EXPECT_EQ(output->at("transactions").as_array().size(), 2); + EXPECT_FALSE(output->as_object().contains("limit")); + }); +} + +TEST_F(RPCNFTHistoryHandlerTest, IndexNotSpecificForwardFalse) +{ + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + auto const transactions = genTransactions(MINSEQ + 1, MAXSEQ - 1); + auto const transCursor = + TransactionsAndCursor{transactions, TransactionsCursor{12, 34}}; + ON_CALL(*rawBackendPtr, fetchNFTTransactions) + .WillByDefault(Return(transCursor)); + EXPECT_CALL( + *rawBackendPtr, + fetchNFTTransactions( + testing::_, + testing::_, + false, + testing::Optional( + testing::Eq(TransactionsCursor{MAXSEQ, INT32_MAX})), + testing::_)) + .Times(1); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NFTHistoryHandler{mockBackendPtr}}; + auto const static input = boost::json::parse(fmt::format( + R"({{ + "nft_id":"{}", + "ledger_index_min": {}, + "ledger_index_max": {}, + "forward": false + }})", + NFTID, + -1, + -1)); + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_EQ(output->at("nft_id").as_string(), NFTID); + EXPECT_EQ(output->at("ledger_index_min").as_uint64(), MINSEQ); + EXPECT_EQ(output->at("ledger_index_max").as_uint64(), MAXSEQ); + EXPECT_EQ( + output->at("marker").as_object(), + json::parse(R"({"ledger":12,"seq":34})")); + EXPECT_EQ(output->at("transactions").as_array().size(), 2); + EXPECT_FALSE(output->as_object().contains("limit")); + }); +} + +TEST_F(RPCNFTHistoryHandlerTest, BinaryTrue) +{ + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + auto const transactions = genTransactions(MINSEQ + 1, MAXSEQ - 1); + auto const transCursor = + TransactionsAndCursor{transactions, TransactionsCursor{12, 34}}; + ON_CALL(*rawBackendPtr, fetchNFTTransactions) + .WillByDefault(Return(transCursor)); + EXPECT_CALL( + *rawBackendPtr, + fetchNFTTransactions( + testing::_, + testing::_, + false, + testing::Optional( + testing::Eq(TransactionsCursor{MAXSEQ, INT32_MAX})), + testing::_)) + .Times(1); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NFTHistoryHandler{mockBackendPtr}}; + auto const static input = boost::json::parse(fmt::format( + R"({{ + "nft_id":"{}", + "ledger_index_min": {}, + "ledger_index_max": {}, + "binary": true + }})", + NFTID, + -1, + -1)); + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_EQ(output->at("nft_id").as_string(), NFTID); + EXPECT_EQ(output->at("ledger_index_min").as_uint64(), MINSEQ); + EXPECT_EQ(output->at("ledger_index_max").as_uint64(), MAXSEQ); + EXPECT_EQ( + output->at("marker").as_object(), + json::parse(R"({"ledger":12,"seq":34})")); + EXPECT_EQ(output->at("transactions").as_array().size(), 2); + EXPECT_EQ( + output->at("transactions") + .as_array()[0] + .as_object() + .at("meta") + .as_string(), + "201C00000000F8E5110061E762400000000000001681144B4E9C06F24296074F7B" + "C48F92A97916C6DC5EA9E1E1E5110061E76240000000000000178114D31252CF90" + "2EF8DD8451243869B38667CBD89DF3E1E1F1031000"); + EXPECT_EQ( + output->at("transactions") + .as_array()[0] + .as_object() + .at("tx_blob") + .as_string(), + "120000240000002061400000000000000168400000000000000173047465737481" + "144B4E9C06F24296074F7BC48F92A97916C6DC5EA98314D31252CF902EF8DD8451" + "243869B38667CBD89DF3"); + EXPECT_EQ( + output->at("transactions") + .as_array()[0] + .as_object() + .at("date") + .as_uint64(), + 1); + + EXPECT_FALSE(output->as_object().contains("limit")); + }); +} + +TEST_F(RPCNFTHistoryHandlerTest, LimitAndMarker) +{ + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + auto const transactions = genTransactions(MINSEQ + 1, MAXSEQ - 1); + auto const transCursor = + TransactionsAndCursor{transactions, TransactionsCursor{12, 34}}; + ON_CALL(*rawBackendPtr, fetchNFTTransactions) + .WillByDefault(Return(transCursor)); + EXPECT_CALL( + *rawBackendPtr, + fetchNFTTransactions( + testing::_, + testing::_, + false, + testing::Optional(testing::Eq(TransactionsCursor{10, 11})), + testing::_)) + .Times(1); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NFTHistoryHandler{mockBackendPtr}}; + auto const static input = boost::json::parse(fmt::format( + R"({{ + "nft_id":"{}", + "ledger_index_min": {}, + "ledger_index_max": {}, + "limit": 2, + "forward": false, + "marker": {{"ledger":10,"seq":11}} + }})", + NFTID, + -1, + -1)); + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_EQ(output->at("nft_id").as_string(), NFTID); + EXPECT_EQ(output->at("ledger_index_min").as_uint64(), MINSEQ); + EXPECT_EQ(output->at("ledger_index_max").as_uint64(), MAXSEQ); + EXPECT_EQ(output->at("limit").as_uint64(), 2); + EXPECT_EQ( + output->at("marker").as_object(), + json::parse(R"({"ledger":12,"seq":34})")); + EXPECT_EQ(output->at("transactions").as_array().size(), 2); + }); +} + +TEST_F(RPCNFTHistoryHandlerTest, SpecificLedgerIndex) +{ + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + // adjust the order for forward->false + auto const transactions = genTransactions(MAXSEQ - 1, MINSEQ + 1); + auto const transCursor = + TransactionsAndCursor{transactions, TransactionsCursor{12, 34}}; + ON_CALL(*rawBackendPtr, fetchNFTTransactions) + .WillByDefault(Return(transCursor)); + EXPECT_CALL( + *rawBackendPtr, + fetchNFTTransactions( + testing::_, + testing::_, + false, + testing::Optional( + testing::Eq(TransactionsCursor{MAXSEQ - 1, INT32_MAX})), + testing::_)) + .Times(1); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, MAXSEQ - 1); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(MAXSEQ - 1, _)) + .WillByDefault(Return(ledgerinfo)); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NFTHistoryHandler{mockBackendPtr}}; + auto const static input = boost::json::parse(fmt::format( + R"({{ + "nft_id":"{}", + "ledger_index":{} + }})", + NFTID, + MAXSEQ - 1)); + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_EQ(output->at("nft_id").as_string(), NFTID); + EXPECT_EQ(output->at("ledger_index_min").as_uint64(), MAXSEQ - 1); + EXPECT_EQ(output->at("ledger_index_max").as_uint64(), MAXSEQ - 1); + EXPECT_FALSE(output->as_object().contains("limit")); + EXPECT_FALSE(output->as_object().contains("marker")); + EXPECT_EQ(output->at("transactions").as_array().size(), 1); + }); +} + +TEST_F(RPCNFTHistoryHandlerTest, SpecificNonexistLedgerIntIndex) +{ + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(MAXSEQ - 1, _)) + .WillByDefault(Return(std::nullopt)); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NFTHistoryHandler{mockBackendPtr}}; + auto const static input = boost::json::parse(fmt::format( + R"({{ + "nft_id":"{}", + "ledger_index":{} + }})", + NFTID, + MAXSEQ - 1)); + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_FALSE(output); + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound"); + }); +} + +TEST_F(RPCNFTHistoryHandlerTest, SpecificNonexistLedgerStringIndex) +{ + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(MAXSEQ - 1, _)) + .WillByDefault(Return(std::nullopt)); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NFTHistoryHandler{mockBackendPtr}}; + auto const static input = boost::json::parse(fmt::format( + R"({{ + "nft_id":"{}", + "ledger_index":"{}" + }})", + NFTID, + MAXSEQ - 1)); + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_FALSE(output); + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound"); + }); +} + +TEST_F(RPCNFTHistoryHandlerTest, SpecificLedgerHash) +{ + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + // adjust the order for forward->false + auto const transactions = genTransactions(MAXSEQ - 1, MINSEQ + 1); + auto const transCursor = + TransactionsAndCursor{transactions, TransactionsCursor{12, 34}}; + ON_CALL(*rawBackendPtr, fetchNFTTransactions) + .WillByDefault(Return(transCursor)); + EXPECT_CALL( + *rawBackendPtr, + fetchNFTTransactions( + testing::_, + testing::_, + false, + testing::Optional( + testing::Eq(TransactionsCursor{MAXSEQ - 1, INT32_MAX})), + testing::_)) + .Times(1); + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, MAXSEQ - 1); + EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)) + .WillByDefault(Return(ledgerinfo)); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NFTHistoryHandler{mockBackendPtr}}; + auto const static input = boost::json::parse(fmt::format( + R"({{ + "nft_id":"{}", + "ledger_hash":"{}" + }})", + NFTID, + LEDGERHASH)); + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_EQ(output->at("nft_id").as_string(), NFTID); + EXPECT_EQ(output->at("ledger_index_min").as_uint64(), MAXSEQ - 1); + EXPECT_EQ(output->at("ledger_index_max").as_uint64(), MAXSEQ - 1); + EXPECT_FALSE(output->as_object().contains("limit")); + EXPECT_FALSE(output->as_object().contains("marker")); + EXPECT_EQ(output->at("transactions").as_array().size(), 1); + }); +} + +TEST_F(RPCNFTHistoryHandlerTest, TxLessThanMinSeq) +{ + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + auto const transactions = genTransactions(MAXSEQ - 1, MINSEQ + 1); + auto const transCursor = + TransactionsAndCursor{transactions, TransactionsCursor{12, 34}}; + ON_CALL(*rawBackendPtr, fetchNFTTransactions) + .WillByDefault(Return(transCursor)); + EXPECT_CALL( + *rawBackendPtr, + fetchNFTTransactions( + testing::_, + testing::_, + false, + testing::Optional( + testing::Eq(TransactionsCursor{MAXSEQ - 1, INT32_MAX})), + testing::_)) + .Times(1); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NFTHistoryHandler{mockBackendPtr}}; + auto const static input = boost::json::parse(fmt::format( + R"({{ + "nft_id":"{}", + "ledger_index_min": {}, + "ledger_index_max": {}, + "forward": false + }})", + NFTID, + MINSEQ + 2, + MAXSEQ - 1)); + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_EQ(output->at("nft_id").as_string(), NFTID); + EXPECT_EQ(output->at("ledger_index_min").as_uint64(), MINSEQ + 2); + EXPECT_EQ(output->at("ledger_index_max").as_uint64(), MAXSEQ - 1); + EXPECT_EQ(output->at("transactions").as_array().size(), 1); + EXPECT_FALSE(output->as_object().contains("limit")); + EXPECT_FALSE(output->as_object().contains("marker")); + }); +} + +TEST_F(RPCNFTHistoryHandlerTest, TxLargerThanMaxSeq) +{ + mockBackendPtr->updateRange(MINSEQ); // min + mockBackendPtr->updateRange(MAXSEQ); // max + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + auto const transactions = genTransactions(MAXSEQ - 1, MINSEQ + 1); + auto const transCursor = + TransactionsAndCursor{transactions, TransactionsCursor{12, 34}}; + ON_CALL(*rawBackendPtr, fetchNFTTransactions) + .WillByDefault(Return(transCursor)); + EXPECT_CALL( + *rawBackendPtr, + fetchNFTTransactions( + testing::_, + testing::_, + false, + testing::Optional( + testing::Eq(TransactionsCursor{MAXSEQ - 2, INT32_MAX})), + testing::_)) + .Times(1); + + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{NFTHistoryHandler{mockBackendPtr}}; + auto const static input = boost::json::parse(fmt::format( + R"({{ + "nft_id":"{}", + "ledger_index_min": {}, + "ledger_index_max": {}, + "forward": false + }})", + NFTID, + MINSEQ + 1, + MAXSEQ - 2)); + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_EQ(output->at("nft_id").as_string(), NFTID); + EXPECT_EQ(output->at("ledger_index_min").as_uint64(), MINSEQ + 1); + EXPECT_EQ(output->at("ledger_index_max").as_uint64(), MAXSEQ - 2); + EXPECT_EQ(output->at("transactions").as_array().size(), 1); + EXPECT_FALSE(output->as_object().contains("limit")); + EXPECT_EQ( + output->at("marker").as_object(), + json::parse(R"({"ledger":12,"seq":34})")); + }); +}