feat: Support account_mptoken_issuances and account_mptokens (#2680)

Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
This commit is contained in:
yinyiqian1
2025-10-29 10:17:43 -04:00
committed by GitHub
parent 3b61a85ba0
commit eed757e0c4
15 changed files with 2452 additions and 13 deletions

View File

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

View File

@@ -35,6 +35,8 @@ handledRpcs()
"account_currencies",
"account_info",
"account_lines",
"account_mptoken_issuances",
"account_mptokens",
"account_nfts",
"account_objects",
"account_offers",

View File

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

View 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

View 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

View 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

View 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

View File

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

View File

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

View File

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

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

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

View File

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

View File

@@ -3756,7 +3756,6 @@ TEST_F(RPCLedgerEntryTest, SyntheticMPTIssuanceID)
"Issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"LedgerEntryType": "MPTokenIssuance",
"MPTokenMetadata": "6D65746164617461",
"MaximumAmount": "0",
"OutstandingAmount": "0",
"OwnerNode": "0",
"PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000",

View File

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