mirror of
https://github.com/XRPLF/clio.git
synced 2026-08-19 03:30:53 +00:00
feat: Add mptoken_issuance_history RPC (#3141)
feat: add mptoken_issuance_history RPC Summary Adds mptoken_issuance_history, a Clio-only method that returns the transaction history for a given MPT issuance — the MPT-scoped sibling of nft_history. You can optionally filter by account and/or tx_type. The index tables, backend fetch methods, and live ETL indexing landed earlier; this wires up the handler on top of them. The handler - Routes to fetchMPTokenIssuanceTransactions or fetchAccountMPTokenIssuanceTransactions depending on whether account is set. When tx_type is given, it filters on TransactionType post-fetch, exactly like account_tx. - Follows the nft_history/account_tx conventions for ledger ranges, markers, binary, forward, and limit ([1,100], default 50), including api-version response branching. - Gated on backfill completion so it never returns partial history: until this node's MPTTransactionHistoryMigrator reports Migrated, requests get notReady with a message pointing at the --migrate command. Once migrated, the result is cached and the check is skipped — the method turns on automatically, no restart. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,7 @@ target_sources(
|
||||
handlers/LedgerIndex.cpp
|
||||
handlers/LedgerRange.cpp
|
||||
handlers/MPTHolders.cpp
|
||||
handlers/MPTokenIssuanceHistory.cpp
|
||||
handlers/NFTsByIssuer.cpp
|
||||
handlers/NFTBuyOffers.cpp
|
||||
handlers/NFTHistory.cpp
|
||||
|
||||
@@ -35,6 +35,7 @@ handledRpcs()
|
||||
"ledger_index",
|
||||
"ledger_range",
|
||||
"mpt_holders",
|
||||
"mptoken_issuance_history",
|
||||
"nfts_by_issuer",
|
||||
"nft_history",
|
||||
"nft_buy_offers",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#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"
|
||||
@@ -91,6 +92,8 @@ 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",
|
||||
|
||||
378
src/rpc/handlers/MPTokenIssuanceHistory.cpp
Normal file
378
src/rpc/handlers/MPTokenIssuanceHistory.cpp
Normal file
@@ -0,0 +1,378 @@
|
||||
#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 <boost/json/conversion.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <boost/json/value_from.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/protocol/LedgerHeader.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
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::SequenceRange, Status>
|
||||
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<int32_t>::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<boost::json::object>
|
||||
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<std::string>(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<MPTokenIssuanceHistoryHandler::Input>,
|
||||
boost::json::value const& jv
|
||||
)
|
||||
{
|
||||
auto const& jsonObject = jv.as_object();
|
||||
auto input = MPTokenIssuanceHistoryHandler::Input{};
|
||||
|
||||
input.mptIssuanceID = boost::json::value_to<std::string>(jsonObject.at(JS(mpt_issuance_id)));
|
||||
|
||||
if (jsonObject.contains(JS(account)))
|
||||
input.account = boost::json::value_to<std::string>(jsonObject.at(JS(account)));
|
||||
|
||||
if (jsonObject.contains("tx_type")) {
|
||||
input.transactionTypeInLowercase =
|
||||
boost::json::value_to<std::string>(jsonObject.at("tx_type"));
|
||||
}
|
||||
|
||||
if (jsonObject.contains(JS(ledger_index_min)) &&
|
||||
util::integralValueAs<int32_t>(jsonObject.at(JS(ledger_index_min))) != -1)
|
||||
input.ledgerIndexMin = util::integralValueAs<uint32_t>(jsonObject.at(JS(ledger_index_min)));
|
||||
|
||||
if (jsonObject.contains(JS(ledger_index_max)) &&
|
||||
util::integralValueAs<int32_t>(jsonObject.at(JS(ledger_index_max))) != -1)
|
||||
input.ledgerIndexMax = util::integralValueAs<uint32_t>(jsonObject.at(JS(ledger_index_max)));
|
||||
|
||||
if (jsonObject.contains(JS(ledger_hash)))
|
||||
input.ledgerHash = boost::json::value_to<std::string>(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<uint32_t>(jsonObject.at(JS(limit)));
|
||||
|
||||
if (jsonObject.contains(JS(marker))) {
|
||||
input.marker = MPTokenIssuanceHistoryHandler::Marker{
|
||||
.ledger = util::integralValueAs<uint32_t>(
|
||||
jsonObject.at(JS(marker)).as_object().at(JS(ledger))
|
||||
),
|
||||
.seq =
|
||||
util::integralValueAs<uint32_t>(jsonObject.at(JS(marker)).as_object().at(JS(seq)))
|
||||
};
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
} // namespace rpc
|
||||
316
src/rpc/handlers/MPTokenIssuanceHistory.hpp
Normal file
316
src/rpc/handlers/MPTokenIssuanceHistory.hpp
Normal file
@@ -0,0 +1,316 @@
|
||||
#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 <boost/json/array.hpp>
|
||||
#include <boost/json/conversion.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
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<BackendInterface> 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<std::atomic_bool> migrated_ = std::make_shared<std::atomic_bool>(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<uint32_t> limit;
|
||||
std::optional<Marker> 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<std::string> account;
|
||||
std::optional<std::string> transactionTypeInLowercase;
|
||||
std::optional<std::string> ledgerHash;
|
||||
std::optional<uint32_t> ledgerIndex;
|
||||
std::optional<int32_t> ledgerIndexMin;
|
||||
std::optional<int32_t> ledgerIndexMax;
|
||||
bool binary = false;
|
||||
bool forward = false;
|
||||
std::optional<uint32_t> limit;
|
||||
std::optional<Marker> marker;
|
||||
};
|
||||
|
||||
using Result = HandlerReturnType<Output>;
|
||||
|
||||
/**
|
||||
* @brief Construct a new MPTokenIssuanceHistoryHandler object.
|
||||
*
|
||||
* @param sharedPtrBackend The backend to use.
|
||||
*/
|
||||
explicit MPTokenIssuanceHistoryHandler(std::shared_ptr<BackendInterface> 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<std::string>{},
|
||||
modifiers::ToLower{},
|
||||
validation::OneOf<std::string>(
|
||||
typesKeysInLowercase.cbegin(), typesKeysInLowercase.cend()
|
||||
),
|
||||
},
|
||||
{JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator},
|
||||
{JS(ledger_index), validation::CustomValidators::ledgerIndexValidator},
|
||||
{JS(ledger_index_min), validation::Type<int32_t>{}},
|
||||
{JS(ledger_index_max), validation::Type<int32_t>{}},
|
||||
{JS(binary), validation::Type<bool>{}},
|
||||
{JS(forward), validation::Type<bool>{}},
|
||||
{JS(limit),
|
||||
validation::Type<uint32_t>{},
|
||||
validation::Min(1u),
|
||||
modifiers::Clamp<int32_t>{kLimitMin, kLimitMax}},
|
||||
{JS(marker),
|
||||
meta::WithCustomError{
|
||||
validation::Type<boost::json::object>{},
|
||||
Status{RippledError::RpcInvalidParams, "invalidMarker"}
|
||||
},
|
||||
meta::Section{
|
||||
{JS(ledger), validation::Required{}, validation::Type<uint32_t>{}},
|
||||
{JS(seq), validation::Required{}, validation::Type<uint32_t>{}},
|
||||
}},
|
||||
};
|
||||
|
||||
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<SequenceRange, Status>
|
||||
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<boost::json::object>
|
||||
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<Input>, 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
|
||||
@@ -128,6 +128,7 @@ 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
|
||||
|
||||
2176
tests/unit/rpc/handlers/MPTokenIssuanceHistoryTests.cpp
Normal file
2176
tests/unit/rpc/handlers/MPTokenIssuanceHistoryTests.cpp
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user