From 8e091871cba90d54fe2cb1ef337677ac7147a29f Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 20 Aug 2026 15:42:22 +0100 Subject: [PATCH] chore: Revert "feat: Add mptoken_issuance_history RPC" (#3183) --- src/rpc/CMakeLists.txt | 1 - src/rpc/RPCCenter.cpp | 1 - src/rpc/common/impl/HandlerProvider.cpp | 3 - src/rpc/handlers/MPTokenIssuanceHistory.cpp | 378 --- src/rpc/handlers/MPTokenIssuanceHistory.hpp | 316 --- tests/unit/CMakeLists.txt | 1 - .../handlers/MPTokenIssuanceHistoryTests.cpp | 2176 ----------------- 7 files changed, 2876 deletions(-) delete mode 100644 src/rpc/handlers/MPTokenIssuanceHistory.cpp delete mode 100644 src/rpc/handlers/MPTokenIssuanceHistory.hpp delete mode 100644 tests/unit/rpc/handlers/MPTokenIssuanceHistoryTests.cpp diff --git a/src/rpc/CMakeLists.txt b/src/rpc/CMakeLists.txt index e9ca6dc81..9691de44d 100644 --- a/src/rpc/CMakeLists.txt +++ b/src/rpc/CMakeLists.txt @@ -44,7 +44,6 @@ target_sources( handlers/LedgerIndex.cpp handlers/LedgerRange.cpp handlers/MPTHolders.cpp - handlers/MPTokenIssuanceHistory.cpp handlers/NFTsByIssuer.cpp handlers/NFTBuyOffers.cpp handlers/NFTHistory.cpp diff --git a/src/rpc/RPCCenter.cpp b/src/rpc/RPCCenter.cpp index 22fa9d8b9..8280ded91 100644 --- a/src/rpc/RPCCenter.cpp +++ b/src/rpc/RPCCenter.cpp @@ -35,7 +35,6 @@ handledRpcs() "ledger_index", "ledger_range", "mpt_holders", - "mptoken_issuance_history", "nfts_by_issuer", "nft_history", "nft_buy_offers", diff --git a/src/rpc/common/impl/HandlerProvider.cpp b/src/rpc/common/impl/HandlerProvider.cpp index c199753c8..dae6121f2 100644 --- a/src/rpc/common/impl/HandlerProvider.cpp +++ b/src/rpc/common/impl/HandlerProvider.cpp @@ -30,7 +30,6 @@ #include "rpc/handlers/LedgerIndex.hpp" #include "rpc/handlers/LedgerRange.hpp" #include "rpc/handlers/MPTHolders.hpp" -#include "rpc/handlers/MPTokenIssuanceHistory.hpp" #include "rpc/handlers/NFTBuyOffers.hpp" #include "rpc/handlers/NFTHistory.hpp" #include "rpc/handlers/NFTInfo.hpp" @@ -92,8 +91,6 @@ ProductionHandlerProvider::ProductionHandlerProvider( {"ledger_range", {.handler = LedgerRangeHandler{backend}}}, {"mpt_holders", {.handler = MPTHoldersHandler{backend}, .isClioOnly = true}}, // clio only - {"mptoken_issuance_history", - {.handler = MPTokenIssuanceHistoryHandler{backend}, .isClioOnly = true}}, // clio only {"nfts_by_issuer", {.handler = NFTsByIssuerHandler{backend}, .isClioOnly = true}}, // clio only {"nft_history", diff --git a/src/rpc/handlers/MPTokenIssuanceHistory.cpp b/src/rpc/handlers/MPTokenIssuanceHistory.cpp deleted file mode 100644 index 813528ed3..000000000 --- a/src/rpc/handlers/MPTokenIssuanceHistory.cpp +++ /dev/null @@ -1,378 +0,0 @@ -#include "rpc/handlers/MPTokenIssuanceHistory.hpp" - -#include "data/Types.hpp" -#include "rpc/Errors.hpp" -#include "rpc/JS.hpp" -#include "rpc/RPCHelpers.hpp" -#include "rpc/common/Types.hpp" -#include "util/Assert.hpp" -#include "util/JsonUtils.hpp" -#include "util/Profiler.hpp" -#include "util/log/Logger.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace rpc { - -namespace { - -/** - * @brief The migrator status that reports a completed backfill. - * - * This is a literal to keep migration headers out of RPC; it must match the string form of - * `migration::MigratorStatus::Status::Migrated`. - */ -constexpr auto kMigratedStatus = "Migrated"; - -} // namespace - -MPTokenIssuanceHistoryHandler::Result -MPTokenIssuanceHistoryHandler::process( - MPTokenIssuanceHistoryHandler::Input const& input, - Context const& ctx -) const -{ - if (auto const available = verifyHistoryAvailable(ctx); not available.has_value()) - return Error{available.error()}; - - auto const range = resolveSequenceRange(input, ctx); - if (not range.has_value()) - return Error{range.error()}; - - auto const mptIssuanceID = xrpl::uint192{input.mptIssuanceID.c_str()}; - - auto const [page, timeDiff] = - util::timed([&] { return fetchTransactions(input, ctx, mptIssuanceID, *range); }); - LOG(log_.info()) << "db fetch took " << timeDiff - << " milliseconds - num blobs = " << page.txns.size(); - - Output response; - response.mptIssuanceID = xrpl::to_string(mptIssuanceID); - response.ledgerIndexMin = range->min; - response.ledgerIndexMax = range->max; - response.limit = input.limit; - - processTransactionsPage(input, ctx, *range, page, response); - - return response; -} - -MaybeError -MPTokenIssuanceHistoryHandler::verifyHistoryAvailable(Context const& ctx) const -{ - if (migrated_->load(std::memory_order_relaxed)) - return {}; - - auto const statusString = sharedPtrBackend_->fetchMigratorStatus(kMigratorName, ctx.yield); - if (statusString.has_value() and *statusString == kMigratedStatus) { - migrated_->store(true, std::memory_order_relaxed); - return {}; - } - - // Fail closed: partial history must never be served. - return Error{Status{ - RippledError::RpcNotReady, - "mptoken_issuance_history is not available on this server because the required " - "transaction-history backfill has not completed." - }}; -} - -std::expected -MPTokenIssuanceHistoryHandler::resolveSequenceRange(Input const& input, Context const& ctx) const -{ - auto const ledgerRange = sharedPtrBackend_->fetchLedgerRange(); - ASSERT(ledgerRange.has_value(), "MPTokenIssuanceHistory's ledger range must be available"); - - auto const [dbMinSeq, dbMaxSeq] = *ledgerRange; // NOLINT(bugprone-unchecked-optional-access) - auto resolved = SequenceRange{.min = dbMinSeq, .max = dbMaxSeq}; - - if (input.ledgerIndexMin.has_value()) { - if (dbMaxSeq < input.ledgerIndexMin || dbMinSeq > input.ledgerIndexMin) - return Error{Status{RippledError::RpcLgrIdxMalformed, "ledgerSeqMinOutOfRange"}}; - - resolved.min = *input.ledgerIndexMin; - } - - if (input.ledgerIndexMax.has_value()) { - if (dbMaxSeq < input.ledgerIndexMax || dbMinSeq > input.ledgerIndexMax) - return Error{Status{RippledError::RpcLgrIdxMalformed, "ledgerSeqMaxOutOfRange"}}; - - resolved.max = *input.ledgerIndexMax; - } - - if (resolved.min > resolved.max) - return Error{Status{RippledError::RpcLgrIdxsInvalid}}; - - if (input.ledgerHash.has_value() || input.ledgerIndex.has_value()) { - // rippled does not have this check - if (input.ledgerIndexMax.has_value() || input.ledgerIndexMin.has_value()) - return Error{Status{RippledError::RpcInvalidParams, "containsLedgerSpecifierAndRange"}}; - - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( - *sharedPtrBackend_, ctx.yield, input.ledgerHash, input.ledgerIndex, dbMaxSeq - ); - - if (not expectedLgrInfo.has_value()) - return Error{expectedLgrInfo.error()}; - - resolved.max = resolved.min = expectedLgrInfo->seq; - } - - return resolved; -} - -data::TransactionsAndCursor -MPTokenIssuanceHistoryHandler::fetchTransactions( - Input const& input, - Context const& ctx, - xrpl::uint192 const& mptIssuanceID, - SequenceRange range -) const -{ - // Construct the database cursor as {ledgerSequence, transactionIndex}. - auto const startCursor = [&]() -> data::TransactionsCursor { - if (input.marker.has_value()) - return {input.marker->ledger, input.marker->seq}; - - // Forward iteration starts at the first possible transaction in the lowest ledger. - if (input.forward) - return {range.min, 0}; - - // Reverse iteration starts after all possible transactions in the highest ledger. - return {range.max, std::numeric_limits::max()}; - }(); - - auto const limit = input.limit.value_or(kLimitDefault); - - // tx_type is applied post-fetch, as account_tx does. - if (input.account.has_value()) { - auto const account = accountFromStringStrict(*input.account); - if (not account.has_value()) { - ASSERT(false, "Account must be decodable after spec validation"); - std::unreachable(); - } - return sharedPtrBackend_->fetchAccountMPTokenIssuanceTransactions( - mptIssuanceID, *account, limit, input.forward, startCursor, ctx.yield - ); - } - - return sharedPtrBackend_->fetchMPTokenIssuanceTransactions( - mptIssuanceID, limit, input.forward, startCursor, ctx.yield - ); -} - -void -MPTokenIssuanceHistoryHandler::processTransactionsPage( - Input const& input, - Context const& ctx, - SequenceRange range, - data::TransactionsAndCursor const& page, - Output& response -) const -{ - if (page.cursor.has_value()) { - response.marker = { - .ledger = page.cursor->ledgerSequence, .seq = page.cursor->transactionIndex - }; - } - - for (auto const& txnPlusMeta : page.txns) { - // A hash with no matching Transactions row yields a default-constructed record in-position. - // Skip it before the range check so it neither shortens the page nor disturbs the marker. - if (txnPlusMeta.transaction.empty() || txnPlusMeta.metadata.empty()) { - LOG(log_.warn()) << "Skipping index entry with no matching transaction record; " - "mpt_issuance_id = " - << input.mptIssuanceID; - continue; - } - - // Stop once iteration passes the far edge of the requested range. - if ((txnPlusMeta.ledgerSequence < range.min && !input.forward) || - (txnPlusMeta.ledgerSequence > range.max && input.forward)) { - response.marker = std::nullopt; - break; - } - if (txnPlusMeta.ledgerSequence > range.max && !input.forward) { - LOG(log_.debug()) << "Skipping over transactions from incomplete ledger"; - continue; - } - - if (auto obj = transactionToJsonIfTypeMatches(txnPlusMeta, input, ctx); obj.has_value()) - response.transactions.push_back(std::move(*obj)); - } -} - -std::optional -MPTokenIssuanceHistoryHandler::transactionToJsonIfTypeMatches( - data::TransactionAndMetadata const& txnPlusMeta, - Input const& input, - Context const& ctx -) const -{ - // The type filter needs the expanded form to read TransactionType, even for binary output. - if (!input.binary || input.transactionTypeInLowercase.has_value()) { - auto [txn, meta] = toExpandedJson(txnPlusMeta, ctx.apiVersion); - - if (txn.contains(JS(TransactionType)) && input.transactionTypeInLowercase.has_value() && - util::toLower(boost::json::value_to(txn[JS(TransactionType)])) != - *input.transactionTypeInLowercase) - return std::nullopt; - - if (!input.binary) - return expandedTransactionToJson(std::move(txn), std::move(meta), txnPlusMeta, ctx); - } - - auto obj = toJsonWithBinaryTx(txnPlusMeta, ctx.apiVersion); - obj[JS(ledger_index)] = txnPlusMeta.ledgerSequence; - obj[JS(date)] = txnPlusMeta.date; - obj[JS(validated)] = true; - - return obj; -} - -boost::json::object -MPTokenIssuanceHistoryHandler::expandedTransactionToJson( - boost::json::object txn, - boost::json::object meta, - data::TransactionAndMetadata const& txnPlusMeta, - Context const& ctx -) const -{ - auto const txKey = ctx.apiVersion > 1u ? JS(tx_json) : JS(tx); - - boost::json::object obj; - obj[JS(meta)] = std::move(meta); - obj[txKey] = std::move(txn); - obj[txKey].as_object()[JS(ledger_index)] = txnPlusMeta.ledgerSequence; - obj[txKey].as_object()[JS(date)] = txnPlusMeta.date; - - if (ctx.apiVersion > 1u) { - obj[JS(ledger_index)] = txnPlusMeta.ledgerSequence; - if (obj[txKey].as_object().contains(JS(hash))) { - obj[JS(hash)] = obj[txKey].at(JS(hash)); - obj[txKey].as_object().erase(JS(hash)); - } - if (auto const lgrInfo = - sharedPtrBackend_->fetchLedgerBySequence(txnPlusMeta.ledgerSequence, ctx.yield); - lgrInfo.has_value()) { - obj[JS(close_time_iso)] = xrpl::toStringIso(lgrInfo->closeTime); - obj[JS(ledger_hash)] = xrpl::strHex(lgrInfo->hash); - } - } - - obj[JS(validated)] = true; - - return obj; -} - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - MPTokenIssuanceHistoryHandler::Output const& output -) -{ - jv = { - {JS(mpt_issuance_id), output.mptIssuanceID}, - {JS(ledger_index_min), output.ledgerIndexMin}, - {JS(ledger_index_max), output.ledgerIndexMax}, - {JS(transactions), output.transactions}, - {JS(validated), output.validated}, - }; - - if (output.marker.has_value()) - jv.as_object()[JS(marker)] = boost::json::value_from(*(output.marker)); - - if (output.limit.has_value()) - jv.as_object()[JS(limit)] = *(output.limit); -} - -void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - MPTokenIssuanceHistoryHandler::Marker const& marker -) -{ - jv = { - {JS(ledger), marker.ledger}, - {JS(seq), marker.seq}, - }; -} - -MPTokenIssuanceHistoryHandler::Input -tag_invoke( - boost::json::value_to_tag, - boost::json::value const& jv -) -{ - auto const& jsonObject = jv.as_object(); - auto input = MPTokenIssuanceHistoryHandler::Input{}; - - input.mptIssuanceID = boost::json::value_to(jsonObject.at(JS(mpt_issuance_id))); - - if (jsonObject.contains(JS(account))) - input.account = boost::json::value_to(jsonObject.at(JS(account))); - - if (jsonObject.contains("tx_type")) { - input.transactionTypeInLowercase = - boost::json::value_to(jsonObject.at("tx_type")); - } - - if (jsonObject.contains(JS(ledger_index_min)) && - util::integralValueAs(jsonObject.at(JS(ledger_index_min))) != -1) - input.ledgerIndexMin = util::integralValueAs(jsonObject.at(JS(ledger_index_min))); - - if (jsonObject.contains(JS(ledger_index_max)) && - util::integralValueAs(jsonObject.at(JS(ledger_index_max))) != -1) - input.ledgerIndexMax = util::integralValueAs(jsonObject.at(JS(ledger_index_max))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(jsonObject.at(JS(ledger_hash))); - - if (jsonObject.contains(JS(ledger_index))) { - auto const expectedLedgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index))); - if (expectedLedgerIndex.has_value()) - input.ledgerIndex = *expectedLedgerIndex; - } - - 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 = util::integralValueAs(jsonObject.at(JS(limit))); - - if (jsonObject.contains(JS(marker))) { - input.marker = MPTokenIssuanceHistoryHandler::Marker{ - .ledger = util::integralValueAs( - jsonObject.at(JS(marker)).as_object().at(JS(ledger)) - ), - .seq = - util::integralValueAs(jsonObject.at(JS(marker)).as_object().at(JS(seq))) - }; - } - - return input; -} - -} // namespace rpc diff --git a/src/rpc/handlers/MPTokenIssuanceHistory.hpp b/src/rpc/handlers/MPTokenIssuanceHistory.hpp deleted file mode 100644 index b5f565bff..000000000 --- a/src/rpc/handlers/MPTokenIssuanceHistory.hpp +++ /dev/null @@ -1,316 +0,0 @@ -#pragma once - -#include "data/BackendInterface.hpp" -#include "data/Types.hpp" -#include "rpc/Errors.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/MetaProcessors.hpp" -#include "rpc/common/Modifiers.hpp" -#include "rpc/common/Specs.hpp" -#include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" -#include "util/TxUtils.hpp" -#include "util/log/Logger.hpp" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace rpc { - -/** - * @brief The mptoken_issuance_history command returns past transactions associated with the queried - * MPTokenIssuance, optionally filtered by an affected account and/or transaction type. - * - * @note This is a Clio-only method. Requests fail with `notReady` until the issuance-history - * backfill reports `Migrated`, so partial history is never served. - */ -class MPTokenIssuanceHistoryHandler { - util::Logger log_{"RPC"}; - std::shared_ptr sharedPtrBackend_; - - /** - * @brief Whether the issuance-history backfill has completed. - * - * The status is monotonic, so the terminal result is cached across handler copies. - */ - std::shared_ptr migrated_ = std::make_shared(false); - -public: - static constexpr auto kLimitMin = 1; - static constexpr auto kLimitMax = 100; - static constexpr auto kLimitDefault = 50; - - /** - * @brief The name used to query the issuance-history migrator's status. - * - * This is a literal to keep Cassandra migration headers out of RPC. - */ - static constexpr char const* kMigratorName = "MPTokenIssuanceHistoryMigrator"; - - /** - * @brief A struct to hold the marker data. - */ - struct Marker { - uint32_t ledger; - uint32_t seq; - }; - - /** - * @brief A struct to hold the output data of the command. - */ - struct Output { - std::string mptIssuanceID; - uint32_t ledgerIndexMin{0}; - uint32_t ledgerIndexMax{0}; - std::optional limit; - std::optional marker; - /** @todo Use a domain-specific type instead of JSON. */ - boost::json::array transactions; - /** @todo Send validated through the RPC framework. */ - bool validated = true; - }; - - /** - * @brief A struct to hold the input data for the command. - * - * @note When no ledger selector is provided, the request uses the backend's full available - * ledger range. - */ - struct Input { - std::string mptIssuanceID; - std::optional account; - std::optional transactionTypeInLowercase; - 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 = HandlerReturnType; - - /** - * @brief Construct a new MPTokenIssuanceHistoryHandler object. - * - * @param sharedPtrBackend The backend to use. - */ - explicit MPTokenIssuanceHistoryHandler(std::shared_ptr sharedPtrBackend) - : sharedPtrBackend_(std::move(sharedPtrBackend)) - { - } - - /** - * @brief Returns the API specification for the command. - * - * @param apiVersion The api version to return the spec for. - * @return The spec for the given apiVersion. - */ - static RpcSpecConstRef - spec([[maybe_unused]] uint32_t apiVersion) - { - auto const& typesKeysInLowercase = util::getTxTypesInLowercase(); - static auto const kRpcSpec = RpcSpec{ - {JS(mpt_issuance_id), - validation::Required{}, - validation::CustomValidators::uint192HexStringValidator}, - {JS(account), validation::CustomValidators::accountValidator}, - { - "tx_type", - validation::Type{}, - modifiers::ToLower{}, - validation::OneOf( - typesKeysInLowercase.cbegin(), typesKeysInLowercase.cend() - ), - }, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::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::Min(1u), - modifiers::Clamp{kLimitMin, kLimitMax}}, - {JS(marker), - meta::WithCustomError{ - validation::Type{}, - Status{RippledError::RpcInvalidParams, "invalidMarker"} - }, - meta::Section{ - {JS(ledger), validation::Required{}, validation::Type{}}, - {JS(seq), validation::Required{}, validation::Type{}}, - }}, - }; - - return kRpcSpec; - } - - /** - * @brief Process the MPTokenIssuanceHistory command. - * - * @param input The input data for the command. - * @param ctx The context of the request. - * @return The result of the operation. - */ - [[nodiscard]] Result - process(Input const& input, Context const& ctx) const; - -private: - /** - * @brief The inclusive range of ledger sequences a request is restricted to. - */ - struct SequenceRange { - uint32_t min; - uint32_t max; - }; - - /** - * @brief Check that the transaction-history backfill has completed. - * - * The terminal Migrated status is cached, so the backend is only consulted until the backfill - * reports completion. - * - * @param ctx The context of the request. - * @return An empty result if the history is complete; an error otherwise. - */ - [[nodiscard]] MaybeError - verifyHistoryAvailable(Context const& ctx) const; - - /** - * @brief Resolve the ledger sequence range to search from the request's ledger specifiers. - * - * Starts from the server's full ledger range, then narrows it by ledger_index_min / - * ledger_index_max, or collapses it to a single sequence if ledger_hash / ledger_index is - * given. - * - * @param input The input data for the command. - * @param ctx The context of the request. - * @return The resolved range if the specifiers are valid; an error otherwise. - */ - [[nodiscard]] std::expected - resolveSequenceRange(Input const& input, Context const& ctx) const; - - /** - * @brief Fetch one page of transactions for the issuance, optionally restricted to an account. - * - * When the request has no marker, a forward page starts at the lower bound of @p range and a - * reverse page starts after the highest possible transaction at its upper bound. - * - * @param input The input data for the command. - * @param ctx The context of the request. - * @param mptIssuanceID The MPTokenIssuance ID to fetch transactions for. - * @param range The resolved ledger sequence range. - * @return The fetched transactions and the cursor to resume from. - */ - [[nodiscard]] data::TransactionsAndCursor - fetchTransactions( - Input const& input, - Context const& ctx, - xrpl::uint192 const& mptIssuanceID, - SequenceRange range - ) const; - - /** - * @brief Process a fetched transaction page into the response. - * - * Appends transactions that pass the range and type filters. The response marker is taken from - * the fetched page, then cleared if the page runs past the requested range because there is - * nothing left to page through. - * - * @param input The input data for the command. - * @param ctx The context of the request. - * @param range The resolved ledger sequence range. - * @param page The transactions fetched from the database and the cursor to resume from. - * @param [out] response The response to append to. - */ - void - processTransactionsPage( - Input const& input, - Context const& ctx, - SequenceRange range, - data::TransactionsAndCursor const& page, - Output& response - ) const; - - /** - * @brief Convert one transaction to its JSON representation, unless its type is filtered out. - * - * @param txnPlusMeta The transaction and its metadata. - * @param input The input data for the command. - * @param ctx The context of the request. - * @return The JSON representation, or nullopt if the transaction's type does not match the - * requested one. - */ - [[nodiscard]] std::optional - transactionToJsonIfTypeMatches( - data::TransactionAndMetadata const& txnPlusMeta, - Input const& input, - Context const& ctx - ) const; - - /** - * @brief Assemble the API-specific JSON of an expanded transaction and its metadata. - * - * For API v2 and later, the transaction is enriched with ledger information when the - * corresponding ledger header is available. - * - * @param txn The expanded transaction JSON. - * @param meta The expanded metadata JSON. - * @param txnPlusMeta The transaction and its metadata the JSON was expanded from. - * @param ctx The context of the request. - * @return The JSON representation of the transaction. - */ - [[nodiscard]] boost::json::object - expandedTransactionToJson( - boost::json::object txn, - boost::json::object meta, - data::TransactionAndMetadata const& txnPlusMeta, - Context const& ctx - ) const; - - /** - * @brief Convert the Output to a JSON object. - * - * @param [out] jv The JSON object to convert to. - * @param output The output to convert. - */ - friend void - tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Output const& output); - - /** - * @brief Convert a JSON object to Input type. - * - * @param jv The JSON object to convert. - * @return Input parsed from the JSON object. - */ - friend Input - tag_invoke(boost::json::value_to_tag, boost::json::value const& jv); - - /** - * @brief Convert the Marker to a JSON object. - * - * @param [out] jv The JSON object to convert to. - * @param marker The marker to convert. - */ - friend void - tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Marker const& marker); -}; - -} // namespace rpc diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index e2dc42df0..e67d37663 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -128,7 +128,6 @@ target_sources( rpc/handlers/LedgerRangeTests.cpp rpc/handlers/LedgerTests.cpp rpc/handlers/MPTHoldersTests.cpp - rpc/handlers/MPTokenIssuanceHistoryTests.cpp rpc/handlers/NFTBuyOffersTests.cpp rpc/handlers/NFTHistoryTests.cpp rpc/handlers/NFTInfoTests.cpp diff --git a/tests/unit/rpc/handlers/MPTokenIssuanceHistoryTests.cpp b/tests/unit/rpc/handlers/MPTokenIssuanceHistoryTests.cpp deleted file mode 100644 index f2ceed962..000000000 --- a/tests/unit/rpc/handlers/MPTokenIssuanceHistoryTests.cpp +++ /dev/null @@ -1,2176 +0,0 @@ -#include "data/Types.hpp" -#include "migration/MigratiorStatus.hpp" -#include "rpc/Errors.hpp" -#include "rpc/common/AnyHandler.hpp" -#include "rpc/common/Types.hpp" -#include "rpc/handlers/MPTokenIssuanceHistory.hpp" -#include "util/HandlerBaseTestFixture.hpp" -#include "util/LoggerFixtures.hpp" -#include "util/NameGenerator.hpp" -#include "util/TestObject.hpp" - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -using namespace rpc; -using namespace data; -using namespace testing; - -namespace { -constexpr auto kMinSeq = 10; -constexpr auto kMaxSeq = 30; -constexpr auto kAccount = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; -constexpr auto kAccount2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; -constexpr auto kLedgerHash = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; -constexpr auto kCurrency = "0158415500000000C1F76FF6ECB0BAC600000000"; -// Valid 48-hex MPT issuance ID (from MPTHoldersTests.cpp) -constexpr auto kMptId = "000004C463C52827307480341125DA0577DEFC38405B0E3E"; -constexpr auto kMptIdLowercase = "000004c463c52827307480341125da0577defc38405b0e3e"; -constexpr auto kApiVersion = 2; - -auto const kMigratedStatus = - migration::MigratorStatus{migration::MigratorStatus::Status::Migrated}.toString(); -auto const kNotMigratedStatus = - migration::MigratorStatus{migration::MigratorStatus::Status::NotMigrated}.toString(); - -} // namespace - -struct RPCMPTokenIssuanceHistoryHandlerTest : HandlerBaseTest { - RPCMPTokenIssuanceHistoryHandlerTest() - { - backend_->setRange(kMinSeq, kMaxSeq); - ON_CALL(*backend_, fetchMigratorStatus) - .WillByDefault(Return(std::optional{kMigratedStatus})); - } -}; - -/** - * @brief Enables handler log capture below the test runner's default fatal severity. - */ -struct RPCMPTokenIssuanceHistoryHandlerLogTest : RPCMPTokenIssuanceHistoryHandlerTest, - LoggerFixture {}; - -struct MPTokenIssuanceHistoryParamTestCaseBundle { - std::string testName; - std::string testJson; - std::string expectedError; - std::string expectedErrorMessage; -}; - -struct MPTokenIssuanceHistoryParameterTest - : public RPCMPTokenIssuanceHistoryHandlerTest, - public WithParamInterface {}; - -static auto -generateTestValuesForParametersTest() -{ - return std::vector{ - MPTokenIssuanceHistoryParamTestCaseBundle{ - .testName = "MissingMptIssuanceID", - .testJson = R"JSON({})JSON", - .expectedError = "invalidParams", - .expectedErrorMessage = "Required field 'mpt_issuance_id' missing" - }, - MPTokenIssuanceHistoryParamTestCaseBundle{ - .testName = "MalformedMptIssuanceID", - .testJson = R"JSON({"mpt_issuance_id": "NOTAHEXSTRING"})JSON", - .expectedError = "invalidParams", - .expectedErrorMessage = "mpt_issuance_idMalformed" - }, - MPTokenIssuanceHistoryParamTestCaseBundle{ - .testName = "BinaryNotBool", - .testJson = - R"JSON({"mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405B0E3E", "binary": 1})JSON", - .expectedError = "invalidParams", - .expectedErrorMessage = "Invalid parameters." - }, - MPTokenIssuanceHistoryParamTestCaseBundle{ - .testName = "ForwardNotBool", - .testJson = - R"JSON({"mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405B0E3E", "forward": 1})JSON", - .expectedError = "invalidParams", - .expectedErrorMessage = "Invalid parameters." - }, - MPTokenIssuanceHistoryParamTestCaseBundle{ - .testName = "LedgerIndexMinNotInt", - .testJson = - R"JSON({"mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405B0E3E", "ledger_index_min": "x"})JSON", - .expectedError = "invalidParams", - .expectedErrorMessage = "Invalid parameters." - }, - MPTokenIssuanceHistoryParamTestCaseBundle{ - .testName = "LedgerIndexMaxNotInt", - .testJson = - R"JSON({"mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405B0E3E", "ledger_index_max": "x"})JSON", - .expectedError = "invalidParams", - .expectedErrorMessage = "Invalid parameters." - }, - MPTokenIssuanceHistoryParamTestCaseBundle{ - .testName = "BadAccount", - .testJson = - R"JSON({"mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405B0E3E", "account": "not_a_valid_account"})JSON", - .expectedError = "actMalformed", - .expectedErrorMessage = "accountMalformed" - }, - MPTokenIssuanceHistoryParamTestCaseBundle{ - .testName = "UnknownTxType", - .testJson = - R"JSON({"mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405B0E3E", "tx_type": "NotARealType"})JSON", - .expectedError = "invalidParams", - .expectedErrorMessage = "Invalid field 'tx_type'." - }, - MPTokenIssuanceHistoryParamTestCaseBundle{ - .testName = "MarkerNotObject", - .testJson = - R"JSON({"mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405B0E3E", "marker": 101})JSON", - .expectedError = "invalidParams", - .expectedErrorMessage = "invalidMarker" - }, - MPTokenIssuanceHistoryParamTestCaseBundle{ - .testName = "MarkerMissingSeq", - .testJson = R"JSON({ - "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405B0E3E", - "marker": {"ledger": 123} - })JSON", - .expectedError = "invalidParams", - .expectedErrorMessage = "Required field 'seq' missing" - }, - MPTokenIssuanceHistoryParamTestCaseBundle{ - .testName = "MarkerMissingLedger", - .testJson = R"JSON({ - "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405B0E3E", - "marker": {"seq": 123} - })JSON", - .expectedError = "invalidParams", - .expectedErrorMessage = "Required field 'ledger' missing" - }, - MPTokenIssuanceHistoryParamTestCaseBundle{ - .testName = "LedgerIndexInvalid", - .testJson = - R"JSON({"mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405B0E3E", "ledger_index": "x"})JSON", - .expectedError = "invalidParams", - .expectedErrorMessage = "ledgerIndexMalformed" - }, - MPTokenIssuanceHistoryParamTestCaseBundle{ - .testName = "LedgerHashInvalid", - .testJson = - R"JSON({"mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405B0E3E", "ledger_hash": "x"})JSON", - .expectedError = "invalidParams", - .expectedErrorMessage = "ledger_hashMalformed" - }, - MPTokenIssuanceHistoryParamTestCaseBundle{ - .testName = "LedgerHashNotString", - .testJson = - R"JSON({"mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405B0E3E", "ledger_hash": 123})JSON", - .expectedError = "invalidParams", - .expectedErrorMessage = "ledger_hashNotString" - }, - MPTokenIssuanceHistoryParamTestCaseBundle{ - .testName = "LimitNotInt", - .testJson = - R"JSON({"mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405B0E3E", "limit": "123"})JSON", - .expectedError = "invalidParams", - .expectedErrorMessage = "Invalid parameters." - }, - MPTokenIssuanceHistoryParamTestCaseBundle{ - .testName = "LimitNegative", - .testJson = - R"JSON({"mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405B0E3E", "limit": -1})JSON", - .expectedError = "invalidParams", - .expectedErrorMessage = "Invalid parameters." - }, - MPTokenIssuanceHistoryParamTestCaseBundle{ - .testName = "LimitZero", - .testJson = - R"JSON({"mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405B0E3E", "limit": 0})JSON", - .expectedError = "invalidParams", - .expectedErrorMessage = "Invalid parameters." - }, - }; -} - -INSTANTIATE_TEST_CASE_P( - RPCMPTokenIssuanceHistoryGroup1, - MPTokenIssuanceHistoryParameterTest, - ValuesIn(generateTestValuesForParametersTest()), - tests::util::kNameGenerator -); - -TEST_P(MPTokenIssuanceHistoryParameterTest, InvalidParams) -{ - auto const testBundle = GetParam(); - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse(testBundle.testJson); - auto const output = handler.process(req, Context{yield}); - ASSERT_FALSE(output); - auto const err = rpc::makeError(output.result.error()); - EXPECT_EQ(err.at("error").as_string(), testBundle.expectedError); - EXPECT_EQ(err.at("error_message").as_string(), testBundle.expectedErrorMessage); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, LedgerIndexMinOutOfRange) -{ - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_min": 9 - }})JSON", - kMptId - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_FALSE(output); - auto const err = rpc::makeError(output.result.error()); - EXPECT_EQ(err.at("error").as_string(), "lgrIdxMalformed"); - EXPECT_EQ(err.at("error_message").as_string(), "ledgerSeqMinOutOfRange"); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, LedgerIndexMaxOutOfRange) -{ - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_max": 31 - }})JSON", - kMptId - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_FALSE(output); - auto const err = rpc::makeError(output.result.error()); - EXPECT_EQ(err.at("error").as_string(), "lgrIdxMalformed"); - EXPECT_EQ(err.at("error_message").as_string(), "ledgerSeqMaxOutOfRange"); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, InvertedLedgerRange) -{ - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_min": 20, - "ledger_index_max": 11 - }})JSON", - kMptId - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_FALSE(output); - auto const err = rpc::makeError(output.result.error()); - EXPECT_EQ(err.at("error").as_string(), "lgrIdxsInvalid"); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, ContainsLedgerSpecifierAndRange) -{ - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_min": 11, - "ledger_index_max": 20, - "ledger_index": 10 - }})JSON", - kMptId - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_FALSE(output); - auto const err = rpc::makeError(output.result.error()); - EXPECT_EQ(err.at("error").as_string(), "invalidParams"); - EXPECT_EQ(err.at("error_message").as_string(), "containsLedgerSpecifierAndRange"); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, GateNotMigratedReturnsNotReady) -{ - ON_CALL(*backend_, fetchMigratorStatus) - .WillByDefault(Return(std::optional{kNotMigratedStatus})); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = - boost::json::parse(fmt::format(R"JSON({{"mpt_issuance_id": "{}"}})JSON", kMptId)); - auto const output = handler.process(req, Context{yield}); - ASSERT_FALSE(output); - auto const err = rpc::makeError(output.result.error()); - EXPECT_EQ(err.at("error").as_string(), "notReady"); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, GateMissingStatusReturnsNotReady) -{ - ON_CALL(*backend_, fetchMigratorStatus).WillByDefault(Return(std::nullopt)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = - boost::json::parse(fmt::format(R"JSON({{"mpt_issuance_id": "{}"}})JSON", kMptId)); - auto const output = handler.process(req, Context{yield}); - ASSERT_FALSE(output); - auto const err = rpc::makeError(output.result.error()); - EXPECT_EQ(err.at("error").as_string(), "notReady"); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, GateUnknownStatusStringReturnsNotReady) -{ - ON_CALL(*backend_, fetchMigratorStatus) - .WillByDefault(Return(std::optional{"NotAStatus"})); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = - boost::json::parse(fmt::format(R"JSON({{"mpt_issuance_id": "{}"}})JSON", kMptId)); - auto const output = handler.process(req, Context{yield}); - ASSERT_FALSE(output); - auto const err = rpc::makeError(output.result.error()); - EXPECT_EQ(err.at("error").as_string(), "notReady"); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, GateNotReadyErrorMessageContent) -{ - ON_CALL(*backend_, fetchMigratorStatus) - .WillByDefault(Return(std::optional{kNotMigratedStatus})); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = - boost::json::parse(fmt::format(R"JSON({{"mpt_issuance_id": "{}"}})JSON", kMptId)); - auto const output = handler.process(req, Context{yield}); - ASSERT_FALSE(output); - auto const err = rpc::makeError(output.result.error()); - EXPECT_EQ(err.at("error").as_string(), "notReady"); - auto const msg = err.at("error_message").as_string(); - EXPECT_TRUE(msg.find("backfill has not completed") != std::string::npos); - // The remedy is the operator's, not the caller's: no CLI invocation in a client-facing - // error. - EXPECT_TRUE(msg.find("clio_server") == std::string::npos); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, GateMigratedServesRequest) -{ - auto const transCursor = TransactionsAndCursor{.txns = {}, .cursor = std::nullopt}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = - boost::json::parse(fmt::format(R"JSON({{"mpt_issuance_id": "{}"}})JSON", kMptId)); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_TRUE(output.result->as_object().contains("mpt_issuance_id")); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, GateCachedMigratedShortCircuit) -{ - EXPECT_CALL(*backend_, fetchMigratorStatus(MPTokenIssuanceHistoryHandler::kMigratorName, _)) - .Times(1) - .WillOnce(Return(std::optional{kMigratedStatus})); - - auto const transCursor = TransactionsAndCursor{.txns = {}, .cursor = std::nullopt}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - // Same handler instance across both calls: the cached Migrated flag skips the second check. - auto anyHandler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - - runSpawn([&](auto yield) { - auto const req = - boost::json::parse(fmt::format(R"JSON({{"mpt_issuance_id": "{}"}})JSON", kMptId)); - auto const output1 = anyHandler.process(req, Context{yield}); - ASSERT_TRUE(output1); - auto const output2 = anyHandler.process(req, Context{yield}); - ASSERT_TRUE(output2); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, GateCachedMigratedSurvivesHandlerCopy) -{ - EXPECT_CALL(*backend_, fetchMigratorStatus(MPTokenIssuanceHistoryHandler::kMigratorName, _)) - .Times(1) - .WillOnce(Return(std::optional{kMigratedStatus})); - - auto const transCursor = TransactionsAndCursor{.txns = {}, .cursor = std::nullopt}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - // Every request gets a copy of the registered handler, so the cached flag survives the copy. - auto original = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const copy = original; - - runSpawn([&](auto yield) { - auto const req = - boost::json::parse(fmt::format(R"JSON({{"mpt_issuance_id": "{}"}})JSON", kMptId)); - auto const output = original.process(req, Context{yield}); - ASSERT_TRUE(output); - auto const outputFromCopy = copy.process(req, Context{yield}); - ASSERT_TRUE(outputFromCopy); - }); -} - -static std::vector -genTransactions(uint32_t seq1, uint32_t seq2) -{ - auto transactions = std::vector{}; - - auto trans1 = TransactionAndMetadata(); - xrpl::STObject const obj1 = createPaymentTransactionObject(kAccount, kAccount2, 1, 1, 32); - trans1.transaction = obj1.getSerializer().peekData(); - trans1.ledgerSequence = seq1; - xrpl::STObject const meta1 = createPaymentTransactionMetaObject(kAccount, kAccount2, 22, 23); - trans1.metadata = meta1.getSerializer().peekData(); - trans1.date = 1; - transactions.push_back(trans1); - - auto trans2 = TransactionAndMetadata(); - xrpl::STObject const obj2 = createPaymentTransactionObject(kAccount, kAccount2, 1, 1, 32); - trans2.transaction = obj2.getSerializer().peekData(); - trans2.ledgerSequence = seq2; - xrpl::STObject const meta2 = createPaymentTransactionMetaObject(kAccount, kAccount2, 22, 23); - trans2.metadata = meta2.getSerializer().peekData(); - trans2.date = 2; - transactions.push_back(trans2); - - return transactions; -} - -// Build a mixed-type page: one Payment (at seqPayment) and one OfferCreate (at seqOffer). -static std::vector -genMixedTypeTransactions(uint32_t seqPayment, uint32_t seqOffer) -{ - auto transactions = std::vector{}; - - auto payment = TransactionAndMetadata(); - xrpl::STObject const paymentObj = createPaymentTransactionObject(kAccount, kAccount2, 1, 1, 32); - payment.transaction = paymentObj.getSerializer().peekData(); - payment.ledgerSequence = seqPayment; - xrpl::STObject const paymentMeta = - createPaymentTransactionMetaObject(kAccount, kAccount2, 22, 23); - payment.metadata = paymentMeta.getSerializer().peekData(); - payment.date = 1; - transactions.push_back(payment); - - auto offer = TransactionAndMetadata(); - xrpl::STObject const offerObj = - createCreateOfferTransactionObject(kAccount, 2, 100, kCurrency, kAccount2, 200, 300); - offer.transaction = offerObj.getSerializer().peekData(); - offer.ledgerSequence = seqOffer; - xrpl::STObject const offerMeta = - createMetaDataForCreateOffer(kCurrency, kAccount, 100, 200, 300); - offer.metadata = offerMeta.getSerializer().peekData(); - offer.date = 2; - transactions.push_back(offer); - - return transactions; -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, RoutingWithoutAccountCallsFetchMPTIssuanceTxns) -{ - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - EXPECT_CALL(*backend_, fetchMPTokenIssuanceTransactions).Times(1); - EXPECT_CALL(*backend_, fetchAccountMPTokenIssuanceTransactions).Times(0); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = - boost::json::parse(fmt::format(R"JSON({{"mpt_issuance_id": "{}"}})JSON", kMptId)); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 2); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, RoutingWithAccountCallsFetchAccountMPTIssuanceTxns) -{ - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchAccountMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - EXPECT_CALL(*backend_, fetchAccountMPTokenIssuanceTransactions).Times(1); - EXPECT_CALL(*backend_, fetchMPTokenIssuanceTransactions).Times(0); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format(R"JSON({{"mpt_issuance_id": "{}", "account": "{}"}})JSON", kMptId, kAccount) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 2); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, ForwardCursorSeedFromMinIndex) -{ - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - EXPECT_CALL( - *backend_, - fetchMPTokenIssuanceTransactions( - _, _, true, testing::Optional(testing::Eq(TransactionsCursor{kMinSeq + 1, 0})), _ - ) - ) - .Times(1); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_min": {}, - "ledger_index_max": {}, - "forward": true - }})JSON", - kMptId, - kMinSeq + 1, - kMaxSeq - 1 - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMinSeq + 1); - EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq - 1); - EXPECT_EQ( - output.result->at("marker").as_object(), - boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") - ); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, ReverseCursorSeedFromMaxIndex) -{ - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - EXPECT_CALL( - *backend_, - fetchMPTokenIssuanceTransactions( - _, - _, - false, - testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq - 1, INT32_MAX})), - _ - ) - ) - .Times(1); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_min": {}, - "ledger_index_max": {}, - "forward": false - }})JSON", - kMptId, - kMinSeq + 1, - kMaxSeq - 1 - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, NoIndexSpecifiedForwardSeedsFromGlobalMin) -{ - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - EXPECT_CALL( - *backend_, - fetchMPTokenIssuanceTransactions( - _, _, true, testing::Optional(testing::Eq(TransactionsCursor{kMinSeq, 0})), _ - ) - ) - .Times(1); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_min": -1, - "ledger_index_max": -1, - "forward": true - }})JSON", - kMptId - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMinSeq); - EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, NoIndexSpecifiedReverseSeedsFromGlobalMax) -{ - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - EXPECT_CALL( - *backend_, - fetchMPTokenIssuanceTransactions( - _, _, false, testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq, INT32_MAX})), _ - ) - ) - .Times(1); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_min": -1, - "ledger_index_max": -1, - "forward": false - }})JSON", - kMptId - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMinSeq); - EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, IndexSpecificForwardFalseV1) -{ - constexpr auto kOutput = R"JSON({ - "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405B0E3E", - "ledger_index_min": 11, - "ledger_index_max": 29, - "transactions": [ - { - "meta": { - "AffectedNodes": [ - { - "ModifiedNode": { - "FinalFields": { - "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", - "Balance": "22" - }, - "LedgerEntryType": "AccountRoot" - } - }, - { - "ModifiedNode": { - "FinalFields": { - "Account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", - "Balance": "23" - }, - "LedgerEntryType": "AccountRoot" - } - } - ], - "TransactionIndex": 0, - "TransactionResult": "tesSUCCESS", - "delivered_amount": "unavailable" - }, - "tx": { - "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", - "Amount": "1", - "Destination": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", - "Fee": "1", - "Sequence": 32, - "SigningPubKey": "74657374", - "TransactionType": "Payment", - "hash": "51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", - "DeliverMax": "1", - "ledger_index": 11, - "date": 1 - }, - "validated": true - }, - { - "meta": { - "AffectedNodes": [ - { - "ModifiedNode": { - "FinalFields": { - "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", - "Balance": "22" - }, - "LedgerEntryType": "AccountRoot" - } - }, - { - "ModifiedNode": { - "FinalFields": { - "Account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", - "Balance": "23" - }, - "LedgerEntryType": "AccountRoot" - } - } - ], - "TransactionIndex": 0, - "TransactionResult": "tesSUCCESS", - "delivered_amount": "unavailable" - }, - "tx": { - "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", - "Amount": "1", - "Destination": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", - "Fee": "1", - "Sequence": 32, - "SigningPubKey": "74657374", - "TransactionType": "Payment", - "hash": "51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", - "DeliverMax": "1", - "ledger_index": 29, - "date": 2 - }, - "validated": true - } - ], - "validated": true, - "marker": { - "ledger": 12, - "seq": 34 - } - })JSON"; - - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - EXPECT_CALL( - *backend_, - fetchMPTokenIssuanceTransactions( - _, - _, - false, - testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq - 1, INT32_MAX})), - _ - ) - ) - .Times(1); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_min": {}, - "ledger_index_max": {}, - "forward": false - }})JSON", - kMptId, - kMinSeq + 1, - kMaxSeq - 1 - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result.value(), boost::json::parse(kOutput)); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, IndexSpecificForwardFalseV2) -{ - constexpr auto kOutput = R"JSON({ - "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405B0E3E", - "ledger_index_min": 11, - "ledger_index_max": 29, - "transactions": [ - { - "meta": { - "AffectedNodes": [ - { - "ModifiedNode": { - "FinalFields": { - "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", - "Balance": "22" - }, - "LedgerEntryType": "AccountRoot" - } - }, - { - "ModifiedNode": { - "FinalFields": { - "Account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", - "Balance": "23" - }, - "LedgerEntryType": "AccountRoot" - } - } - ], - "TransactionIndex": 0, - "TransactionResult": "tesSUCCESS", - "delivered_amount": "unavailable" - }, - "tx_json": { - "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", - "Destination": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", - "Fee": "1", - "Sequence": 32, - "SigningPubKey": "74657374", - "TransactionType": "Payment", - "DeliverMax": "1", - "ledger_index": 11, - "date": 1 - }, - "hash": "51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", - "ledger_index": 11, - "close_time_iso": "2000-01-01T00:00:00Z", - "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", - "validated": true - }, - { - "meta": { - "AffectedNodes": [ - { - "ModifiedNode": { - "FinalFields": { - "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", - "Balance": "22" - }, - "LedgerEntryType": "AccountRoot" - } - }, - { - "ModifiedNode": { - "FinalFields": { - "Account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", - "Balance": "23" - }, - "LedgerEntryType": "AccountRoot" - } - } - ], - "TransactionIndex": 0, - "TransactionResult": "tesSUCCESS", - "delivered_amount": "unavailable" - }, - "tx_json": { - "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", - "Destination": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", - "Fee": "1", - "Sequence": 32, - "SigningPubKey": "74657374", - "TransactionType": "Payment", - "DeliverMax": "1", - "ledger_index": 29, - "date": 2 - }, - "hash": "51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", - "ledger_index": 29, - "close_time_iso": "2000-01-01T00:00:00Z", - "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", - "validated": true - } - ], - "validated": true, - "marker": { - "ledger": 12, - "seq": 34 - } - })JSON"; - - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - EXPECT_CALL( - *backend_, - fetchMPTokenIssuanceTransactions( - _, - _, - false, - testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq - 1, INT32_MAX})), - _ - ) - ) - .WillOnce(Return(transCursor)); - - auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); - ON_CALL(*backend_, fetchLedgerBySequence).WillByDefault(Return(ledgerHeader)); - EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(2); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_min": {}, - "ledger_index_max": {}, - "forward": false - }})JSON", - kMptId, - kMinSeq + 1, - kMaxSeq - 1 - ) - ); - auto const output = - handler.process(req, Context{.yield = yield, .apiVersion = kApiVersion}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result.value(), boost::json::parse(kOutput)); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, BinaryTrueV1) -{ - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - EXPECT_CALL( - *backend_, - fetchMPTokenIssuanceTransactions( - _, _, false, testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq, INT32_MAX})), _ - ) - ) - .Times(1); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_min": -1, - "ledger_index_max": -1, - "binary": true - }})JSON", - kMptId - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("mpt_issuance_id").as_string(), kMptId); - EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMinSeq); - EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq); - EXPECT_EQ( - output.result->at("marker").as_object(), - boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") - ); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 2); - auto const& firstTx = output.result->at("transactions").as_array()[0].as_object(); - EXPECT_TRUE(firstTx.contains("tx_blob")); - EXPECT_TRUE(firstTx.contains("meta")); - EXPECT_TRUE(firstTx.contains("ledger_index")); - EXPECT_TRUE(firstTx.contains("date")); - EXPECT_TRUE(firstTx.contains("validated")); - EXPECT_FALSE(output.result->as_object().contains("limit")); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, BinaryTrueV2) -{ - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - EXPECT_CALL( - *backend_, - fetchMPTokenIssuanceTransactions( - _, _, false, testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq, INT32_MAX})), _ - ) - ) - .WillOnce(Return(transCursor)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_min": -1, - "ledger_index_max": -1, - "binary": true - }})JSON", - kMptId - ) - ); - auto const output = - handler.process(req, Context{.yield = yield, .apiVersion = kApiVersion}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("mpt_issuance_id").as_string(), kMptId); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 2); - auto const& firstTx = output.result->at("transactions").as_array()[0].as_object(); - // V2 binary uses meta_blob instead of meta - EXPECT_TRUE(firstTx.contains("tx_blob")); - EXPECT_TRUE(firstTx.contains("meta_blob")); - EXPECT_TRUE(firstTx.contains("ledger_index")); - EXPECT_TRUE(firstTx.contains("date")); - EXPECT_TRUE(firstTx.contains("validated")); - EXPECT_EQ( - firstTx.at("meta_blob").as_string(), - "201C00000000F8E5110061E762400000000000001681144B4E9C06F24296074F7B" - "C48F92A97916C6DC5EA9E1E1E5110061E76240000000000000178114D31252CF90" - "2EF8DD8451243869B38667CBD89DF3E1E1F1031000" - ); - EXPECT_EQ( - firstTx.at("tx_blob").as_string(), - "120000240000002061400000000000000168400000000000000173047465737481" - "144B4E9C06F24296074F7BC48F92A97916C6DC5EA98314D31252CF902EF8DD8451" - "243869B38667CBD89DF3" - ); - EXPECT_FALSE(output.result->as_object().contains("limit")); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, LimitAndMarkerRoundTrip) -{ - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - EXPECT_CALL( - *backend_, - fetchMPTokenIssuanceTransactions( - _, _, false, testing::Optional(testing::Eq(TransactionsCursor{10, 11})), _ - ) - ) - .Times(1); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_min": -1, - "ledger_index_max": -1, - "limit": 2, - "forward": false, - "marker": {{"ledger": 10, "seq": 11}} - }})JSON", - kMptId - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("mpt_issuance_id").as_string(), kMptId); - EXPECT_EQ(output.result->at("limit").as_uint64(), 2); - EXPECT_EQ( - output.result->at("marker").as_object(), - boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") - ); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 2); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, LimitMoreThanMax) -{ - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_min": {}, - "ledger_index_max": {}, - "forward": false, - "limit": {} - }})JSON", - kMptId, - kMinSeq + 1, - kMaxSeq - 1, - MPTokenIssuanceHistoryHandler::kLimitMax + 1 - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("limit").as_uint64(), MPTokenIssuanceHistoryHandler::kLimitMax); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, LimitNotSetDefaultUsedAndNotInResponse) -{ - auto const transCursor = TransactionsAndCursor{.txns = {}, .cursor = std::nullopt}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = - boost::json::parse(fmt::format(R"JSON({{"mpt_issuance_id": "{}"}})JSON", kMptId)); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_FALSE(output.result->as_object().contains("limit")); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, TxBelowMinSeqClipped) -{ - // Reverse: first tx at kMaxSeq-1 is in range, second at kMinSeq+1 is below minIndex. - auto const transactions = genTransactions(kMaxSeq - 1, kMinSeq + 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - EXPECT_CALL( - *backend_, - fetchMPTokenIssuanceTransactions( - _, - _, - false, - testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq - 1, INT32_MAX})), - _ - ) - ) - .Times(1); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_min": {}, - "ledger_index_max": {}, - "forward": false - }})JSON", - kMptId, - kMinSeq + 2, - kMaxSeq - 1 - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 1); - EXPECT_FALSE(output.result->as_object().contains("marker")); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, TxAboveMaxSeqClipped) -{ - // Reverse: first tx at kMaxSeq-1 is above maxIndex, second at kMinSeq+1 is in range. - auto const transactions = genTransactions(kMaxSeq - 1, kMinSeq + 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - EXPECT_CALL( - *backend_, - fetchMPTokenIssuanceTransactions( - _, - _, - false, - testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq - 2, INT32_MAX})), - _ - ) - ) - .Times(1); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_min": {}, - "ledger_index_max": {}, - "forward": false - }})JSON", - kMptId, - kMinSeq + 1, - kMaxSeq - 2 - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 1); - EXPECT_EQ( - output.result->at("marker").as_object(), - boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") - ); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, SpecificLedgerIndex) -{ - // reverse traversal; first tx at kMaxSeq-1 (= ledger_index), second below => dropped - auto const transactions = genTransactions(kMaxSeq - 1, kMinSeq + 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - EXPECT_CALL( - *backend_, - fetchMPTokenIssuanceTransactions( - _, - _, - false, - testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq - 1, INT32_MAX})), - _ - ) - ) - .Times(1); - - auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq - 1); - EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); - ON_CALL(*backend_, fetchLedgerBySequence(kMaxSeq - 1, _)).WillByDefault(Return(ledgerHeader)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index": {} - }})JSON", - kMptId, - kMaxSeq - 1 - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("mpt_issuance_id").as_string(), kMptId); - EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMaxSeq - 1); - EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq - 1); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 1); - EXPECT_FALSE(output.result->as_object().contains("limit")); - EXPECT_FALSE(output.result->as_object().contains("marker")); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, SpecificNonExistLedgerIndex) -{ - EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); - ON_CALL(*backend_, fetchLedgerBySequence(kMaxSeq - 1, _)).WillByDefault(Return(std::nullopt)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index": {} - }})JSON", - kMptId, - kMaxSeq - 1 - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_FALSE(output); - auto const err = rpc::makeError(output.result.error()); - EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, SpecificLedgerHash) -{ - auto const transactions = genTransactions(kMaxSeq - 1, kMinSeq + 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - EXPECT_CALL( - *backend_, - fetchMPTokenIssuanceTransactions( - _, - _, - false, - testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq - 1, INT32_MAX})), - _ - ) - ) - .Times(1); - - auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq - 1); - EXPECT_CALL(*backend_, fetchLedgerByHash).Times(1); - ON_CALL(*backend_, fetchLedgerByHash(xrpl::uint256{kLedgerHash}, _)) - .WillByDefault(Return(ledgerHeader)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_hash": "{}" - }})JSON", - kMptId, - kLedgerHash - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("mpt_issuance_id").as_string(), kMptId); - EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMaxSeq - 1); - EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq - 1); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 1); - EXPECT_FALSE(output.result->as_object().contains("limit")); - EXPECT_FALSE(output.result->as_object().contains("marker")); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, EmptyResultForUnseenId) -{ - auto const transCursor = TransactionsAndCursor{.txns = {}, .cursor = std::nullopt}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = - boost::json::parse(fmt::format(R"JSON({{"mpt_issuance_id": "{}"}})JSON", kMptId)); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("mpt_issuance_id").as_string(), kMptId); - EXPECT_TRUE(output.result->at("transactions").as_array().empty()); - EXPECT_FALSE(output.result->as_object().contains("marker")); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, EmptyResultForUnseenIdWithAccount) -{ - auto const transCursor = TransactionsAndCursor{.txns = {}, .cursor = std::nullopt}; - ON_CALL(*backend_, fetchAccountMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format(R"JSON({{"mpt_issuance_id": "{}", "account": "{}"}})JSON", kMptId, kAccount) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("mpt_issuance_id").as_string(), kMptId); - EXPECT_TRUE(output.result->at("transactions").as_array().empty()); - EXPECT_FALSE(output.result->as_object().contains("marker")); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, MissingBlobMidPageSkipped) -{ - auto transactions = std::vector{}; - - auto trans1 = TransactionAndMetadata(); - xrpl::STObject const obj1 = createPaymentTransactionObject(kAccount, kAccount2, 1, 1, 32); - trans1.transaction = obj1.getSerializer().peekData(); - trans1.ledgerSequence = kMinSeq + 1; - xrpl::STObject const meta1 = createPaymentTransactionMetaObject(kAccount, kAccount2, 22, 23); - trans1.metadata = meta1.getSerializer().peekData(); - trans1.date = 1; - transactions.push_back(trans1); - - auto emptyTrans = TransactionAndMetadata(); - emptyTrans.ledgerSequence = kMinSeq + 2; - emptyTrans.date = 2; - transactions.push_back(emptyTrans); - - auto trans3 = TransactionAndMetadata(); - xrpl::STObject const obj3 = createPaymentTransactionObject(kAccount, kAccount2, 1, 1, 32); - trans3.transaction = obj3.getSerializer().peekData(); - trans3.ledgerSequence = kMaxSeq - 1; - xrpl::STObject const meta3 = createPaymentTransactionMetaObject(kAccount, kAccount2, 22, 23); - trans3.metadata = meta3.getSerializer().peekData(); - trans3.date = 3; - transactions.push_back(trans3); - - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_min": -1, - "ledger_index_max": -1, - "forward": false - }})JSON", - kMptId - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 2); - EXPECT_EQ( - output.result->at("marker").as_object(), - boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") - ); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, MissingBlobInBinaryModeSkipped) -{ - // Page of [valid, empty, valid]: the empty record is skipped, marker unaffected. - auto transactions = std::vector{}; - - auto trans1 = TransactionAndMetadata(); - xrpl::STObject const obj1 = createPaymentTransactionObject(kAccount, kAccount2, 1, 1, 32); - trans1.transaction = obj1.getSerializer().peekData(); - trans1.ledgerSequence = kMaxSeq - 1; - xrpl::STObject const meta1 = createPaymentTransactionMetaObject(kAccount, kAccount2, 22, 23); - trans1.metadata = meta1.getSerializer().peekData(); - trans1.date = 1; - transactions.push_back(trans1); - - auto emptyTrans = TransactionAndMetadata(); - emptyTrans.ledgerSequence = kMinSeq + 2; - emptyTrans.date = 2; - transactions.push_back(emptyTrans); - - auto trans3 = TransactionAndMetadata(); - xrpl::STObject const obj3 = createPaymentTransactionObject(kAccount, kAccount2, 1, 1, 32); - trans3.transaction = obj3.getSerializer().peekData(); - trans3.ledgerSequence = kMinSeq + 1; - xrpl::STObject const meta3 = createPaymentTransactionMetaObject(kAccount, kAccount2, 22, 23); - trans3.metadata = meta3.getSerializer().peekData(); - trans3.date = 3; - transactions.push_back(trans3); - - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{5, 6}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "binary": true - }})JSON", - kMptId - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 2); - for (auto const& tx : output.result->at("transactions").as_array()) - EXPECT_TRUE(tx.as_object().contains("tx_blob")); - EXPECT_EQ( - output.result->at("marker").as_object(), - boost::json::parse(R"JSON({"ledger": 5, "seq": 6})JSON") - ); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, TxTypeFilterSparseMixedPageKeepsMarker) -{ - // Filtering 1 of 2 yields a sparse page, but the marker rides the raw page boundary. - auto const transactions = genMixedTypeTransactions(kMaxSeq - 1, kMinSeq + 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - EXPECT_CALL(*backend_, fetchAccountMPTokenIssuanceTransactions).Times(0); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "tx_type": "Payment" - }})JSON", - kMptId - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - ASSERT_EQ(output.result->at("transactions").as_array().size(), 1); - auto const& tx = output.result->at("transactions").as_array()[0].as_object(); - EXPECT_EQ(tx.at("tx").as_object().at("TransactionType").as_string(), "Payment"); - EXPECT_EQ( - output.result->at("marker").as_object(), - boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") - ); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, TxTypeFilterNonMatchReturnsEmpty) -{ - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "tx_type": "OfferCreate" - }})JSON", - kMptId - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 0); - EXPECT_EQ( - output.result->at("marker").as_object(), - boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") - ); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, TxTypeFilterCaseInsensitive) -{ - // ToLower modifier means mixed-case "pAyMeNt" still matches. - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "tx_type": "pAyMeNt" - }})JSON", - kMptId - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 2); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, TxTypeFilterSparseMixedPageWithAccountKeepsMarker) -{ - // As the non-account sparse-page test, but via the account routing path. - auto const transactions = genMixedTypeTransactions(kMaxSeq - 1, kMinSeq + 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchAccountMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - EXPECT_CALL(*backend_, fetchAccountMPTokenIssuanceTransactions).Times(1); - EXPECT_CALL(*backend_, fetchMPTokenIssuanceTransactions).Times(0); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "account": "{}", - "tx_type": "Payment" - }})JSON", - kMptId, - kAccount - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - ASSERT_EQ(output.result->at("transactions").as_array().size(), 1); - auto const& tx = output.result->at("transactions").as_array()[0].as_object(); - EXPECT_EQ(tx.at("tx").as_object().at("TransactionType").as_string(), "Payment"); - EXPECT_EQ( - output.result->at("marker").as_object(), - boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") - ); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, TxTypeFilterWithAccountNonMatchReturnsEmpty) -{ - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchAccountMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "account": "{}", - "tx_type": "OfferCreate" - }})JSON", - kMptId, - kAccount - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 0); - EXPECT_EQ( - output.result->at("marker").as_object(), - boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") - ); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, ForwardTxAboveMaxSeqClippedAndMarkerCleared) -{ - // Forward: second tx exceeds maxIndex, so it is dropped and the marker cleared. - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - EXPECT_CALL( - *backend_, - fetchMPTokenIssuanceTransactions( - _, _, true, testing::Optional(testing::Eq(TransactionsCursor{kMinSeq + 1, 0})), _ - ) - ) - .Times(1); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_min": {}, - "ledger_index_max": {}, - "forward": true - }})JSON", - kMptId, - kMinSeq + 1, - kMaxSeq - 2 - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 1); - EXPECT_FALSE(output.result->as_object().contains("marker")); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, BinaryWithTxTypeFilterV1) -{ - // binary + tx_type: expand to filter by type, then emit the survivors in binary form. - auto const transactions = genMixedTypeTransactions(kMaxSeq - 1, kMinSeq + 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "binary": true, - "tx_type": "Payment" - }})JSON", - kMptId - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - ASSERT_EQ(output.result->at("transactions").as_array().size(), 1); - auto const& tx = output.result->at("transactions").as_array()[0].as_object(); - EXPECT_TRUE(tx.contains("tx_blob")); - EXPECT_TRUE(tx.contains("meta")); - EXPECT_TRUE(tx.contains("ledger_index")); - EXPECT_TRUE(tx.contains("date")); - EXPECT_EQ( - output.result->at("marker").as_object(), - boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") - ); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, BinaryWithTxTypeFilterV2) -{ - // As above, but V2 binary uses meta_blob. - auto const transactions = genMixedTypeTransactions(kMaxSeq - 1, kMinSeq + 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "binary": true, - "tx_type": "Payment" - }})JSON", - kMptId - ) - ); - auto const output = - handler.process(req, Context{.yield = yield, .apiVersion = kApiVersion}); - ASSERT_TRUE(output); - ASSERT_EQ(output.result->at("transactions").as_array().size(), 1); - auto const& tx = output.result->at("transactions").as_array()[0].as_object(); - EXPECT_TRUE(tx.contains("tx_blob")); - EXPECT_TRUE(tx.contains("meta_blob")); - EXPECT_TRUE(tx.contains("ledger_index")); - EXPECT_TRUE(tx.contains("date")); - EXPECT_EQ( - output.result->at("marker").as_object(), - boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") - ); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, ResponseAlwaysHasMandatoryFields) -{ - auto const transCursor = TransactionsAndCursor{.txns = {}, .cursor = std::nullopt}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = - boost::json::parse(fmt::format(R"JSON({{"mpt_issuance_id": "{}"}})JSON", kMptId)); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - auto const& obj = output.result->as_object(); - EXPECT_TRUE(obj.contains("mpt_issuance_id")); - EXPECT_TRUE(obj.contains("ledger_index_min")); - EXPECT_TRUE(obj.contains("ledger_index_max")); - EXPECT_TRUE(obj.contains("transactions")); - EXPECT_TRUE(obj.contains("validated")); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, LedgerIndexMinAboveRangeMax) -{ - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_min": {} - }})JSON", - kMptId, - kMaxSeq + 1 - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_FALSE(output); - auto const err = rpc::makeError(output.result.error()); - EXPECT_EQ(err.at("error").as_string(), "lgrIdxMalformed"); - EXPECT_EQ(err.at("error_message").as_string(), "ledgerSeqMinOutOfRange"); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, LedgerIndexMaxBelowRangeMin) -{ - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_max": {} - }})JSON", - kMptId, - kMinSeq - 1 - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_FALSE(output); - auto const err = rpc::makeError(output.result.error()); - EXPECT_EQ(err.at("error").as_string(), "lgrIdxMalformed"); - EXPECT_EQ(err.at("error_message").as_string(), "ledgerSeqMaxOutOfRange"); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, LedgerHashWithOnlyLedgerIndexMin) -{ - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_hash": "{}", - "ledger_index_min": {} - }})JSON", - kMptId, - kLedgerHash, - kMinSeq + 1 - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_FALSE(output); - auto const err = rpc::makeError(output.result.error()); - EXPECT_EQ(err.at("error").as_string(), "invalidParams"); - EXPECT_EQ(err.at("error_message").as_string(), "containsLedgerSpecifierAndRange"); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, LedgerHashWithOnlyLedgerIndexMax) -{ - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_hash": "{}", - "ledger_index_max": {} - }})JSON", - kMptId, - kLedgerHash, - kMaxSeq - 1 - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_FALSE(output); - auto const err = rpc::makeError(output.result.error()); - EXPECT_EQ(err.at("error").as_string(), "invalidParams"); - EXPECT_EQ(err.at("error_message").as_string(), "containsLedgerSpecifierAndRange"); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, LedgerIndexValidatedStringIsNotASpecifier) -{ - // "validated" resolves to no concrete index, so the request keeps the full ledger range. - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(0); - EXPECT_CALL(*backend_, fetchLedgerByHash).Times(0); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index": "validated" - }})JSON", - kMptId - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMinSeq); - EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 2); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, LedgerIndexNumericStringAccepted) -{ - auto const transactions = genTransactions(kMaxSeq - 1, kMinSeq + 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq - 1); - ON_CALL(*backend_, fetchLedgerBySequence(kMaxSeq - 1, _)).WillByDefault(Return(ledgerHeader)); - EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index": "{}" - }})JSON", - kMptId, - kMaxSeq - 1 - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMaxSeq - 1); - EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq - 1); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 1); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, MissingMetadataOnlySkipped) -{ - // Page of [no metadata, valid]: the incomplete record is skipped, marker unaffected. - auto transactions = std::vector{}; - - auto noMeta = TransactionAndMetadata(); - xrpl::STObject const noMetaObj = createPaymentTransactionObject(kAccount, kAccount2, 1, 1, 32); - noMeta.transaction = noMetaObj.getSerializer().peekData(); - noMeta.ledgerSequence = kMaxSeq - 1; - noMeta.date = 1; - transactions.push_back(noMeta); - - auto complete = TransactionAndMetadata(); - xrpl::STObject const completeObj = - createPaymentTransactionObject(kAccount, kAccount2, 1, 1, 32); - complete.transaction = completeObj.getSerializer().peekData(); - complete.ledgerSequence = kMinSeq + 1; - xrpl::STObject const completeMeta = - createPaymentTransactionMetaObject(kAccount, kAccount2, 22, 23); - complete.metadata = completeMeta.getSerializer().peekData(); - complete.date = 2; - transactions.push_back(complete); - - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = - boost::json::parse(fmt::format(R"JSON({{"mpt_issuance_id": "{}"}})JSON", kMptId)); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - ASSERT_EQ(output.result->at("transactions").as_array().size(), 1); - auto const& tx = output.result->at("transactions").as_array()[0].as_object(); - EXPECT_EQ(tx.at("tx").as_object().at("ledger_index").as_uint64(), kMinSeq + 1); - EXPECT_EQ( - output.result->at("marker").as_object(), - boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") - ); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, V2MissingLedgerHeaderOmitsCloseTimeAndLedgerHash) -{ - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = TransactionsAndCursor{.txns = transactions, .cursor = std::nullopt}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - ON_CALL(*backend_, fetchLedgerBySequence).WillByDefault(Return(std::nullopt)); - EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(2); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = - boost::json::parse(fmt::format(R"JSON({{"mpt_issuance_id": "{}"}})JSON", kMptId)); - auto const output = - handler.process(req, Context{.yield = yield, .apiVersion = kApiVersion}); - ASSERT_TRUE(output); - ASSERT_EQ(output.result->at("transactions").as_array().size(), 2); - for (auto const& tx : output.result->at("transactions").as_array()) { - auto const& obj = tx.as_object(); - EXPECT_FALSE(obj.contains("close_time_iso")); - EXPECT_FALSE(obj.contains("ledger_hash")); - EXPECT_TRUE(obj.contains("hash")); - EXPECT_TRUE(obj.contains("tx_json")); - EXPECT_TRUE(obj.contains("ledger_index")); - EXPECT_TRUE(obj.at("validated").as_bool()); - } - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, ForwardTxBelowMinSeqNotClipped) -{ - // Forward traversal only clips above maxIndex; one below minIndex is kept, as account_tx does. - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - EXPECT_CALL( - *backend_, - fetchMPTokenIssuanceTransactions( - _, _, true, testing::Optional(testing::Eq(TransactionsCursor{kMinSeq + 2, 0})), _ - ) - ) - .Times(1); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "ledger_index_min": {}, - "ledger_index_max": {}, - "forward": true - }})JSON", - kMptId, - kMinSeq + 2, - kMaxSeq - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 2); - EXPECT_EQ( - output.result->at("marker").as_object(), - boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") - ); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, MarkerSeedsCursorInForwardMode) -{ - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = TransactionsAndCursor{.txns = transactions, .cursor = std::nullopt}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - EXPECT_CALL( - *backend_, - fetchMPTokenIssuanceTransactions( - _, _, true, testing::Optional(testing::Eq(TransactionsCursor{21, 22})), _ - ) - ) - .Times(1); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "forward": true, - "marker": {{"ledger": 21, "seq": 22}} - }})JSON", - kMptId - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_FALSE(output.result->as_object().contains("marker")); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, LimitAtMinIsPassedThrough) -{ - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); - auto const transCursor = - TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - EXPECT_CALL( - *backend_, - fetchMPTokenIssuanceTransactions( - _, static_cast(MPTokenIssuanceHistoryHandler::kLimitMin), _, _, _ - ) - ) - .Times(1); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format( - R"JSON({{ - "mpt_issuance_id": "{}", - "limit": {} - }})JSON", - kMptId, - MPTokenIssuanceHistoryHandler::kLimitMin - ) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("limit").as_uint64(), MPTokenIssuanceHistoryHandler::kLimitMin); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, DefaultLimitPassedToBackend) -{ - auto const transCursor = TransactionsAndCursor{.txns = {}, .cursor = std::nullopt}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - EXPECT_CALL( - *backend_, - fetchMPTokenIssuanceTransactions( - _, static_cast(MPTokenIssuanceHistoryHandler::kLimitDefault), _, _, _ - ) - ) - .Times(1); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = - boost::json::parse(fmt::format(R"JSON({{"mpt_issuance_id": "{}"}})JSON", kMptId)); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, MptIssuanceIDIsNormalizedInOutput) -{ - auto const transCursor = TransactionsAndCursor{.txns = {}, .cursor = std::nullopt}; - ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - - EXPECT_CALL(*backend_, fetchMPTokenIssuanceTransactions(xrpl::uint192{kMptId}, _, _, _, _)) - .Times(1); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = boost::json::parse( - fmt::format(R"JSON({{"mpt_issuance_id": "{}"}})JSON", kMptIdLowercase) - ); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("mpt_issuance_id").as_string(), kMptId); - }); -} - -TEST_F(RPCMPTokenIssuanceHistoryHandlerLogTest, LogsFetchDurationAndSkippedIndexEntry) -{ - auto transactions = std::vector{}; - - auto trans1 = TransactionAndMetadata(); - xrpl::STObject const obj1 = createPaymentTransactionObject(kAccount, kAccount2, 1, 1, 32); - trans1.transaction = obj1.getSerializer().peekData(); - trans1.ledgerSequence = kMinSeq + 1; - xrpl::STObject const meta1 = createPaymentTransactionMetaObject(kAccount, kAccount2, 22, 23); - trans1.metadata = meta1.getSerializer().peekData(); - trans1.date = 1; - transactions.push_back(trans1); - - auto emptyTrans = TransactionAndMetadata(); - emptyTrans.ledgerSequence = kMinSeq + 2; - emptyTrans.date = 2; - transactions.push_back(emptyTrans); - - auto const transCursor = TransactionsAndCursor{.txns = transactions, .cursor = std::nullopt}; - EXPECT_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillOnce(Return(transCursor)); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{MPTokenIssuanceHistoryHandler{backend_}}; - auto const req = - boost::json::parse(fmt::format(R"JSON({{"mpt_issuance_id": "{}"}})JSON", kMptId)); - auto const output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 1); - }); - - auto const logs = getLoggerString(); - EXPECT_THAT( - logs, ContainsRegex(R"(inf:RPC - db fetch took [0-9]+ milliseconds - num blobs = 2)") - ); - EXPECT_THAT( - logs, - HasSubstr( - fmt::format( - "war:RPC - Skipping index entry with no matching transaction record; " - "mpt_issuance_id = {}", - kMptId - ) - ) - ); -}