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