mirror of
https://github.com/XRPLF/clio.git
synced 2025-11-04 20:05:51 +00:00
feat: Implement MPT changes (#1147)
Implements https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0033d-multi-purpose-tokens
This commit is contained in:
@@ -364,6 +364,25 @@ public:
|
||||
boost::asio::yield_context yield
|
||||
) const = 0;
|
||||
|
||||
/**
|
||||
* @brief Fetches all holders' balances for a MPTIssuanceID
|
||||
*
|
||||
* @param mptID MPTIssuanceID you wish you query.
|
||||
* @param limit Paging limit.
|
||||
* @param cursorIn Optional cursor to allow us to pick up from where we last left off.
|
||||
* @param ledgerSequence The ledger sequence to fetch for
|
||||
* @param yield Currently executing coroutine.
|
||||
* @return std::vector<Blob> of MPToken balances and an optional marker
|
||||
*/
|
||||
virtual MPTHoldersAndCursor
|
||||
fetchMPTHolders(
|
||||
ripple::uint192 const& mptID,
|
||||
std::uint32_t const limit,
|
||||
std::optional<ripple::AccountID> const& cursorIn,
|
||||
std::uint32_t const ledgerSequence,
|
||||
boost::asio::yield_context yield
|
||||
) const = 0;
|
||||
|
||||
/**
|
||||
* @brief Fetches a specific ledger object.
|
||||
*
|
||||
@@ -617,6 +636,14 @@ public:
|
||||
virtual void
|
||||
writeNFTTransactions(std::vector<NFTTransactionsData> const& data) = 0;
|
||||
|
||||
/**
|
||||
* @brief Write accounts that started holding onto a MPT.
|
||||
*
|
||||
* @param data A vector of MPT ID and account pairs
|
||||
*/
|
||||
virtual void
|
||||
writeMPTHolders(std::vector<MPTHolderData> const& data) = 0;
|
||||
|
||||
/**
|
||||
* @brief Write a new successor.
|
||||
*
|
||||
|
||||
@@ -547,6 +547,45 @@ public:
|
||||
return ret;
|
||||
}
|
||||
|
||||
MPTHoldersAndCursor
|
||||
fetchMPTHolders(
|
||||
ripple::uint192 const& mptID,
|
||||
std::uint32_t const limit,
|
||||
std::optional<ripple::AccountID> const& cursorIn,
|
||||
std::uint32_t const ledgerSequence,
|
||||
boost::asio::yield_context yield
|
||||
) const override
|
||||
{
|
||||
auto const holderEntries = executor_.read(
|
||||
yield, schema_->selectMPTHolders, mptID, cursorIn.value_or(ripple::AccountID(0)), Limit{limit}
|
||||
);
|
||||
|
||||
auto const& holderResults = holderEntries.value();
|
||||
if (not holderResults.hasRows()) {
|
||||
LOG(log_.debug()) << "No rows returned";
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<ripple::uint256> mptKeys;
|
||||
std::optional<ripple::AccountID> cursor;
|
||||
for (auto const [holder] : extract<ripple::AccountID>(holderResults)) {
|
||||
mptKeys.push_back(ripple::keylet::mptoken(mptID, holder).key);
|
||||
cursor = holder;
|
||||
}
|
||||
|
||||
auto mptObjects = doFetchLedgerObjects(mptKeys, ledgerSequence, yield);
|
||||
|
||||
auto it = std::remove_if(mptObjects.begin(), mptObjects.end(), [](Blob const& mpt) { return mpt.size() == 0; });
|
||||
|
||||
mptObjects.erase(it, mptObjects.end());
|
||||
|
||||
ASSERT(mptKeys.size() <= limit, "Number of keys can't exceed the limit");
|
||||
if (mptKeys.size() == limit)
|
||||
return {mptObjects, cursor};
|
||||
|
||||
return {mptObjects, {}};
|
||||
}
|
||||
|
||||
std::optional<Blob>
|
||||
doFetchLedgerObject(ripple::uint256 const& key, std::uint32_t const sequence, boost::asio::yield_context yield)
|
||||
const override
|
||||
@@ -905,6 +944,16 @@ public:
|
||||
executor_.write(std::move(statements));
|
||||
}
|
||||
|
||||
void
|
||||
writeMPTHolders(std::vector<MPTHolderData> const& data) override
|
||||
{
|
||||
std::vector<Statement> statements;
|
||||
for (auto [mptId, holder] : data)
|
||||
statements.push_back(schema_->insertMPTHolder.bind(std::move(mptId), std::move(holder)));
|
||||
|
||||
executor_.write(std::move(statements));
|
||||
}
|
||||
|
||||
void
|
||||
startWrites() const override
|
||||
{
|
||||
|
||||
@@ -172,6 +172,14 @@ struct NFTsData {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Represents an MPT and holder pair
|
||||
*/
|
||||
struct MPTHolderData {
|
||||
ripple::uint192 mptID;
|
||||
ripple::AccountID holder;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Check whether the supplied object is an offer.
|
||||
*
|
||||
|
||||
@@ -233,6 +233,14 @@ struct NFTsAndCursor {
|
||||
std::optional<ripple::uint256> cursor;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Represents an array of MPTokens
|
||||
*/
|
||||
struct MPTHoldersAndCursor {
|
||||
std::vector<Blob> mptokens;
|
||||
std::optional<ripple::AccountID> cursor;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Stores a range of sequences as a min and max pair.
|
||||
*/
|
||||
|
||||
@@ -257,6 +257,19 @@ public:
|
||||
qualifiedTableName(settingsProvider_.get(), "nf_token_transactions")
|
||||
));
|
||||
|
||||
statements.emplace_back(fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS {}
|
||||
(
|
||||
mpt_id blob,
|
||||
holder blob,
|
||||
PRIMARY KEY (mpt_id, holder)
|
||||
)
|
||||
WITH CLUSTERING ORDER BY (holder ASC)
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "mp_token_holders")
|
||||
));
|
||||
|
||||
return statements;
|
||||
}();
|
||||
|
||||
@@ -393,6 +406,17 @@ public:
|
||||
));
|
||||
}();
|
||||
|
||||
PreparedStatement insertMPTHolder = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
INSERT INTO {}
|
||||
(mpt_id, holder)
|
||||
VALUES (?, ?)
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "mp_token_holders")
|
||||
));
|
||||
}();
|
||||
|
||||
PreparedStatement insertLedgerHeader = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
@@ -687,6 +711,20 @@ public:
|
||||
));
|
||||
}();
|
||||
|
||||
PreparedStatement selectMPTHolders = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT holder
|
||||
FROM {}
|
||||
WHERE mpt_id = ?
|
||||
AND holder > ?
|
||||
ORDER BY holder ASC
|
||||
LIMIT ?
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "mp_token_holders")
|
||||
));
|
||||
}();
|
||||
|
||||
PreparedStatement selectLedgerByHash = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
|
||||
@@ -106,9 +106,9 @@ public:
|
||||
using UintByteTupleType = std::tuple<uint32_t, ripple::uint256>;
|
||||
using ByteVectorType = std::vector<ripple::uint256>;
|
||||
|
||||
if constexpr (std::is_same_v<DecayedType, ripple::uint256>) {
|
||||
if constexpr (std::is_same_v<DecayedType, ripple::uint256> || std::is_same_v<DecayedType, ripple::uint192>) {
|
||||
auto const rc = bindBytes(value.data(), value.size());
|
||||
throwErrorIfNeeded(rc, "Bind ripple::uint256");
|
||||
throwErrorIfNeeded(rc, "Bind ripple::base_uint");
|
||||
} else if constexpr (std::is_same_v<DecayedType, ripple::AccountID>) {
|
||||
auto const rc = bindBytes(value.data(), value.size());
|
||||
throwErrorIfNeeded(rc, "Bind ripple::AccountID");
|
||||
|
||||
@@ -10,6 +10,7 @@ target_sources(
|
||||
NetworkValidatedLedgers.cpp
|
||||
NFTHelpers.cpp
|
||||
Source.cpp
|
||||
MPTHelpers.cpp
|
||||
impl/AmendmentBlockHandler.cpp
|
||||
impl/ForwardingSource.cpp
|
||||
impl/GrpcSource.cpp
|
||||
|
||||
78
src/etl/MPTHelpers.cpp
Normal file
78
src/etl/MPTHelpers.cpp
Normal file
@@ -0,0 +1,78 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "data/BackendInterface.hpp"
|
||||
#include "data/DBHelpers.hpp"
|
||||
#include "data/Types.hpp"
|
||||
|
||||
#include <fmt/core.h>
|
||||
#include <ripple/protocol/STBase.h>
|
||||
#include <ripple/protocol/STTx.h>
|
||||
#include <ripple/protocol/TxMeta.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace etl {
|
||||
|
||||
/**
|
||||
* @brief Get the MPToken created from a transaction
|
||||
*
|
||||
* @param txMeta Transaction metadata
|
||||
* @return MPT and holder account pair
|
||||
*/
|
||||
static std::optional<MPTHolderData>
|
||||
getMPTokenAuthorize(ripple::TxMeta const& txMeta)
|
||||
{
|
||||
for (ripple::STObject const& node : txMeta.getNodes()) {
|
||||
if (node.getFieldU16(ripple::sfLedgerEntryType) != ripple::ltMPTOKEN)
|
||||
continue;
|
||||
|
||||
if (node.getFName() == ripple::sfCreatedNode) {
|
||||
auto const& newMPT = node.peekAtField(ripple::sfNewFields).downcast<ripple::STObject>();
|
||||
return MPTHolderData{newMPT[ripple::sfMPTokenIssuanceID], newMPT.getAccountID(ripple::sfAccount)};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::optional<MPTHolderData>
|
||||
getMPTHolderFromTx(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
|
||||
{
|
||||
if (txMeta.getResultTER() != ripple::tesSUCCESS || sttx.getTxnType() != ripple::TxType::ttMPTOKEN_AUTHORIZE)
|
||||
return {};
|
||||
|
||||
return getMPTokenAuthorize(txMeta);
|
||||
}
|
||||
|
||||
std::optional<MPTHolderData>
|
||||
getMPTHolderFromObj(std::string const& key, std::string const& blob)
|
||||
{
|
||||
ripple::STLedgerEntry const sle =
|
||||
ripple::STLedgerEntry(ripple::SerialIter{blob.data(), blob.size()}, ripple::uint256::fromVoid(key.data()));
|
||||
|
||||
if (sle.getFieldU16(ripple::sfLedgerEntryType) != ripple::ltMPTOKEN)
|
||||
return {};
|
||||
|
||||
auto const mptIssuanceID = sle[ripple::sfMPTokenIssuanceID];
|
||||
auto const holder = sle.getAccountID(ripple::sfAccount);
|
||||
|
||||
return MPTHolderData{mptIssuanceID, holder};
|
||||
}
|
||||
|
||||
} // namespace etl
|
||||
50
src/etl/MPTHelpers.hpp
Normal file
50
src/etl/MPTHelpers.hpp
Normal file
@@ -0,0 +1,50 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
/** @file */
|
||||
#pragma once
|
||||
|
||||
#include "data/DBHelpers.hpp"
|
||||
|
||||
#include <ripple/protocol/STTx.h>
|
||||
#include <ripple/protocol/TxMeta.h>
|
||||
|
||||
namespace etl {
|
||||
|
||||
/**
|
||||
* @brief Pull MPT data from TX via ETLService.
|
||||
*
|
||||
* @param txMeta Transaction metadata
|
||||
* @param sttx The transaction
|
||||
* @return The MPTIssuanceID and holder pair as a optional
|
||||
*/
|
||||
std::optional<MPTHolderData>
|
||||
getMPTHolderFromTx(ripple::TxMeta const& txMeta, ripple::STTx const& sttx);
|
||||
|
||||
/**
|
||||
* @brief Pull MPT data from ledger object via loadInitialLedger.
|
||||
*
|
||||
* @param key The owner key
|
||||
* @param blob Object data as blob
|
||||
* @return The MPTIssuanceID and holder pair as a optional
|
||||
*/
|
||||
std::optional<MPTHolderData>
|
||||
getMPTHolderFromObj(std::string const& key, std::string const& blob);
|
||||
|
||||
} // namespace etl
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "data/BackendInterface.hpp"
|
||||
#include "data/Types.hpp"
|
||||
#include "etl/ETLHelpers.hpp"
|
||||
#include "etl/MPTHelpers.hpp"
|
||||
#include "etl/NFTHelpers.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
@@ -154,6 +155,11 @@ public:
|
||||
backend.writeSuccessor(std::move(lastKey_), request_.ledger().sequence(), std::string{obj.key()});
|
||||
lastKey_ = obj.key();
|
||||
backend.writeNFTs(getNFTDataFromObj(request_.ledger().sequence(), obj.key(), obj.data()));
|
||||
|
||||
auto const maybeMPTHolder = getMPTHolderFromObj(obj.key(), obj.data());
|
||||
if (maybeMPTHolder)
|
||||
backend.writeMPTHolders({*maybeMPTHolder});
|
||||
|
||||
backend.writeLedgerObject(
|
||||
std::move(*obj.mutable_key()), request_.ledger().sequence(), std::move(*obj.mutable_data())
|
||||
);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "data/BackendInterface.hpp"
|
||||
#include "data/DBHelpers.hpp"
|
||||
#include "data/Types.hpp"
|
||||
#include "etl/MPTHelpers.hpp"
|
||||
#include "etl/NFTHelpers.hpp"
|
||||
#include "etl/SystemState.hpp"
|
||||
#include "etl/impl/LedgerFetcher.hpp"
|
||||
@@ -55,6 +56,7 @@ struct FormattedTransactionsData {
|
||||
std::vector<AccountTransactionsData> accountTxData;
|
||||
std::vector<NFTTransactionsData> nfTokenTxData;
|
||||
std::vector<NFTsData> nfTokensData;
|
||||
std::vector<MPTHolderData> mptHoldersData;
|
||||
};
|
||||
|
||||
namespace etl::impl {
|
||||
@@ -124,6 +126,10 @@ public:
|
||||
if (maybeNFT)
|
||||
result.nfTokensData.push_back(*maybeNFT);
|
||||
|
||||
auto const maybeMPTHolder = getMPTHolderFromTx(txMeta, sttx);
|
||||
if (maybeMPTHolder)
|
||||
result.mptHoldersData.push_back(*maybeMPTHolder);
|
||||
|
||||
result.accountTxData.emplace_back(txMeta, sttx.getTransactionID());
|
||||
static constexpr std::size_t KEY_SIZE = 32;
|
||||
std::string keyStr{reinterpret_cast<char const*>(sttx.getTransactionID().data()), KEY_SIZE};
|
||||
@@ -240,6 +246,7 @@ public:
|
||||
backend_->writeAccountTransactions(std::move(insertTxResult.accountTxData));
|
||||
backend_->writeNFTs(insertTxResult.nfTokensData);
|
||||
backend_->writeNFTTransactions(insertTxResult.nfTokenTxData);
|
||||
backend_->writeMPTHolders(insertTxResult.mptHoldersData);
|
||||
}
|
||||
|
||||
backend_->finishWrites(sequence);
|
||||
|
||||
@@ -213,6 +213,7 @@ private:
|
||||
backend_->writeAccountTransactions(std::move(insertTxResultOp->accountTxData));
|
||||
backend_->writeNFTs(insertTxResultOp->nfTokensData);
|
||||
backend_->writeNFTTransactions(insertTxResultOp->nfTokenTxData);
|
||||
backend_->writeMPTHolders(insertTxResultOp->mptHoldersData);
|
||||
|
||||
auto [success, duration] =
|
||||
::util::timed<std::chrono::duration<double>>([&]() { return backend_->finishWrites(lgrInfo.seq); });
|
||||
|
||||
@@ -203,6 +203,7 @@ TransactionFeed::pub(
|
||||
pubObj[JS(meta)] = rpc::toJson(*meta);
|
||||
rpc::insertDeliveredAmount(pubObj[JS(meta)].as_object(), tx, meta, txMeta.date);
|
||||
rpc::insertDeliverMaxAlias(pubObj[txKey].as_object(), version);
|
||||
rpc::insertMPTIssuanceID(pubObj[JS(meta)].as_object(), tx, meta);
|
||||
|
||||
pubObj[JS(type)] = "transaction";
|
||||
pubObj[JS(validated)] = true;
|
||||
|
||||
@@ -33,6 +33,7 @@ target_sources(
|
||||
handlers/LedgerEntry.cpp
|
||||
handlers/LedgerIndex.cpp
|
||||
handlers/LedgerRange.cpp
|
||||
handlers/MPTHolders.cpp
|
||||
handlers/NFTsByIssuer.cpp
|
||||
handlers/NFTBuyOffers.cpp
|
||||
handlers/NFTHistory.cpp
|
||||
|
||||
@@ -259,6 +259,7 @@ toExpandedJson(
|
||||
auto metaJson = toJson(*meta);
|
||||
insertDeliveredAmount(metaJson, txn, meta, blobs.date);
|
||||
insertDeliverMaxAlias(txnJson, apiVersion);
|
||||
insertMPTIssuanceID(metaJson, txn, meta);
|
||||
|
||||
if (nftEnabled == NFTokenjson::ENABLE) {
|
||||
Json::Value nftJson;
|
||||
@@ -314,6 +315,67 @@ insertDeliveredAmount(
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the delivered amount
|
||||
*
|
||||
* @param meta The metadata
|
||||
* @return The mpt_issuance_id or std::nullopt if not available
|
||||
*/
|
||||
static std::optional<ripple::uint192>
|
||||
getMPTIssuanceID(std::shared_ptr<ripple::TxMeta const> const& meta)
|
||||
{
|
||||
ripple::TxMeta const& transactionMeta = *meta;
|
||||
|
||||
for (ripple::STObject const& node : transactionMeta.getNodes()) {
|
||||
if (node.getFieldU16(ripple::sfLedgerEntryType) != ripple::ltMPTOKEN_ISSUANCE ||
|
||||
node.getFName() != ripple::sfCreatedNode)
|
||||
continue;
|
||||
|
||||
auto const& mptNode = node.peekAtField(ripple::sfNewFields).downcast<ripple::STObject>();
|
||||
return ripple::makeMptID(mptNode[ripple::sfSequence], mptNode[ripple::sfIssuer]);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if transaction has a new MPToken created
|
||||
*
|
||||
* @param txn The transaction
|
||||
* @param meta The metadata
|
||||
* @return true if the transaction can have a mpt_issuance_id
|
||||
*/
|
||||
static bool
|
||||
canHaveMPTIssuanceID(std::shared_ptr<ripple::STTx const> const& txn, std::shared_ptr<ripple::TxMeta const> const& meta)
|
||||
{
|
||||
if (txn->getTxnType() != ripple::ttMPTOKEN_ISSUANCE_CREATE)
|
||||
return false;
|
||||
|
||||
if (meta->getResultTER() != ripple::tesSUCCESS)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
insertMPTIssuanceID(
|
||||
boost::json::object& metaJson,
|
||||
std::shared_ptr<ripple::STTx const> const& txn,
|
||||
std::shared_ptr<ripple::TxMeta const> const& meta
|
||||
)
|
||||
{
|
||||
if (!canHaveMPTIssuanceID(txn, meta))
|
||||
return false;
|
||||
|
||||
if (auto const id = getMPTIssuanceID(meta)) {
|
||||
metaJson[JS(mpt_issuance_id)] = ripple::to_string(*id);
|
||||
return true;
|
||||
}
|
||||
|
||||
assert(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
insertDeliverMaxAlias(boost::json::object& txJson, std::uint32_t const apiVersion)
|
||||
{
|
||||
|
||||
@@ -191,6 +191,21 @@ insertDeliveredAmount(
|
||||
uint32_t date
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Add "mpt_issuance_id" into MPTokenIssuanceCreate transaction json.
|
||||
*
|
||||
* @param metaJson The metadata json object to add "MPTokenIssuanceID"
|
||||
* @param txn The transaction object
|
||||
* @param meta The metadata object
|
||||
* @return true if the "mpt_issuance_id" is added to the metadata json object
|
||||
*/
|
||||
bool
|
||||
insertMPTIssuanceID(
|
||||
boost::json::object& metaJson,
|
||||
std::shared_ptr<ripple::STTx const> const& txn,
|
||||
std::shared_ptr<ripple::TxMeta const> const& meta
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Convert STBase object to JSON
|
||||
*
|
||||
|
||||
@@ -89,16 +89,19 @@ checkIsU32Numeric(std::string_view sv)
|
||||
return ec == std::errc();
|
||||
}
|
||||
|
||||
CustomValidator CustomValidators::Uint160HexStringValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
return makeHexStringValidator<ripple::uint160>(value, key);
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::Uint192HexStringValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
return makeHexStringValidator<ripple::uint192>(value, key);
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::Uint256HexStringValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_string())
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotString"}};
|
||||
|
||||
ripple::uint256 ledgerHash;
|
||||
if (!ledgerHash.parseHex(boost::json::value_to<std::string>(value)))
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "Malformed"}};
|
||||
|
||||
return MaybeError{};
|
||||
return makeHexStringValidator<ripple::uint256>(value, key);
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::LedgerIndexValidator =
|
||||
|
||||
@@ -458,6 +458,21 @@ public:
|
||||
[[nodiscard]] bool
|
||||
checkIsU32Numeric(std::string_view sv);
|
||||
|
||||
template <class HexType>
|
||||
requires(std::is_same_v<HexType, ripple::uint160> || std::is_same_v<HexType, ripple::uint192> || std::is_same_v<HexType, ripple::uint256>)
|
||||
MaybeError
|
||||
makeHexStringValidator(boost::json::value const& value, std::string_view key)
|
||||
{
|
||||
if (!value.is_string())
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotString"}};
|
||||
|
||||
HexType parsedInt;
|
||||
if (!parsedInt.parseHex(value.as_string().c_str()))
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "Malformed"}};
|
||||
|
||||
return MaybeError{};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A group of custom validation functions
|
||||
*/
|
||||
@@ -492,6 +507,22 @@ struct CustomValidators final {
|
||||
*/
|
||||
static CustomValidator AccountMarkerValidator;
|
||||
|
||||
/**
|
||||
* @brief Provides a commonly used validator for uint160(AccountID) hex string.
|
||||
*
|
||||
* It must be a string and also a decodable hex.
|
||||
* AccountID uses this validator.
|
||||
*/
|
||||
static CustomValidator Uint160HexStringValidator;
|
||||
|
||||
/**
|
||||
* @brief Provides a commonly used validator for uint192 hex string.
|
||||
*
|
||||
* It must be a string and also a decodable hex.
|
||||
* MPTIssuanceID uses this validator.
|
||||
*/
|
||||
static CustomValidator Uint192HexStringValidator;
|
||||
|
||||
/**
|
||||
* @brief Provides a commonly used validator for uint256 hex string.
|
||||
*
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
#include "rpc/handlers/LedgerEntry.hpp"
|
||||
#include "rpc/handlers/LedgerIndex.hpp"
|
||||
#include "rpc/handlers/LedgerRange.hpp"
|
||||
#include "rpc/handlers/MPTHolders.hpp"
|
||||
#include "rpc/handlers/NFTBuyOffers.hpp"
|
||||
#include "rpc/handlers/NFTHistory.hpp"
|
||||
#include "rpc/handlers/NFTInfo.hpp"
|
||||
@@ -97,6 +98,7 @@ ProductionHandlerProvider::ProductionHandlerProvider(
|
||||
{"ledger_entry", {LedgerEntryHandler{backend}}},
|
||||
{"ledger_index", {LedgerIndexHandler{backend}, true}}, // clio only
|
||||
{"ledger_range", {LedgerRangeHandler{backend}}},
|
||||
{"mpt_holders", {MPTHoldersHandler{backend}, true}}, // clio only
|
||||
{"nfts_by_issuer", {NFTsByIssuerHandler{backend}, true}}, // clio only
|
||||
{"nft_history", {NFTHistoryHandler{backend}, true}}, // clio only
|
||||
{"nft_buy_offers", {NFTBuyOffersHandler{backend}}},
|
||||
|
||||
@@ -145,6 +145,16 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx)
|
||||
}
|
||||
} else if (input.oracleNode) {
|
||||
key = input.oracleNode.value();
|
||||
} else if (input.mptIssuance) {
|
||||
auto const mptIssuanceID = ripple::uint192{std::string_view(*(input.mptIssuance))};
|
||||
key = ripple::keylet::mptIssuance(mptIssuanceID).key;
|
||||
} else if (input.mptoken) {
|
||||
auto const holder =
|
||||
ripple::parseBase58<ripple::AccountID>(boost::json::value_to<std::string>(input.mptoken->at(JS(account))));
|
||||
auto const mptIssuanceID =
|
||||
ripple::uint192{std::string_view(boost::json::value_to<std::string>(input.mptoken->at(JS(mpt_issuance_id))))
|
||||
};
|
||||
key = ripple::keylet::mptoken(mptIssuanceID, *holder).key;
|
||||
} else {
|
||||
// Must specify 1 of the following fields to indicate what type
|
||||
if (ctx.apiVersion == 1)
|
||||
@@ -277,6 +287,7 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
|
||||
{JS(xchain_owned_create_account_claim_id), ripple::ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID},
|
||||
{JS(xchain_owned_claim_id), ripple::ltXCHAIN_OWNED_CLAIM_ID},
|
||||
{JS(oracle), ripple::ltORACLE},
|
||||
{JS(mptoken), ripple::ltMPTOKEN},
|
||||
};
|
||||
|
||||
auto const parseBridgeFromJson = [](boost::json::value const& bridgeJson) {
|
||||
@@ -317,6 +328,8 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
|
||||
input.accountRoot = boost::json::value_to<std::string>(jv.at(JS(account_root)));
|
||||
} else if (jsonObject.contains(JS(did))) {
|
||||
input.did = boost::json::value_to<std::string>(jv.at(JS(did)));
|
||||
} else if (jsonObject.contains(JS(mpt_issuance))) {
|
||||
input.mptIssuance = boost::json::value_to<std::string>(jv.at(JS(mpt_issuance)));
|
||||
}
|
||||
// no need to check if_object again, validator only allows string or object
|
||||
else if (jsonObject.contains(JS(directory))) {
|
||||
@@ -348,6 +361,8 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
|
||||
);
|
||||
} else if (jsonObject.contains(JS(oracle))) {
|
||||
input.oracleNode = parseOracleFromJson(jv.at(JS(oracle)));
|
||||
} else if (jsonObject.contains(JS(mptoken))) {
|
||||
input.mptoken = jv.at(JS(mptoken)).as_object();
|
||||
}
|
||||
|
||||
if (jsonObject.contains("include_deleted"))
|
||||
|
||||
@@ -91,6 +91,8 @@ public:
|
||||
std::optional<std::string> accountRoot;
|
||||
// account id to address did object
|
||||
std::optional<std::string> did;
|
||||
// mpt issuance id to address mptIssuance object
|
||||
std::optional<std::string> mptIssuance;
|
||||
// TODO: extract into custom objects, remove json from Input
|
||||
std::optional<boost::json::object> directory;
|
||||
std::optional<boost::json::object> offer;
|
||||
@@ -99,6 +101,7 @@ public:
|
||||
std::optional<boost::json::object> depositPreauth;
|
||||
std::optional<boost::json::object> ticket;
|
||||
std::optional<boost::json::object> amm;
|
||||
std::optional<boost::json::object> mptoken;
|
||||
std::optional<ripple::STXChainBridge> bridge;
|
||||
std::optional<std::string> bridgeAccount;
|
||||
std::optional<uint32_t> chainClaimId;
|
||||
@@ -315,6 +318,35 @@ public:
|
||||
},
|
||||
meta::WithCustomError{modifiers::ToNumber{}, Status(ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID)}},
|
||||
}}},
|
||||
{JS(mpt_issuance),
|
||||
meta::WithCustomError{
|
||||
validation::CustomValidators::Uint192HexStringValidator, Status(ClioError::rpcMALFORMED_REQUEST)
|
||||
}},
|
||||
{JS(mptoken),
|
||||
meta::WithCustomError{
|
||||
validation::Type<std::string, boost::json::object>{}, Status(ClioError::rpcMALFORMED_REQUEST)
|
||||
},
|
||||
meta::IfType<std::string>{malformedRequestHexStringValidator},
|
||||
meta::IfType<boost::json::object>{
|
||||
meta::Section{
|
||||
{
|
||||
JS(account),
|
||||
meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)},
|
||||
meta::WithCustomError{
|
||||
validation::CustomValidators::AccountBase58Validator,
|
||||
Status(ClioError::rpcMALFORMED_ADDRESS)
|
||||
},
|
||||
},
|
||||
{
|
||||
JS(mpt_issuance_id),
|
||||
meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)},
|
||||
meta::WithCustomError{
|
||||
validation::CustomValidators::Uint192HexStringValidator,
|
||||
Status(ClioError::rpcMALFORMED_REQUEST)
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
{JS(ledger), check::Deprecated{}},
|
||||
{"include_deleted", validation::Type<bool>{}},
|
||||
};
|
||||
|
||||
138
src/rpc/handlers/MPTHolders.cpp
Normal file
138
src/rpc/handlers/MPTHolders.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "rpc/handlers/MPTHolders.hpp"
|
||||
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/RPCHelpers.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
|
||||
#include <boost/json/conversion.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <ripple/basics/base_uint.h>
|
||||
#include <ripple/basics/strHex.h>
|
||||
#include <ripple/protocol/AccountID.h>
|
||||
#include <ripple/protocol/ErrorCodes.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/LedgerHeader.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <ripple/protocol/nft.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
using namespace ripple;
|
||||
|
||||
namespace rpc {
|
||||
|
||||
MPTHoldersHandler::Result
|
||||
MPTHoldersHandler::process(MPTHoldersHandler::Input input, Context const& ctx) const
|
||||
{
|
||||
auto const range = sharedPtrBackend_->fetchLedgerRange();
|
||||
auto const lgrInfoOrStatus = getLedgerHeaderFromHashOrSeq(
|
||||
*sharedPtrBackend_, ctx.yield, input.ledgerHash, input.ledgerIndex, range->maxSequence
|
||||
);
|
||||
if (auto const status = std::get_if<Status>(&lgrInfoOrStatus))
|
||||
return Error{*status};
|
||||
|
||||
auto const lgrInfo = std::get<LedgerInfo>(lgrInfoOrStatus);
|
||||
auto const limit = input.limit.value_or(MPTHoldersHandler::LIMIT_DEFAULT);
|
||||
auto const mptID = ripple::uint192{input.mptID.c_str()};
|
||||
|
||||
auto const issuanceLedgerObject =
|
||||
sharedPtrBackend_->fetchLedgerObject(ripple::keylet::mptIssuance(mptID).key, lgrInfo.seq, ctx.yield);
|
||||
if (!issuanceLedgerObject)
|
||||
return Error{Status{RippledError::rpcOBJECT_NOT_FOUND, "objectNotFound"}};
|
||||
|
||||
std::optional<ripple::AccountID> cursor;
|
||||
if (input.marker)
|
||||
cursor = ripple::AccountID{input.marker->c_str()};
|
||||
|
||||
auto const dbResponse = sharedPtrBackend_->fetchMPTHolders(mptID, limit, cursor, lgrInfo.seq, ctx.yield);
|
||||
auto output = MPTHoldersHandler::Output{};
|
||||
output.mptID = to_string(mptID);
|
||||
output.limit = limit;
|
||||
output.ledgerIndex = lgrInfo.seq;
|
||||
|
||||
boost::json::array mpts;
|
||||
for (auto const& mpt : dbResponse.mptokens) {
|
||||
ripple::STLedgerEntry const sle{ripple::SerialIter{mpt.data(), mpt.size()}, keylet::mptIssuance(mptID).key};
|
||||
boost::json::object mptJson;
|
||||
|
||||
mptJson[JS(account)] = toBase58(sle[ripple::sfAccount]);
|
||||
mptJson[JS(flags)] = sle.getFlags();
|
||||
mptJson["mpt_amount"] =
|
||||
toBoostJson(ripple::STUInt64{ripple::sfMPTAmount, sle[ripple::sfMPTAmount]}.getJson(JsonOptions::none));
|
||||
mptJson["mptoken_index"] = ripple::to_string(ripple::keylet::mptoken(mptID, sle[ripple::sfAccount]).key);
|
||||
|
||||
output.mpts.push_back(mptJson);
|
||||
}
|
||||
|
||||
if (dbResponse.cursor.has_value())
|
||||
output.marker = strHex(*dbResponse.cursor);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
void
|
||||
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, MPTHoldersHandler::Output const& output)
|
||||
{
|
||||
jv = {
|
||||
{JS(mpt_issuance_id), output.mptID},
|
||||
{JS(limit), output.limit},
|
||||
{JS(ledger_index), output.ledgerIndex},
|
||||
{"mptokens", output.mpts},
|
||||
{JS(validated), output.validated},
|
||||
};
|
||||
|
||||
if (output.marker.has_value())
|
||||
jv.as_object()[JS(marker)] = *(output.marker);
|
||||
}
|
||||
|
||||
MPTHoldersHandler::Input
|
||||
tag_invoke(boost::json::value_to_tag<MPTHoldersHandler::Input>, boost::json::value const& jv)
|
||||
{
|
||||
auto const& jsonObject = jv.as_object();
|
||||
MPTHoldersHandler::Input input;
|
||||
|
||||
input.mptID = jsonObject.at(JS(mpt_issuance_id)).as_string().c_str();
|
||||
|
||||
if (jsonObject.contains(JS(ledger_hash)))
|
||||
input.ledgerHash = jsonObject.at(JS(ledger_hash)).as_string().c_str();
|
||||
|
||||
if (jsonObject.contains(JS(ledger_index))) {
|
||||
if (!jsonObject.at(JS(ledger_index)).is_string()) {
|
||||
input.ledgerIndex = jsonObject.at(JS(ledger_index)).as_int64();
|
||||
} else if (jsonObject.at(JS(ledger_index)).as_string() != "validated") {
|
||||
input.ledgerIndex = std::stoi(jsonObject.at(JS(ledger_index)).as_string().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonObject.contains(JS(limit)))
|
||||
input.limit = jsonObject.at(JS(limit)).as_int64();
|
||||
|
||||
if (jsonObject.contains(JS(marker)))
|
||||
input.marker = jsonObject.at(JS(marker)).as_string().c_str();
|
||||
|
||||
return input;
|
||||
}
|
||||
} // namespace rpc
|
||||
128
src/rpc/handlers/MPTHolders.hpp
Normal file
128
src/rpc/handlers/MPTHolders.hpp
Normal file
@@ -0,0 +1,128 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "data/BackendInterface.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/common/Modifiers.hpp"
|
||||
#include "rpc/common/Specs.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "rpc/common/Validators.hpp"
|
||||
|
||||
namespace rpc {
|
||||
|
||||
/**
|
||||
* @brief The mpt_holders command asks the Clio server for all holders of a particular MPTokenIssuance.
|
||||
*/
|
||||
class MPTHoldersHandler {
|
||||
std::shared_ptr<BackendInterface> sharedPtrBackend_;
|
||||
|
||||
public:
|
||||
static auto constexpr LIMIT_MIN = 1;
|
||||
static auto constexpr LIMIT_MAX = 100;
|
||||
static auto constexpr LIMIT_DEFAULT = 50;
|
||||
|
||||
/**
|
||||
* @brief A struct to hold the output data of the command
|
||||
*/
|
||||
struct Output {
|
||||
boost::json::array mpts;
|
||||
uint32_t ledgerIndex;
|
||||
std::string mptID;
|
||||
bool validated = true;
|
||||
uint32_t limit;
|
||||
std::optional<std::string> marker;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A struct to hold the input data for the command
|
||||
*/
|
||||
struct Input {
|
||||
std::string mptID;
|
||||
std::optional<std::string> ledgerHash;
|
||||
std::optional<uint32_t> ledgerIndex;
|
||||
std::optional<std::string> marker;
|
||||
std::optional<uint32_t> limit;
|
||||
};
|
||||
|
||||
using Result = HandlerReturnType<Output>;
|
||||
|
||||
/**
|
||||
* @brief Construct a new MPTHoldersHandler object
|
||||
*
|
||||
* @param sharedPtrBackend The backend to use
|
||||
*/
|
||||
MPTHoldersHandler(std::shared_ptr<BackendInterface> const& sharedPtrBackend) : sharedPtrBackend_(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)
|
||||
{
|
||||
static auto const rpcSpec = RpcSpec{
|
||||
{JS(mpt_issuance_id), validation::Required{}, validation::CustomValidators::Uint192HexStringValidator},
|
||||
{JS(ledger_hash), validation::CustomValidators::Uint256HexStringValidator},
|
||||
{JS(ledger_index), validation::CustomValidators::LedgerIndexValidator},
|
||||
{JS(limit),
|
||||
validation::Type<uint32_t>{},
|
||||
validation::Min(1u),
|
||||
modifiers::Clamp<int32_t>{LIMIT_MIN, LIMIT_MAX}},
|
||||
{JS(marker), validation::CustomValidators::Uint160HexStringValidator},
|
||||
};
|
||||
|
||||
return rpcSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Process the MPTHolders command
|
||||
*
|
||||
* @param input The input data for the command
|
||||
* @param ctx The context of the request
|
||||
* @return The result of the operation
|
||||
*/
|
||||
Result
|
||||
process(Input input, Context const& ctx) const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @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);
|
||||
};
|
||||
} // namespace rpc
|
||||
@@ -113,6 +113,8 @@ class LedgerTypes {
|
||||
LedgerTypeAttribute::AccountOwnedLedgerType(JS(did), ripple::ltDID),
|
||||
LedgerTypeAttribute::AccountOwnedLedgerType(JS(oracle), ripple::ltORACLE),
|
||||
LedgerTypeAttribute::ChainLedgerType(JS(nunl), ripple::ltNEGATIVE_UNL),
|
||||
LedgerTypeAttribute::DeletionBlockerLedgerType(JS(mpt_issuance), ripple::ltMPTOKEN_ISSUANCE),
|
||||
LedgerTypeAttribute::DeletionBlockerLedgerType(JS(mptoken), ripple::ltMPTOKEN),
|
||||
};
|
||||
|
||||
public:
|
||||
|
||||
@@ -205,4 +205,17 @@ struct MockBackend : public BackendInterface {
|
||||
MOCK_METHOD(void, doWriteLedgerObject, (std::string&&, std::uint32_t const, std::string&&), (override));
|
||||
|
||||
MOCK_METHOD(bool, doFinishWrites, (), (override));
|
||||
|
||||
MOCK_METHOD(void, writeMPTHolders, ((std::vector<MPTHolderData> const&)), (override));
|
||||
|
||||
MOCK_METHOD(
|
||||
MPTHoldersAndCursor,
|
||||
fetchMPTHolders,
|
||||
(ripple::uint192 const& mptID,
|
||||
std::uint32_t const,
|
||||
(std::optional<ripple::AccountID> const&),
|
||||
std::uint32_t const,
|
||||
boost::asio::yield_context),
|
||||
(const, override)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1104,6 +1104,43 @@ CreateLPTCurrency(std::string_view assetCurrency, std::string_view asset2Currenc
|
||||
);
|
||||
}
|
||||
|
||||
ripple::STObject
|
||||
CreateMPTIssuanceObject(std::string_view accountId, std::uint32_t seq, std::string_view metadata)
|
||||
{
|
||||
ripple::STObject mptIssuance(ripple::sfLedgerEntry);
|
||||
mptIssuance.setAccountID(ripple::sfIssuer, GetAccountIDWithString(accountId));
|
||||
mptIssuance.setFieldU16(ripple::sfLedgerEntryType, ripple::ltMPTOKEN_ISSUANCE);
|
||||
mptIssuance.setFieldU32(ripple::sfFlags, 0);
|
||||
mptIssuance.setFieldU32(ripple::sfSequence, seq);
|
||||
mptIssuance.setFieldU64(ripple::sfOwnerNode, 0);
|
||||
mptIssuance.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{});
|
||||
mptIssuance.setFieldU32(ripple::sfPreviousTxnLgrSeq, 0);
|
||||
mptIssuance.setFieldU64(ripple::sfMaximumAmount, 0);
|
||||
mptIssuance.setFieldU64(ripple::sfOutstandingAmount, 0);
|
||||
ripple::Slice const sliceMetadata(metadata.data(), metadata.size());
|
||||
mptIssuance.setFieldVL(ripple::sfMPTokenMetadata, sliceMetadata);
|
||||
|
||||
return mptIssuance;
|
||||
}
|
||||
|
||||
ripple::STObject
|
||||
CreateMPTokenObject(std::string_view accountId, ripple::uint192 issuanceID, std::uint64_t mptAmount)
|
||||
{
|
||||
ripple::STObject mptoken(ripple::sfLedgerEntry);
|
||||
mptoken.setAccountID(ripple::sfAccount, GetAccountIDWithString(accountId));
|
||||
mptoken[ripple::sfMPTokenIssuanceID] = issuanceID;
|
||||
mptoken.setFieldU16(ripple::sfLedgerEntryType, ripple::ltMPTOKEN);
|
||||
mptoken.setFieldU32(ripple::sfFlags, 0);
|
||||
mptoken.setFieldU64(ripple::sfOwnerNode, 0);
|
||||
mptoken.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{});
|
||||
mptoken.setFieldU32(ripple::sfPreviousTxnLgrSeq, 0);
|
||||
|
||||
if (mptAmount)
|
||||
mptoken.setFieldU64(ripple::sfMPTAmount, mptAmount);
|
||||
|
||||
return mptoken;
|
||||
}
|
||||
|
||||
ripple::STObject
|
||||
CreateOraclePriceData(
|
||||
uint64_t assetPrice,
|
||||
|
||||
@@ -393,6 +393,12 @@ CreateDidObject(std::string_view accountId, std::string_view didDoc, std::string
|
||||
[[nodiscard]] ripple::Currency
|
||||
CreateLPTCurrency(std::string_view assetCurrency, std::string_view asset2Currency);
|
||||
|
||||
[[nodiscard]] ripple::STObject
|
||||
CreateMPTIssuanceObject(std::string_view accountId, std::uint32_t seq, std::string_view metadata);
|
||||
|
||||
[[nodiscard]] ripple::STObject
|
||||
CreateMPTokenObject(std::string_view accountId, ripple::uint192 issuanceID, std::uint64_t mptAmount = 1);
|
||||
|
||||
[[nodiscard]] ripple::STObject
|
||||
CreateOraclePriceData(
|
||||
uint64_t assetPrice,
|
||||
|
||||
@@ -77,6 +77,7 @@ target_sources(
|
||||
rpc/handlers/LedgerIndexTests.cpp
|
||||
rpc/handlers/LedgerRangeTests.cpp
|
||||
rpc/handlers/LedgerTests.cpp
|
||||
rpc/handlers/MPTHoldersTests.cpp
|
||||
rpc/handlers/NFTBuyOffersTests.cpp
|
||||
rpc/handlers/NFTHistoryTests.cpp
|
||||
rpc/handlers/NFTInfoTests.cpp
|
||||
|
||||
@@ -515,6 +515,40 @@ TEST_F(RPCBaseTest, AccountMarkerValidator)
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, Uint160HexStringValidator)
|
||||
{
|
||||
auto const spec = RpcSpec{{"marker", CustomValidators::Uint160HexStringValidator}};
|
||||
auto passingInput = json::parse(R"({ "marker": "F609A18102218C75767209946A77523CBD97E225"})");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
auto failingInput = json::parse(R"({ "marker": 160})");
|
||||
auto err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
ASSERT_EQ(err.error().message, "markerNotString");
|
||||
|
||||
failingInput = json::parse(R"({ "marker": "F609A18102218C75767209946A77523CBD97E2253515BC"})");
|
||||
err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
ASSERT_EQ(err.error().message, "markerMalformed");
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, Uint192HexStringValidator)
|
||||
{
|
||||
auto const spec = RpcSpec{{"mpt_issuance_id", CustomValidators::Uint192HexStringValidator}};
|
||||
auto passingInput = json::parse(R"({ "mpt_issuance_id": "0000012F27A9DE73EAA1E8831FA253E19030A17E2D038198"})");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
auto failingInput = json::parse(R"({ "mpt_issuance_id": 192})");
|
||||
auto err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
ASSERT_EQ(err.error().message, "mpt_issuance_idNotString");
|
||||
|
||||
failingInput = json::parse(R"({ "mpt_issuance_id": "0000012F27A9DE73EAA1E8831FA253E19030A17E2D038198983515BC"})");
|
||||
err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
ASSERT_EQ(err.error().message, "mpt_issuance_idMalformed");
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, Uint256HexStringValidator)
|
||||
{
|
||||
auto const spec = RpcSpec{{"transaction", CustomValidators::Uint256HexStringValidator}};
|
||||
|
||||
@@ -1626,3 +1626,95 @@ TEST_F(RPCAccountObjectsHandlerTest, LimitMoreThanMax)
|
||||
EXPECT_EQ(*output.result, json::parse(expectedOut));
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCAccountObjectsHandlerTest, TypeFilterMPTIssuanceType)
|
||||
{
|
||||
backend->setRange(MINSEQ, MAXSEQ);
|
||||
auto const ledgerinfo = CreateLedgerHeader(LEDGERHASH, MAXSEQ);
|
||||
EXPECT_CALL(*backend, fetchLedgerBySequence).WillOnce(Return(ledgerinfo));
|
||||
|
||||
auto const account = GetAccountIDWithString(ACCOUNT);
|
||||
auto const accountKk = ripple::keylet::account(account).key;
|
||||
EXPECT_CALL(*backend, doFetchLedgerObject(accountKk, MAXSEQ, _)).WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const ownerDir = CreateOwnerDirLedgerObject({ripple::uint256{INDEX1}}, INDEX1);
|
||||
auto const ownerDirKk = ripple::keylet::ownerDir(account).key;
|
||||
EXPECT_CALL(*backend, doFetchLedgerObject(ownerDirKk, 30, _)).WillOnce(Return(ownerDir.getSerializer().peekData()));
|
||||
|
||||
// nft null
|
||||
auto const nftMaxKK = ripple::keylet::nftpage_max(account).key;
|
||||
EXPECT_CALL(*backend, doFetchLedgerObject(nftMaxKK, 30, _)).WillOnce(Return(std::nullopt));
|
||||
|
||||
std::vector<Blob> bbs;
|
||||
// put 1 mpt issuance
|
||||
auto const issuanceObject = CreateMPTIssuanceObject(ACCOUNT, 2, "metadata");
|
||||
bbs.push_back(issuanceObject.getSerializer().peekData());
|
||||
|
||||
EXPECT_CALL(*backend, doFetchLedgerObjects).WillOnce(Return(bbs));
|
||||
|
||||
auto static const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"account": "{}",
|
||||
"type": "mpt_issuance"
|
||||
}})",
|
||||
ACCOUNT
|
||||
));
|
||||
|
||||
auto const handler = AnyHandler{AccountObjectsHandler{backend}};
|
||||
runSpawn([&](auto yield) {
|
||||
auto const output = handler.process(input, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
auto const& accountObjects = output.result->as_object().at("account_objects").as_array();
|
||||
ASSERT_EQ(accountObjects.size(), 1);
|
||||
EXPECT_EQ(accountObjects.front().at("LedgerEntryType").as_string(), "MPTokenIssuance");
|
||||
|
||||
// make sure mptID is synethetically parsed if object is mptIssuance
|
||||
EXPECT_EQ(
|
||||
accountObjects.front().at("mpt_issuance_id").as_string(),
|
||||
ripple::to_string(ripple::makeMptID(2, GetAccountIDWithString(ACCOUNT)))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCAccountObjectsHandlerTest, TypeFilterMPTokenType)
|
||||
{
|
||||
backend->setRange(MINSEQ, MAXSEQ);
|
||||
auto const ledgerinfo = CreateLedgerHeader(LEDGERHASH, MAXSEQ);
|
||||
EXPECT_CALL(*backend, fetchLedgerBySequence).WillOnce(Return(ledgerinfo));
|
||||
|
||||
auto const account = GetAccountIDWithString(ACCOUNT);
|
||||
auto const accountKk = ripple::keylet::account(account).key;
|
||||
EXPECT_CALL(*backend, doFetchLedgerObject(accountKk, MAXSEQ, _)).WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const ownerDir = CreateOwnerDirLedgerObject({ripple::uint256{INDEX1}}, INDEX1);
|
||||
auto const ownerDirKk = ripple::keylet::ownerDir(account).key;
|
||||
EXPECT_CALL(*backend, doFetchLedgerObject(ownerDirKk, 30, _)).WillOnce(Return(ownerDir.getSerializer().peekData()));
|
||||
|
||||
// nft null
|
||||
auto const nftMaxKK = ripple::keylet::nftpage_max(account).key;
|
||||
EXPECT_CALL(*backend, doFetchLedgerObject(nftMaxKK, 30, _)).WillOnce(Return(std::nullopt));
|
||||
|
||||
std::vector<Blob> bbs;
|
||||
// put 1 mpt issuance
|
||||
auto const mptokenObject = CreateMPTokenObject(ACCOUNT, ripple::makeMptID(2, GetAccountIDWithString(ACCOUNT)));
|
||||
bbs.push_back(mptokenObject.getSerializer().peekData());
|
||||
|
||||
EXPECT_CALL(*backend, doFetchLedgerObjects).WillOnce(Return(bbs));
|
||||
|
||||
auto static const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"account": "{}",
|
||||
"type": "mptoken"
|
||||
}})",
|
||||
ACCOUNT
|
||||
));
|
||||
|
||||
auto const handler = AnyHandler{AccountObjectsHandler{backend}};
|
||||
runSpawn([&](auto yield) {
|
||||
auto const output = handler.process(input, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
auto const& accountObjects = output.result->as_object().at("account_objects").as_array();
|
||||
ASSERT_EQ(accountObjects.size(), 1);
|
||||
EXPECT_EQ(accountObjects.front().at("LedgerEntryType").as_string(), "MPToken");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -729,6 +729,88 @@ TEST_F(RPCLedgerDataHandlerTest, JsonLimitMoreThanMax)
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCLedgerDataHandlerTest, TypeFilterMPTIssuance)
|
||||
{
|
||||
backend->setRange(RANGEMIN, RANGEMAX);
|
||||
|
||||
EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1);
|
||||
ON_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _))
|
||||
.WillByDefault(Return(CreateLedgerHeader(LEDGERHASH, RANGEMAX)));
|
||||
|
||||
std::vector<Blob> bbs;
|
||||
EXPECT_CALL(*backend, doFetchSuccessorKey).Times(1);
|
||||
ON_CALL(*backend, doFetchSuccessorKey(_, RANGEMAX, _)).WillByDefault(Return(ripple::uint256{INDEX2}));
|
||||
|
||||
auto const issuance = CreateMPTIssuanceObject(ACCOUNT, 2, "metadata");
|
||||
bbs.push_back(issuance.getSerializer().peekData());
|
||||
|
||||
ON_CALL(*backend, doFetchLedgerObjects).WillByDefault(Return(bbs));
|
||||
EXPECT_CALL(*backend, doFetchLedgerObjects).Times(1);
|
||||
|
||||
runSpawn([&, this](auto yield) {
|
||||
auto const handler = AnyHandler{LedgerDataHandler{backend}};
|
||||
auto const req = json::parse(R"({
|
||||
"limit":1,
|
||||
"type":"mpt_issuance"
|
||||
})");
|
||||
|
||||
auto output = handler.process(req, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_TRUE(output.result->as_object().contains("ledger"));
|
||||
EXPECT_EQ(output.result->as_object().at("state").as_array().size(), 1);
|
||||
EXPECT_EQ(output.result->as_object().at("marker").as_string(), INDEX2);
|
||||
EXPECT_EQ(output.result->as_object().at("ledger_hash").as_string(), LEDGERHASH);
|
||||
EXPECT_EQ(output.result->as_object().at("ledger_index").as_uint64(), RANGEMAX);
|
||||
|
||||
auto const& objects = output.result->as_object().at("state").as_array();
|
||||
EXPECT_EQ(objects.front().at("LedgerEntryType").as_string(), "MPTokenIssuance");
|
||||
|
||||
// make sure mptID is synethetically parsed if object is mptIssuance
|
||||
EXPECT_EQ(
|
||||
objects.front().at("mpt_issuance_id").as_string(),
|
||||
ripple::to_string(ripple::makeMptID(2, GetAccountIDWithString(ACCOUNT)))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCLedgerDataHandlerTest, TypeFilterMPToken)
|
||||
{
|
||||
backend->setRange(RANGEMIN, RANGEMAX);
|
||||
|
||||
EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1);
|
||||
ON_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _))
|
||||
.WillByDefault(Return(CreateLedgerHeader(LEDGERHASH, RANGEMAX)));
|
||||
|
||||
std::vector<Blob> bbs;
|
||||
EXPECT_CALL(*backend, doFetchSuccessorKey).Times(1);
|
||||
ON_CALL(*backend, doFetchSuccessorKey(_, RANGEMAX, _)).WillByDefault(Return(ripple::uint256{INDEX2}));
|
||||
|
||||
auto const mptoken = CreateMPTokenObject(ACCOUNT, ripple::makeMptID(2, GetAccountIDWithString(ACCOUNT)));
|
||||
bbs.push_back(mptoken.getSerializer().peekData());
|
||||
|
||||
ON_CALL(*backend, doFetchLedgerObjects).WillByDefault(Return(bbs));
|
||||
EXPECT_CALL(*backend, doFetchLedgerObjects).Times(1);
|
||||
|
||||
runSpawn([&, this](auto yield) {
|
||||
auto const handler = AnyHandler{LedgerDataHandler{backend}};
|
||||
auto const req = json::parse(R"({
|
||||
"limit":1,
|
||||
"type":"mptoken"
|
||||
})");
|
||||
|
||||
auto output = handler.process(req, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_TRUE(output.result->as_object().contains("ledger"));
|
||||
EXPECT_EQ(output.result->as_object().at("state").as_array().size(), 1);
|
||||
EXPECT_EQ(output.result->as_object().at("marker").as_string(), INDEX2);
|
||||
EXPECT_EQ(output.result->as_object().at("ledger_hash").as_string(), LEDGERHASH);
|
||||
EXPECT_EQ(output.result->as_object().at("ledger_index").as_uint64(), RANGEMAX);
|
||||
|
||||
auto const& objects = output.result->as_object().at("state").as_array();
|
||||
EXPECT_EQ(objects.front().at("LedgerEntryType").as_string(), "MPToken");
|
||||
});
|
||||
}
|
||||
|
||||
TEST(RPCLedgerDataHandlerSpecTest, DeprecatedFields)
|
||||
{
|
||||
boost::json::value const json{
|
||||
|
||||
@@ -1759,6 +1759,76 @@ generateTestValuesForParametersTest()
|
||||
"malformedRequest",
|
||||
"Malformed request."
|
||||
},
|
||||
ParamTestCaseBundle{
|
||||
"InvalidMPTIssuanceStringIndex",
|
||||
R"({
|
||||
"mpt_issuance": "invalid"
|
||||
})",
|
||||
"malformedRequest",
|
||||
"Malformed request."
|
||||
},
|
||||
ParamTestCaseBundle{
|
||||
"InvalidMPTIssuanceType",
|
||||
R"({
|
||||
"mpt_issuance": 0
|
||||
})",
|
||||
"malformedRequest",
|
||||
"Malformed request."
|
||||
},
|
||||
ParamTestCaseBundle{
|
||||
"InvalidMPTokenStringIndex",
|
||||
R"({
|
||||
"mptoken": "invalid"
|
||||
})",
|
||||
"malformedRequest",
|
||||
"Malformed request."
|
||||
},
|
||||
ParamTestCaseBundle{
|
||||
"InvalidMPTokenObject",
|
||||
fmt::format(
|
||||
R"({{
|
||||
"mptoken": {{}}
|
||||
}})"
|
||||
),
|
||||
"malformedRequest",
|
||||
"Malformed request."
|
||||
},
|
||||
ParamTestCaseBundle{
|
||||
"MissingMPTokenID",
|
||||
fmt::format(
|
||||
R"({{
|
||||
"mptoken": {{
|
||||
"account": "{}"
|
||||
}}
|
||||
}})",
|
||||
ACCOUNT
|
||||
),
|
||||
"malformedRequest",
|
||||
"Malformed request."
|
||||
},
|
||||
ParamTestCaseBundle{
|
||||
"InvalidMPTokenAccount",
|
||||
fmt::format(
|
||||
R"({{
|
||||
"mptoken": {{
|
||||
"mpt_issuance_id": "0000019315EABA24E6135A4B5CE2899E0DA791206413B33D",
|
||||
"account": 1
|
||||
}}
|
||||
}})"
|
||||
),
|
||||
"malformedAddress",
|
||||
"Malformed address."
|
||||
},
|
||||
ParamTestCaseBundle{
|
||||
"InvalidMPTokenType",
|
||||
fmt::format(
|
||||
R"({{
|
||||
"mptoken": 0
|
||||
}})"
|
||||
),
|
||||
"malformedRequest",
|
||||
"Malformed request."
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2397,6 +2467,46 @@ generateTestValuesForNormalPathTest()
|
||||
)
|
||||
)
|
||||
},
|
||||
NormalPathTestBundle{
|
||||
"MPTIssuance",
|
||||
fmt::format(
|
||||
R"({{
|
||||
"binary": true,
|
||||
"mpt_issuance": "{}"
|
||||
}})",
|
||||
ripple::to_string(ripple::makeMptID(2, account1))
|
||||
),
|
||||
ripple::keylet::mptIssuance(ripple::makeMptID(2, account1)).key,
|
||||
CreateMPTIssuanceObject(ACCOUNT, 2, "metadata")
|
||||
},
|
||||
NormalPathTestBundle{
|
||||
"MPTokenViaIndex",
|
||||
fmt::format(
|
||||
R"({{
|
||||
"binary": true,
|
||||
"mptoken": "{}"
|
||||
}})",
|
||||
INDEX1
|
||||
),
|
||||
ripple::uint256{INDEX1},
|
||||
CreateMPTokenObject(ACCOUNT, ripple::makeMptID(2, account1))
|
||||
},
|
||||
NormalPathTestBundle{
|
||||
"MPTokenViaObject",
|
||||
fmt::format(
|
||||
R"({{
|
||||
"binary": true,
|
||||
"mptoken": {{
|
||||
"account": "{}",
|
||||
"mpt_issuance_id": "{}"
|
||||
}}
|
||||
}})",
|
||||
ACCOUNT,
|
||||
ripple::to_string(ripple::makeMptID(2, account1))
|
||||
),
|
||||
ripple::keylet::mptoken(ripple::makeMptID(2, account1), account1).key,
|
||||
CreateMPTokenObject(ACCOUNT, ripple::makeMptID(2, account1))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2944,6 +3054,56 @@ TEST_F(RPCLedgerEntryTest, ObjectSeqNotExist)
|
||||
});
|
||||
}
|
||||
|
||||
// this testcase will test the if response includes synthetic mpt_issuance_id
|
||||
TEST_F(RPCLedgerEntryTest, SyntheticMPTIssuanceID)
|
||||
{
|
||||
static auto constexpr OUT = R"({
|
||||
"ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652",
|
||||
"ledger_index":30,
|
||||
"validated":true,
|
||||
"index":"FD7E7EFAE2A20E75850D0E0590B205E2F74DC472281768CD6E03988069816336",
|
||||
"node":{
|
||||
"Flags":0,
|
||||
"Issuer":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||
"LedgerEntryType":"MPTokenIssuance",
|
||||
"MPTokenMetadata":"6D65746164617461",
|
||||
"MaximumAmount":"0",
|
||||
"OutstandingAmount":"0",
|
||||
"OwnerNode":"0",
|
||||
"PreviousTxnID":"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"PreviousTxnLgrSeq":0,
|
||||
"Sequence":2,
|
||||
"index":"FD7E7EFAE2A20E75850D0E0590B205E2F74DC472281768CD6E03988069816336",
|
||||
"mpt_issuance_id":"000000024B4E9C06F24296074F7BC48F92A97916C6DC5EA9"
|
||||
}
|
||||
})";
|
||||
|
||||
auto const mptId = ripple::makeMptID(2, GetAccountIDWithString(ACCOUNT));
|
||||
|
||||
backend->setRange(RANGEMIN, RANGEMAX);
|
||||
// return valid ledgerHeader
|
||||
auto const ledgerHeader = CreateLedgerHeader(LEDGERHASH, RANGEMAX);
|
||||
EXPECT_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillRepeatedly(Return(ledgerHeader));
|
||||
|
||||
// return valid ledger entry which can be deserialized
|
||||
auto const ledgerEntry = CreateMPTIssuanceObject(ACCOUNT, 2, "metadata");
|
||||
EXPECT_CALL(*backend, doFetchLedgerObject(ripple::keylet::mptIssuance(mptId).key, RANGEMAX, _))
|
||||
.WillRepeatedly(Return(ledgerEntry.getSerializer().peekData()));
|
||||
|
||||
runSpawn([&, this](auto yield) {
|
||||
auto const handler = AnyHandler{LedgerEntryHandler{backend}};
|
||||
auto const req = json::parse(fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance": "{}"
|
||||
}})",
|
||||
ripple::to_string(mptId)
|
||||
));
|
||||
auto const output = handler.process(req, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(*output.result, json::parse(OUT));
|
||||
});
|
||||
}
|
||||
|
||||
using RPCLedgerEntryDeathTest = RPCLedgerEntryTest;
|
||||
|
||||
TEST_F(RPCLedgerEntryDeathTest, RangeNotAvailable)
|
||||
|
||||
651
tests/unit/rpc/handlers/MPTHoldersTests.cpp
Normal file
651
tests/unit/rpc/handlers/MPTHoldersTests.cpp
Normal file
@@ -0,0 +1,651 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "data/Types.hpp"
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/common/AnyHandler.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "rpc/handlers/MPTHolders.hpp"
|
||||
#include "util/HandlerBaseTestFixture.hpp"
|
||||
#include "util/TestObject.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <ripple/basics/base_uint.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/LedgerHeader.h>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace rpc;
|
||||
namespace json = boost::json;
|
||||
using namespace testing;
|
||||
|
||||
// constexpr static auto ISSUER_ACCOUNT = "rsS8ju2jYabSKJ6uzLarAS1gEzvRQ6JAiF";
|
||||
constexpr static auto HOLDER1_ACCOUNT = "rrnAZCqMahreZrKMcZU3t2DZ6yUndT4ubN";
|
||||
constexpr static auto HOLDER2_ACCOUNT = "rEiNkzogdHEzUxPfsri5XSMqtXUixf2Yx";
|
||||
constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
|
||||
constexpr static auto MPTID = "000004C463C52827307480341125DA0577DEFC38405B0E3E";
|
||||
|
||||
static std::string MPTOUT1 =
|
||||
R"({
|
||||
"account": "rrnAZCqMahreZrKMcZU3t2DZ6yUndT4ubN",
|
||||
"flags": 0,
|
||||
"mpt_amount": "1",
|
||||
"mptoken_index": "D137F2E5A5767A06CB7A8F060ADE442A30CFF95028E1AF4B8767E3A56877205A"
|
||||
})";
|
||||
|
||||
static std::string MPTOUT2 =
|
||||
R"({
|
||||
"account": "rEiNkzogdHEzUxPfsri5XSMqtXUixf2Yx",
|
||||
"flags": 0,
|
||||
"mpt_amount": "1",
|
||||
"mptoken_index": "36D91DEE5EFE4A93119A8B84C944A528F2B444329F3846E49FE921040DE17E65"
|
||||
})";
|
||||
|
||||
class RPCMPTHoldersHandlerTest : public HandlerBaseTest {};
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, NonHexLedgerHash)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend}};
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"ledger_hash": "xxx"
|
||||
}})",
|
||||
MPTID
|
||||
));
|
||||
auto const output = handler.process(input, Context{std::ref(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(), "ledger_hashMalformed");
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, NonStringLedgerHash)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend}};
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"ledger_hash": 123
|
||||
}})",
|
||||
MPTID
|
||||
));
|
||||
auto const output = handler.process(input, Context{std::ref(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(), "ledger_hashNotString");
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, InvalidLedgerIndexString)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend}};
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"ledger_index": "notvalidated"
|
||||
}})",
|
||||
MPTID
|
||||
));
|
||||
auto const output = handler.process(input, Context{std::ref(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(), "ledgerIndexMalformed");
|
||||
});
|
||||
}
|
||||
|
||||
// error case: issuer invalid format, length is incorrect
|
||||
TEST_F(RPCMPTHoldersHandlerTest, MPTIDInvalidFormat)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend}};
|
||||
auto const input = json::parse(R"({
|
||||
"mpt_issuance_id": "xxx"
|
||||
})");
|
||||
auto const output = handler.process(input, Context{std::ref(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(), "mpt_issuance_idMalformed");
|
||||
});
|
||||
}
|
||||
|
||||
// error case: issuer missing
|
||||
TEST_F(RPCMPTHoldersHandlerTest, MPTIDMissing)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend}};
|
||||
auto const input = json::parse(R"({})");
|
||||
auto const output = handler.process(input, Context{std::ref(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(), "Required field 'mpt_issuance_id' missing");
|
||||
});
|
||||
}
|
||||
|
||||
// error case: issuer invalid format
|
||||
TEST_F(RPCMPTHoldersHandlerTest, MPTIDNotString)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend}};
|
||||
auto const input = json::parse(R"({
|
||||
"mpt_issuance_id": 12
|
||||
})");
|
||||
auto const output = handler.process(input, Context{std::ref(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(), "mpt_issuance_idNotString");
|
||||
});
|
||||
}
|
||||
|
||||
// error case: invalid marker format
|
||||
TEST_F(RPCMPTHoldersHandlerTest, MarkerInvalidFormat)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend}};
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"marker": "xxx"
|
||||
}})",
|
||||
MPTID
|
||||
));
|
||||
auto const output = handler.process(input, Context{std::ref(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(), "markerMalformed");
|
||||
});
|
||||
}
|
||||
|
||||
// error case: invalid marker type
|
||||
TEST_F(RPCMPTHoldersHandlerTest, MarkerNotString)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend}};
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"marker": 1
|
||||
}})",
|
||||
MPTID
|
||||
));
|
||||
auto const output = handler.process(input, Context{std::ref(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(), "markerNotString");
|
||||
});
|
||||
}
|
||||
|
||||
// error case ledger non exist via hash
|
||||
TEST_F(RPCMPTHoldersHandlerTest, NonExistLedgerViaLedgerHash)
|
||||
{
|
||||
// mock fetchLedgerByHash return empty
|
||||
EXPECT_CALL(*backend, fetchLedgerByHash).Times(1);
|
||||
ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _))
|
||||
.WillByDefault(Return(std::optional<ripple::LedgerInfo>{}));
|
||||
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"ledger_hash": "{}"
|
||||
}})",
|
||||
MPTID,
|
||||
LEDGERHASH
|
||||
));
|
||||
runSpawn([&, this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend}};
|
||||
auto const output = handler.process(input, Context{std::ref(yield)});
|
||||
ASSERT_FALSE(output);
|
||||
|
||||
auto const err = rpc::makeError(output.result.error());
|
||||
EXPECT_EQ(err.at("error").as_string(), "lgrNotFound");
|
||||
EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
|
||||
});
|
||||
}
|
||||
|
||||
// error case ledger non exist via index
|
||||
TEST_F(RPCMPTHoldersHandlerTest, NonExistLedgerViaLedgerStringIndex)
|
||||
{
|
||||
backend->setRange(10, 30);
|
||||
// mock fetchLedgerBySequence return empty
|
||||
EXPECT_CALL(*backend, fetchLedgerBySequence).WillOnce(Return(std::optional<ripple::LedgerInfo>{}));
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"ledger_index": "4"
|
||||
}})",
|
||||
MPTID
|
||||
));
|
||||
runSpawn([&, this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend}};
|
||||
auto const output = handler.process(input, Context{std::ref(yield)});
|
||||
ASSERT_FALSE(output);
|
||||
auto const err = rpc::makeError(output.result.error());
|
||||
EXPECT_EQ(err.at("error").as_string(), "lgrNotFound");
|
||||
EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, NonExistLedgerViaLedgerIntIndex)
|
||||
{
|
||||
backend->setRange(10, 30);
|
||||
// mock fetchLedgerBySequence return empty
|
||||
EXPECT_CALL(*backend, fetchLedgerBySequence).WillOnce(Return(std::optional<ripple::LedgerInfo>{}));
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"ledger_index": 4
|
||||
}})",
|
||||
MPTID
|
||||
));
|
||||
runSpawn([&, this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend}};
|
||||
auto const output = handler.process(input, Context{std::ref(yield)});
|
||||
ASSERT_FALSE(output);
|
||||
auto const err = rpc::makeError(output.result.error());
|
||||
EXPECT_EQ(err.at("error").as_string(), "lgrNotFound");
|
||||
EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
|
||||
});
|
||||
}
|
||||
|
||||
// error case ledger > max seq via hash
|
||||
// idk why this case will happen in reality
|
||||
TEST_F(RPCMPTHoldersHandlerTest, NonExistLedgerViaLedgerHash2)
|
||||
{
|
||||
backend->setRange(10, 30);
|
||||
// mock fetchLedgerByHash return ledger but seq is 31 > 30
|
||||
auto ledgerinfo = CreateLedgerHeader(LEDGERHASH, 31);
|
||||
ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(ledgerinfo));
|
||||
EXPECT_CALL(*backend, fetchLedgerByHash).Times(1);
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"ledger_hash": "{}"
|
||||
}})",
|
||||
MPTID,
|
||||
LEDGERHASH
|
||||
));
|
||||
runSpawn([&, this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend}};
|
||||
auto const output = handler.process(input, Context{std::ref(yield)});
|
||||
ASSERT_FALSE(output);
|
||||
auto const err = rpc::makeError(output.result.error());
|
||||
EXPECT_EQ(err.at("error").as_string(), "lgrNotFound");
|
||||
EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
|
||||
});
|
||||
}
|
||||
|
||||
// error case ledger > max seq via index
|
||||
TEST_F(RPCMPTHoldersHandlerTest, NonExistLedgerViaLedgerIndex2)
|
||||
{
|
||||
backend->setRange(10, 30);
|
||||
// no need to check from db,call fetchLedgerBySequence 0 time
|
||||
// differ from previous logic
|
||||
EXPECT_CALL(*backend, fetchLedgerBySequence).Times(0);
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"ledger_index": "31"
|
||||
}})",
|
||||
MPTID
|
||||
));
|
||||
runSpawn([&, this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend}};
|
||||
auto const output = handler.process(input, Context{std::ref(yield)});
|
||||
ASSERT_FALSE(output);
|
||||
auto const err = rpc::makeError(output.result.error());
|
||||
EXPECT_EQ(err.at("error").as_string(), "lgrNotFound");
|
||||
EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
|
||||
});
|
||||
}
|
||||
|
||||
// normal case when MPT does not exist
|
||||
TEST_F(RPCMPTHoldersHandlerTest, MPTNotFound)
|
||||
{
|
||||
backend->setRange(10, 30);
|
||||
auto ledgerinfo = CreateLedgerHeader(LEDGERHASH, 30);
|
||||
ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(ledgerinfo));
|
||||
EXPECT_CALL(*backend, fetchLedgerByHash).Times(1);
|
||||
ON_CALL(*backend, doFetchLedgerObject).WillByDefault(Return(std::optional<Blob>{}));
|
||||
EXPECT_CALL(*backend, doFetchLedgerObject).Times(1);
|
||||
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"ledger_hash": "{}"
|
||||
}})",
|
||||
MPTID,
|
||||
LEDGERHASH
|
||||
));
|
||||
runSpawn([&, this](boost::asio::yield_context yield) {
|
||||
auto handler = AnyHandler{MPTHoldersHandler{this->backend}};
|
||||
auto const output = handler.process(input, Context{yield});
|
||||
ASSERT_FALSE(output);
|
||||
auto const err = rpc::makeError(output.result.error());
|
||||
EXPECT_EQ(err.at("error").as_string(), "objectNotFound");
|
||||
EXPECT_EQ(err.at("error_message").as_string(), "objectNotFound");
|
||||
});
|
||||
}
|
||||
|
||||
// normal case when mpt has one holder
|
||||
TEST_F(RPCMPTHoldersHandlerTest, DefaultParameters)
|
||||
{
|
||||
auto const currentOutput = fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"limit":50,
|
||||
"ledger_index": 30,
|
||||
"mptokens": [{}],
|
||||
"validated": true
|
||||
}})",
|
||||
MPTID,
|
||||
MPTOUT1
|
||||
);
|
||||
|
||||
backend->setRange(10, 30);
|
||||
auto ledgerInfo = CreateLedgerHeader(LEDGERHASH, 30);
|
||||
EXPECT_CALL(*backend, fetchLedgerBySequence).WillOnce(Return(ledgerInfo));
|
||||
auto const issuanceKk = ripple::keylet::mptIssuance(ripple::uint192(MPTID)).key;
|
||||
ON_CALL(*backend, doFetchLedgerObject(issuanceKk, 30, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const mptoken = CreateMPTokenObject(HOLDER1_ACCOUNT, ripple::uint192(MPTID));
|
||||
std::vector<Blob> const mpts = {mptoken.getSerializer().peekData()};
|
||||
ON_CALL(*backend, fetchMPTHolders).WillByDefault(Return(MPTHoldersAndCursor{mpts, {}}));
|
||||
EXPECT_CALL(
|
||||
*backend, fetchMPTHolders(ripple::uint192(MPTID), testing::_, testing::Eq(std::nullopt), Const(30), testing::_)
|
||||
)
|
||||
.Times(1);
|
||||
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}"
|
||||
}})",
|
||||
MPTID
|
||||
));
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto handler = AnyHandler{MPTHoldersHandler{this->backend}};
|
||||
auto const output = handler.process(input, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(json::parse(currentOutput), *output.result);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, CustomAmounts)
|
||||
{
|
||||
// it's not possible to have locked_amount to be greater than mpt_amount,
|
||||
// we are simply testing the response parsing of the api
|
||||
auto const currentOutput = fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"limit":50,
|
||||
"ledger_index": 30,
|
||||
"mptokens": [{{
|
||||
"account": "rrnAZCqMahreZrKMcZU3t2DZ6yUndT4ubN",
|
||||
"flags": 0,
|
||||
"mpt_amount": "0",
|
||||
"mptoken_index": "D137F2E5A5767A06CB7A8F060ADE442A30CFF95028E1AF4B8767E3A56877205A"
|
||||
}}],
|
||||
"validated": true
|
||||
}})",
|
||||
MPTID
|
||||
);
|
||||
|
||||
backend->setRange(10, 30);
|
||||
auto ledgerInfo = CreateLedgerHeader(LEDGERHASH, 30);
|
||||
EXPECT_CALL(*backend, fetchLedgerBySequence).WillOnce(Return(ledgerInfo));
|
||||
auto const issuanceKk = ripple::keylet::mptIssuance(ripple::uint192(MPTID)).key;
|
||||
ON_CALL(*backend, doFetchLedgerObject(issuanceKk, 30, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const mptoken = CreateMPTokenObject(HOLDER1_ACCOUNT, ripple::uint192(MPTID), 0);
|
||||
std::vector<Blob> const mpts = {mptoken.getSerializer().peekData()};
|
||||
ON_CALL(*backend, fetchMPTHolders).WillByDefault(Return(MPTHoldersAndCursor{mpts, {}}));
|
||||
EXPECT_CALL(
|
||||
*backend, fetchMPTHolders(ripple::uint192(MPTID), testing::_, testing::Eq(std::nullopt), Const(30), testing::_)
|
||||
)
|
||||
.Times(1);
|
||||
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}"
|
||||
}})",
|
||||
MPTID
|
||||
));
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto handler = AnyHandler{MPTHoldersHandler{this->backend}};
|
||||
auto const output = handler.process(input, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(json::parse(currentOutput), *output.result);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, SpecificLedgerIndex)
|
||||
{
|
||||
auto const specificLedger = 20;
|
||||
auto const currentOutput = fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"limit":50,
|
||||
"ledger_index": {},
|
||||
"mptokens": [{}],
|
||||
"validated": true
|
||||
}})",
|
||||
MPTID,
|
||||
specificLedger,
|
||||
MPTOUT1
|
||||
);
|
||||
|
||||
backend->setRange(10, 30);
|
||||
auto ledgerInfo = CreateLedgerHeader(LEDGERHASH, specificLedger);
|
||||
ON_CALL(*backend, fetchLedgerBySequence(specificLedger, _)).WillByDefault(Return(ledgerInfo));
|
||||
EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1);
|
||||
auto const issuanceKk = ripple::keylet::mptIssuance(ripple::uint192(MPTID)).key;
|
||||
ON_CALL(*backend, doFetchLedgerObject(issuanceKk, specificLedger, _))
|
||||
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const mptoken = CreateMPTokenObject(HOLDER1_ACCOUNT, ripple::uint192(MPTID));
|
||||
std::vector<Blob> const mpts = {mptoken.getSerializer().peekData()};
|
||||
ON_CALL(*backend, fetchMPTHolders).WillByDefault(Return(MPTHoldersAndCursor{mpts, {}}));
|
||||
EXPECT_CALL(
|
||||
*backend,
|
||||
fetchMPTHolders(
|
||||
ripple::uint192(MPTID), testing::_, testing::Eq(std::nullopt), Const(specificLedger), testing::_
|
||||
)
|
||||
)
|
||||
.Times(1);
|
||||
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"ledger_index": {}
|
||||
}})",
|
||||
MPTID,
|
||||
specificLedger
|
||||
));
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto handler = AnyHandler{MPTHoldersHandler{this->backend}};
|
||||
auto const output = handler.process(input, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(json::parse(currentOutput), *output.result);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, MarkerParameter)
|
||||
{
|
||||
auto const currentOutput = fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"limit":50,
|
||||
"ledger_index": 30,
|
||||
"mptokens": [{}],
|
||||
"validated": true,
|
||||
"marker": "{}"
|
||||
}})",
|
||||
MPTID,
|
||||
MPTOUT2,
|
||||
ripple::strHex(GetAccountIDWithString(HOLDER1_ACCOUNT))
|
||||
);
|
||||
|
||||
backend->setRange(10, 30);
|
||||
auto ledgerInfo = CreateLedgerHeader(LEDGERHASH, 30);
|
||||
EXPECT_CALL(*backend, fetchLedgerBySequence).WillOnce(Return(ledgerInfo));
|
||||
auto const issuanceKk = ripple::keylet::mptIssuance(ripple::uint192(MPTID)).key;
|
||||
ON_CALL(*backend, doFetchLedgerObject(issuanceKk, 30, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const mptoken = CreateMPTokenObject(HOLDER2_ACCOUNT, ripple::uint192(MPTID));
|
||||
std::vector<Blob> const mpts = {mptoken.getSerializer().peekData()};
|
||||
auto const marker = GetAccountIDWithString(HOLDER1_ACCOUNT);
|
||||
ON_CALL(*backend, fetchMPTHolders).WillByDefault(Return(MPTHoldersAndCursor{mpts, marker}));
|
||||
EXPECT_CALL(
|
||||
*backend, fetchMPTHolders(ripple::uint192(MPTID), testing::_, testing::Eq(marker), Const(30), testing::_)
|
||||
)
|
||||
.Times(1);
|
||||
|
||||
auto const HOLDER1_ACCOUNTID = ripple::strHex(GetAccountIDWithString(HOLDER1_ACCOUNT));
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"marker": "{}"
|
||||
}})",
|
||||
MPTID,
|
||||
HOLDER1_ACCOUNTID
|
||||
));
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto handler = AnyHandler{MPTHoldersHandler{this->backend}};
|
||||
auto const output = handler.process(input, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(json::parse(currentOutput), *output.result);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, MultipleMPTs)
|
||||
{
|
||||
auto const currentOutput = fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"limit":50,
|
||||
"ledger_index": 30,
|
||||
"mptokens": [{}, {}],
|
||||
"validated": true
|
||||
}})",
|
||||
MPTID,
|
||||
MPTOUT1,
|
||||
MPTOUT2
|
||||
);
|
||||
|
||||
backend->setRange(10, 30);
|
||||
auto ledgerInfo = CreateLedgerHeader(LEDGERHASH, 30);
|
||||
EXPECT_CALL(*backend, fetchLedgerBySequence).WillOnce(Return(ledgerInfo));
|
||||
auto const issuanceKk = ripple::keylet::mptIssuance(ripple::uint192(MPTID)).key;
|
||||
ON_CALL(*backend, doFetchLedgerObject(issuanceKk, 30, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const mptoken1 = CreateMPTokenObject(HOLDER1_ACCOUNT, ripple::uint192(MPTID));
|
||||
auto const mptoken2 = CreateMPTokenObject(HOLDER2_ACCOUNT, ripple::uint192(MPTID));
|
||||
std::vector<Blob> const mpts = {mptoken1.getSerializer().peekData(), mptoken2.getSerializer().peekData()};
|
||||
ON_CALL(*backend, fetchMPTHolders).WillByDefault(Return(MPTHoldersAndCursor{mpts, {}}));
|
||||
EXPECT_CALL(
|
||||
*backend, fetchMPTHolders(ripple::uint192(MPTID), testing::_, testing::Eq(std::nullopt), Const(30), testing::_)
|
||||
)
|
||||
.Times(1);
|
||||
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}"
|
||||
}})",
|
||||
MPTID
|
||||
));
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto handler = AnyHandler{MPTHoldersHandler{this->backend}};
|
||||
auto const output = handler.process(input, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(json::parse(currentOutput), *output.result);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, LimitMoreThanMAx)
|
||||
{
|
||||
auto const currentOutput = fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"limit":100,
|
||||
"ledger_index": 30,
|
||||
"mptokens": [{}],
|
||||
"validated": true
|
||||
}})",
|
||||
MPTID,
|
||||
MPTOUT1
|
||||
);
|
||||
|
||||
backend->setRange(10, 30);
|
||||
auto ledgerInfo = CreateLedgerHeader(LEDGERHASH, 30);
|
||||
EXPECT_CALL(*backend, fetchLedgerBySequence).WillOnce(Return(ledgerInfo));
|
||||
auto const issuanceKk = ripple::keylet::mptIssuance(ripple::uint192(MPTID)).key;
|
||||
ON_CALL(*backend, doFetchLedgerObject(issuanceKk, 30, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const mptoken = CreateMPTokenObject(HOLDER1_ACCOUNT, ripple::uint192(MPTID));
|
||||
std::vector<Blob> const mpts = {mptoken.getSerializer().peekData()};
|
||||
ON_CALL(*backend, fetchMPTHolders).WillByDefault(Return(MPTHoldersAndCursor{mpts, {}}));
|
||||
EXPECT_CALL(
|
||||
*backend,
|
||||
fetchMPTHolders(
|
||||
ripple::uint192(MPTID),
|
||||
Const(MPTHoldersHandler::LIMIT_MAX),
|
||||
testing::Eq(std::nullopt),
|
||||
Const(30),
|
||||
testing::_
|
||||
)
|
||||
)
|
||||
.Times(1);
|
||||
|
||||
auto const input = json::parse(fmt::format(
|
||||
R"({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"limit": {}
|
||||
}})",
|
||||
MPTID,
|
||||
MPTHoldersHandler::LIMIT_MAX + 1
|
||||
));
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto handler = AnyHandler{MPTHoldersHandler{this->backend}};
|
||||
auto const output = handler.process(input, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(json::parse(currentOutput), *output.result);
|
||||
});
|
||||
}
|
||||
@@ -52,6 +52,8 @@ TEST(LedgerUtilsTests, LedgerObjectTypeList)
|
||||
JS(xchain_owned_claim_id),
|
||||
JS(xchain_owned_create_account_claim_id),
|
||||
JS(did),
|
||||
JS(mpt_issuance),
|
||||
JS(mptoken),
|
||||
JS(oracle),
|
||||
JS(nunl)
|
||||
};
|
||||
@@ -83,7 +85,9 @@ TEST(LedgerUtilsTests, AccountOwnedTypeList)
|
||||
JS(xchain_owned_claim_id),
|
||||
JS(xchain_owned_create_account_claim_id),
|
||||
JS(did),
|
||||
JS(oracle)
|
||||
JS(oracle),
|
||||
JS(mpt_issuance),
|
||||
JS(mptoken)
|
||||
};
|
||||
|
||||
static_assert(std::size(correctTypes) == accountOwned.size());
|
||||
@@ -121,7 +125,9 @@ TEST(LedgerUtilsTests, DeletionBlockerTypes)
|
||||
ripple::ltRIPPLE_STATE,
|
||||
ripple::ltXCHAIN_OWNED_CLAIM_ID,
|
||||
ripple::ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID,
|
||||
ripple::ltBRIDGE
|
||||
ripple::ltBRIDGE,
|
||||
ripple::ltMPTOKEN_ISSUANCE,
|
||||
ripple::ltMPTOKEN
|
||||
};
|
||||
|
||||
static_assert(std::size(deletionBlockers) == testedTypes.size());
|
||||
|
||||
Reference in New Issue
Block a user