feat: Implement MPT changes (#1147)

Implements https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0033d-multi-purpose-tokens
This commit is contained in:
Shawn Xie
2024-11-11 11:02:02 -05:00
committed by GitHub
parent 891fd1e7bf
commit 6b61984e0e
34 changed files with 1797 additions and 12 deletions

View File

@@ -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.
*

View File

@@ -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
{

View File

@@ -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.
*

View File

@@ -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.
*/

View File

@@ -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"(

View File

@@ -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");

View File

@@ -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
View 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
View 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

View File

@@ -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())
);

View File

@@ -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);

View File

@@ -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); });

View File

@@ -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;

View File

@@ -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

View File

@@ -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)
{

View File

@@ -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
*

View File

@@ -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 =

View File

@@ -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.
*

View File

@@ -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}}},

View File

@@ -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"))

View File

@@ -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>{}},
};

View 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

View 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

View File

@@ -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:

View File

@@ -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)
);
};

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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}};

View File

@@ -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");
});
}

View File

@@ -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{

View File

@@ -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)

View 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);
});
}

View File

@@ -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());