mirror of
https://github.com/XRPLF/clio.git
synced 2025-12-06 17:27:58 +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
|
boost::asio::yield_context yield
|
||||||
) const = 0;
|
) 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.
|
* @brief Fetches a specific ledger object.
|
||||||
*
|
*
|
||||||
@@ -617,6 +636,14 @@ public:
|
|||||||
virtual void
|
virtual void
|
||||||
writeNFTTransactions(std::vector<NFTTransactionsData> const& data) = 0;
|
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.
|
* @brief Write a new successor.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -547,6 +547,45 @@ public:
|
|||||||
return ret;
|
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>
|
std::optional<Blob>
|
||||||
doFetchLedgerObject(ripple::uint256 const& key, std::uint32_t const sequence, boost::asio::yield_context yield)
|
doFetchLedgerObject(ripple::uint256 const& key, std::uint32_t const sequence, boost::asio::yield_context yield)
|
||||||
const override
|
const override
|
||||||
@@ -905,6 +944,16 @@ public:
|
|||||||
executor_.write(std::move(statements));
|
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
|
void
|
||||||
startWrites() const override
|
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.
|
* @brief Check whether the supplied object is an offer.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -233,6 +233,14 @@ struct NFTsAndCursor {
|
|||||||
std::optional<ripple::uint256> cursor;
|
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.
|
* @brief Stores a range of sequences as a min and max pair.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -257,6 +257,19 @@ public:
|
|||||||
qualifiedTableName(settingsProvider_.get(), "nf_token_transactions")
|
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;
|
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]() {
|
PreparedStatement insertLedgerHeader = [this]() {
|
||||||
return handle_.get().prepare(fmt::format(
|
return handle_.get().prepare(fmt::format(
|
||||||
R"(
|
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]() {
|
PreparedStatement selectLedgerByHash = [this]() {
|
||||||
return handle_.get().prepare(fmt::format(
|
return handle_.get().prepare(fmt::format(
|
||||||
R"(
|
R"(
|
||||||
|
|||||||
@@ -106,9 +106,9 @@ public:
|
|||||||
using UintByteTupleType = std::tuple<uint32_t, ripple::uint256>;
|
using UintByteTupleType = std::tuple<uint32_t, ripple::uint256>;
|
||||||
using ByteVectorType = std::vector<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());
|
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>) {
|
} else if constexpr (std::is_same_v<DecayedType, ripple::AccountID>) {
|
||||||
auto const rc = bindBytes(value.data(), value.size());
|
auto const rc = bindBytes(value.data(), value.size());
|
||||||
throwErrorIfNeeded(rc, "Bind ripple::AccountID");
|
throwErrorIfNeeded(rc, "Bind ripple::AccountID");
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ target_sources(
|
|||||||
NetworkValidatedLedgers.cpp
|
NetworkValidatedLedgers.cpp
|
||||||
NFTHelpers.cpp
|
NFTHelpers.cpp
|
||||||
Source.cpp
|
Source.cpp
|
||||||
|
MPTHelpers.cpp
|
||||||
impl/AmendmentBlockHandler.cpp
|
impl/AmendmentBlockHandler.cpp
|
||||||
impl/ForwardingSource.cpp
|
impl/ForwardingSource.cpp
|
||||||
impl/GrpcSource.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/BackendInterface.hpp"
|
||||||
#include "data/Types.hpp"
|
#include "data/Types.hpp"
|
||||||
#include "etl/ETLHelpers.hpp"
|
#include "etl/ETLHelpers.hpp"
|
||||||
|
#include "etl/MPTHelpers.hpp"
|
||||||
#include "etl/NFTHelpers.hpp"
|
#include "etl/NFTHelpers.hpp"
|
||||||
#include "util/Assert.hpp"
|
#include "util/Assert.hpp"
|
||||||
#include "util/log/Logger.hpp"
|
#include "util/log/Logger.hpp"
|
||||||
@@ -154,6 +155,11 @@ public:
|
|||||||
backend.writeSuccessor(std::move(lastKey_), request_.ledger().sequence(), std::string{obj.key()});
|
backend.writeSuccessor(std::move(lastKey_), request_.ledger().sequence(), std::string{obj.key()});
|
||||||
lastKey_ = obj.key();
|
lastKey_ = obj.key();
|
||||||
backend.writeNFTs(getNFTDataFromObj(request_.ledger().sequence(), obj.key(), obj.data()));
|
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(
|
backend.writeLedgerObject(
|
||||||
std::move(*obj.mutable_key()), request_.ledger().sequence(), std::move(*obj.mutable_data())
|
std::move(*obj.mutable_key()), request_.ledger().sequence(), std::move(*obj.mutable_data())
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include "data/BackendInterface.hpp"
|
#include "data/BackendInterface.hpp"
|
||||||
#include "data/DBHelpers.hpp"
|
#include "data/DBHelpers.hpp"
|
||||||
#include "data/Types.hpp"
|
#include "data/Types.hpp"
|
||||||
|
#include "etl/MPTHelpers.hpp"
|
||||||
#include "etl/NFTHelpers.hpp"
|
#include "etl/NFTHelpers.hpp"
|
||||||
#include "etl/SystemState.hpp"
|
#include "etl/SystemState.hpp"
|
||||||
#include "etl/impl/LedgerFetcher.hpp"
|
#include "etl/impl/LedgerFetcher.hpp"
|
||||||
@@ -55,6 +56,7 @@ struct FormattedTransactionsData {
|
|||||||
std::vector<AccountTransactionsData> accountTxData;
|
std::vector<AccountTransactionsData> accountTxData;
|
||||||
std::vector<NFTTransactionsData> nfTokenTxData;
|
std::vector<NFTTransactionsData> nfTokenTxData;
|
||||||
std::vector<NFTsData> nfTokensData;
|
std::vector<NFTsData> nfTokensData;
|
||||||
|
std::vector<MPTHolderData> mptHoldersData;
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace etl::impl {
|
namespace etl::impl {
|
||||||
@@ -124,6 +126,10 @@ public:
|
|||||||
if (maybeNFT)
|
if (maybeNFT)
|
||||||
result.nfTokensData.push_back(*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());
|
result.accountTxData.emplace_back(txMeta, sttx.getTransactionID());
|
||||||
static constexpr std::size_t KEY_SIZE = 32;
|
static constexpr std::size_t KEY_SIZE = 32;
|
||||||
std::string keyStr{reinterpret_cast<char const*>(sttx.getTransactionID().data()), KEY_SIZE};
|
std::string keyStr{reinterpret_cast<char const*>(sttx.getTransactionID().data()), KEY_SIZE};
|
||||||
@@ -240,6 +246,7 @@ public:
|
|||||||
backend_->writeAccountTransactions(std::move(insertTxResult.accountTxData));
|
backend_->writeAccountTransactions(std::move(insertTxResult.accountTxData));
|
||||||
backend_->writeNFTs(insertTxResult.nfTokensData);
|
backend_->writeNFTs(insertTxResult.nfTokensData);
|
||||||
backend_->writeNFTTransactions(insertTxResult.nfTokenTxData);
|
backend_->writeNFTTransactions(insertTxResult.nfTokenTxData);
|
||||||
|
backend_->writeMPTHolders(insertTxResult.mptHoldersData);
|
||||||
}
|
}
|
||||||
|
|
||||||
backend_->finishWrites(sequence);
|
backend_->finishWrites(sequence);
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ private:
|
|||||||
backend_->writeAccountTransactions(std::move(insertTxResultOp->accountTxData));
|
backend_->writeAccountTransactions(std::move(insertTxResultOp->accountTxData));
|
||||||
backend_->writeNFTs(insertTxResultOp->nfTokensData);
|
backend_->writeNFTs(insertTxResultOp->nfTokensData);
|
||||||
backend_->writeNFTTransactions(insertTxResultOp->nfTokenTxData);
|
backend_->writeNFTTransactions(insertTxResultOp->nfTokenTxData);
|
||||||
|
backend_->writeMPTHolders(insertTxResultOp->mptHoldersData);
|
||||||
|
|
||||||
auto [success, duration] =
|
auto [success, duration] =
|
||||||
::util::timed<std::chrono::duration<double>>([&]() { return backend_->finishWrites(lgrInfo.seq); });
|
::util::timed<std::chrono::duration<double>>([&]() { return backend_->finishWrites(lgrInfo.seq); });
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ TransactionFeed::pub(
|
|||||||
pubObj[JS(meta)] = rpc::toJson(*meta);
|
pubObj[JS(meta)] = rpc::toJson(*meta);
|
||||||
rpc::insertDeliveredAmount(pubObj[JS(meta)].as_object(), tx, meta, txMeta.date);
|
rpc::insertDeliveredAmount(pubObj[JS(meta)].as_object(), tx, meta, txMeta.date);
|
||||||
rpc::insertDeliverMaxAlias(pubObj[txKey].as_object(), version);
|
rpc::insertDeliverMaxAlias(pubObj[txKey].as_object(), version);
|
||||||
|
rpc::insertMPTIssuanceID(pubObj[JS(meta)].as_object(), tx, meta);
|
||||||
|
|
||||||
pubObj[JS(type)] = "transaction";
|
pubObj[JS(type)] = "transaction";
|
||||||
pubObj[JS(validated)] = true;
|
pubObj[JS(validated)] = true;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ target_sources(
|
|||||||
handlers/LedgerEntry.cpp
|
handlers/LedgerEntry.cpp
|
||||||
handlers/LedgerIndex.cpp
|
handlers/LedgerIndex.cpp
|
||||||
handlers/LedgerRange.cpp
|
handlers/LedgerRange.cpp
|
||||||
|
handlers/MPTHolders.cpp
|
||||||
handlers/NFTsByIssuer.cpp
|
handlers/NFTsByIssuer.cpp
|
||||||
handlers/NFTBuyOffers.cpp
|
handlers/NFTBuyOffers.cpp
|
||||||
handlers/NFTHistory.cpp
|
handlers/NFTHistory.cpp
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ toExpandedJson(
|
|||||||
auto metaJson = toJson(*meta);
|
auto metaJson = toJson(*meta);
|
||||||
insertDeliveredAmount(metaJson, txn, meta, blobs.date);
|
insertDeliveredAmount(metaJson, txn, meta, blobs.date);
|
||||||
insertDeliverMaxAlias(txnJson, apiVersion);
|
insertDeliverMaxAlias(txnJson, apiVersion);
|
||||||
|
insertMPTIssuanceID(metaJson, txn, meta);
|
||||||
|
|
||||||
if (nftEnabled == NFTokenjson::ENABLE) {
|
if (nftEnabled == NFTokenjson::ENABLE) {
|
||||||
Json::Value nftJson;
|
Json::Value nftJson;
|
||||||
@@ -314,6 +315,67 @@ insertDeliveredAmount(
|
|||||||
return false;
|
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
|
void
|
||||||
insertDeliverMaxAlias(boost::json::object& txJson, std::uint32_t const apiVersion)
|
insertDeliverMaxAlias(boost::json::object& txJson, std::uint32_t const apiVersion)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -191,6 +191,21 @@ insertDeliveredAmount(
|
|||||||
uint32_t date
|
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
|
* @brief Convert STBase object to JSON
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -89,16 +89,19 @@ checkIsU32Numeric(std::string_view sv)
|
|||||||
return ec == std::errc();
|
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 CustomValidators::Uint256HexStringValidator =
|
||||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||||
if (!value.is_string())
|
return makeHexStringValidator<ripple::uint256>(value, key);
|
||||||
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{};
|
|
||||||
}};
|
}};
|
||||||
|
|
||||||
CustomValidator CustomValidators::LedgerIndexValidator =
|
CustomValidator CustomValidators::LedgerIndexValidator =
|
||||||
|
|||||||
@@ -458,6 +458,21 @@ public:
|
|||||||
[[nodiscard]] bool
|
[[nodiscard]] bool
|
||||||
checkIsU32Numeric(std::string_view sv);
|
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
|
* @brief A group of custom validation functions
|
||||||
*/
|
*/
|
||||||
@@ -492,6 +507,22 @@ struct CustomValidators final {
|
|||||||
*/
|
*/
|
||||||
static CustomValidator AccountMarkerValidator;
|
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.
|
* @brief Provides a commonly used validator for uint256 hex string.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
#include "rpc/handlers/LedgerEntry.hpp"
|
#include "rpc/handlers/LedgerEntry.hpp"
|
||||||
#include "rpc/handlers/LedgerIndex.hpp"
|
#include "rpc/handlers/LedgerIndex.hpp"
|
||||||
#include "rpc/handlers/LedgerRange.hpp"
|
#include "rpc/handlers/LedgerRange.hpp"
|
||||||
|
#include "rpc/handlers/MPTHolders.hpp"
|
||||||
#include "rpc/handlers/NFTBuyOffers.hpp"
|
#include "rpc/handlers/NFTBuyOffers.hpp"
|
||||||
#include "rpc/handlers/NFTHistory.hpp"
|
#include "rpc/handlers/NFTHistory.hpp"
|
||||||
#include "rpc/handlers/NFTInfo.hpp"
|
#include "rpc/handlers/NFTInfo.hpp"
|
||||||
@@ -97,6 +98,7 @@ ProductionHandlerProvider::ProductionHandlerProvider(
|
|||||||
{"ledger_entry", {LedgerEntryHandler{backend}}},
|
{"ledger_entry", {LedgerEntryHandler{backend}}},
|
||||||
{"ledger_index", {LedgerIndexHandler{backend}, true}}, // clio only
|
{"ledger_index", {LedgerIndexHandler{backend}, true}}, // clio only
|
||||||
{"ledger_range", {LedgerRangeHandler{backend}}},
|
{"ledger_range", {LedgerRangeHandler{backend}}},
|
||||||
|
{"mpt_holders", {MPTHoldersHandler{backend}, true}}, // clio only
|
||||||
{"nfts_by_issuer", {NFTsByIssuerHandler{backend}, true}}, // clio only
|
{"nfts_by_issuer", {NFTsByIssuerHandler{backend}, true}}, // clio only
|
||||||
{"nft_history", {NFTHistoryHandler{backend}, true}}, // clio only
|
{"nft_history", {NFTHistoryHandler{backend}, true}}, // clio only
|
||||||
{"nft_buy_offers", {NFTBuyOffersHandler{backend}}},
|
{"nft_buy_offers", {NFTBuyOffersHandler{backend}}},
|
||||||
|
|||||||
@@ -145,6 +145,16 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx)
|
|||||||
}
|
}
|
||||||
} else if (input.oracleNode) {
|
} else if (input.oracleNode) {
|
||||||
key = input.oracleNode.value();
|
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 {
|
} else {
|
||||||
// Must specify 1 of the following fields to indicate what type
|
// Must specify 1 of the following fields to indicate what type
|
||||||
if (ctx.apiVersion == 1)
|
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_create_account_claim_id), ripple::ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID},
|
||||||
{JS(xchain_owned_claim_id), ripple::ltXCHAIN_OWNED_CLAIM_ID},
|
{JS(xchain_owned_claim_id), ripple::ltXCHAIN_OWNED_CLAIM_ID},
|
||||||
{JS(oracle), ripple::ltORACLE},
|
{JS(oracle), ripple::ltORACLE},
|
||||||
|
{JS(mptoken), ripple::ltMPTOKEN},
|
||||||
};
|
};
|
||||||
|
|
||||||
auto const parseBridgeFromJson = [](boost::json::value const& bridgeJson) {
|
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)));
|
input.accountRoot = boost::json::value_to<std::string>(jv.at(JS(account_root)));
|
||||||
} else if (jsonObject.contains(JS(did))) {
|
} else if (jsonObject.contains(JS(did))) {
|
||||||
input.did = boost::json::value_to<std::string>(jv.at(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
|
// no need to check if_object again, validator only allows string or object
|
||||||
else if (jsonObject.contains(JS(directory))) {
|
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))) {
|
} else if (jsonObject.contains(JS(oracle))) {
|
||||||
input.oracleNode = parseOracleFromJson(jv.at(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"))
|
if (jsonObject.contains("include_deleted"))
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ public:
|
|||||||
std::optional<std::string> accountRoot;
|
std::optional<std::string> accountRoot;
|
||||||
// account id to address did object
|
// account id to address did object
|
||||||
std::optional<std::string> did;
|
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
|
// TODO: extract into custom objects, remove json from Input
|
||||||
std::optional<boost::json::object> directory;
|
std::optional<boost::json::object> directory;
|
||||||
std::optional<boost::json::object> offer;
|
std::optional<boost::json::object> offer;
|
||||||
@@ -99,6 +101,7 @@ public:
|
|||||||
std::optional<boost::json::object> depositPreauth;
|
std::optional<boost::json::object> depositPreauth;
|
||||||
std::optional<boost::json::object> ticket;
|
std::optional<boost::json::object> ticket;
|
||||||
std::optional<boost::json::object> amm;
|
std::optional<boost::json::object> amm;
|
||||||
|
std::optional<boost::json::object> mptoken;
|
||||||
std::optional<ripple::STXChainBridge> bridge;
|
std::optional<ripple::STXChainBridge> bridge;
|
||||||
std::optional<std::string> bridgeAccount;
|
std::optional<std::string> bridgeAccount;
|
||||||
std::optional<uint32_t> chainClaimId;
|
std::optional<uint32_t> chainClaimId;
|
||||||
@@ -315,6 +318,35 @@ public:
|
|||||||
},
|
},
|
||||||
meta::WithCustomError{modifiers::ToNumber{}, Status(ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID)}},
|
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{}},
|
{JS(ledger), check::Deprecated{}},
|
||||||
{"include_deleted", validation::Type<bool>{}},
|
{"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(did), ripple::ltDID),
|
||||||
LedgerTypeAttribute::AccountOwnedLedgerType(JS(oracle), ripple::ltORACLE),
|
LedgerTypeAttribute::AccountOwnedLedgerType(JS(oracle), ripple::ltORACLE),
|
||||||
LedgerTypeAttribute::ChainLedgerType(JS(nunl), ripple::ltNEGATIVE_UNL),
|
LedgerTypeAttribute::ChainLedgerType(JS(nunl), ripple::ltNEGATIVE_UNL),
|
||||||
|
LedgerTypeAttribute::DeletionBlockerLedgerType(JS(mpt_issuance), ripple::ltMPTOKEN_ISSUANCE),
|
||||||
|
LedgerTypeAttribute::DeletionBlockerLedgerType(JS(mptoken), ripple::ltMPTOKEN),
|
||||||
};
|
};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -205,4 +205,17 @@ struct MockBackend : public BackendInterface {
|
|||||||
MOCK_METHOD(void, doWriteLedgerObject, (std::string&&, std::uint32_t const, std::string&&), (override));
|
MOCK_METHOD(void, doWriteLedgerObject, (std::string&&, std::uint32_t const, std::string&&), (override));
|
||||||
|
|
||||||
MOCK_METHOD(bool, doFinishWrites, (), (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
|
ripple::STObject
|
||||||
CreateOraclePriceData(
|
CreateOraclePriceData(
|
||||||
uint64_t assetPrice,
|
uint64_t assetPrice,
|
||||||
|
|||||||
@@ -393,6 +393,12 @@ CreateDidObject(std::string_view accountId, std::string_view didDoc, std::string
|
|||||||
[[nodiscard]] ripple::Currency
|
[[nodiscard]] ripple::Currency
|
||||||
CreateLPTCurrency(std::string_view assetCurrency, std::string_view asset2Currency);
|
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
|
[[nodiscard]] ripple::STObject
|
||||||
CreateOraclePriceData(
|
CreateOraclePriceData(
|
||||||
uint64_t assetPrice,
|
uint64_t assetPrice,
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ target_sources(
|
|||||||
rpc/handlers/LedgerIndexTests.cpp
|
rpc/handlers/LedgerIndexTests.cpp
|
||||||
rpc/handlers/LedgerRangeTests.cpp
|
rpc/handlers/LedgerRangeTests.cpp
|
||||||
rpc/handlers/LedgerTests.cpp
|
rpc/handlers/LedgerTests.cpp
|
||||||
|
rpc/handlers/MPTHoldersTests.cpp
|
||||||
rpc/handlers/NFTBuyOffersTests.cpp
|
rpc/handlers/NFTBuyOffersTests.cpp
|
||||||
rpc/handlers/NFTHistoryTests.cpp
|
rpc/handlers/NFTHistoryTests.cpp
|
||||||
rpc/handlers/NFTInfoTests.cpp
|
rpc/handlers/NFTInfoTests.cpp
|
||||||
|
|||||||
@@ -515,6 +515,40 @@ TEST_F(RPCBaseTest, AccountMarkerValidator)
|
|||||||
ASSERT_TRUE(spec.process(passingInput));
|
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)
|
TEST_F(RPCBaseTest, Uint256HexStringValidator)
|
||||||
{
|
{
|
||||||
auto const spec = RpcSpec{{"transaction", CustomValidators::Uint256HexStringValidator}};
|
auto const spec = RpcSpec{{"transaction", CustomValidators::Uint256HexStringValidator}};
|
||||||
|
|||||||
@@ -1626,3 +1626,95 @@ TEST_F(RPCAccountObjectsHandlerTest, LimitMoreThanMax)
|
|||||||
EXPECT_EQ(*output.result, json::parse(expectedOut));
|
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)
|
TEST(RPCLedgerDataHandlerSpecTest, DeprecatedFields)
|
||||||
{
|
{
|
||||||
boost::json::value const json{
|
boost::json::value const json{
|
||||||
|
|||||||
@@ -1759,6 +1759,76 @@ generateTestValuesForParametersTest()
|
|||||||
"malformedRequest",
|
"malformedRequest",
|
||||||
"Malformed request."
|
"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;
|
using RPCLedgerEntryDeathTest = RPCLedgerEntryTest;
|
||||||
|
|
||||||
TEST_F(RPCLedgerEntryDeathTest, RangeNotAvailable)
|
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_claim_id),
|
||||||
JS(xchain_owned_create_account_claim_id),
|
JS(xchain_owned_create_account_claim_id),
|
||||||
JS(did),
|
JS(did),
|
||||||
|
JS(mpt_issuance),
|
||||||
|
JS(mptoken),
|
||||||
JS(oracle),
|
JS(oracle),
|
||||||
JS(nunl)
|
JS(nunl)
|
||||||
};
|
};
|
||||||
@@ -83,7 +85,9 @@ TEST(LedgerUtilsTests, AccountOwnedTypeList)
|
|||||||
JS(xchain_owned_claim_id),
|
JS(xchain_owned_claim_id),
|
||||||
JS(xchain_owned_create_account_claim_id),
|
JS(xchain_owned_create_account_claim_id),
|
||||||
JS(did),
|
JS(did),
|
||||||
JS(oracle)
|
JS(oracle),
|
||||||
|
JS(mpt_issuance),
|
||||||
|
JS(mptoken)
|
||||||
};
|
};
|
||||||
|
|
||||||
static_assert(std::size(correctTypes) == accountOwned.size());
|
static_assert(std::size(correctTypes) == accountOwned.size());
|
||||||
@@ -121,7 +125,9 @@ TEST(LedgerUtilsTests, DeletionBlockerTypes)
|
|||||||
ripple::ltRIPPLE_STATE,
|
ripple::ltRIPPLE_STATE,
|
||||||
ripple::ltXCHAIN_OWNED_CLAIM_ID,
|
ripple::ltXCHAIN_OWNED_CLAIM_ID,
|
||||||
ripple::ltXCHAIN_OWNED_CREATE_ACCOUNT_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());
|
static_assert(std::size(deletionBlockers) == testedTypes.size());
|
||||||
|
|||||||
Reference in New Issue
Block a user