Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 100 additions & 54 deletions src/rpc/handlers/MPTHolders.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "rpc/handlers/MPTHolders.hpp"

#include "data/Types.hpp"
#include "rpc/Errors.hpp"
#include "rpc/JS.hpp"
#include "rpc/RPCHelpers.hpp"
Expand All @@ -24,14 +25,84 @@
#include <cstdint>
#include <optional>
#include <string>
#include <vector>

using namespace xrpl;

namespace rpc {

namespace {

/**
* @brief Serialize a single MPToken ledger object blob into the mpt_holders JSON shape.
*
* @param mptID The MPTokenIssuance ID the holder belongs to.
* @param mpt The serialized MPToken ledger object.
* @return The holder entry as a JSON object.
*/
boost::json::object
mpTokenToJson(xrpl::uint192 const& mptID, data::Blob const& mpt)
{
xrpl::STLedgerEntry const sle{
xrpl::SerialIter{mpt.data(), mpt.size()}, keylet::mptokenIssuance(mptID).key
};
boost::json::object mptJson;

mptJson[JS(account)] = toBase58(sle[xrpl::sfAccount]);
mptJson[JS(flags)] = sle.getFlags();
mptJson[JS(mpt_amount)] = toBoostJson(
xrpl::STUInt64{xrpl::sfMPTAmount, sle[xrpl::sfMPTAmount]}.getJson(JsonOptions::Values::None)
);
mptJson[JS(mptoken_index)] =
xrpl::to_string(xrpl::keylet::mptoken(mptID, sle[xrpl::sfAccount]).key);

if (sle.isFieldPresent(xrpl::sfLockedAmount)) {
mptJson["locked_amount"] = toBoostJson(
xrpl::STUInt64{xrpl::sfLockedAmount, sle[xrpl::sfLockedAmount]}.getJson(
JsonOptions::Values::None
)
);
}

if (sle.isFieldPresent(xrpl::sfConfidentialBalanceInbox)) {
mptJson[JS(confidential_balance_inbox)] =
xrpl::strHex(sle.getFieldVL(xrpl::sfConfidentialBalanceInbox));
}

if (sle.isFieldPresent(xrpl::sfConfidentialBalanceSpending)) {
mptJson[JS(confidential_balance_spending)] =
xrpl::strHex(sle.getFieldVL(xrpl::sfConfidentialBalanceSpending));
}

if (sle.isFieldPresent(xrpl::sfConfidentialBalanceVersion))
mptJson[JS(confidential_balance_version)] = sle[xrpl::sfConfidentialBalanceVersion];

if (sle.isFieldPresent(xrpl::sfIssuerEncryptedBalance)) {
mptJson[JS(issuer_encrypted_balance)] =
xrpl::strHex(sle.getFieldVL(xrpl::sfIssuerEncryptedBalance));
}

if (sle.isFieldPresent(xrpl::sfAuditorEncryptedBalance)) {
mptJson[JS(auditor_encrypted_balance)] =
xrpl::strHex(sle.getFieldVL(xrpl::sfAuditorEncryptedBalance));
}

if (sle.isFieldPresent(xrpl::sfHolderEncryptionKey)) {
mptJson[JS(holder_encryption_key)] =
xrpl::strHex(sle.getFieldVL(xrpl::sfHolderEncryptionKey));
}

return mptJson;
}

} // namespace

MPTHoldersHandler::Result
MPTHoldersHandler::process(MPTHoldersHandler::Input const& input, Context const& ctx) const
{
if (input.accounts && input.marker)
return Error{Status{RippledError::RpcInvalidParams, "accountsWithMarker"}};

auto const range = sharedPtrBackend_->fetchLedgerRange();
ASSERT(range.has_value(), "MPTHolder's ledger range must be available");

Expand All @@ -55,72 +126,41 @@ MPTHoldersHandler::process(MPTHoldersHandler::Input const& input, Context const&
if (!issuanceLedgerObject)
return Error{Status{RippledError::RpcObjectNotFound, "objectNotFound"}};

std::optional<xrpl::AccountID> cursor;
if (input.marker)
cursor = xrpl::AccountID{input.marker->c_str()};

auto const dbResponse =
sharedPtrBackend_->fetchMPTHolders(mptID, limit, cursor, lgrInfo.seq, ctx.yield);
auto output = MPTHoldersHandler::Output{};
output.mptID = to_string(mptID);
output.limit = limit;
output.ledgerIndex = lgrInfo.seq;

boost::json::array const mpts;
for (auto const& mpt : dbResponse.mptokens) {
xrpl::STLedgerEntry const sle{
xrpl::SerialIter{mpt.data(), mpt.size()}, keylet::mptokenIssuance(mptID).key
};
boost::json::object mptJson;

mptJson[JS(account)] = toBase58(sle[xrpl::sfAccount]);
mptJson[JS(flags)] = sle.getFlags();
mptJson[JS(mpt_amount)] = toBoostJson(
xrpl::STUInt64{xrpl::sfMPTAmount, sle[xrpl::sfMPTAmount]}.getJson(
JsonOptions::Values::None
)
);
mptJson[JS(mptoken_index)] =
xrpl::to_string(xrpl::keylet::mptoken(mptID, sle[xrpl::sfAccount]).key);

if (sle.isFieldPresent(xrpl::sfLockedAmount)) {
mptJson["locked_amount"] = toBoostJson(
xrpl::STUInt64{xrpl::sfLockedAmount, sle[xrpl::sfLockedAmount]}.getJson(
JsonOptions::Values::None
)
);
// Account-list filter: bounded lookup by key, so non-holders are dropped and no
// paging marker is produced.
if (input.accounts) {
std::vector<xrpl::uint256> keys;
keys.reserve(input.accounts->size());
for (auto const& account : *input.accounts) {
auto const accountID = accountFromStringStrict(account);
ASSERT(accountID.has_value(), "Account must be valid after spec validation");
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
keys.push_back(xrpl::keylet::mptoken(mptID, *accountID).key);
}
Comment on lines +139 to 144

if (sle.isFieldPresent(xrpl::sfConfidentialBalanceInbox)) {
mptJson[JS(confidential_balance_inbox)] =
xrpl::strHex(sle.getFieldVL(xrpl::sfConfidentialBalanceInbox));
auto const mptObjects = sharedPtrBackend_->fetchLedgerObjects(keys, lgrInfo.seq, ctx.yield);
for (auto const& mpt : mptObjects) {
if (not mpt.empty())
output.mpts.push_back(mpTokenToJson(mptID, mpt));
}

if (sle.isFieldPresent(xrpl::sfConfidentialBalanceSpending)) {
mptJson[JS(confidential_balance_spending)] =
xrpl::strHex(sle.getFieldVL(xrpl::sfConfidentialBalanceSpending));
}

if (sle.isFieldPresent(xrpl::sfConfidentialBalanceVersion))
mptJson[JS(confidential_balance_version)] = sle[xrpl::sfConfidentialBalanceVersion];

if (sle.isFieldPresent(xrpl::sfIssuerEncryptedBalance)) {
mptJson[JS(issuer_encrypted_balance)] =
xrpl::strHex(sle.getFieldVL(xrpl::sfIssuerEncryptedBalance));
}
return output;
}

if (sle.isFieldPresent(xrpl::sfAuditorEncryptedBalance)) {
mptJson[JS(auditor_encrypted_balance)] =
xrpl::strHex(sle.getFieldVL(xrpl::sfAuditorEncryptedBalance));
}
std::optional<xrpl::AccountID> cursor;
if (input.marker)
cursor = xrpl::AccountID{input.marker->c_str()};

if (sle.isFieldPresent(xrpl::sfHolderEncryptionKey)) {
mptJson[JS(holder_encryption_key)] =
xrpl::strHex(sle.getFieldVL(xrpl::sfHolderEncryptionKey));
}
auto const dbResponse =
sharedPtrBackend_->fetchMPTHolders(mptID, limit, cursor, lgrInfo.seq, ctx.yield);

output.mpts.push_back(mptJson);
}
for (auto const& mpt : dbResponse.mptokens)
output.mpts.push_back(mpTokenToJson(mptID, mpt));

if (dbResponse.cursor.has_value())
output.marker = strHex(*dbResponse.cursor);
Expand Down Expand Up @@ -170,6 +210,12 @@ tag_invoke(boost::json::value_to_tag<MPTHoldersHandler::Input>, boost::json::val
if (jsonObject.contains(JS(marker)))
input.marker = jsonObject.at(JS(marker)).as_string().c_str();

if (jsonObject.contains(JS(accounts))) {
auto& accounts = input.accounts.emplace();
for (auto const& account : jsonObject.at(JS(accounts)).as_array())
accounts.emplace_back(account.as_string().c_str());
}

return input;
}
} // namespace rpc
42 changes: 42 additions & 0 deletions src/rpc/handlers/MPTHolders.hpp
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
#pragma once

#include "data/BackendInterface.hpp"
#include "rpc/Errors.hpp"
#include "rpc/JS.hpp"
#include "rpc/common/Modifiers.hpp"
#include "rpc/common/Specs.hpp"
#include "rpc/common/Types.hpp"
#include "rpc/common/Validators.hpp"
#include "util/AccountUtils.hpp"

#include <boost/json/array.hpp>
#include <boost/json/conversion.hpp>
#include <boost/json/value.hpp>
#include <boost/json/value_to.hpp>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/jss.h>

#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

namespace rpc {

Expand All @@ -30,6 +37,7 @@ class MPTHoldersHandler {
static constexpr auto kLimitMin = 1;
static constexpr auto kLimitMax = 100;
static constexpr auto kLimitDefault = 50;
static constexpr auto kMaxAccounts = 100;

/**
* @brief A struct to hold the output data of the command
Expand All @@ -52,6 +60,7 @@ class MPTHoldersHandler {
std::optional<uint32_t> ledgerIndex;
std::optional<std::string> marker;
std::optional<uint32_t> limit;
std::optional<std::vector<std::string>> accounts;
};

using Result = HandlerReturnType<Output>;
Expand All @@ -75,6 +84,38 @@ class MPTHoldersHandler {
static RpcSpecConstRef
spec([[maybe_unused]] uint32_t apiVersion)
{
// Optional filter: when present, return only these accounts' MPToken state for the
// issuance.
static auto const kAccountsValidator = validation::CustomValidator{
[](boost::json::value const& value, std::string_view key) -> MaybeError {
if (!value.is_array()) {
return Error{
Status{RippledError::RpcInvalidParams, std::string{key} + "NotArray"}
};
}

auto const& accounts = value.as_array();
if (accounts.empty() || accounts.size() > static_cast<std::size_t>(kMaxAccounts)) {
return Error{
Status{RippledError::RpcInvalidParams, std::string{key} + "Malformed"}
};
}

for (auto const& account : accounts) {
if (!account.is_string() ||
!util::parseBase58Wrapper<xrpl::AccountID>(
boost::json::value_to<std::string>(account)
)) {
return Error{
Status{RippledError::RpcInvalidParams, std::string{key} + "Malformed"}
};
}
}

return MaybeError{};
}
};

static auto const kRpcSpec = RpcSpec{
{JS(mpt_issuance_id),
validation::Required{},
Expand All @@ -86,6 +127,7 @@ class MPTHoldersHandler {
validation::Min(1u),
modifiers::Clamp<int32_t>{kLimitMin, kLimitMax}},
{JS(marker), validation::CustomValidators::uint160HexStringValidator},
{JS(accounts), kAccountsValidator},
};

return kRpcSpec;
Expand Down
Loading
Loading