mirror of
				https://github.com/XRPLF/clio.git
				synced 2025-11-04 03:45:50 +00:00 
			
		
		
		
	feat: Support account_mptoken_issuances and account_mptokens (#2680)
Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
This commit is contained in:
		@@ -24,6 +24,8 @@ target_sources(
 | 
			
		||||
          handlers/AccountCurrencies.cpp
 | 
			
		||||
          handlers/AccountInfo.cpp
 | 
			
		||||
          handlers/AccountLines.cpp
 | 
			
		||||
          handlers/AccountMPTokenIssuances.cpp
 | 
			
		||||
          handlers/AccountMPTokens.cpp
 | 
			
		||||
          handlers/AccountNFTs.cpp
 | 
			
		||||
          handlers/AccountObjects.cpp
 | 
			
		||||
          handlers/AccountOffers.cpp
 | 
			
		||||
 
 | 
			
		||||
@@ -35,6 +35,8 @@ handledRpcs()
 | 
			
		||||
        "account_currencies",
 | 
			
		||||
        "account_info",
 | 
			
		||||
        "account_lines",
 | 
			
		||||
        "account_mptoken_issuances",
 | 
			
		||||
        "account_mptokens",
 | 
			
		||||
        "account_nfts",
 | 
			
		||||
        "account_objects",
 | 
			
		||||
        "account_offers",
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,8 @@
 | 
			
		||||
#include "rpc/handlers/AccountCurrencies.hpp"
 | 
			
		||||
#include "rpc/handlers/AccountInfo.hpp"
 | 
			
		||||
#include "rpc/handlers/AccountLines.hpp"
 | 
			
		||||
#include "rpc/handlers/AccountMPTokenIssuances.hpp"
 | 
			
		||||
#include "rpc/handlers/AccountMPTokens.hpp"
 | 
			
		||||
#include "rpc/handlers/AccountNFTs.hpp"
 | 
			
		||||
#include "rpc/handlers/AccountObjects.hpp"
 | 
			
		||||
#include "rpc/handlers/AccountOffers.hpp"
 | 
			
		||||
@@ -85,6 +87,9 @@ ProductionHandlerProvider::ProductionHandlerProvider(
 | 
			
		||||
          {"account_currencies", {.handler = AccountCurrenciesHandler{backend}}},
 | 
			
		||||
          {"account_info", {.handler = AccountInfoHandler{backend, amendmentCenter}}},
 | 
			
		||||
          {"account_lines", {.handler = AccountLinesHandler{backend}}},
 | 
			
		||||
          {"account_mptoken_issuances",
 | 
			
		||||
           {.handler = AccountMPTokenIssuancesHandler{backend}, .isClioOnly = true}},              // clio only
 | 
			
		||||
          {"account_mptokens", {.handler = AccountMPTokensHandler{backend}, .isClioOnly = true}},  // clio only
 | 
			
		||||
          {"account_nfts", {.handler = AccountNFTsHandler{backend}}},
 | 
			
		||||
          {"account_objects", {.handler = AccountObjectsHandler{backend}}},
 | 
			
		||||
          {"account_offers", {.handler = AccountOffersHandler{backend}}},
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										237
									
								
								src/rpc/handlers/AccountMPTokenIssuances.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								src/rpc/handlers/AccountMPTokenIssuances.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,237 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2025, 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/AccountMPTokenIssuances.hpp"
 | 
			
		||||
 | 
			
		||||
#include "rpc/Errors.hpp"
 | 
			
		||||
#include "rpc/JS.hpp"
 | 
			
		||||
#include "rpc/RPCHelpers.hpp"
 | 
			
		||||
#include "rpc/common/Types.hpp"
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
#include "util/JsonUtils.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/json/conversion.hpp>
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
#include <boost/json/value.hpp>
 | 
			
		||||
#include <boost/json/value_to.hpp>
 | 
			
		||||
#include <xrpl/basics/strHex.h>
 | 
			
		||||
#include <xrpl/protocol/AccountID.h>
 | 
			
		||||
#include <xrpl/protocol/Indexes.h>
 | 
			
		||||
#include <xrpl/protocol/LedgerFormats.h>
 | 
			
		||||
#include <xrpl/protocol/LedgerHeader.h>
 | 
			
		||||
#include <xrpl/protocol/SField.h>
 | 
			
		||||
#include <xrpl/protocol/STAmount.h>
 | 
			
		||||
#include <xrpl/protocol/STLedgerEntry.h>
 | 
			
		||||
#include <xrpl/protocol/UintTypes.h>
 | 
			
		||||
#include <xrpl/protocol/jss.h>
 | 
			
		||||
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace rpc {
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
AccountMPTokenIssuancesHandler::addMPTokenIssuance(
 | 
			
		||||
    std::vector<MPTokenIssuanceResponse>& issuances,
 | 
			
		||||
    ripple::SLE const& sle,
 | 
			
		||||
    ripple::AccountID const& account
 | 
			
		||||
)
 | 
			
		||||
{
 | 
			
		||||
    MPTokenIssuanceResponse issuance;
 | 
			
		||||
 | 
			
		||||
    issuance.issuer = ripple::to_string(account);
 | 
			
		||||
    issuance.sequence = sle.getFieldU32(ripple::sfSequence);
 | 
			
		||||
    auto const flags = sle.getFieldU32(ripple::sfFlags);
 | 
			
		||||
 | 
			
		||||
    auto const setFlag = [&](std::optional<bool>& field, std::uint32_t mask) {
 | 
			
		||||
        if ((flags & mask) != 0u)
 | 
			
		||||
            field = true;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    setFlag(issuance.mptLocked, ripple::lsfMPTLocked);
 | 
			
		||||
    setFlag(issuance.mptCanLock, ripple::lsfMPTCanLock);
 | 
			
		||||
    setFlag(issuance.mptRequireAuth, ripple::lsfMPTRequireAuth);
 | 
			
		||||
    setFlag(issuance.mptCanEscrow, ripple::lsfMPTCanEscrow);
 | 
			
		||||
    setFlag(issuance.mptCanTrade, ripple::lsfMPTCanTrade);
 | 
			
		||||
    setFlag(issuance.mptCanTransfer, ripple::lsfMPTCanTransfer);
 | 
			
		||||
    setFlag(issuance.mptCanClawback, ripple::lsfMPTCanClawback);
 | 
			
		||||
 | 
			
		||||
    if (sle.isFieldPresent(ripple::sfTransferFee))
 | 
			
		||||
        issuance.transferFee = sle.getFieldU16(ripple::sfTransferFee);
 | 
			
		||||
 | 
			
		||||
    if (sle.isFieldPresent(ripple::sfAssetScale))
 | 
			
		||||
        issuance.assetScale = sle.getFieldU8(ripple::sfAssetScale);
 | 
			
		||||
 | 
			
		||||
    if (sle.isFieldPresent(ripple::sfMaximumAmount))
 | 
			
		||||
        issuance.maximumAmount = sle.getFieldU64(ripple::sfMaximumAmount);
 | 
			
		||||
 | 
			
		||||
    if (sle.isFieldPresent(ripple::sfOutstandingAmount))
 | 
			
		||||
        issuance.outstandingAmount = sle.getFieldU64(ripple::sfOutstandingAmount);
 | 
			
		||||
 | 
			
		||||
    if (sle.isFieldPresent(ripple::sfLockedAmount))
 | 
			
		||||
        issuance.lockedAmount = sle.getFieldU64(ripple::sfLockedAmount);
 | 
			
		||||
 | 
			
		||||
    if (sle.isFieldPresent(ripple::sfMPTokenMetadata))
 | 
			
		||||
        issuance.mptokenMetadata = ripple::strHex(sle.getFieldVL(ripple::sfMPTokenMetadata));
 | 
			
		||||
 | 
			
		||||
    if (sle.isFieldPresent(ripple::sfDomainID))
 | 
			
		||||
        issuance.domainID = ripple::strHex(sle.getFieldH256(ripple::sfDomainID));
 | 
			
		||||
 | 
			
		||||
    issuances.push_back(issuance);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AccountMPTokenIssuancesHandler::Result
 | 
			
		||||
AccountMPTokenIssuancesHandler::process(AccountMPTokenIssuancesHandler::Input const& input, Context const& ctx) const
 | 
			
		||||
{
 | 
			
		||||
    auto const range = sharedPtrBackend_->fetchLedgerRange();
 | 
			
		||||
    ASSERT(range.has_value(), "AccountMPTokenIssuances' ledger range must be available");
 | 
			
		||||
    auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq(
 | 
			
		||||
        *sharedPtrBackend_, ctx.yield, input.ledgerHash, input.ledgerIndex, range->maxSequence
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!expectedLgrInfo.has_value())
 | 
			
		||||
        return Error{expectedLgrInfo.error()};
 | 
			
		||||
 | 
			
		||||
    auto const& lgrInfo = expectedLgrInfo.value();
 | 
			
		||||
    auto const accountID = accountFromStringStrict(input.account);
 | 
			
		||||
    auto const accountLedgerObject =
 | 
			
		||||
        sharedPtrBackend_->fetchLedgerObject(ripple::keylet::account(*accountID).key, lgrInfo.seq, ctx.yield);
 | 
			
		||||
 | 
			
		||||
    if (not accountLedgerObject.has_value())
 | 
			
		||||
        return Error{Status{RippledError::rpcACT_NOT_FOUND}};
 | 
			
		||||
 | 
			
		||||
    Output response;
 | 
			
		||||
    response.issuances.reserve(input.limit);
 | 
			
		||||
 | 
			
		||||
    auto const addToResponse = [&](ripple::SLE const& sle) {
 | 
			
		||||
        if (sle.getType() == ripple::ltMPTOKEN_ISSUANCE) {
 | 
			
		||||
            addMPTokenIssuance(response.issuances, sle, *accountID);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    auto const expectedNext = traverseOwnedNodes(
 | 
			
		||||
        *sharedPtrBackend_, *accountID, lgrInfo.seq, input.limit, input.marker, ctx.yield, addToResponse
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!expectedNext.has_value())
 | 
			
		||||
        return Error{expectedNext.error()};
 | 
			
		||||
 | 
			
		||||
    auto const nextMarker = expectedNext.value();
 | 
			
		||||
 | 
			
		||||
    response.account = input.account;
 | 
			
		||||
    response.limit = input.limit;
 | 
			
		||||
 | 
			
		||||
    response.ledgerHash = ripple::strHex(lgrInfo.hash);
 | 
			
		||||
    response.ledgerIndex = lgrInfo.seq;
 | 
			
		||||
 | 
			
		||||
    if (nextMarker.isNonZero())
 | 
			
		||||
        response.marker = nextMarker.toString();
 | 
			
		||||
 | 
			
		||||
    return response;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AccountMPTokenIssuancesHandler::Input
 | 
			
		||||
tag_invoke(boost::json::value_to_tag<AccountMPTokenIssuancesHandler::Input>, boost::json::value const& jv)
 | 
			
		||||
{
 | 
			
		||||
    auto input = AccountMPTokenIssuancesHandler::Input{};
 | 
			
		||||
    auto const& jsonObject = jv.as_object();
 | 
			
		||||
 | 
			
		||||
    input.account = boost::json::value_to<std::string>(jv.at(JS(account)));
 | 
			
		||||
 | 
			
		||||
    if (jsonObject.contains(JS(limit)))
 | 
			
		||||
        input.limit = util::integralValueAs<uint32_t>(jv.at(JS(limit)));
 | 
			
		||||
 | 
			
		||||
    if (jsonObject.contains(JS(marker)))
 | 
			
		||||
        input.marker = boost::json::value_to<std::string>(jv.at(JS(marker)));
 | 
			
		||||
 | 
			
		||||
    if (jsonObject.contains(JS(ledger_hash)))
 | 
			
		||||
        input.ledgerHash = boost::json::value_to<std::string>(jv.at(JS(ledger_hash)));
 | 
			
		||||
 | 
			
		||||
    if (jsonObject.contains(JS(ledger_index))) {
 | 
			
		||||
        if (!jsonObject.at(JS(ledger_index)).is_string()) {
 | 
			
		||||
            input.ledgerIndex = util::integralValueAs<uint32_t>(jv.at(JS(ledger_index)));
 | 
			
		||||
        } else if (jsonObject.at(JS(ledger_index)).as_string() != "validated") {
 | 
			
		||||
            input.ledgerIndex = std::stoi(boost::json::value_to<std::string>(jv.at(JS(ledger_index))));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return input;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, AccountMPTokenIssuancesHandler::Output const& output)
 | 
			
		||||
{
 | 
			
		||||
    using boost::json::value_from;
 | 
			
		||||
 | 
			
		||||
    auto obj = boost::json::object{
 | 
			
		||||
        {JS(account), output.account},
 | 
			
		||||
        {JS(ledger_hash), output.ledgerHash},
 | 
			
		||||
        {JS(ledger_index), output.ledgerIndex},
 | 
			
		||||
        {JS(validated), output.validated},
 | 
			
		||||
        {JS(limit), output.limit},
 | 
			
		||||
        {"mpt_issuances", value_from(output.issuances)},
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (output.marker.has_value())
 | 
			
		||||
        obj[JS(marker)] = *output.marker;
 | 
			
		||||
 | 
			
		||||
    jv = std::move(obj);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
tag_invoke(
 | 
			
		||||
    boost::json::value_from_tag,
 | 
			
		||||
    boost::json::value& jv,
 | 
			
		||||
    AccountMPTokenIssuancesHandler::MPTokenIssuanceResponse const& issuance
 | 
			
		||||
)
 | 
			
		||||
{
 | 
			
		||||
    auto obj = boost::json::object{
 | 
			
		||||
        {JS(issuer), issuance.issuer},
 | 
			
		||||
        {JS(sequence), issuance.sequence},
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    auto const setIfPresent = [&](boost::json::string_view field, auto const& value) {
 | 
			
		||||
        if (value.has_value()) {
 | 
			
		||||
            obj[field] = *value;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    setIfPresent("transfer_fee", issuance.transferFee);
 | 
			
		||||
    setIfPresent("asset_scale", issuance.assetScale);
 | 
			
		||||
    setIfPresent("maximum_amount", issuance.maximumAmount);
 | 
			
		||||
    setIfPresent("outstanding_amount", issuance.outstandingAmount);
 | 
			
		||||
    setIfPresent("locked_amount", issuance.lockedAmount);
 | 
			
		||||
    setIfPresent("mptoken_metadata", issuance.mptokenMetadata);
 | 
			
		||||
    setIfPresent("domain_id", issuance.domainID);
 | 
			
		||||
 | 
			
		||||
    setIfPresent("mpt_locked", issuance.mptLocked);
 | 
			
		||||
    setIfPresent("mpt_can_lock", issuance.mptCanLock);
 | 
			
		||||
    setIfPresent("mpt_require_auth", issuance.mptRequireAuth);
 | 
			
		||||
    setIfPresent("mpt_can_escrow", issuance.mptCanEscrow);
 | 
			
		||||
    setIfPresent("mpt_can_trade", issuance.mptCanTrade);
 | 
			
		||||
    setIfPresent("mpt_can_transfer", issuance.mptCanTransfer);
 | 
			
		||||
    setIfPresent("mpt_can_clawback", issuance.mptCanClawback);
 | 
			
		||||
 | 
			
		||||
    jv = std::move(obj);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace rpc
 | 
			
		||||
							
								
								
									
										196
									
								
								src/rpc/handlers/AccountMPTokenIssuances.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								src/rpc/handlers/AccountMPTokenIssuances.hpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,196 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2025, 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/Errors.hpp"
 | 
			
		||||
#include "rpc/JS.hpp"
 | 
			
		||||
#include "rpc/common/Checkers.hpp"
 | 
			
		||||
#include "rpc/common/MetaProcessors.hpp"
 | 
			
		||||
#include "rpc/common/Modifiers.hpp"
 | 
			
		||||
#include "rpc/common/Specs.hpp"
 | 
			
		||||
#include "rpc/common/Types.hpp"
 | 
			
		||||
#include "rpc/common/Validators.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/json/conversion.hpp>
 | 
			
		||||
#include <boost/json/value.hpp>
 | 
			
		||||
#include <xrpl/protocol/AccountID.h>
 | 
			
		||||
#include <xrpl/protocol/ErrorCodes.h>
 | 
			
		||||
#include <xrpl/protocol/STLedgerEntry.h>
 | 
			
		||||
#include <xrpl/protocol/jss.h>
 | 
			
		||||
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace rpc {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief The account_mptoken_issuances method returns information about all MPTokenIssuance objects the account has
 | 
			
		||||
 * created.
 | 
			
		||||
 */
 | 
			
		||||
class AccountMPTokenIssuancesHandler {
 | 
			
		||||
    // dependencies
 | 
			
		||||
    std::shared_ptr<BackendInterface> sharedPtrBackend_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    static constexpr auto kLIMIT_MIN = 10;
 | 
			
		||||
    static constexpr auto kLIMIT_MAX = 400;
 | 
			
		||||
    static constexpr auto kLIMIT_DEFAULT = 200;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief A struct to hold data for one MPTokenIssuance response.
 | 
			
		||||
     */
 | 
			
		||||
    struct MPTokenIssuanceResponse {
 | 
			
		||||
        std::string issuer;
 | 
			
		||||
        uint32_t sequence{};
 | 
			
		||||
 | 
			
		||||
        std::optional<uint16_t> transferFee{};
 | 
			
		||||
        std::optional<uint8_t> assetScale{};
 | 
			
		||||
 | 
			
		||||
        std::optional<std::uint64_t> maximumAmount;
 | 
			
		||||
        std::optional<std::uint64_t> outstandingAmount;
 | 
			
		||||
        std::optional<std::uint64_t> lockedAmount;
 | 
			
		||||
        std::optional<std::string> mptokenMetadata;
 | 
			
		||||
        std::optional<std::string> domainID;
 | 
			
		||||
 | 
			
		||||
        std::optional<bool> mptLocked;
 | 
			
		||||
        std::optional<bool> mptCanLock;
 | 
			
		||||
        std::optional<bool> mptRequireAuth;
 | 
			
		||||
        std::optional<bool> mptCanEscrow;
 | 
			
		||||
        std::optional<bool> mptCanTrade;
 | 
			
		||||
        std::optional<bool> mptCanTransfer;
 | 
			
		||||
        std::optional<bool> mptCanClawback;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief A struct to hold the output data of the command.
 | 
			
		||||
     */
 | 
			
		||||
    struct Output {
 | 
			
		||||
        std::string account;
 | 
			
		||||
        std::vector<MPTokenIssuanceResponse> issuances;
 | 
			
		||||
        std::string ledgerHash;
 | 
			
		||||
        uint32_t ledgerIndex{};
 | 
			
		||||
        bool validated = true;
 | 
			
		||||
        std::optional<std::string> marker;
 | 
			
		||||
        uint32_t limit{};
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief A struct to hold the input data for the command.
 | 
			
		||||
     */
 | 
			
		||||
    struct Input {
 | 
			
		||||
        std::string account;
 | 
			
		||||
        std::optional<std::string> ledgerHash;
 | 
			
		||||
        std::optional<uint32_t> ledgerIndex;
 | 
			
		||||
        uint32_t limit = kLIMIT_DEFAULT;
 | 
			
		||||
        std::optional<std::string> marker;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    using Result = HandlerReturnType<Output>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Construct a new AccountMPTokenIssuancesHandler object.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sharedPtrBackend The backend to use.
 | 
			
		||||
     */
 | 
			
		||||
    AccountMPTokenIssuancesHandler(std::shared_ptr<BackendInterface> sharedPtrBackend)
 | 
			
		||||
        : sharedPtrBackend_(std::move(sharedPtrBackend))
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Returns the API specification for the command.
 | 
			
		||||
     *
 | 
			
		||||
     * @param apiVersion The API version to return the spec for.
 | 
			
		||||
     * @return The spec for the given API version.
 | 
			
		||||
     */
 | 
			
		||||
    static RpcSpecConstRef
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion)
 | 
			
		||||
    {
 | 
			
		||||
        static auto const kRPC_SPEC = RpcSpec{
 | 
			
		||||
            {JS(account),
 | 
			
		||||
             validation::Required{},
 | 
			
		||||
             meta::WithCustomError{
 | 
			
		||||
                 validation::CustomValidators::accountValidator, Status(RippledError::rpcACT_MALFORMED)
 | 
			
		||||
             }},
 | 
			
		||||
            {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator},
 | 
			
		||||
            {JS(limit),
 | 
			
		||||
             validation::Type<uint32_t>{},
 | 
			
		||||
             validation::Min(1u),
 | 
			
		||||
             modifiers::Clamp<int32_t>{kLIMIT_MIN, kLIMIT_MAX}},
 | 
			
		||||
            {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator},
 | 
			
		||||
            {JS(marker), validation::CustomValidators::accountMarkerValidator},
 | 
			
		||||
            {JS(ledger), check::Deprecated{}},
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return kRPC_SPEC;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Process the AccountMPTokenIssuances 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 const& input, Context const& ctx) const;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    static void
 | 
			
		||||
    addMPTokenIssuance(
 | 
			
		||||
        std::vector<MPTokenIssuanceResponse>& issuances,
 | 
			
		||||
        ripple::SLE const& sle,
 | 
			
		||||
        ripple::AccountID const& account
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Convert the MPTokenIssuanceResponse to a JSON object
 | 
			
		||||
     *
 | 
			
		||||
     * @param [out] jv The JSON object to convert to
 | 
			
		||||
     * @param issuance The MPTokenIssuance response to convert
 | 
			
		||||
     */
 | 
			
		||||
    friend void
 | 
			
		||||
    tag_invoke(boost::json::value_from_tag, boost::json::value& jv, MPTokenIssuanceResponse const& issuance);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace rpc
 | 
			
		||||
							
								
								
									
										191
									
								
								src/rpc/handlers/AccountMPTokens.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								src/rpc/handlers/AccountMPTokens.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,191 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2025, 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/AccountMPTokens.hpp"
 | 
			
		||||
 | 
			
		||||
#include "rpc/Errors.hpp"
 | 
			
		||||
#include "rpc/JS.hpp"
 | 
			
		||||
#include "rpc/RPCHelpers.hpp"
 | 
			
		||||
#include "rpc/common/Types.hpp"
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
#include "util/JsonUtils.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/json/conversion.hpp>
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
#include <boost/json/value.hpp>
 | 
			
		||||
#include <boost/json/value_to.hpp>
 | 
			
		||||
#include <xrpl/basics/strHex.h>
 | 
			
		||||
#include <xrpl/protocol/AccountID.h>
 | 
			
		||||
#include <xrpl/protocol/Indexes.h>
 | 
			
		||||
#include <xrpl/protocol/LedgerFormats.h>
 | 
			
		||||
#include <xrpl/protocol/LedgerHeader.h>
 | 
			
		||||
#include <xrpl/protocol/SField.h>
 | 
			
		||||
#include <xrpl/protocol/STAmount.h>
 | 
			
		||||
#include <xrpl/protocol/STLedgerEntry.h>
 | 
			
		||||
#include <xrpl/protocol/UintTypes.h>
 | 
			
		||||
#include <xrpl/protocol/jss.h>
 | 
			
		||||
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace rpc {
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
AccountMPTokensHandler::addMPToken(std::vector<MPTokenResponse>& mpts, ripple::SLE const& sle)
 | 
			
		||||
{
 | 
			
		||||
    MPTokenResponse token{};
 | 
			
		||||
    auto const flags = sle.getFieldU32(ripple::sfFlags);
 | 
			
		||||
 | 
			
		||||
    token.account = ripple::to_string(sle.getAccountID(ripple::sfAccount));
 | 
			
		||||
    token.MPTokenIssuanceID = ripple::strHex(sle.getFieldH192(ripple::sfMPTokenIssuanceID));
 | 
			
		||||
    token.MPTAmount = sle.getFieldU64(ripple::sfMPTAmount);
 | 
			
		||||
 | 
			
		||||
    if (sle.isFieldPresent(ripple::sfLockedAmount))
 | 
			
		||||
        token.lockedAmount = sle.getFieldU64(ripple::sfLockedAmount);
 | 
			
		||||
 | 
			
		||||
    auto const setFlag = [&](std::optional<bool>& field, std::uint32_t mask) {
 | 
			
		||||
        if ((flags & mask) != 0u)
 | 
			
		||||
            field = true;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    setFlag(token.mptLocked, ripple::lsfMPTLocked);
 | 
			
		||||
    setFlag(token.mptAuthorized, ripple::lsfMPTAuthorized);
 | 
			
		||||
 | 
			
		||||
    mpts.push_back(token);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AccountMPTokensHandler::Result
 | 
			
		||||
AccountMPTokensHandler::process(AccountMPTokensHandler::Input const& input, Context const& ctx) const
 | 
			
		||||
{
 | 
			
		||||
    auto const range = sharedPtrBackend_->fetchLedgerRange();
 | 
			
		||||
    ASSERT(range.has_value(), "AccountMPTokens' ledger range must be available");
 | 
			
		||||
    auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq(
 | 
			
		||||
        *sharedPtrBackend_, ctx.yield, input.ledgerHash, input.ledgerIndex, range->maxSequence
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!expectedLgrInfo.has_value())
 | 
			
		||||
        return Error{expectedLgrInfo.error()};
 | 
			
		||||
 | 
			
		||||
    auto const& lgrInfo = expectedLgrInfo.value();
 | 
			
		||||
    auto const accountID = accountFromStringStrict(input.account);
 | 
			
		||||
    auto const accountLedgerObject =
 | 
			
		||||
        sharedPtrBackend_->fetchLedgerObject(ripple::keylet::account(*accountID).key, lgrInfo.seq, ctx.yield);
 | 
			
		||||
 | 
			
		||||
    if (not accountLedgerObject.has_value())
 | 
			
		||||
        return Error{Status{RippledError::rpcACT_NOT_FOUND}};
 | 
			
		||||
 | 
			
		||||
    Output response;
 | 
			
		||||
    response.mpts.reserve(input.limit);
 | 
			
		||||
 | 
			
		||||
    auto const addToResponse = [&](ripple::SLE const& sle) {
 | 
			
		||||
        if (sle.getType() == ripple::ltMPTOKEN) {
 | 
			
		||||
            addMPToken(response.mpts, sle);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    auto const expectedNext = traverseOwnedNodes(
 | 
			
		||||
        *sharedPtrBackend_, *accountID, lgrInfo.seq, input.limit, input.marker, ctx.yield, addToResponse
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!expectedNext.has_value())
 | 
			
		||||
        return Error{expectedNext.error()};
 | 
			
		||||
 | 
			
		||||
    auto const& nextMarker = expectedNext.value();
 | 
			
		||||
 | 
			
		||||
    response.account = input.account;
 | 
			
		||||
    response.limit = input.limit;
 | 
			
		||||
 | 
			
		||||
    response.ledgerHash = ripple::strHex(lgrInfo.hash);
 | 
			
		||||
    response.ledgerIndex = lgrInfo.seq;
 | 
			
		||||
 | 
			
		||||
    if (nextMarker.isNonZero())
 | 
			
		||||
        response.marker = nextMarker.toString();
 | 
			
		||||
 | 
			
		||||
    return response;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AccountMPTokensHandler::Input
 | 
			
		||||
tag_invoke(boost::json::value_to_tag<AccountMPTokensHandler::Input>, boost::json::value const& jv)
 | 
			
		||||
{
 | 
			
		||||
    AccountMPTokensHandler::Input input{};
 | 
			
		||||
    auto const& jsonObject = jv.as_object();
 | 
			
		||||
 | 
			
		||||
    input.account = boost::json::value_to<std::string>(jv.at(JS(account)));
 | 
			
		||||
 | 
			
		||||
    if (jsonObject.contains(JS(limit)))
 | 
			
		||||
        input.limit = util::integralValueAs<uint32_t>(jv.at(JS(limit)));
 | 
			
		||||
    if (jsonObject.contains(JS(marker)))
 | 
			
		||||
        input.marker = boost::json::value_to<std::string>(jv.at(JS(marker)));
 | 
			
		||||
    if (jsonObject.contains(JS(ledger_hash)))
 | 
			
		||||
        input.ledgerHash = boost::json::value_to<std::string>(jv.at(JS(ledger_hash)));
 | 
			
		||||
    if (jsonObject.contains(JS(ledger_index))) {
 | 
			
		||||
        if (!jv.at(JS(ledger_index)).is_string()) {
 | 
			
		||||
            input.ledgerIndex = util::integralValueAs<uint32_t>(jv.at(JS(ledger_index)));
 | 
			
		||||
        } else if (boost::json::value_to<std::string>(jv.at(JS(ledger_index))) != "validated") {
 | 
			
		||||
            input.ledgerIndex = std::stoi(boost::json::value_to<std::string>(jv.at(JS(ledger_index))));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return input;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, AccountMPTokensHandler::Output const& output)
 | 
			
		||||
{
 | 
			
		||||
    auto obj = boost::json::object{
 | 
			
		||||
        {JS(account), output.account},
 | 
			
		||||
        {JS(ledger_hash), output.ledgerHash},
 | 
			
		||||
        {JS(ledger_index), output.ledgerIndex},
 | 
			
		||||
        {JS(validated), output.validated},
 | 
			
		||||
        {JS(limit), output.limit},
 | 
			
		||||
        {"mptokens", boost::json::value_from(output.mpts)},
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (output.marker.has_value())
 | 
			
		||||
        obj[JS(marker)] = *output.marker;
 | 
			
		||||
 | 
			
		||||
    jv = std::move(obj);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, AccountMPTokensHandler::MPTokenResponse const& mptoken)
 | 
			
		||||
{
 | 
			
		||||
    auto obj = boost::json::object{
 | 
			
		||||
        {JS(account), mptoken.account},
 | 
			
		||||
        {JS(mpt_issuance_id), mptoken.MPTokenIssuanceID},
 | 
			
		||||
        {JS(mpt_amount), mptoken.MPTAmount},
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    auto const setIfPresent = [&](boost::json::string_view field, auto const& value) {
 | 
			
		||||
        if (value.has_value()) {
 | 
			
		||||
            obj[field] = *value;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    setIfPresent("locked_amount", mptoken.lockedAmount);
 | 
			
		||||
    setIfPresent("mpt_locked", mptoken.mptLocked);
 | 
			
		||||
    setIfPresent("mpt_authorized", mptoken.mptAuthorized);
 | 
			
		||||
 | 
			
		||||
    jv = std::move(obj);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace rpc
 | 
			
		||||
							
								
								
									
										178
									
								
								src/rpc/handlers/AccountMPTokens.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								src/rpc/handlers/AccountMPTokens.hpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,178 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2025, 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/Errors.hpp"
 | 
			
		||||
#include "rpc/JS.hpp"
 | 
			
		||||
#include "rpc/common/Checkers.hpp"
 | 
			
		||||
#include "rpc/common/MetaProcessors.hpp"
 | 
			
		||||
#include "rpc/common/Modifiers.hpp"
 | 
			
		||||
#include "rpc/common/Specs.hpp"
 | 
			
		||||
#include "rpc/common/Types.hpp"
 | 
			
		||||
#include "rpc/common/Validators.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/json/conversion.hpp>
 | 
			
		||||
#include <boost/json/value.hpp>
 | 
			
		||||
#include <xrpl/protocol/AccountID.h>
 | 
			
		||||
#include <xrpl/protocol/ErrorCodes.h>
 | 
			
		||||
#include <xrpl/protocol/STLedgerEntry.h>
 | 
			
		||||
#include <xrpl/protocol/jss.h>
 | 
			
		||||
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace rpc {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief The account_mptokens method returns information about the MPTokens the account currently holds.
 | 
			
		||||
 */
 | 
			
		||||
class AccountMPTokensHandler {
 | 
			
		||||
    // dependencies
 | 
			
		||||
    std::shared_ptr<BackendInterface> sharedPtrBackend_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    static constexpr auto kLIMIT_MIN = 10;
 | 
			
		||||
    static constexpr auto kLIMIT_MAX = 400;
 | 
			
		||||
    static constexpr auto kLIMIT_DEFAULT = 200;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief A struct to hold data for one MPToken response.
 | 
			
		||||
     */
 | 
			
		||||
    struct MPTokenResponse {
 | 
			
		||||
        std::string account;
 | 
			
		||||
        std::string MPTokenIssuanceID;
 | 
			
		||||
        uint64_t MPTAmount{};
 | 
			
		||||
        std::optional<uint64_t> lockedAmount;
 | 
			
		||||
 | 
			
		||||
        std::optional<bool> mptLocked;
 | 
			
		||||
        std::optional<bool> mptAuthorized;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief A struct to hold the output data of the command.
 | 
			
		||||
     */
 | 
			
		||||
    struct Output {
 | 
			
		||||
        std::string account;
 | 
			
		||||
        std::vector<MPTokenResponse> mpts;
 | 
			
		||||
        std::string ledgerHash;
 | 
			
		||||
        uint32_t ledgerIndex{};
 | 
			
		||||
        bool validated = true;
 | 
			
		||||
        std::optional<std::string> marker;
 | 
			
		||||
        uint32_t limit{};
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief A struct to hold the input data for the command.
 | 
			
		||||
     */
 | 
			
		||||
    struct Input {
 | 
			
		||||
        std::string account;
 | 
			
		||||
        std::optional<std::string> ledgerHash;
 | 
			
		||||
        std::optional<uint32_t> ledgerIndex;
 | 
			
		||||
        uint32_t limit = kLIMIT_DEFAULT;
 | 
			
		||||
        std::optional<std::string> marker;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    using Result = HandlerReturnType<Output>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Construct a new AccountMPTokensHandler object.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sharedPtrBackend The backend to use.
 | 
			
		||||
     */
 | 
			
		||||
    AccountMPTokensHandler(std::shared_ptr<BackendInterface> sharedPtrBackend)
 | 
			
		||||
        : sharedPtrBackend_(std::move(sharedPtrBackend))
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Returns the API specification for the command.
 | 
			
		||||
     *
 | 
			
		||||
     * @param apiVersion The API version to return the spec for.
 | 
			
		||||
     * @return The spec for the given API version.
 | 
			
		||||
     */
 | 
			
		||||
    static RpcSpecConstRef
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion)
 | 
			
		||||
    {
 | 
			
		||||
        static auto const kRPC_SPEC = RpcSpec{
 | 
			
		||||
            {JS(account),
 | 
			
		||||
             validation::Required{},
 | 
			
		||||
             meta::WithCustomError{
 | 
			
		||||
                 validation::CustomValidators::accountValidator, Status(RippledError::rpcACT_MALFORMED)
 | 
			
		||||
             }},
 | 
			
		||||
            {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator},
 | 
			
		||||
            {JS(limit),
 | 
			
		||||
             validation::Type<uint32_t>{},
 | 
			
		||||
             validation::Min(1u),
 | 
			
		||||
             modifiers::Clamp<int32_t>{kLIMIT_MIN, kLIMIT_MAX}},
 | 
			
		||||
            {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator},
 | 
			
		||||
            {JS(marker), validation::CustomValidators::accountMarkerValidator},
 | 
			
		||||
            {JS(ledger), check::Deprecated{}},
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return kRPC_SPEC;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Process the AccountMPTokens 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 const& input, Context const& ctx) const;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    static void
 | 
			
		||||
    addMPToken(std::vector<MPTokenResponse>& mpts, ripple::SLE const& sle);
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Convert the MPTokenResponse to a JSON object
 | 
			
		||||
     *
 | 
			
		||||
     * @param [out] jv The JSON object to convert to
 | 
			
		||||
     * @param mptoken The MPToken response to convert
 | 
			
		||||
     */
 | 
			
		||||
    friend void
 | 
			
		||||
    tag_invoke(boost::json::value_from_tag, boost::json::value& jv, MPTokenResponse const& mptoken);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace rpc
 | 
			
		||||
@@ -1442,38 +1442,69 @@ createLptCurrency(std::string_view assetCurrency, std::string_view asset2Currenc
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ripple::STObject
 | 
			
		||||
createMptIssuanceObject(std::string_view accountId, std::uint32_t seq, std::string_view metadata)
 | 
			
		||||
createMptIssuanceObject(
 | 
			
		||||
    std::string_view accountId,
 | 
			
		||||
    std::uint32_t seq,
 | 
			
		||||
    std::optional<std::string_view> metadata,
 | 
			
		||||
    std::uint32_t flags,
 | 
			
		||||
    std::uint64_t outstandingAmount,
 | 
			
		||||
    std::optional<std::uint16_t> transferFee,
 | 
			
		||||
    std::optional<std::uint8_t> assetScale,
 | 
			
		||||
    std::optional<std::uint64_t> maxAmount,
 | 
			
		||||
    std::optional<std::uint64_t> lockedAmount,
 | 
			
		||||
    std::optional<std::string_view> domainId
 | 
			
		||||
)
 | 
			
		||||
{
 | 
			
		||||
    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::sfFlags, flags);
 | 
			
		||||
    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);
 | 
			
		||||
    mptIssuance.setFieldU64(ripple::sfOutstandingAmount, outstandingAmount);
 | 
			
		||||
 | 
			
		||||
    if (transferFee.has_value())
 | 
			
		||||
        mptIssuance.setFieldU16(ripple::sfTransferFee, *transferFee);
 | 
			
		||||
    if (assetScale.has_value())
 | 
			
		||||
        mptIssuance.setFieldU8(ripple::sfAssetScale, *assetScale);
 | 
			
		||||
    if (maxAmount.has_value())
 | 
			
		||||
        mptIssuance.setFieldU64(ripple::sfMaximumAmount, *maxAmount);
 | 
			
		||||
    if (lockedAmount.has_value())
 | 
			
		||||
        mptIssuance.setFieldU64(ripple::sfLockedAmount, *lockedAmount);
 | 
			
		||||
    if (metadata.has_value()) {
 | 
			
		||||
        ripple::Slice const sliceMetadata(metadata->data(), metadata->size());
 | 
			
		||||
        mptIssuance.setFieldVL(ripple::sfMPTokenMetadata, sliceMetadata);
 | 
			
		||||
    }
 | 
			
		||||
    if (domainId.has_value())
 | 
			
		||||
        mptIssuance.setFieldH256(ripple::sfDomainID, ripple::uint256{*domainId});
 | 
			
		||||
 | 
			
		||||
    return mptIssuance;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ripple::STObject
 | 
			
		||||
createMpTokenObject(std::string_view accountId, ripple::uint192 issuanceID, std::uint64_t mptAmount)
 | 
			
		||||
createMpTokenObject(
 | 
			
		||||
    std::string_view accountId,
 | 
			
		||||
    ripple::uint192 issuanceID,
 | 
			
		||||
    std::uint64_t mptAmount,
 | 
			
		||||
    std::uint32_t flags,
 | 
			
		||||
    std::optional<uint64_t> lockedAmount
 | 
			
		||||
)
 | 
			
		||||
{
 | 
			
		||||
    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.setFieldU32(ripple::sfFlags, flags);
 | 
			
		||||
    mptoken.setFieldU64(ripple::sfOwnerNode, 0);
 | 
			
		||||
    mptoken.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{});
 | 
			
		||||
    mptoken.setFieldU32(ripple::sfPreviousTxnLgrSeq, 0);
 | 
			
		||||
 | 
			
		||||
    if (mptAmount != 0u)
 | 
			
		||||
        mptoken.setFieldU64(ripple::sfMPTAmount, mptAmount);
 | 
			
		||||
    if (lockedAmount.has_value())
 | 
			
		||||
        mptoken.setFieldU64(ripple::sfLockedAmount, *lockedAmount);
 | 
			
		||||
 | 
			
		||||
    return mptoken;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -451,10 +451,27 @@ createDidObject(std::string_view accountId, std::string_view didDoc, std::string
 | 
			
		||||
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);
 | 
			
		||||
createMptIssuanceObject(
 | 
			
		||||
    std::string_view accountId,
 | 
			
		||||
    std::uint32_t seq,
 | 
			
		||||
    std::optional<std::string_view> metadata = std::nullopt,
 | 
			
		||||
    std::uint32_t flags = 0,
 | 
			
		||||
    std::uint64_t outstandingAmount = 0,
 | 
			
		||||
    std::optional<std::uint16_t> transferFee = std::nullopt,
 | 
			
		||||
    std::optional<std::uint8_t> assetScale = std::nullopt,
 | 
			
		||||
    std::optional<std::uint64_t> maxAmount = std::nullopt,
 | 
			
		||||
    std::optional<std::uint64_t> lockedAmount = std::nullopt,
 | 
			
		||||
    std::optional<std::string_view> domainId = std::nullopt
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
[[nodiscard]] ripple::STObject
 | 
			
		||||
createMpTokenObject(std::string_view accountId, ripple::uint192 issuanceID, std::uint64_t mptAmount = 1);
 | 
			
		||||
createMpTokenObject(
 | 
			
		||||
    std::string_view accountId,
 | 
			
		||||
    ripple::uint192 issuanceID,
 | 
			
		||||
    std::uint64_t mptAmount = 1,
 | 
			
		||||
    std::uint32_t flags = 0,
 | 
			
		||||
    std::optional<uint64_t> lockedAmount = std::nullopt
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
[[nodiscard]] ripple::STObject
 | 
			
		||||
createMPTIssuanceCreateTx(std::string_view accountId, uint32_t fee, uint32_t seq);
 | 
			
		||||
 
 | 
			
		||||
@@ -100,6 +100,8 @@ target_sources(
 | 
			
		||||
          rpc/handlers/AccountCurrenciesTests.cpp
 | 
			
		||||
          rpc/handlers/AccountInfoTests.cpp
 | 
			
		||||
          rpc/handlers/AccountLinesTests.cpp
 | 
			
		||||
          rpc/handlers/AccountMPTokenIssuancesTests.cpp
 | 
			
		||||
          rpc/handlers/AccountMPTokensTests.cpp
 | 
			
		||||
          rpc/handlers/AccountNFTsTests.cpp
 | 
			
		||||
          rpc/handlers/AccountObjectsTests.cpp
 | 
			
		||||
          rpc/handlers/AccountOffersTests.cpp
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										830
									
								
								tests/unit/rpc/handlers/AccountMPTokenIssuancesTests.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										830
									
								
								tests/unit/rpc/handlers/AccountMPTokenIssuancesTests.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,830 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2025, 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/AccountMPTokenIssuances.hpp"
 | 
			
		||||
#include "util/HandlerBaseTestFixture.hpp"
 | 
			
		||||
#include "util/NameGenerator.hpp"
 | 
			
		||||
#include "util/TestObject.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/json/parse.hpp>
 | 
			
		||||
#include <boost/json/value.hpp>
 | 
			
		||||
#include <boost/json/value_to.hpp>
 | 
			
		||||
#include <fmt/format.h>
 | 
			
		||||
#include <gmock/gmock.h>
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
#include <xrpl/basics/base_uint.h>
 | 
			
		||||
#include <xrpl/protocol/Indexes.h>
 | 
			
		||||
#include <xrpl/protocol/LedgerFormats.h>
 | 
			
		||||
#include <xrpl/protocol/LedgerHeader.h>
 | 
			
		||||
#include <xrpl/protocol/SField.h>
 | 
			
		||||
#include <xrpl/protocol/STObject.h>
 | 
			
		||||
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
using namespace rpc;
 | 
			
		||||
using namespace data;
 | 
			
		||||
namespace json = boost::json;
 | 
			
		||||
using namespace testing;
 | 
			
		||||
 | 
			
		||||
namespace {
 | 
			
		||||
 | 
			
		||||
constexpr auto kLEDGER_HASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
 | 
			
		||||
constexpr auto kACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
 | 
			
		||||
constexpr auto kISSUANCE_INDEX1 = "A6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321";
 | 
			
		||||
constexpr auto kISSUANCE_INDEX2 = "B6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC322";
 | 
			
		||||
 | 
			
		||||
// unique values for issuance1
 | 
			
		||||
constexpr uint64_t kISSUANCE1_MAX_AMOUNT = 10000;
 | 
			
		||||
constexpr uint64_t kISSUANCE1_OUTSTANDING_AMOUNT = 5000;
 | 
			
		||||
constexpr uint8_t kISSUANCE1_ASSET_SCALE = 2;
 | 
			
		||||
 | 
			
		||||
// unique values for issuance2
 | 
			
		||||
constexpr uint64_t kISSUANCE2_MAX_AMOUNT = 20000;
 | 
			
		||||
constexpr uint64_t kISSUANCE2_OUTSTANDING_AMOUNT = 800;
 | 
			
		||||
constexpr uint64_t kISSUANCE2_LOCKED_AMOUNT = 100;
 | 
			
		||||
constexpr uint16_t kISSUANCE2_TRANSFER_FEE = 5;
 | 
			
		||||
constexpr auto kISSUANCE2_METADATA = "test-meta";
 | 
			
		||||
constexpr auto kISSUANCE2_METADATA_HEX = "746573742D6D657461";
 | 
			
		||||
constexpr auto kISSUANCE2_DOMAIN_ID_HEX = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321";
 | 
			
		||||
 | 
			
		||||
// define expected JSON for mpt issuances
 | 
			
		||||
auto const kISSUANCE_OUT1 = fmt::format(
 | 
			
		||||
    R"JSON({{
 | 
			
		||||
        "issuer": "{}",
 | 
			
		||||
        "sequence": 1,
 | 
			
		||||
        "maximum_amount": {},
 | 
			
		||||
        "outstanding_amount": {},
 | 
			
		||||
        "asset_scale": {},
 | 
			
		||||
        "mpt_can_escrow": true,
 | 
			
		||||
        "mpt_can_trade": true,
 | 
			
		||||
        "mpt_require_auth": true,
 | 
			
		||||
        "mpt_can_transfer": true
 | 
			
		||||
    }})JSON",
 | 
			
		||||
    kACCOUNT,
 | 
			
		||||
    kISSUANCE1_MAX_AMOUNT,
 | 
			
		||||
    kISSUANCE1_OUTSTANDING_AMOUNT,
 | 
			
		||||
    kISSUANCE1_ASSET_SCALE
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
auto const kISSUANCE_OUT2 = fmt::format(
 | 
			
		||||
    R"JSON({{
 | 
			
		||||
        "issuer": "{}",
 | 
			
		||||
        "sequence": 2,
 | 
			
		||||
        "maximum_amount": {},
 | 
			
		||||
        "outstanding_amount": {},
 | 
			
		||||
        "locked_amount": {},
 | 
			
		||||
        "transfer_fee": {},
 | 
			
		||||
        "mptoken_metadata": "{}",
 | 
			
		||||
        "domain_id": "{}",
 | 
			
		||||
        "mpt_can_lock": true,
 | 
			
		||||
        "mpt_locked": true,
 | 
			
		||||
        "mpt_can_clawback": true
 | 
			
		||||
    }})JSON",
 | 
			
		||||
    kACCOUNT,
 | 
			
		||||
    kISSUANCE2_MAX_AMOUNT,
 | 
			
		||||
    kISSUANCE2_OUTSTANDING_AMOUNT,
 | 
			
		||||
    kISSUANCE2_LOCKED_AMOUNT,
 | 
			
		||||
    kISSUANCE2_TRANSFER_FEE,
 | 
			
		||||
    kISSUANCE2_METADATA_HEX,
 | 
			
		||||
    kISSUANCE2_DOMAIN_ID_HEX
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
}  // namespace
 | 
			
		||||
 | 
			
		||||
struct RPCAccountMPTokenIssuancesHandlerTest : HandlerBaseTest {
 | 
			
		||||
    RPCAccountMPTokenIssuancesHandlerTest()
 | 
			
		||||
    {
 | 
			
		||||
        backend_->setRange(10, 30);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct AccountMPTokenIssuancesParamTestCaseBundle {
 | 
			
		||||
    std::string testName;
 | 
			
		||||
    std::string testJson;
 | 
			
		||||
    std::string expectedError;
 | 
			
		||||
    std::string expectedErrorMessage;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct AccountMPTokenIssuancesParameterTest : RPCAccountMPTokenIssuancesHandlerTest,
 | 
			
		||||
                                              WithParamInterface<AccountMPTokenIssuancesParamTestCaseBundle> {};
 | 
			
		||||
 | 
			
		||||
// generate values for invalid params test
 | 
			
		||||
static auto
 | 
			
		||||
generateTestValuesForInvalidParamsTest()
 | 
			
		||||
{
 | 
			
		||||
    return std::vector<AccountMPTokenIssuancesParamTestCaseBundle>{
 | 
			
		||||
        {"NonHexLedgerHash",
 | 
			
		||||
         fmt::format(R"JSON({{ "account": "{}", "ledger_hash": "xxx" }})JSON", kACCOUNT),
 | 
			
		||||
         "invalidParams",
 | 
			
		||||
         "ledger_hashMalformed"},
 | 
			
		||||
        {"NonStringLedgerHash",
 | 
			
		||||
         fmt::format(R"JSON({{ "account": "{}", "ledger_hash": 123 }})JSON", kACCOUNT),
 | 
			
		||||
         "invalidParams",
 | 
			
		||||
         "ledger_hashNotString"},
 | 
			
		||||
        {"InvalidLedgerIndexString",
 | 
			
		||||
         fmt::format(R"JSON({{ "account": "{}", "ledger_index": "notvalidated" }})JSON", kACCOUNT),
 | 
			
		||||
         "invalidParams",
 | 
			
		||||
         "ledgerIndexMalformed"},
 | 
			
		||||
        {"MarkerNotString",
 | 
			
		||||
         fmt::format(R"JSON({{ "account": "{}", "marker": 9 }})JSON", kACCOUNT),
 | 
			
		||||
         "invalidParams",
 | 
			
		||||
         "markerNotString"},
 | 
			
		||||
        {"InvalidMarkerContent",
 | 
			
		||||
         fmt::format(R"JSON({{ "account": "{}", "marker": "123invalid" }})JSON", kACCOUNT),
 | 
			
		||||
         "invalidParams",
 | 
			
		||||
         "Malformed cursor."},
 | 
			
		||||
        {"AccountMissing", R"JSON({ "limit": 10 })JSON", "invalidParams", "Required field 'account' missing"},
 | 
			
		||||
        {"AccountNotString", R"JSON({ "account": 123 })JSON", "actMalformed", "Account malformed."},
 | 
			
		||||
        {"AccountMalformed",
 | 
			
		||||
         R"JSON({ "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jp" })JSON",
 | 
			
		||||
         "actMalformed",
 | 
			
		||||
         "Account malformed."},
 | 
			
		||||
        {"LimitNotInteger",
 | 
			
		||||
         fmt::format(R"JSON({{ "account": "{}", "limit": "t" }})JSON", kACCOUNT),
 | 
			
		||||
         "invalidParams",
 | 
			
		||||
         "Invalid parameters."},
 | 
			
		||||
        {"LimitNegative",
 | 
			
		||||
         fmt::format(R"JSON({{ "account": "{}", "limit": -1 }})JSON", kACCOUNT),
 | 
			
		||||
         "invalidParams",
 | 
			
		||||
         "Invalid parameters."},
 | 
			
		||||
        {"LimitZero",
 | 
			
		||||
         fmt::format(R"JSON({{ "account": "{}", "limit": 0 }})JSON", kACCOUNT),
 | 
			
		||||
         "invalidParams",
 | 
			
		||||
         "Invalid parameters."},
 | 
			
		||||
        {"LimitTypeInvalid",
 | 
			
		||||
         fmt::format(R"JSON({{ "account": "{}", "limit": true }})JSON", kACCOUNT),
 | 
			
		||||
         "invalidParams",
 | 
			
		||||
         "Invalid parameters."}
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
INSTANTIATE_TEST_SUITE_P(
 | 
			
		||||
    RPCAccountMPTokenIssuancesInvalidParamsGroup,
 | 
			
		||||
    AccountMPTokenIssuancesParameterTest,
 | 
			
		||||
    ValuesIn(generateTestValuesForInvalidParamsTest()),
 | 
			
		||||
    tests::util::kNAME_GENERATOR
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
// test invalid params bundle
 | 
			
		||||
TEST_P(AccountMPTokenIssuancesParameterTest, InvalidParams)
 | 
			
		||||
{
 | 
			
		||||
    auto const testBundle = GetParam();
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{backend_}};
 | 
			
		||||
        auto const req = json::parse(testBundle.testJson);
 | 
			
		||||
        auto const output = handler.process(req, Context{yield});
 | 
			
		||||
        ASSERT_FALSE(output);
 | 
			
		||||
 | 
			
		||||
        auto const err = rpc::makeError(output.result.error());
 | 
			
		||||
        EXPECT_EQ(err.at("error").as_string(), testBundle.expectedError);
 | 
			
		||||
        EXPECT_EQ(err.at("error_message").as_string(), testBundle.expectedErrorMessage);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ledger not found via hash
 | 
			
		||||
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, NonExistLedgerViaLedgerHash)
 | 
			
		||||
{
 | 
			
		||||
    // mock fetchLedgerByHash return empty
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerByHash(ripple::uint256{kLEDGER_HASH}, _))
 | 
			
		||||
        .WillOnce(Return(std::optional<ripple::LedgerHeader>{}));
 | 
			
		||||
 | 
			
		||||
    auto const input = json::parse(
 | 
			
		||||
        fmt::format(
 | 
			
		||||
            R"JSON({{
 | 
			
		||||
                "account": "{}",
 | 
			
		||||
                "ledger_hash": "{}"
 | 
			
		||||
            }})JSON",
 | 
			
		||||
            kACCOUNT,
 | 
			
		||||
            kLEDGER_HASH
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
    runSpawn([&, this](boost::asio::yield_context yield) {
 | 
			
		||||
        auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{backend_}};
 | 
			
		||||
        auto const output = handler.process(input, Context{.yield = 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");
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ledger not found via string index
 | 
			
		||||
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, NonExistLedgerViaLedgerStringIndex)
 | 
			
		||||
{
 | 
			
		||||
    // mock fetchLedgerBySequence return empty
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(std::optional<ripple::LedgerHeader>{}));
 | 
			
		||||
 | 
			
		||||
    auto const input = json::parse(
 | 
			
		||||
        fmt::format(
 | 
			
		||||
            R"JSON({{
 | 
			
		||||
                "account": "{}",
 | 
			
		||||
                "ledger_index": "4"
 | 
			
		||||
            }})JSON",
 | 
			
		||||
            kACCOUNT
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{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(), "lgrNotFound");
 | 
			
		||||
        EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ledger not found via int index
 | 
			
		||||
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, NonExistLedgerViaLedgerIntIndex)
 | 
			
		||||
{
 | 
			
		||||
    // mock fetchLedgerBySequence return empty
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(std::optional<ripple::LedgerHeader>{}));
 | 
			
		||||
 | 
			
		||||
    auto const input = json::parse(
 | 
			
		||||
        fmt::format(
 | 
			
		||||
            R"JSON({{
 | 
			
		||||
                "account": "{}",
 | 
			
		||||
                "ledger_index": 4
 | 
			
		||||
            }})JSON",
 | 
			
		||||
            kACCOUNT
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{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(), "lgrNotFound");
 | 
			
		||||
        EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ledger not found via hash (seq > max)
 | 
			
		||||
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, LedgerSeqOutOfRangeByHash)
 | 
			
		||||
{
 | 
			
		||||
    auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 31);
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerByHash(ripple::uint256{kLEDGER_HASH}, _)).WillOnce(Return(ledgerHeader));
 | 
			
		||||
    auto const input = json::parse(
 | 
			
		||||
        fmt::format(
 | 
			
		||||
            R"JSON({{
 | 
			
		||||
                "account": "{}",
 | 
			
		||||
                "ledger_hash": "{}"
 | 
			
		||||
            }})JSON",
 | 
			
		||||
            kACCOUNT,
 | 
			
		||||
            kLEDGER_HASH
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{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(), "lgrNotFound");
 | 
			
		||||
        EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ledger not found via index (seq > max)
 | 
			
		||||
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, LedgerSeqOutOfRangeByIndex)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(0);
 | 
			
		||||
    auto const input = json::parse(
 | 
			
		||||
        fmt::format(
 | 
			
		||||
            R"JSON({{
 | 
			
		||||
                "account": "{}",
 | 
			
		||||
                "ledger_index": "31"
 | 
			
		||||
            }})JSON",
 | 
			
		||||
            kACCOUNT
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{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(), "lgrNotFound");
 | 
			
		||||
        EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// account not exist
 | 
			
		||||
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, NonExistAccount)
 | 
			
		||||
{
 | 
			
		||||
    auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerByHash(ripple::uint256{kLEDGER_HASH}, _)).WillOnce(Return(ledgerHeader));
 | 
			
		||||
    // fetch account object return empty
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObject).WillOnce(Return(std::optional<Blob>{}));
 | 
			
		||||
 | 
			
		||||
    auto const input = json::parse(
 | 
			
		||||
        fmt::format(
 | 
			
		||||
            R"JSON({{
 | 
			
		||||
                "account": "{}",
 | 
			
		||||
                "ledger_hash": "{}"
 | 
			
		||||
            }})JSON",
 | 
			
		||||
            kACCOUNT,
 | 
			
		||||
            kLEDGER_HASH
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{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(), "actNotFound");
 | 
			
		||||
        EXPECT_EQ(err.at("error_message").as_string(), "Account not found.");
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// fetch mptoken issuances via account successfully
 | 
			
		||||
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, DefaultParameters)
 | 
			
		||||
{
 | 
			
		||||
    auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
 | 
			
		||||
 | 
			
		||||
    // return non-empty account
 | 
			
		||||
    auto account = getAccountIdWithString(kACCOUNT);
 | 
			
		||||
    auto accountKk = ripple::keylet::account(account).key;
 | 
			
		||||
    auto owneDirKk = ripple::keylet::ownerDir(account).key;
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
 | 
			
		||||
 | 
			
		||||
    // return two mptoken issuance objects
 | 
			
		||||
    ripple::STObject const ownerDir = createOwnerDirLedgerObject(
 | 
			
		||||
        {ripple::uint256{kISSUANCE_INDEX1}, ripple::uint256{kISSUANCE_INDEX2}}, kISSUANCE_INDEX1
 | 
			
		||||
    );
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillByDefault(Return(ownerDir.getSerializer().peekData()));
 | 
			
		||||
 | 
			
		||||
    // mocking mptoken issuance ledger objects
 | 
			
		||||
    std::vector<Blob> bbs;
 | 
			
		||||
    auto const issuance1 = createMptIssuanceObject(
 | 
			
		||||
        kACCOUNT,
 | 
			
		||||
        1,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        ripple::lsfMPTCanTrade | ripple::lsfMPTRequireAuth | ripple::lsfMPTCanTransfer | ripple::lsfMPTCanEscrow,
 | 
			
		||||
        kISSUANCE1_OUTSTANDING_AMOUNT,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        kISSUANCE1_ASSET_SCALE,
 | 
			
		||||
        kISSUANCE1_MAX_AMOUNT
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    auto const issuance2 = createMptIssuanceObject(
 | 
			
		||||
        kACCOUNT,
 | 
			
		||||
        2,
 | 
			
		||||
        kISSUANCE2_METADATA,
 | 
			
		||||
        ripple::lsfMPTLocked | ripple::lsfMPTCanLock | ripple::lsfMPTCanClawback,
 | 
			
		||||
        kISSUANCE2_OUTSTANDING_AMOUNT,
 | 
			
		||||
        kISSUANCE2_TRANSFER_FEE,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        kISSUANCE2_MAX_AMOUNT,
 | 
			
		||||
        kISSUANCE2_LOCKED_AMOUNT,
 | 
			
		||||
        kISSUANCE2_DOMAIN_ID_HEX
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    bbs.push_back(issuance1.getSerializer().peekData());
 | 
			
		||||
    bbs.push_back(issuance2.getSerializer().peekData());
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
 | 
			
		||||
 | 
			
		||||
    runSpawn([this](auto yield) {
 | 
			
		||||
        auto const expected = fmt::format(
 | 
			
		||||
            R"JSON({{
 | 
			
		||||
                "account": "{}",
 | 
			
		||||
                "ledger_hash": "{}",
 | 
			
		||||
                "ledger_index": 30,
 | 
			
		||||
                "validated": true,
 | 
			
		||||
                "limit": {},
 | 
			
		||||
                "mpt_issuances": [
 | 
			
		||||
                    {},
 | 
			
		||||
                    {}
 | 
			
		||||
                ]
 | 
			
		||||
            }})JSON",
 | 
			
		||||
            kACCOUNT,
 | 
			
		||||
            kLEDGER_HASH,
 | 
			
		||||
            AccountMPTokenIssuancesHandler::kLIMIT_DEFAULT,
 | 
			
		||||
            kISSUANCE_OUT1,
 | 
			
		||||
            kISSUANCE_OUT2
 | 
			
		||||
        );
 | 
			
		||||
        auto const input = json::parse(fmt::format(R"JSON({{"account": "{}"}})JSON", kACCOUNT));
 | 
			
		||||
        auto handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
 | 
			
		||||
 | 
			
		||||
        auto const output = handler.process(input, Context{yield});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        EXPECT_EQ(json::parse(expected), *output.result);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, UseLimit)
 | 
			
		||||
{
 | 
			
		||||
    constexpr int limit = 20;
 | 
			
		||||
    auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
 | 
			
		||||
    ON_CALL(*backend_, fetchLedgerBySequence).WillByDefault(Return(ledgerHeader));
 | 
			
		||||
 | 
			
		||||
    auto account = getAccountIdWithString(kACCOUNT);
 | 
			
		||||
    auto accountKk = ripple::keylet::account(account).key;
 | 
			
		||||
    auto owneDirKk = ripple::keylet::ownerDir(account).key;
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
 | 
			
		||||
 | 
			
		||||
    std::vector<ripple::uint256> indexes;
 | 
			
		||||
    std::vector<Blob> bbs;
 | 
			
		||||
 | 
			
		||||
    for (int i = 0; i < 50; ++i) {
 | 
			
		||||
        indexes.emplace_back(ripple::uint256{kISSUANCE_INDEX1});
 | 
			
		||||
        auto const issuance = createMptIssuanceObject(kACCOUNT, i);
 | 
			
		||||
        bbs.push_back(issuance.getSerializer().peekData());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ripple::STObject ownerDir = createOwnerDirLedgerObject(indexes, kISSUANCE_INDEX1);
 | 
			
		||||
    ownerDir.setFieldU64(ripple::sfIndexNext, 99);
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillByDefault(Return(ownerDir.getSerializer().peekData()));
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObject).Times(7);
 | 
			
		||||
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(bbs));
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(3);
 | 
			
		||||
 | 
			
		||||
    runSpawn([this, limit](auto yield) {
 | 
			
		||||
        auto const input = json::parse(
 | 
			
		||||
            fmt::format(
 | 
			
		||||
                R"JSON({{
 | 
			
		||||
                    "account": "{}",
 | 
			
		||||
                    "limit": {}
 | 
			
		||||
                }})JSON",
 | 
			
		||||
                kACCOUNT,
 | 
			
		||||
                limit
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        auto handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
 | 
			
		||||
        auto const resultJson = (*output.result).as_object();
 | 
			
		||||
        EXPECT_EQ(resultJson.at("mpt_issuances").as_array().size(), limit);
 | 
			
		||||
        ASSERT_TRUE(resultJson.contains("marker"));
 | 
			
		||||
        EXPECT_THAT(boost::json::value_to<std::string>(resultJson.at("marker")), EndsWith(",0"));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    runSpawn([this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{backend_}};
 | 
			
		||||
        auto const input = json::parse(
 | 
			
		||||
            fmt::format(
 | 
			
		||||
                R"JSON({{
 | 
			
		||||
                    "account": "{}",
 | 
			
		||||
                    "limit": {}
 | 
			
		||||
                }})JSON",
 | 
			
		||||
                kACCOUNT,
 | 
			
		||||
                AccountMPTokenIssuancesHandler::kLIMIT_MIN - 1
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
        auto const output = handler.process(input, Context{yield});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        EXPECT_EQ((*output.result).as_object().at("limit").as_uint64(), AccountMPTokenIssuancesHandler::kLIMIT_MIN);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    runSpawn([this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{backend_}};
 | 
			
		||||
        auto const input = json::parse(
 | 
			
		||||
            fmt::format(
 | 
			
		||||
                R"JSON({{
 | 
			
		||||
                    "account": "{}",
 | 
			
		||||
                    "limit": {}
 | 
			
		||||
                }})JSON",
 | 
			
		||||
                kACCOUNT,
 | 
			
		||||
                AccountMPTokenIssuancesHandler::kLIMIT_MAX + 1
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
        auto const output = handler.process(input, Context{yield});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        EXPECT_EQ((*output.result).as_object().at("limit").as_uint64(), AccountMPTokenIssuancesHandler::kLIMIT_MAX);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, MarkerOutput)
 | 
			
		||||
{
 | 
			
		||||
    constexpr auto kNEXT_PAGE = 99;
 | 
			
		||||
    constexpr auto kLIMIT = 15;
 | 
			
		||||
    auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
 | 
			
		||||
 | 
			
		||||
    auto account = getAccountIdWithString(kACCOUNT);
 | 
			
		||||
    auto accountKk = ripple::keylet::account(account).key;
 | 
			
		||||
    auto ownerDirKk = ripple::keylet::ownerDir(account).key;
 | 
			
		||||
    auto ownerDir2Kk = ripple::keylet::page(ripple::keylet::ownerDir(account), kNEXT_PAGE).key;
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObject).Times(3);
 | 
			
		||||
 | 
			
		||||
    std::vector<ripple::uint256> indexes;
 | 
			
		||||
    for (int i = 0; i < 10; ++i) {
 | 
			
		||||
        indexes.emplace_back(kISSUANCE_INDEX1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::vector<Blob> bbs;
 | 
			
		||||
    for (int i = 0; i < kLIMIT; ++i) {
 | 
			
		||||
        bbs.push_back(createMptIssuanceObject(kACCOUNT, i).getSerializer().peekData());
 | 
			
		||||
    }
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
 | 
			
		||||
 | 
			
		||||
    // mock the first directory page
 | 
			
		||||
    ripple::STObject ownerDir1 = createOwnerDirLedgerObject(indexes, kISSUANCE_INDEX1);
 | 
			
		||||
    ownerDir1.setFieldU64(ripple::sfIndexNext, kNEXT_PAGE);
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
 | 
			
		||||
        .WillByDefault(Return(ownerDir1.getSerializer().peekData()));
 | 
			
		||||
 | 
			
		||||
    // mock the second directory page
 | 
			
		||||
    ripple::STObject ownerDir2 = createOwnerDirLedgerObject(indexes, kISSUANCE_INDEX2);
 | 
			
		||||
    ownerDir2.setFieldU64(ripple::sfIndexNext, 0);
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(ownerDir2Kk, _, _))
 | 
			
		||||
        .WillByDefault(Return(ownerDir2.getSerializer().peekData()));
 | 
			
		||||
 | 
			
		||||
    runSpawn([this, kLIMIT, kNEXT_PAGE](auto yield) {
 | 
			
		||||
        auto const input = json::parse(
 | 
			
		||||
            fmt::format(
 | 
			
		||||
                R"JSON({{
 | 
			
		||||
                    "account": "{}",
 | 
			
		||||
                    "limit": {}
 | 
			
		||||
                }})JSON",
 | 
			
		||||
                kACCOUNT,
 | 
			
		||||
                kLIMIT
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
        auto handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        auto const& resultJson = (*output.result).as_object();
 | 
			
		||||
        EXPECT_EQ(
 | 
			
		||||
            boost::json::value_to<std::string>(resultJson.at("marker")),
 | 
			
		||||
            fmt::format("{},{}", kISSUANCE_INDEX1, kNEXT_PAGE)
 | 
			
		||||
        );
 | 
			
		||||
        EXPECT_EQ(resultJson.at("mpt_issuances").as_array().size(), kLIMIT);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, MarkerInput)
 | 
			
		||||
{
 | 
			
		||||
    constexpr auto kNEXT_PAGE = 99;
 | 
			
		||||
    constexpr auto kLIMIT = 15;
 | 
			
		||||
 | 
			
		||||
    auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
 | 
			
		||||
 | 
			
		||||
    auto account = getAccountIdWithString(kACCOUNT);
 | 
			
		||||
    auto accountKk = ripple::keylet::account(account).key;
 | 
			
		||||
    auto ownerDirKk = ripple::keylet::page(ripple::keylet::ownerDir(account), kNEXT_PAGE).key;
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObject).Times(3);
 | 
			
		||||
 | 
			
		||||
    std::vector<Blob> bbs;
 | 
			
		||||
    std::vector<ripple::uint256> indexes;
 | 
			
		||||
    for (int i = 0; i < kLIMIT; ++i) {
 | 
			
		||||
        indexes.emplace_back(kISSUANCE_INDEX1);
 | 
			
		||||
        bbs.push_back(createMptIssuanceObject(kACCOUNT, i).getSerializer().peekData());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ripple::STObject ownerDir = createOwnerDirLedgerObject(indexes, kISSUANCE_INDEX1);
 | 
			
		||||
    ownerDir.setFieldU64(ripple::sfIndexNext, 0);
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
 | 
			
		||||
        .WillByDefault(Return(ownerDir.getSerializer().peekData()));
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
 | 
			
		||||
 | 
			
		||||
    runSpawn([this, kLIMIT, kNEXT_PAGE](auto yield) {
 | 
			
		||||
        auto const input = json::parse(
 | 
			
		||||
            fmt::format(
 | 
			
		||||
                R"JSON({{
 | 
			
		||||
                    "account": "{}",
 | 
			
		||||
                    "limit": {},
 | 
			
		||||
                    "marker": "{},{}"
 | 
			
		||||
                }})JSON",
 | 
			
		||||
                kACCOUNT,
 | 
			
		||||
                kLIMIT,
 | 
			
		||||
                kISSUANCE_INDEX1,
 | 
			
		||||
                kNEXT_PAGE
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
        auto handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
 | 
			
		||||
        auto const& resultJson = (*output.result).as_object();
 | 
			
		||||
        EXPECT_TRUE(resultJson.if_contains("marker") == nullptr);
 | 
			
		||||
        EXPECT_EQ(resultJson.at("mpt_issuances").as_array().size(), kLIMIT - 1);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, LimitLessThanMin)
 | 
			
		||||
{
 | 
			
		||||
    auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
 | 
			
		||||
 | 
			
		||||
    auto account = getAccountIdWithString(kACCOUNT);
 | 
			
		||||
    auto accountKk = ripple::keylet::account(account).key;
 | 
			
		||||
    auto owneDirKk = ripple::keylet::ownerDir(account).key;
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
 | 
			
		||||
 | 
			
		||||
    ripple::STObject const ownerDir = createOwnerDirLedgerObject(
 | 
			
		||||
        {ripple::uint256{kISSUANCE_INDEX1}, ripple::uint256{kISSUANCE_INDEX2}}, kISSUANCE_INDEX1
 | 
			
		||||
    );
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillByDefault(Return(ownerDir.getSerializer().peekData()));
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObject).Times(2);
 | 
			
		||||
 | 
			
		||||
    std::vector<Blob> bbs;
 | 
			
		||||
    auto const issuance1 = createMptIssuanceObject(
 | 
			
		||||
        kACCOUNT,
 | 
			
		||||
        1,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        ripple::lsfMPTCanTrade | ripple::lsfMPTRequireAuth | ripple::lsfMPTCanTransfer | ripple::lsfMPTCanEscrow,
 | 
			
		||||
        kISSUANCE1_OUTSTANDING_AMOUNT,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        kISSUANCE1_ASSET_SCALE,
 | 
			
		||||
        kISSUANCE1_MAX_AMOUNT
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    auto const issuance2 = createMptIssuanceObject(
 | 
			
		||||
        kACCOUNT,
 | 
			
		||||
        2,
 | 
			
		||||
        kISSUANCE2_METADATA,
 | 
			
		||||
        ripple::lsfMPTLocked | ripple::lsfMPTCanLock | ripple::lsfMPTCanClawback,
 | 
			
		||||
        kISSUANCE2_OUTSTANDING_AMOUNT,
 | 
			
		||||
        kISSUANCE2_TRANSFER_FEE,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        kISSUANCE2_MAX_AMOUNT,
 | 
			
		||||
        kISSUANCE2_LOCKED_AMOUNT,
 | 
			
		||||
        kISSUANCE2_DOMAIN_ID_HEX
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    bbs.push_back(issuance1.getSerializer().peekData());
 | 
			
		||||
    bbs.push_back(issuance2.getSerializer().peekData());
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
 | 
			
		||||
 | 
			
		||||
    runSpawn([this](auto yield) {
 | 
			
		||||
        auto const input = json::parse(
 | 
			
		||||
            fmt::format(
 | 
			
		||||
                R"JSON({{
 | 
			
		||||
                    "account": "{}",
 | 
			
		||||
                    "limit": {}
 | 
			
		||||
                }})JSON",
 | 
			
		||||
                kACCOUNT,
 | 
			
		||||
                AccountMPTokenIssuancesHandler::kLIMIT_MIN - 1
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        auto const correctOutput = fmt::format(
 | 
			
		||||
            R"JSON({{
 | 
			
		||||
                "account": "{}",
 | 
			
		||||
                "ledger_hash": "{}",
 | 
			
		||||
                "ledger_index": 30,
 | 
			
		||||
                "validated": true,
 | 
			
		||||
                "limit": {},
 | 
			
		||||
                "mpt_issuances": [
 | 
			
		||||
                    {},
 | 
			
		||||
                    {}
 | 
			
		||||
                ]
 | 
			
		||||
            }})JSON",
 | 
			
		||||
            kACCOUNT,
 | 
			
		||||
            kLEDGER_HASH,
 | 
			
		||||
            AccountMPTokenIssuancesHandler::kLIMIT_MIN,
 | 
			
		||||
            kISSUANCE_OUT1,
 | 
			
		||||
            kISSUANCE_OUT2
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        auto handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        EXPECT_EQ(json::parse(correctOutput), *output.result);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, LimitMoreThanMax)
 | 
			
		||||
{
 | 
			
		||||
    auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
 | 
			
		||||
 | 
			
		||||
    auto account = getAccountIdWithString(kACCOUNT);
 | 
			
		||||
    auto accountKk = ripple::keylet::account(account).key;
 | 
			
		||||
    auto owneDirKk = ripple::keylet::ownerDir(account).key;
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
 | 
			
		||||
 | 
			
		||||
    ripple::STObject const ownerDir = createOwnerDirLedgerObject(
 | 
			
		||||
        {ripple::uint256{kISSUANCE_INDEX1}, ripple::uint256{kISSUANCE_INDEX2}}, kISSUANCE_INDEX1
 | 
			
		||||
    );
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillByDefault(Return(ownerDir.getSerializer().peekData()));
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObject).Times(2);
 | 
			
		||||
 | 
			
		||||
    std::vector<Blob> bbs;
 | 
			
		||||
    auto const issuance1 = createMptIssuanceObject(
 | 
			
		||||
        kACCOUNT,
 | 
			
		||||
        1,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        ripple::lsfMPTCanTrade | ripple::lsfMPTRequireAuth | ripple::lsfMPTCanTransfer | ripple::lsfMPTCanEscrow,
 | 
			
		||||
        kISSUANCE1_OUTSTANDING_AMOUNT,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        kISSUANCE1_ASSET_SCALE,
 | 
			
		||||
        kISSUANCE1_MAX_AMOUNT
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    auto const issuance2 = createMptIssuanceObject(
 | 
			
		||||
        kACCOUNT,
 | 
			
		||||
        2,
 | 
			
		||||
        kISSUANCE2_METADATA,
 | 
			
		||||
        ripple::lsfMPTLocked | ripple::lsfMPTCanLock | ripple::lsfMPTCanClawback,
 | 
			
		||||
        kISSUANCE2_OUTSTANDING_AMOUNT,
 | 
			
		||||
        kISSUANCE2_TRANSFER_FEE,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        kISSUANCE2_MAX_AMOUNT,
 | 
			
		||||
        kISSUANCE2_LOCKED_AMOUNT,
 | 
			
		||||
        kISSUANCE2_DOMAIN_ID_HEX
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    bbs.push_back(issuance1.getSerializer().peekData());
 | 
			
		||||
    bbs.push_back(issuance2.getSerializer().peekData());
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
 | 
			
		||||
 | 
			
		||||
    runSpawn([this](auto yield) {
 | 
			
		||||
        auto const input = json::parse(
 | 
			
		||||
            fmt::format(
 | 
			
		||||
                R"JSON({{
 | 
			
		||||
                    "account": "{}",
 | 
			
		||||
                    "limit": {}
 | 
			
		||||
                }})JSON",
 | 
			
		||||
                kACCOUNT,
 | 
			
		||||
                AccountMPTokenIssuancesHandler::kLIMIT_MAX + 1
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        auto const correctOutput = fmt::format(
 | 
			
		||||
            R"JSON({{
 | 
			
		||||
                "account": "{}",
 | 
			
		||||
                "ledger_hash": "{}",
 | 
			
		||||
                "ledger_index": 30,
 | 
			
		||||
                "validated": true,
 | 
			
		||||
                "limit": {},
 | 
			
		||||
                "mpt_issuances": [
 | 
			
		||||
                    {},
 | 
			
		||||
                    {}
 | 
			
		||||
                ]
 | 
			
		||||
            }})JSON",
 | 
			
		||||
            kACCOUNT,
 | 
			
		||||
            kLEDGER_HASH,
 | 
			
		||||
            AccountMPTokenIssuancesHandler::kLIMIT_MAX,
 | 
			
		||||
            kISSUANCE_OUT1,
 | 
			
		||||
            kISSUANCE_OUT2
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        auto handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        EXPECT_EQ(json::parse(correctOutput), *output.result);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, EmptyResult)
 | 
			
		||||
{
 | 
			
		||||
    auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
 | 
			
		||||
 | 
			
		||||
    auto account = getAccountIdWithString(kACCOUNT);
 | 
			
		||||
    auto accountKk = ripple::keylet::account(account).key;
 | 
			
		||||
    auto owneDirKk = ripple::keylet::ownerDir(account).key;
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
 | 
			
		||||
 | 
			
		||||
    ripple::STObject const ownerDir = createOwnerDirLedgerObject({}, kISSUANCE_INDEX1);
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillByDefault(Return(ownerDir.getSerializer().peekData()));
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObject).Times(2);
 | 
			
		||||
 | 
			
		||||
    runSpawn([this](auto yield) {
 | 
			
		||||
        auto const input = json::parse(
 | 
			
		||||
            fmt::format(
 | 
			
		||||
                R"JSON({{
 | 
			
		||||
                    "account": "{}"
 | 
			
		||||
                }})JSON",
 | 
			
		||||
                kACCOUNT
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
        auto handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        EXPECT_EQ((*output.result).as_object().at("mpt_issuances").as_array().size(), 0);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										747
									
								
								tests/unit/rpc/handlers/AccountMPTokensTests.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										747
									
								
								tests/unit/rpc/handlers/AccountMPTokensTests.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,747 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2025, 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/AccountMPTokens.hpp"
 | 
			
		||||
#include "util/HandlerBaseTestFixture.hpp"
 | 
			
		||||
#include "util/NameGenerator.hpp"
 | 
			
		||||
#include "util/TestObject.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/json/parse.hpp>
 | 
			
		||||
#include <boost/json/value.hpp>
 | 
			
		||||
#include <boost/json/value_to.hpp>
 | 
			
		||||
#include <fmt/format.h>
 | 
			
		||||
#include <gmock/gmock.h>
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
#include <xrpl/basics/base_uint.h>
 | 
			
		||||
#include <xrpl/protocol/Indexes.h>
 | 
			
		||||
#include <xrpl/protocol/LedgerFormats.h>
 | 
			
		||||
#include <xrpl/protocol/LedgerHeader.h>
 | 
			
		||||
#include <xrpl/protocol/SField.h>
 | 
			
		||||
#include <xrpl/protocol/STObject.h>
 | 
			
		||||
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
using namespace rpc;
 | 
			
		||||
using namespace data;
 | 
			
		||||
namespace json = boost::json;
 | 
			
		||||
using namespace testing;
 | 
			
		||||
 | 
			
		||||
namespace {
 | 
			
		||||
 | 
			
		||||
constexpr auto kLEDGER_HASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
 | 
			
		||||
constexpr auto kACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
 | 
			
		||||
constexpr auto kISSUANCE_ID_HEX = "00080000B43A1A953EADDB3314A73523789947C752044C49";
 | 
			
		||||
constexpr auto kTOKEN_INDEX1 = "A6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321";
 | 
			
		||||
constexpr auto kTOKEN_INDEX2 = "B6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC322";
 | 
			
		||||
 | 
			
		||||
constexpr uint64_t kTOKEN1_AMOUNT = 500;
 | 
			
		||||
constexpr uint64_t kTOKEN1_LOCKED_AMOUNT = 50;
 | 
			
		||||
constexpr uint64_t kTOKEN2_AMOUNT = 250;
 | 
			
		||||
 | 
			
		||||
// define expected JSON for mptokens
 | 
			
		||||
auto const kTOKEN_OUT1 = fmt::format(
 | 
			
		||||
    R"JSON({{
 | 
			
		||||
        "account": "{}",
 | 
			
		||||
        "mpt_issuance_id": "{}",
 | 
			
		||||
        "mpt_amount": {},
 | 
			
		||||
        "locked_amount": {},
 | 
			
		||||
        "mpt_locked": true
 | 
			
		||||
    }})JSON",
 | 
			
		||||
    kACCOUNT,
 | 
			
		||||
    kISSUANCE_ID_HEX,
 | 
			
		||||
    kTOKEN1_AMOUNT,
 | 
			
		||||
    kTOKEN1_LOCKED_AMOUNT
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
auto const kTOKEN_OUT2 = fmt::format(
 | 
			
		||||
    R"JSON({{
 | 
			
		||||
        "account": "{}",
 | 
			
		||||
        "mpt_issuance_id": "{}",
 | 
			
		||||
        "mpt_amount": {},
 | 
			
		||||
        "mpt_authorized": true
 | 
			
		||||
    }})JSON",
 | 
			
		||||
    kACCOUNT,
 | 
			
		||||
    kISSUANCE_ID_HEX,
 | 
			
		||||
    kTOKEN2_AMOUNT
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
}  // namespace
 | 
			
		||||
 | 
			
		||||
struct RPCAccountMPTokensHandlerTest : HandlerBaseTest {
 | 
			
		||||
    RPCAccountMPTokensHandlerTest()
 | 
			
		||||
    {
 | 
			
		||||
        backend_->setRange(10, 30);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct AccountMPTokensParamTestCaseBundle {
 | 
			
		||||
    std::string testName;
 | 
			
		||||
    std::string testJson;
 | 
			
		||||
    std::string expectedError;
 | 
			
		||||
    std::string expectedErrorMessage;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct AccountMPTokensParameterTest : RPCAccountMPTokensHandlerTest,
 | 
			
		||||
                                      WithParamInterface<AccountMPTokensParamTestCaseBundle> {};
 | 
			
		||||
 | 
			
		||||
// generate values for invalid params test
 | 
			
		||||
static auto
 | 
			
		||||
generateTestValuesForInvalidParamsTest()
 | 
			
		||||
{
 | 
			
		||||
    return std::vector<AccountMPTokensParamTestCaseBundle>{
 | 
			
		||||
        {"NonHexLedgerHash",
 | 
			
		||||
         fmt::format(R"JSON({{ "account": "{}", "ledger_hash": "xxx" }})JSON", kACCOUNT),
 | 
			
		||||
         "invalidParams",
 | 
			
		||||
         "ledger_hashMalformed"},
 | 
			
		||||
        {"NonStringLedgerHash",
 | 
			
		||||
         fmt::format(R"JSON({{ "account": "{}", "ledger_hash": 123 }})JSON", kACCOUNT),
 | 
			
		||||
         "invalidParams",
 | 
			
		||||
         "ledger_hashNotString"},
 | 
			
		||||
        {"InvalidLedgerIndexString",
 | 
			
		||||
         fmt::format(R"JSON({{ "account": "{}", "ledger_index": "notvalidated" }})JSON", kACCOUNT),
 | 
			
		||||
         "invalidParams",
 | 
			
		||||
         "ledgerIndexMalformed"},
 | 
			
		||||
        {"MarkerNotString",
 | 
			
		||||
         fmt::format(R"JSON({{ "account": "{}", "marker": 9 }})JSON", kACCOUNT),
 | 
			
		||||
         "invalidParams",
 | 
			
		||||
         "markerNotString"},
 | 
			
		||||
        {"InvalidMarkerContent",
 | 
			
		||||
         fmt::format(R"JSON({{ "account": "{}", "marker": "123invalid" }})JSON", kACCOUNT),
 | 
			
		||||
         "invalidParams",
 | 
			
		||||
         "Malformed cursor."},
 | 
			
		||||
        {"AccountMissing", R"JSON({ "limit": 10 })JSON", "invalidParams", "Required field 'account' missing"},
 | 
			
		||||
        {"AccountNotString", R"JSON({ "account": 123 })JSON", "actMalformed", "Account malformed."},
 | 
			
		||||
        {"AccountMalformed",
 | 
			
		||||
         fmt::format(R"JSON({{ "account": "{}" }})JSON", "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jp"),
 | 
			
		||||
         "actMalformed",
 | 
			
		||||
         "Account malformed."},
 | 
			
		||||
        {"LimitNotInteger",
 | 
			
		||||
         fmt::format(R"JSON({{ "account": "{}", "limit": "t" }})JSON", kACCOUNT),
 | 
			
		||||
         "invalidParams",
 | 
			
		||||
         "Invalid parameters."},
 | 
			
		||||
        {"LimitNegative",
 | 
			
		||||
         fmt::format(R"JSON({{ "account": "{}", "limit": -1 }})JSON", kACCOUNT),
 | 
			
		||||
         "invalidParams",
 | 
			
		||||
         "Invalid parameters."},
 | 
			
		||||
        {"LimitZero",
 | 
			
		||||
         fmt::format(R"JSON({{ "account": "{}", "limit": 0 }})JSON", kACCOUNT),
 | 
			
		||||
         "invalidParams",
 | 
			
		||||
         "Invalid parameters."},
 | 
			
		||||
        {"LimitTypeInvalid",
 | 
			
		||||
         fmt::format(R"JSON({{ "account": "{}", "limit": true }})JSON", kACCOUNT),
 | 
			
		||||
         "invalidParams",
 | 
			
		||||
         "Invalid parameters."}
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
INSTANTIATE_TEST_SUITE_P(
 | 
			
		||||
    RPCAccountMPTokensInvalidParamsGroup,
 | 
			
		||||
    AccountMPTokensParameterTest,
 | 
			
		||||
    ValuesIn(generateTestValuesForInvalidParamsTest()),
 | 
			
		||||
    tests::util::kNAME_GENERATOR
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
// test invalid params bundle
 | 
			
		||||
TEST_P(AccountMPTokensParameterTest, InvalidParams)
 | 
			
		||||
{
 | 
			
		||||
    auto const testBundle = GetParam();
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{AccountMPTokensHandler{backend_}};
 | 
			
		||||
        auto const req = json::parse(testBundle.testJson);
 | 
			
		||||
        auto const output = handler.process(req, Context{yield});
 | 
			
		||||
        ASSERT_FALSE(output);
 | 
			
		||||
        auto const err = rpc::makeError(output.result.error());
 | 
			
		||||
        EXPECT_EQ(err.at("error").as_string(), testBundle.expectedError);
 | 
			
		||||
        EXPECT_EQ(err.at("error_message").as_string(), testBundle.expectedErrorMessage);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RPCAccountMPTokensHandlerTest, NonExistLedgerViaLedgerHash)
 | 
			
		||||
{
 | 
			
		||||
    // mock fetchLedgerByHash to return empty
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerByHash(ripple::uint256{kLEDGER_HASH}, _))
 | 
			
		||||
        .WillOnce(Return(std::optional<ripple::LedgerHeader>{}));
 | 
			
		||||
 | 
			
		||||
    auto const input = json::parse(
 | 
			
		||||
        fmt::format(
 | 
			
		||||
            R"JSON({{
 | 
			
		||||
                "account": "{}",
 | 
			
		||||
                "ledger_hash": "{}"
 | 
			
		||||
            }})JSON",
 | 
			
		||||
            kACCOUNT,
 | 
			
		||||
            kLEDGER_HASH
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{AccountMPTokensHandler{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(), "lgrNotFound");
 | 
			
		||||
        EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RPCAccountMPTokensHandlerTest, NonExistLedgerViaLedgerStringIndex)
 | 
			
		||||
{
 | 
			
		||||
    // mock fetchLedgerBySequence to return empty
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(std::optional<ripple::LedgerHeader>{}));
 | 
			
		||||
 | 
			
		||||
    auto const input = json::parse(
 | 
			
		||||
        fmt::format(
 | 
			
		||||
            R"JSON({{
 | 
			
		||||
                "account": "{}",
 | 
			
		||||
                "ledger_index": "4"
 | 
			
		||||
            }})JSON",
 | 
			
		||||
            kACCOUNT
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{AccountMPTokensHandler{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(), "lgrNotFound");
 | 
			
		||||
        EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RPCAccountMPTokensHandlerTest, NonExistLedgerViaLedgerIntIndex)
 | 
			
		||||
{
 | 
			
		||||
    // mock fetchLedgerBySequence to return empty
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(std::optional<ripple::LedgerHeader>{}));
 | 
			
		||||
 | 
			
		||||
    auto const input = json::parse(
 | 
			
		||||
        fmt::format(
 | 
			
		||||
            R"JSON({{
 | 
			
		||||
                "account": "{}",
 | 
			
		||||
                "ledger_index": 4
 | 
			
		||||
            }})JSON",
 | 
			
		||||
            kACCOUNT
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{AccountMPTokensHandler{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(), "lgrNotFound");
 | 
			
		||||
        EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RPCAccountMPTokensHandlerTest, LedgerSeqOutOfRangeByHash)
 | 
			
		||||
{
 | 
			
		||||
    auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 31);
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerByHash(ripple::uint256{kLEDGER_HASH}, _)).WillOnce(Return(ledgerHeader));
 | 
			
		||||
 | 
			
		||||
    auto const input = json::parse(
 | 
			
		||||
        fmt::format(
 | 
			
		||||
            R"JSON({{
 | 
			
		||||
                "account": "{}",
 | 
			
		||||
                "ledger_hash": "{}"
 | 
			
		||||
            }})JSON",
 | 
			
		||||
            kACCOUNT,
 | 
			
		||||
            kLEDGER_HASH
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{AccountMPTokensHandler{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(), "lgrNotFound");
 | 
			
		||||
        EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RPCAccountMPTokensHandlerTest, LedgerSeqOutOfRangeByIndex)
 | 
			
		||||
{
 | 
			
		||||
    // No need to check from db, call fetchLedgerBySequence 0 times
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(0);
 | 
			
		||||
    auto const input = json::parse(
 | 
			
		||||
        fmt::format(
 | 
			
		||||
            R"JSON({{
 | 
			
		||||
                "account": "{}",
 | 
			
		||||
                "ledger_index": "31"
 | 
			
		||||
            }})JSON",
 | 
			
		||||
            kACCOUNT
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{AccountMPTokensHandler{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(), "lgrNotFound");
 | 
			
		||||
        EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RPCAccountMPTokensHandlerTest, NonExistAccount)
 | 
			
		||||
{
 | 
			
		||||
    auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerByHash(ripple::uint256{kLEDGER_HASH}, _)).WillOnce(Return(ledgerHeader));
 | 
			
		||||
    // fetch account object return empty
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObject).WillOnce(Return(std::optional<Blob>{}));
 | 
			
		||||
 | 
			
		||||
    auto const input = json::parse(
 | 
			
		||||
        fmt::format(
 | 
			
		||||
            R"JSON({{
 | 
			
		||||
                "account": "{}",
 | 
			
		||||
                "ledger_hash": "{}"
 | 
			
		||||
            }})JSON",
 | 
			
		||||
            kACCOUNT,
 | 
			
		||||
            kLEDGER_HASH
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    runSpawn([&, this](auto yield) {
 | 
			
		||||
        auto const handler = AnyHandler{AccountMPTokensHandler{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(), "actNotFound");
 | 
			
		||||
        EXPECT_EQ(err.at("error_message").as_string(), "Account not found.");
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RPCAccountMPTokensHandlerTest, DefaultParameters)
 | 
			
		||||
{
 | 
			
		||||
    auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
 | 
			
		||||
    ON_CALL(*backend_, fetchLedgerBySequence).WillByDefault(Return(ledgerHeader));
 | 
			
		||||
 | 
			
		||||
    auto account = getAccountIdWithString(kACCOUNT);
 | 
			
		||||
    auto accountKk = ripple::keylet::account(account).key;
 | 
			
		||||
    auto owneDirKk = ripple::keylet::ownerDir(account).key;
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
 | 
			
		||||
 | 
			
		||||
    ripple::STObject const ownerDir =
 | 
			
		||||
        createOwnerDirLedgerObject({ripple::uint256{kTOKEN_INDEX1}, ripple::uint256{kTOKEN_INDEX2}}, kTOKEN_INDEX1);
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillByDefault(Return(ownerDir.getSerializer().peekData()));
 | 
			
		||||
 | 
			
		||||
    std::vector<Blob> bbs;
 | 
			
		||||
    auto const token1 = createMpTokenObject(
 | 
			
		||||
        kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), kTOKEN1_AMOUNT, ripple::lsfMPTLocked, kTOKEN1_LOCKED_AMOUNT
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    auto const token2 = createMpTokenObject(
 | 
			
		||||
        kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), kTOKEN2_AMOUNT, ripple::lsfMPTAuthorized, std::nullopt
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    bbs.push_back(token1.getSerializer().peekData());
 | 
			
		||||
    bbs.push_back(token2.getSerializer().peekData());
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
 | 
			
		||||
 | 
			
		||||
    runSpawn([this](auto yield) {
 | 
			
		||||
        auto const expected = fmt::format(
 | 
			
		||||
            R"JSON({{
 | 
			
		||||
                "account": "{}",
 | 
			
		||||
                "ledger_hash": "{}",
 | 
			
		||||
                "ledger_index": 30,
 | 
			
		||||
                "validated": true,
 | 
			
		||||
                "limit": {},
 | 
			
		||||
                "mptokens": [
 | 
			
		||||
                    {},
 | 
			
		||||
                    {}
 | 
			
		||||
                ]
 | 
			
		||||
            }})JSON",
 | 
			
		||||
            kACCOUNT,
 | 
			
		||||
            kLEDGER_HASH,
 | 
			
		||||
            AccountMPTokensHandler::kLIMIT_DEFAULT,
 | 
			
		||||
            kTOKEN_OUT1,
 | 
			
		||||
            kTOKEN_OUT2
 | 
			
		||||
        );
 | 
			
		||||
        auto const input = json::parse(fmt::format(R"JSON({{"account": "{}"}})JSON", kACCOUNT));
 | 
			
		||||
        auto handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        EXPECT_EQ(json::parse(expected), *output.result);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RPCAccountMPTokensHandlerTest, UseLimit)
 | 
			
		||||
{
 | 
			
		||||
    constexpr int limit = 20;
 | 
			
		||||
    auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
 | 
			
		||||
    ON_CALL(*backend_, fetchLedgerBySequence).WillByDefault(Return(ledgerHeader));
 | 
			
		||||
 | 
			
		||||
    auto account = getAccountIdWithString(kACCOUNT);
 | 
			
		||||
    auto accountKk = ripple::keylet::account(account).key;
 | 
			
		||||
    auto owneDirKk = ripple::keylet::ownerDir(account).key;
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
 | 
			
		||||
 | 
			
		||||
    std::vector<ripple::uint256> indexes;
 | 
			
		||||
    std::vector<Blob> bbs;
 | 
			
		||||
 | 
			
		||||
    for (int i = 0; i < 50; ++i) {
 | 
			
		||||
        indexes.emplace_back(ripple::uint256{kTOKEN_INDEX1});
 | 
			
		||||
        auto const token = createMpTokenObject(kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), i, 0, std::nullopt);
 | 
			
		||||
        bbs.push_back(token.getSerializer().peekData());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ripple::STObject ownerDir = createOwnerDirLedgerObject(indexes, kTOKEN_INDEX1);
 | 
			
		||||
    ownerDir.setFieldU64(ripple::sfIndexNext, 99);
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillByDefault(Return(ownerDir.getSerializer().peekData()));
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObject).Times(7);
 | 
			
		||||
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(bbs));
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(3);
 | 
			
		||||
 | 
			
		||||
    runSpawn([this, limit](auto yield) {
 | 
			
		||||
        auto const input = json::parse(
 | 
			
		||||
            fmt::format(
 | 
			
		||||
                R"JSON({{
 | 
			
		||||
                    "account": "{}",
 | 
			
		||||
                    "limit": {}
 | 
			
		||||
                }})JSON",
 | 
			
		||||
                kACCOUNT,
 | 
			
		||||
                limit
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        auto handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
 | 
			
		||||
        auto const resultJson = (*output.result).as_object();
 | 
			
		||||
        EXPECT_EQ(resultJson.at("mptokens").as_array().size(), limit);
 | 
			
		||||
        ASSERT_TRUE(resultJson.contains("marker"));
 | 
			
		||||
        EXPECT_THAT(boost::json::value_to<std::string>(resultJson.at("marker")), EndsWith(",0"));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    runSpawn([this](auto yield) {
 | 
			
		||||
        auto const input = json::parse(
 | 
			
		||||
            fmt::format(
 | 
			
		||||
                R"JSON({{
 | 
			
		||||
                    "account": "{}",
 | 
			
		||||
                    "limit": {}
 | 
			
		||||
                }})JSON",
 | 
			
		||||
                kACCOUNT,
 | 
			
		||||
                AccountMPTokensHandler::kLIMIT_MIN - 1
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        auto handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        EXPECT_EQ((*output.result).as_object().at("limit").as_uint64(), AccountMPTokensHandler::kLIMIT_MIN);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    runSpawn([this](auto yield) {
 | 
			
		||||
        auto const input = json::parse(
 | 
			
		||||
            fmt::format(
 | 
			
		||||
                R"JSON({{
 | 
			
		||||
                    "account": "{}",
 | 
			
		||||
                    "limit": {}
 | 
			
		||||
                }})JSON",
 | 
			
		||||
                kACCOUNT,
 | 
			
		||||
                AccountMPTokensHandler::kLIMIT_MAX + 1
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        auto handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        EXPECT_EQ((*output.result).as_object().at("limit").as_uint64(), AccountMPTokensHandler::kLIMIT_MAX);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RPCAccountMPTokensHandlerTest, MarkerOutput)
 | 
			
		||||
{
 | 
			
		||||
    constexpr auto kNEXT_PAGE = 99;
 | 
			
		||||
    constexpr auto kLIMIT = 15;
 | 
			
		||||
    auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
 | 
			
		||||
 | 
			
		||||
    auto account = getAccountIdWithString(kACCOUNT);
 | 
			
		||||
    auto accountKk = ripple::keylet::account(account).key;
 | 
			
		||||
    auto ownerDirKk = ripple::keylet::ownerDir(account).key;
 | 
			
		||||
    auto ownerDir2Kk = ripple::keylet::page(ripple::keylet::ownerDir(account), kNEXT_PAGE).key;
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
 | 
			
		||||
 | 
			
		||||
    std::vector<Blob> bbs;
 | 
			
		||||
    for (int i = 0; i < kLIMIT; ++i) {
 | 
			
		||||
        bbs.push_back(createMpTokenObject(kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), i, 0, std::nullopt)
 | 
			
		||||
                          .getSerializer()
 | 
			
		||||
                          .peekData());
 | 
			
		||||
    }
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
 | 
			
		||||
 | 
			
		||||
    std::vector<ripple::uint256> indexes1;
 | 
			
		||||
    for (int i = 0; i < 10; ++i) {
 | 
			
		||||
        indexes1.emplace_back(kTOKEN_INDEX1);
 | 
			
		||||
    }
 | 
			
		||||
    ripple::STObject ownerDir1 = createOwnerDirLedgerObject(indexes1, kTOKEN_INDEX1);
 | 
			
		||||
    ownerDir1.setFieldU64(ripple::sfIndexNext, kNEXT_PAGE);
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
 | 
			
		||||
        .WillByDefault(Return(ownerDir1.getSerializer().peekData()));
 | 
			
		||||
 | 
			
		||||
    ripple::STObject ownerDir2 = createOwnerDirLedgerObject(indexes1, kTOKEN_INDEX2);
 | 
			
		||||
    ownerDir2.setFieldU64(ripple::sfIndexNext, 0);
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(ownerDir2Kk, _, _))
 | 
			
		||||
        .WillByDefault(Return(ownerDir2.getSerializer().peekData()));
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObject).Times(3);
 | 
			
		||||
 | 
			
		||||
    runSpawn([this, kLIMIT, kNEXT_PAGE](auto yield) {
 | 
			
		||||
        auto const input = json::parse(
 | 
			
		||||
            fmt::format(
 | 
			
		||||
                R"JSON({{
 | 
			
		||||
                    "account": "{}",
 | 
			
		||||
                    "limit": {}
 | 
			
		||||
                }})JSON",
 | 
			
		||||
                kACCOUNT,
 | 
			
		||||
                kLIMIT
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
        auto handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        auto const& resultJson = (*output.result).as_object();
 | 
			
		||||
        EXPECT_EQ(resultJson.at("mptokens").as_array().size(), kLIMIT);
 | 
			
		||||
        EXPECT_EQ(
 | 
			
		||||
            boost::json::value_to<std::string>(resultJson.at("marker")), fmt::format("{},{}", kTOKEN_INDEX1, kNEXT_PAGE)
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RPCAccountMPTokensHandlerTest, MarkerInput)
 | 
			
		||||
{
 | 
			
		||||
    constexpr auto kNEXT_PAGE = 99;
 | 
			
		||||
    constexpr auto kLIMIT = 15;
 | 
			
		||||
 | 
			
		||||
    auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
 | 
			
		||||
 | 
			
		||||
    auto account = getAccountIdWithString(kACCOUNT);
 | 
			
		||||
    auto accountKk = ripple::keylet::account(account).key;
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
 | 
			
		||||
    auto ownerDirKk = ripple::keylet::page(ripple::keylet::ownerDir(account), kNEXT_PAGE).key;
 | 
			
		||||
 | 
			
		||||
    std::vector<Blob> bbs;
 | 
			
		||||
    std::vector<ripple::uint256> indexes;
 | 
			
		||||
    for (int i = 0; i < kLIMIT; ++i) {
 | 
			
		||||
        indexes.emplace_back(kTOKEN_INDEX1);
 | 
			
		||||
        bbs.push_back(createMpTokenObject(kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), i, 0, std::nullopt)
 | 
			
		||||
                          .getSerializer()
 | 
			
		||||
                          .peekData());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ripple::STObject ownerDir = createOwnerDirLedgerObject(indexes, kTOKEN_INDEX1);
 | 
			
		||||
    ownerDir.setFieldU64(ripple::sfIndexNext, 0);
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
 | 
			
		||||
        .WillByDefault(Return(ownerDir.getSerializer().peekData()));
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObject).Times(3);
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
 | 
			
		||||
 | 
			
		||||
    runSpawn([this, kLIMIT, kNEXT_PAGE](auto yield) {
 | 
			
		||||
        auto const input = json::parse(
 | 
			
		||||
            fmt::format(
 | 
			
		||||
                R"JSON({{
 | 
			
		||||
                    "account": "{}",
 | 
			
		||||
                    "limit": {},
 | 
			
		||||
                    "marker": "{},{}"
 | 
			
		||||
                }})JSON",
 | 
			
		||||
                kACCOUNT,
 | 
			
		||||
                kLIMIT,
 | 
			
		||||
                kTOKEN_INDEX1,
 | 
			
		||||
                kNEXT_PAGE
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
        auto handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        auto const& resultJson = (*output.result).as_object();
 | 
			
		||||
        EXPECT_TRUE(resultJson.if_contains("marker") == nullptr);
 | 
			
		||||
        EXPECT_EQ(resultJson.at("mptokens").as_array().size(), kLIMIT - 1);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RPCAccountMPTokensHandlerTest, LimitLessThanMin)
 | 
			
		||||
{
 | 
			
		||||
    auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
 | 
			
		||||
 | 
			
		||||
    auto account = getAccountIdWithString(kACCOUNT);
 | 
			
		||||
    auto accountKk = ripple::keylet::account(account).key;
 | 
			
		||||
    auto owneDirKk = ripple::keylet::ownerDir(account).key;
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
 | 
			
		||||
 | 
			
		||||
    ripple::STObject const ownerDir =
 | 
			
		||||
        createOwnerDirLedgerObject({ripple::uint256{kTOKEN_INDEX1}, ripple::uint256{kTOKEN_INDEX2}}, kTOKEN_INDEX1);
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillByDefault(Return(ownerDir.getSerializer().peekData()));
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObject).Times(2);
 | 
			
		||||
 | 
			
		||||
    std::vector<Blob> bbs;
 | 
			
		||||
    auto const token1 = createMpTokenObject(
 | 
			
		||||
        kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), kTOKEN1_AMOUNT, ripple::lsfMPTLocked, kTOKEN1_LOCKED_AMOUNT
 | 
			
		||||
    );
 | 
			
		||||
    bbs.push_back(token1.getSerializer().peekData());
 | 
			
		||||
 | 
			
		||||
    auto const token2 = createMpTokenObject(
 | 
			
		||||
        kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), kTOKEN2_AMOUNT, ripple::lsfMPTAuthorized, std::nullopt
 | 
			
		||||
    );
 | 
			
		||||
    bbs.push_back(token2.getSerializer().peekData());
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
 | 
			
		||||
 | 
			
		||||
    runSpawn([this](auto yield) {
 | 
			
		||||
        auto const input = json::parse(
 | 
			
		||||
            fmt::format(
 | 
			
		||||
                R"JSON({{
 | 
			
		||||
                    "account": "{}",
 | 
			
		||||
                    "limit": {}
 | 
			
		||||
                }})JSON",
 | 
			
		||||
                kACCOUNT,
 | 
			
		||||
                AccountMPTokensHandler::kLIMIT_MIN - 1
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        auto const correctOutput = fmt::format(
 | 
			
		||||
            R"JSON({{
 | 
			
		||||
                "account": "{}",
 | 
			
		||||
                "ledger_hash": "{}",
 | 
			
		||||
                "ledger_index": 30,
 | 
			
		||||
                "validated": true,
 | 
			
		||||
                "limit": {},
 | 
			
		||||
                "mptokens": [
 | 
			
		||||
                    {},
 | 
			
		||||
                    {}
 | 
			
		||||
                ]
 | 
			
		||||
            }})JSON",
 | 
			
		||||
            kACCOUNT,
 | 
			
		||||
            kLEDGER_HASH,
 | 
			
		||||
            AccountMPTokensHandler::kLIMIT_MIN,
 | 
			
		||||
            kTOKEN_OUT1,
 | 
			
		||||
            kTOKEN_OUT2
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        auto handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        EXPECT_EQ(json::parse(correctOutput), *output.result);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RPCAccountMPTokensHandlerTest, LimitMoreThanMax)
 | 
			
		||||
{
 | 
			
		||||
    auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
 | 
			
		||||
 | 
			
		||||
    auto account = getAccountIdWithString(kACCOUNT);
 | 
			
		||||
    auto accountKk = ripple::keylet::account(account).key;
 | 
			
		||||
    auto owneDirKk = ripple::keylet::ownerDir(account).key;
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
 | 
			
		||||
 | 
			
		||||
    ripple::STObject const ownerDir =
 | 
			
		||||
        createOwnerDirLedgerObject({ripple::uint256{kTOKEN_INDEX1}, ripple::uint256{kTOKEN_INDEX2}}, kTOKEN_INDEX1);
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillByDefault(Return(ownerDir.getSerializer().peekData()));
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObject).Times(2);
 | 
			
		||||
 | 
			
		||||
    std::vector<Blob> bbs;
 | 
			
		||||
    auto const token1 = createMpTokenObject(
 | 
			
		||||
        kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), kTOKEN1_AMOUNT, ripple::lsfMPTLocked, kTOKEN1_LOCKED_AMOUNT
 | 
			
		||||
    );
 | 
			
		||||
    bbs.push_back(token1.getSerializer().peekData());
 | 
			
		||||
 | 
			
		||||
    auto const token2 = createMpTokenObject(
 | 
			
		||||
        kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), kTOKEN2_AMOUNT, ripple::lsfMPTAuthorized, std::nullopt
 | 
			
		||||
    );
 | 
			
		||||
    bbs.push_back(token2.getSerializer().peekData());
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
 | 
			
		||||
 | 
			
		||||
    runSpawn([this](auto yield) {
 | 
			
		||||
        auto const input = json::parse(
 | 
			
		||||
            fmt::format(
 | 
			
		||||
                R"JSON({{
 | 
			
		||||
                    "account": "{}",
 | 
			
		||||
                    "limit": {}
 | 
			
		||||
                }})JSON",
 | 
			
		||||
                kACCOUNT,
 | 
			
		||||
                AccountMPTokensHandler::kLIMIT_MAX + 1
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        auto const correctOutput = fmt::format(
 | 
			
		||||
            R"JSON({{
 | 
			
		||||
                "account": "{}",
 | 
			
		||||
                "ledger_hash": "{}",
 | 
			
		||||
                "ledger_index": 30,
 | 
			
		||||
                "validated": true,
 | 
			
		||||
                "limit": {},
 | 
			
		||||
                "mptokens": [
 | 
			
		||||
                    {},
 | 
			
		||||
                    {}
 | 
			
		||||
                ]
 | 
			
		||||
            }})JSON",
 | 
			
		||||
            kACCOUNT,
 | 
			
		||||
            kLEDGER_HASH,
 | 
			
		||||
            AccountMPTokensHandler::kLIMIT_MAX,
 | 
			
		||||
            kTOKEN_OUT1,
 | 
			
		||||
            kTOKEN_OUT2
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        auto handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        EXPECT_EQ(json::parse(correctOutput), *output.result);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RPCAccountMPTokensHandlerTest, EmptyResult)
 | 
			
		||||
{
 | 
			
		||||
    auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
 | 
			
		||||
    EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
 | 
			
		||||
 | 
			
		||||
    auto account = getAccountIdWithString(kACCOUNT);
 | 
			
		||||
    auto accountKk = ripple::keylet::account(account).key;
 | 
			
		||||
    auto owneDirKk = ripple::keylet::ownerDir(account).key;
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
 | 
			
		||||
 | 
			
		||||
    ripple::STObject const ownerDir = createOwnerDirLedgerObject({}, kTOKEN_INDEX1);
 | 
			
		||||
    ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillByDefault(Return(ownerDir.getSerializer().peekData()));
 | 
			
		||||
    EXPECT_CALL(*backend_, doFetchLedgerObject).Times(2);
 | 
			
		||||
 | 
			
		||||
    runSpawn([this](auto yield) {
 | 
			
		||||
        auto const input = json::parse(
 | 
			
		||||
            fmt::format(
 | 
			
		||||
                R"JSON({{
 | 
			
		||||
                    "account": "{}"
 | 
			
		||||
                }})JSON",
 | 
			
		||||
                kACCOUNT
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
        auto handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
 | 
			
		||||
        auto const output = handler.process(input, Context{yield});
 | 
			
		||||
        ASSERT_TRUE(output);
 | 
			
		||||
        EXPECT_EQ((*output.result).as_object().at("mptokens").as_array().size(), 0);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
@@ -23,6 +23,8 @@
 | 
			
		||||
#include "rpc/handlers/AccountCurrencies.hpp"
 | 
			
		||||
#include "rpc/handlers/AccountInfo.hpp"
 | 
			
		||||
#include "rpc/handlers/AccountLines.hpp"
 | 
			
		||||
#include "rpc/handlers/AccountMPTokenIssuances.hpp"
 | 
			
		||||
#include "rpc/handlers/AccountMPTokens.hpp"
 | 
			
		||||
#include "rpc/handlers/AccountNFTs.hpp"
 | 
			
		||||
#include "rpc/handlers/AccountObjects.hpp"
 | 
			
		||||
#include "rpc/handlers/AccountOffers.hpp"
 | 
			
		||||
@@ -86,6 +88,8 @@ using AnyHandlerType = Types<
 | 
			
		||||
    AccountCurrenciesHandler,
 | 
			
		||||
    AccountInfoHandler,
 | 
			
		||||
    AccountLinesHandler,
 | 
			
		||||
    AccountMPTokenIssuancesHandler,
 | 
			
		||||
    AccountMPTokensHandler,
 | 
			
		||||
    AccountNFTsHandler,
 | 
			
		||||
    AccountObjectsHandler,
 | 
			
		||||
    AccountOffersHandler,
 | 
			
		||||
 
 | 
			
		||||
@@ -3756,7 +3756,6 @@ TEST_F(RPCLedgerEntryTest, SyntheticMPTIssuanceID)
 | 
			
		||||
            "Issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
            "LedgerEntryType": "MPTokenIssuance",
 | 
			
		||||
            "MPTokenMetadata": "6D65746164617461",
 | 
			
		||||
            "MaximumAmount": "0",
 | 
			
		||||
            "OutstandingAmount": "0",
 | 
			
		||||
            "OwnerNode": "0",
 | 
			
		||||
            "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000",
 | 
			
		||||
 
 | 
			
		||||
@@ -309,7 +309,6 @@ TEST_F(RPCVaultInfoHandlerTest, ValidVaultObjectQueryByVaultID)
 | 
			
		||||
                    "Issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
                    "LedgerEntryType": "MPTokenIssuance",
 | 
			
		||||
                    "MPTokenMetadata": "6D65746164617461",
 | 
			
		||||
                    "MaximumAmount": "0",
 | 
			
		||||
                    "OutstandingAmount": "0",
 | 
			
		||||
                    "OwnerNode": "0",
 | 
			
		||||
                    "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000",
 | 
			
		||||
@@ -393,7 +392,6 @@ TEST_F(RPCVaultInfoHandlerTest, ValidVaultObjectQueryByOwnerAndSeq)
 | 
			
		||||
                    "Issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
                    "LedgerEntryType": "MPTokenIssuance",
 | 
			
		||||
                    "MPTokenMetadata": "6D65746164617461",
 | 
			
		||||
                    "MaximumAmount": "0",
 | 
			
		||||
                    "OutstandingAmount": "0",
 | 
			
		||||
                    "OwnerNode": "0",
 | 
			
		||||
                    "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000",
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user