mirror of
https://github.com/XRPLF/clio.git
synced 2026-09-27 23:48:08 +00:00
feat: Add accounts filter to mpt_holders (#3180)
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
"requires": [
|
||||
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
|
||||
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1782392402.420688",
|
||||
"xrpl-rpc-spec/0.1.13#9777f2a775dc8152b7e3853475c9e177%1789657012.888807",
|
||||
"xrpl-rpc-spec/0.1.14#de860864925a4536a97bb7c4d01caddc%1789662365.154657",
|
||||
"xrpl/3.4.0-rc1#19678cbb46117ef8a669558ad19d6a1f%1789050823.813662",
|
||||
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1782392403.185447",
|
||||
"spdlog/1.17.0#bcbaaf7147bda6ad24ffbd1ac3d7142c%1782736610.443882",
|
||||
|
||||
@@ -17,7 +17,7 @@ class ClioConan(ConanFile):
|
||||
"fmt/12.1.0",
|
||||
"libbacktrace/cci.20210118",
|
||||
"spdlog/1.17.0",
|
||||
"xrpl-rpc-spec/0.1.13",
|
||||
"xrpl-rpc-spec/0.1.14",
|
||||
"xrpl/3.4.0-rc1",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "rpc/handlers/MPTHolders.hpp"
|
||||
|
||||
#include "data/Types.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/RPCHelpers.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
@@ -21,16 +22,89 @@
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
using namespace xrpl;
|
||||
|
||||
namespace rpc {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief Serialize a single MPToken ledger object blob into the mpt_holders JSON.
|
||||
*
|
||||
* @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()}, xrpl::uint256{}};
|
||||
auto const mptokenKey = keylet::mptoken(mptID, sle[xrpl::sfAccount]).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(mptokenKey);
|
||||
|
||||
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
|
||||
{
|
||||
// Account-list filter is a bounded, unpaginated lookup by key: marker/limit does not make sense
|
||||
// alongside it.
|
||||
if (input.accounts) {
|
||||
if (input.marker.has_value())
|
||||
return Error{Status{RippledError::RpcInvalidParams, "accountsWithMarker"}};
|
||||
if (input.limit.has_value())
|
||||
return Error{Status{RippledError::RpcInvalidParams, "accountsWithLimit"}};
|
||||
}
|
||||
|
||||
auto const range = sharedPtrBackend_->fetchLedgerRange();
|
||||
ASSERT(range.has_value(), "MPTHolder's ledger range must be available");
|
||||
|
||||
@@ -44,7 +118,7 @@ MPTHoldersHandler::process(MPTHoldersHandler::Input const& input, Context const&
|
||||
return Error{expectedLgrInfo.error()};
|
||||
|
||||
auto const& lgrInfo = *expectedLgrInfo;
|
||||
auto const limit = input.limit;
|
||||
auto const limit = input.limit.value_or(MPTHoldersHandler::kLimitDefault);
|
||||
auto const& mptID = input.mptID;
|
||||
|
||||
auto const issuanceLedgerObject = sharedPtrBackend_->fetchLedgerObject(
|
||||
@@ -53,69 +127,37 @@ MPTHoldersHandler::process(MPTHoldersHandler::Input const& input, Context const&
|
||||
if (!issuanceLedgerObject)
|
||||
return Error{Status{RippledError::RpcObjectNotFound, "objectNotFound"}};
|
||||
|
||||
auto const dbResponse =
|
||||
sharedPtrBackend_->fetchMPTHolders(mptID, limit, input.marker, 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. Duplicates are dropped in first-seen
|
||||
// order, non-holders are omitted, and no paging marker is produced.
|
||||
if (input.accounts) {
|
||||
std::vector<xrpl::uint256> keys;
|
||||
keys.reserve(input.accounts->size());
|
||||
for (auto const& accountID : *input.accounts) {
|
||||
auto const key = xrpl::keylet::mptoken(mptID, accountID).key;
|
||||
if (not std::ranges::contains(keys, key))
|
||||
keys.push_back(key);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
output.mpts.push_back(mptJson);
|
||||
return output;
|
||||
}
|
||||
|
||||
auto const dbResponse =
|
||||
sharedPtrBackend_->fetchMPTHolders(mptID, limit, input.marker, lgrInfo.seq, ctx.yield);
|
||||
|
||||
for (auto const& mpt : dbResponse.mptokens)
|
||||
output.mpts.push_back(mpTokenToJson(mptID, mpt));
|
||||
|
||||
if (dbResponse.cursor.has_value())
|
||||
output.marker = strHex(*dbResponse.cursor);
|
||||
|
||||
|
||||
@@ -18,8 +18,13 @@
|
||||
namespace rpc {
|
||||
|
||||
/**
|
||||
* @brief The mpt_holders command asks the Clio server for all holders of a particular
|
||||
* @brief The mpt_holders command asks the Clio server for holders of a particular
|
||||
* MPTokenIssuance.
|
||||
*
|
||||
* When `accounts` is provided, those accounts are looked up directly by
|
||||
* `keylet::mptoken` instead of scanning the holder index. Duplicate accounts are
|
||||
* collapsed in first-seen order and non-holders are omitted. This filtered mode is
|
||||
* not paginated, so `marker` and `limit` are rejected.
|
||||
*/
|
||||
class MPTHoldersHandler : public rpc::spec::HandlerFor<rpc::spec::handlers::mpt_holders::Input> {
|
||||
std::shared_ptr<BackendInterface> sharedPtrBackend_;
|
||||
@@ -28,6 +33,7 @@ public:
|
||||
static constexpr auto kLimitMin = rpc::spec::handlers::mpt_holders::kLimitMin;
|
||||
static constexpr auto kLimitMax = rpc::spec::handlers::mpt_holders::kLimitMax;
|
||||
static constexpr auto kLimitDefault = rpc::spec::handlers::mpt_holders::kLimitDefault;
|
||||
static constexpr auto kMaxAccounts = rpc::spec::handlers::mpt_holders::kMaxAccounts;
|
||||
|
||||
/**
|
||||
* @brief A struct to hold the output data of the command
|
||||
|
||||
@@ -194,8 +194,10 @@ generateParameterBookOffersTestBundles()
|
||||
.expectedErrorMessage = "Source currency is malformed."
|
||||
},
|
||||
ParameterTestBundle{
|
||||
// A present-but-non-string currency is reported by validateTakerJSON as an
|
||||
// expectedFieldError ('<field>.currency', not string) before the per-field validators.
|
||||
// Clio deliberately skips the early "is currency a string" check (see kTakerValidator)
|
||||
// and lets it fall through to the section's own currency validator, which is wrapped
|
||||
// in withCustomError(currency, RpcDstAmtMalformed) with no message override, so the
|
||||
// default rippled message/token for that code is what surfaces.
|
||||
.testName = "TakerGetsCurrencyNotString",
|
||||
.testJson = R"JSON({
|
||||
"taker_gets": {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerHeader.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -28,12 +29,12 @@ using namespace testing;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kHoldeR1Account = "rrnAZCqMahreZrKMcZU3t2DZ6yUndT4ubN";
|
||||
constexpr auto kHoldeR2Account = "rEiNkzogdHEzUxPfsri5XSMqtXUixf2Yx";
|
||||
constexpr auto kHolder1Account = "rrnAZCqMahreZrKMcZU3t2DZ6yUndT4ubN";
|
||||
constexpr auto kHolder2Account = "rEiNkzogdHEzUxPfsri5XSMqtXUixf2Yx";
|
||||
constexpr auto kLedgerHash = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
|
||||
constexpr auto kMptId = "000004C463C52827307480341125DA0577DEFC38405B0E3E";
|
||||
|
||||
std::string const kMptOuT1 =
|
||||
std::string const kMptOut1 =
|
||||
R"JSON({
|
||||
"account": "rrnAZCqMahreZrKMcZU3t2DZ6yUndT4ubN",
|
||||
"flags": 0,
|
||||
@@ -41,7 +42,7 @@ std::string const kMptOuT1 =
|
||||
"mptoken_index": "D137F2E5A5767A06CB7A8F060ADE442A30CFF95028E1AF4B8767E3A56877205A"
|
||||
})JSON";
|
||||
|
||||
std::string const kMptOuT2 =
|
||||
std::string const kMptOut2 =
|
||||
R"JSON({
|
||||
"account": "rEiNkzogdHEzUxPfsri5XSMqtXUixf2Yx",
|
||||
"flags": 0,
|
||||
@@ -389,7 +390,7 @@ TEST_F(RPCMPTHoldersHandlerTest, DefaultParameters)
|
||||
"validated": true
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kMptOuT1
|
||||
kMptOut1
|
||||
);
|
||||
|
||||
auto ledgerInfo = createLedgerHeader(kLedgerHash, 30);
|
||||
@@ -398,7 +399,7 @@ TEST_F(RPCMPTHoldersHandlerTest, DefaultParameters)
|
||||
ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _))
|
||||
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const mptoken = createMpTokenObject(kHoldeR1Account, xrpl::uint192(kMptId));
|
||||
auto const mptoken = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId));
|
||||
std::vector<Blob> const mpts = {mptoken.getSerializer().peekData()};
|
||||
ON_CALL(*backend_, fetchMPTHolders)
|
||||
.WillByDefault(Return(MPTHoldersAndCursor{.mptokens = mpts, .cursor = {}}));
|
||||
@@ -452,7 +453,7 @@ TEST_F(RPCMPTHoldersHandlerTest, CustomAmounts)
|
||||
ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _))
|
||||
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const mptoken = createMpTokenObject(kHoldeR1Account, xrpl::uint192(kMptId), 0);
|
||||
auto const mptoken = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId), 0);
|
||||
std::vector<Blob> const mpts = {mptoken.getSerializer().peekData()};
|
||||
ON_CALL(*backend_, fetchMPTHolders)
|
||||
.WillByDefault(Return(MPTHoldersAndCursor{.mptokens = mpts, .cursor = {}}));
|
||||
@@ -511,7 +512,7 @@ TEST_F(RPCMPTHoldersHandlerTest, ConfidentialFields)
|
||||
"validated": true
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kHoldeR1Account,
|
||||
kHolder1Account,
|
||||
kLockedAmount,
|
||||
xrpl::strHex(std::string_view{kConfidentialBalanceInbox}),
|
||||
xrpl::strHex(std::string_view{kConfidentialBalanceSpending}),
|
||||
@@ -528,7 +529,7 @@ TEST_F(RPCMPTHoldersHandlerTest, ConfidentialFields)
|
||||
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const mptoken = createMpTokenObject(
|
||||
kHoldeR1Account,
|
||||
kHolder1Account,
|
||||
xrpl::uint192(kMptId),
|
||||
1,
|
||||
0,
|
||||
@@ -580,7 +581,7 @@ TEST_F(RPCMPTHoldersHandlerTest, SpecificLedgerIndex)
|
||||
}})JSON",
|
||||
kMptId,
|
||||
specificLedger,
|
||||
kMptOuT1
|
||||
kMptOut1
|
||||
);
|
||||
|
||||
auto ledgerInfo = createLedgerHeader(kLedgerHash, specificLedger);
|
||||
@@ -590,7 +591,7 @@ TEST_F(RPCMPTHoldersHandlerTest, SpecificLedgerIndex)
|
||||
ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, specificLedger, _))
|
||||
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const mptoken = createMpTokenObject(kHoldeR1Account, xrpl::uint192(kMptId));
|
||||
auto const mptoken = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId));
|
||||
std::vector<Blob> const mpts = {mptoken.getSerializer().peekData()};
|
||||
ON_CALL(*backend_, fetchMPTHolders)
|
||||
.WillByDefault(Return(MPTHoldersAndCursor{.mptokens = mpts, .cursor = {}}));
|
||||
@@ -636,8 +637,8 @@ TEST_F(RPCMPTHoldersHandlerTest, MarkerParameter)
|
||||
"marker": "{}"
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kMptOuT2,
|
||||
xrpl::strHex(getAccountIdWithString(kHoldeR1Account))
|
||||
kMptOut2,
|
||||
xrpl::strHex(getAccountIdWithString(kHolder1Account))
|
||||
);
|
||||
|
||||
auto ledgerInfo = createLedgerHeader(kLedgerHash, 30);
|
||||
@@ -646,9 +647,9 @@ TEST_F(RPCMPTHoldersHandlerTest, MarkerParameter)
|
||||
ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _))
|
||||
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const mptoken = createMpTokenObject(kHoldeR2Account, xrpl::uint192(kMptId));
|
||||
auto const mptoken = createMpTokenObject(kHolder2Account, xrpl::uint192(kMptId));
|
||||
std::vector<Blob> const mpts = {mptoken.getSerializer().peekData()};
|
||||
auto const marker = getAccountIdWithString(kHoldeR1Account);
|
||||
auto const marker = getAccountIdWithString(kHolder1Account);
|
||||
ON_CALL(*backend_, fetchMPTHolders)
|
||||
.WillByDefault(Return(MPTHoldersAndCursor{.mptokens = mpts, .cursor = marker}));
|
||||
EXPECT_CALL(
|
||||
@@ -659,7 +660,7 @@ TEST_F(RPCMPTHoldersHandlerTest, MarkerParameter)
|
||||
)
|
||||
.Times(1);
|
||||
|
||||
auto const holder1AccountId = xrpl::strHex(getAccountIdWithString(kHoldeR1Account));
|
||||
auto const holder1AccountId = xrpl::strHex(getAccountIdWithString(kHolder1Account));
|
||||
auto const input = boost::json::parse(
|
||||
fmt::format(
|
||||
R"JSON({{
|
||||
@@ -689,8 +690,8 @@ TEST_F(RPCMPTHoldersHandlerTest, MultipleMPTs)
|
||||
"validated": true
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kMptOuT1,
|
||||
kMptOuT2
|
||||
kMptOut1,
|
||||
kMptOut2
|
||||
);
|
||||
|
||||
auto ledgerInfo = createLedgerHeader(kLedgerHash, 30);
|
||||
@@ -699,8 +700,8 @@ TEST_F(RPCMPTHoldersHandlerTest, MultipleMPTs)
|
||||
ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _))
|
||||
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const mptoken1 = createMpTokenObject(kHoldeR1Account, xrpl::uint192(kMptId));
|
||||
auto const mptoken2 = createMpTokenObject(kHoldeR2Account, xrpl::uint192(kMptId));
|
||||
auto const mptoken1 = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId));
|
||||
auto const mptoken2 = createMpTokenObject(kHolder2Account, xrpl::uint192(kMptId));
|
||||
std::vector<Blob> const mpts = {
|
||||
mptoken1.getSerializer().peekData(), mptoken2.getSerializer().peekData()
|
||||
};
|
||||
@@ -730,6 +731,583 @@ TEST_F(RPCMPTHoldersHandlerTest, MultipleMPTs)
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, AccountsNotArray)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend_}};
|
||||
auto const input = boost::json::parse(
|
||||
fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"accounts": "{}"
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kHolder1Account
|
||||
)
|
||||
);
|
||||
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(), "invalidParams");
|
||||
EXPECT_EQ(err.at("error_message").as_string(), "Invalid field 'accounts', not array.");
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, AccountsEmpty)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend_}};
|
||||
auto const input = boost::json::parse(
|
||||
fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"accounts": []
|
||||
}})JSON",
|
||||
kMptId
|
||||
)
|
||||
);
|
||||
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(), "invalidParams");
|
||||
EXPECT_EQ(
|
||||
err.at("error_message").as_string(),
|
||||
"Invalid field 'accounts', not an array of 1 to 100 account IDs."
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, AccountsMalformedEntry)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend_}};
|
||||
auto const input = boost::json::parse(
|
||||
fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"accounts": ["{}", "not-an-account"]
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kHolder1Account
|
||||
)
|
||||
);
|
||||
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(), "invalidParams");
|
||||
EXPECT_EQ(
|
||||
err.at("error_message").as_string(),
|
||||
"Invalid field 'accounts', not an array of account IDs."
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, AccountsTooMany)
|
||||
{
|
||||
std::string accountsList;
|
||||
for (std::size_t i = 0; i <= MPTHoldersHandler::kMaxAccounts; ++i)
|
||||
accountsList += fmt::format(R"("{}",)", kHolder1Account);
|
||||
accountsList.pop_back(); // drop trailing comma
|
||||
|
||||
runSpawn([&, this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend_}};
|
||||
auto const input = boost::json::parse(
|
||||
fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"accounts": [{}]
|
||||
}})JSON",
|
||||
kMptId,
|
||||
accountsList
|
||||
)
|
||||
);
|
||||
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(), "invalidParams");
|
||||
EXPECT_EQ(
|
||||
err.at("error_message").as_string(),
|
||||
"Invalid field 'accounts', not an array of 1 to 100 account IDs."
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, AccountsWithMarker)
|
||||
{
|
||||
auto const marker = xrpl::strHex(getAccountIdWithString(kHolder1Account));
|
||||
runSpawn([&, this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend_}};
|
||||
auto const input = boost::json::parse(
|
||||
fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"accounts": ["{}"],
|
||||
"marker": "{}"
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kHolder1Account,
|
||||
marker
|
||||
)
|
||||
);
|
||||
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(), "invalidParams");
|
||||
EXPECT_EQ(err.at("error_message").as_string(), "accountsWithMarker");
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, AccountsWithLimit)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend_}};
|
||||
auto const input = boost::json::parse(
|
||||
fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"accounts": ["{}"],
|
||||
"limit": 1
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kHolder1Account
|
||||
)
|
||||
);
|
||||
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(), "invalidParams");
|
||||
EXPECT_EQ(err.at("error_message").as_string(), "accountsWithLimit");
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, AccountsFilter)
|
||||
{
|
||||
auto const currentOutput = fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"limit": 50,
|
||||
"ledger_index": 30,
|
||||
"mptokens": [{}, {}],
|
||||
"validated": true
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kMptOut1,
|
||||
kMptOut2
|
||||
);
|
||||
|
||||
auto ledgerInfo = createLedgerHeader(kLedgerHash, 30);
|
||||
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerInfo));
|
||||
auto const issuanceKk = xrpl::keylet::mptokenIssuance(xrpl::uint192(kMptId)).key;
|
||||
ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _))
|
||||
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const mptoken1 = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId));
|
||||
auto const mptoken2 = createMpTokenObject(kHolder2Account, xrpl::uint192(kMptId));
|
||||
std::vector<Blob> const objs = {
|
||||
mptoken1.getSerializer().peekData(), mptoken2.getSerializer().peekData()
|
||||
};
|
||||
// The handler must query exactly the MPToken keys for the requested accounts, in order.
|
||||
std::vector<xrpl::uint256> const expectedKeys = {
|
||||
xrpl::keylet::mptoken(xrpl::uint192(kMptId), getAccountIdWithString(kHolder1Account)).key,
|
||||
xrpl::keylet::mptoken(xrpl::uint192(kMptId), getAccountIdWithString(kHolder2Account)).key
|
||||
};
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObjects(expectedKeys, 30, _)).WillOnce(Return(objs));
|
||||
// The accounts filter must not fall back to the holder-index scan.
|
||||
EXPECT_CALL(*backend_, fetchMPTHolders).Times(0);
|
||||
|
||||
auto const input = boost::json::parse(
|
||||
fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"accounts": ["{}", "{}"]
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kHolder1Account,
|
||||
kHolder2Account
|
||||
)
|
||||
);
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto handler = AnyHandler{MPTHoldersHandler{this->backend_}};
|
||||
auto const output = handler.process(input, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(boost::json::parse(currentOutput), *output.result);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, AccountsFilterMissingHolderOmitted)
|
||||
{
|
||||
auto const currentOutput = fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"limit": 50,
|
||||
"ledger_index": 30,
|
||||
"mptokens": [{}],
|
||||
"validated": true
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kMptOut1
|
||||
);
|
||||
|
||||
auto ledgerInfo = createLedgerHeader(kLedgerHash, 30);
|
||||
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerInfo));
|
||||
auto const issuanceKk = xrpl::keylet::mptokenIssuance(xrpl::uint192(kMptId)).key;
|
||||
ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _))
|
||||
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const mptoken1 = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId));
|
||||
// holder2 is not a holder: its object comes back empty and must be dropped.
|
||||
std::vector<Blob> const objs = {mptoken1.getSerializer().peekData(), Blob{}};
|
||||
ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(objs));
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1);
|
||||
EXPECT_CALL(*backend_, fetchMPTHolders).Times(0);
|
||||
|
||||
auto const input = boost::json::parse(
|
||||
fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"accounts": ["{}", "{}"]
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kHolder1Account,
|
||||
kHolder2Account
|
||||
)
|
||||
);
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto handler = AnyHandler{MPTHoldersHandler{this->backend_}};
|
||||
auto const output = handler.process(input, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(boost::json::parse(currentOutput), *output.result);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, AccountsNonStringEntry)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{MPTHoldersHandler{backend_}};
|
||||
auto const input = boost::json::parse(
|
||||
fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"accounts": ["{}", 123]
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kHolder1Account
|
||||
)
|
||||
);
|
||||
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(), "invalidParams");
|
||||
EXPECT_EQ(
|
||||
err.at("error_message").as_string(),
|
||||
"Invalid field 'accounts', not an array of account IDs."
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, AccountsMaxAllowed)
|
||||
{
|
||||
std::string accountsList;
|
||||
for (std::size_t i = 0; i < MPTHoldersHandler::kMaxAccounts; ++i)
|
||||
accountsList += fmt::format(R"("{}",)", kHolder1Account);
|
||||
accountsList.pop_back(); // drop trailing comma
|
||||
|
||||
auto ledgerInfo = createLedgerHeader(kLedgerHash, 30);
|
||||
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerInfo));
|
||||
auto const issuanceKk = xrpl::keylet::mptokenIssuance(xrpl::uint192(kMptId)).key;
|
||||
ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _))
|
||||
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const mptoken = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId));
|
||||
std::vector<Blob> const objs = {mptoken.getSerializer().peekData()};
|
||||
auto const key =
|
||||
xrpl::keylet::mptoken(xrpl::uint192(kMptId), getAccountIdWithString(kHolder1Account)).key;
|
||||
std::vector<xrpl::uint256> const expectedKeys = {key};
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObjects(expectedKeys, 30, _)).WillOnce(Return(objs));
|
||||
EXPECT_CALL(*backend_, fetchMPTHolders).Times(0);
|
||||
|
||||
auto const input = boost::json::parse(
|
||||
fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"accounts": [{}]
|
||||
}})JSON",
|
||||
kMptId,
|
||||
accountsList
|
||||
)
|
||||
);
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto handler = AnyHandler{MPTHoldersHandler{this->backend_}};
|
||||
auto const output = handler.process(input, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(output.result->as_object().at("mptokens").as_array().size(), 1u);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, AccountsFilterAllAbsent)
|
||||
{
|
||||
auto const currentOutput = fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"limit": 50,
|
||||
"ledger_index": 30,
|
||||
"mptokens": [],
|
||||
"validated": true
|
||||
}})JSON",
|
||||
kMptId
|
||||
);
|
||||
|
||||
auto ledgerInfo = createLedgerHeader(kLedgerHash, 30);
|
||||
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerInfo));
|
||||
auto const issuanceKk = xrpl::keylet::mptokenIssuance(xrpl::uint192(kMptId)).key;
|
||||
ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _))
|
||||
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
std::vector<Blob> const objs = {Blob{}, Blob{}};
|
||||
ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(objs));
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1);
|
||||
EXPECT_CALL(*backend_, fetchMPTHolders).Times(0);
|
||||
|
||||
auto const input = boost::json::parse(
|
||||
fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"accounts": ["{}", "{}"]
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kHolder1Account,
|
||||
kHolder2Account
|
||||
)
|
||||
);
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto handler = AnyHandler{MPTHoldersHandler{this->backend_}};
|
||||
auto const output = handler.process(input, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(boost::json::parse(currentOutput), *output.result);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, AccountsFilterConfidentialFields)
|
||||
{
|
||||
constexpr auto kConfidentialBalanceInbox = "inbox-ciphertext";
|
||||
constexpr auto kConfidentialBalanceSpending = "spending-ciphertext";
|
||||
constexpr auto kConfidentialBalanceVersion = 3u;
|
||||
constexpr auto kIssuerEncryptedBalance = "issuer-balance-ciphertext";
|
||||
constexpr auto kAuditorEncryptedBalance = "auditor-balance-ciphertext";
|
||||
constexpr auto kHolderEncryptionKey = "holder-pubkey";
|
||||
constexpr auto kLockedAmount = 5;
|
||||
|
||||
auto const currentOutput = fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"limit": 50,
|
||||
"ledger_index": 30,
|
||||
"mptokens": [{{
|
||||
"account": "{}",
|
||||
"flags": 0,
|
||||
"mpt_amount": "1",
|
||||
"mptoken_index": "D137F2E5A5767A06CB7A8F060ADE442A30CFF95028E1AF4B8767E3A56877205A",
|
||||
"locked_amount": "{}",
|
||||
"confidential_balance_inbox": "{}",
|
||||
"confidential_balance_spending": "{}",
|
||||
"confidential_balance_version": {},
|
||||
"issuer_encrypted_balance": "{}",
|
||||
"auditor_encrypted_balance": "{}",
|
||||
"holder_encryption_key": "{}"
|
||||
}}],
|
||||
"validated": true
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kHolder1Account,
|
||||
kLockedAmount,
|
||||
xrpl::strHex(std::string_view{kConfidentialBalanceInbox}),
|
||||
xrpl::strHex(std::string_view{kConfidentialBalanceSpending}),
|
||||
kConfidentialBalanceVersion,
|
||||
xrpl::strHex(std::string_view{kIssuerEncryptedBalance}),
|
||||
xrpl::strHex(std::string_view{kAuditorEncryptedBalance}),
|
||||
xrpl::strHex(std::string_view{kHolderEncryptionKey})
|
||||
);
|
||||
|
||||
auto ledgerInfo = createLedgerHeader(kLedgerHash, 30);
|
||||
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerInfo));
|
||||
auto const issuanceKk = xrpl::keylet::mptokenIssuance(xrpl::uint192(kMptId)).key;
|
||||
ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _))
|
||||
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const mptoken = createMpTokenObject(
|
||||
kHolder1Account,
|
||||
xrpl::uint192(kMptId),
|
||||
1,
|
||||
0,
|
||||
kLockedAmount,
|
||||
kConfidentialBalanceInbox,
|
||||
kConfidentialBalanceSpending,
|
||||
kConfidentialBalanceVersion,
|
||||
kIssuerEncryptedBalance,
|
||||
kAuditorEncryptedBalance,
|
||||
kHolderEncryptionKey
|
||||
);
|
||||
std::vector<Blob> const objs = {mptoken.getSerializer().peekData()};
|
||||
ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(objs));
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1);
|
||||
EXPECT_CALL(*backend_, fetchMPTHolders).Times(0);
|
||||
|
||||
auto const input = boost::json::parse(
|
||||
fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"accounts": ["{}"]
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kHolder1Account
|
||||
)
|
||||
);
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto handler = AnyHandler{MPTHoldersHandler{this->backend_}};
|
||||
auto const output = handler.process(input, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(boost::json::parse(currentOutput), *output.result);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, AccountsFilterMPTNotFound)
|
||||
{
|
||||
auto ledgerinfo = createLedgerHeader(kLedgerHash, 30);
|
||||
ON_CALL(*backend_, fetchLedgerByHash(xrpl::uint256{kLedgerHash}, _))
|
||||
.WillByDefault(Return(ledgerinfo));
|
||||
EXPECT_CALL(*backend_, fetchLedgerByHash).Times(1);
|
||||
ON_CALL(*backend_, doFetchLedgerObject).WillByDefault(Return(std::optional<Blob>{}));
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject).Times(1);
|
||||
// Must not attempt to fetch holder objects when the issuance does not exist.
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(0);
|
||||
|
||||
auto const input = boost::json::parse(
|
||||
fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"ledger_hash": "{}",
|
||||
"accounts": ["{}"]
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kLedgerHash,
|
||||
kHolder1Account
|
||||
)
|
||||
);
|
||||
runSpawn([&, this](boost::asio::yield_context yield) {
|
||||
auto handler = AnyHandler{MPTHoldersHandler{this->backend_}};
|
||||
auto const output = handler.process(input, Context{.yield = yield});
|
||||
ASSERT_FALSE(output);
|
||||
auto const err = rpc::makeError(output.result.error());
|
||||
EXPECT_EQ(err.at("error").as_string(), "objectNotFound");
|
||||
EXPECT_EQ(err.at("error_message").as_string(), "objectNotFound");
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, AccountsFilterSpecificLedger)
|
||||
{
|
||||
auto const specificLedger = 20;
|
||||
auto const currentOutput = fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"limit": 50,
|
||||
"ledger_index": {},
|
||||
"mptokens": [{}],
|
||||
"validated": true
|
||||
}})JSON",
|
||||
kMptId,
|
||||
specificLedger,
|
||||
kMptOut1
|
||||
);
|
||||
|
||||
auto ledgerInfo = createLedgerHeader(kLedgerHash, specificLedger);
|
||||
ON_CALL(*backend_, fetchLedgerBySequence(specificLedger, _)).WillByDefault(Return(ledgerInfo));
|
||||
EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1);
|
||||
auto const issuanceKk = xrpl::keylet::mptokenIssuance(xrpl::uint192(kMptId)).key;
|
||||
ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, specificLedger, _))
|
||||
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const mptoken1 = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId));
|
||||
std::vector<Blob> const objs = {mptoken1.getSerializer().peekData()};
|
||||
std::vector<xrpl::uint256> const expectedKeys = {
|
||||
xrpl::keylet::mptoken(xrpl::uint192(kMptId), getAccountIdWithString(kHolder1Account)).key
|
||||
};
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObjects(expectedKeys, specificLedger, _))
|
||||
.WillOnce(Return(objs));
|
||||
EXPECT_CALL(*backend_, fetchMPTHolders).Times(0);
|
||||
|
||||
auto const input = boost::json::parse(
|
||||
fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"ledger_index": {},
|
||||
"accounts": ["{}"]
|
||||
}})JSON",
|
||||
kMptId,
|
||||
specificLedger,
|
||||
kHolder1Account
|
||||
)
|
||||
);
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto handler = AnyHandler{MPTHoldersHandler{this->backend_}};
|
||||
auto const output = handler.process(input, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(boost::json::parse(currentOutput), *output.result);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, AccountsFilterDuplicates)
|
||||
{
|
||||
auto const currentOutput = fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"limit": 50,
|
||||
"ledger_index": 30,
|
||||
"mptokens": [{}, {}],
|
||||
"validated": true
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kMptOut1,
|
||||
kMptOut2
|
||||
);
|
||||
|
||||
auto ledgerInfo = createLedgerHeader(kLedgerHash, 30);
|
||||
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerInfo));
|
||||
auto const issuanceKk = xrpl::keylet::mptokenIssuance(xrpl::uint192(kMptId)).key;
|
||||
ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _))
|
||||
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const mptoken1 = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId));
|
||||
auto const mptoken2 = createMpTokenObject(kHolder2Account, xrpl::uint192(kMptId));
|
||||
std::vector<Blob> const objs = {
|
||||
mptoken1.getSerializer().peekData(), mptoken2.getSerializer().peekData()
|
||||
};
|
||||
std::vector<xrpl::uint256> const expectedKeys = {
|
||||
xrpl::keylet::mptoken(xrpl::uint192(kMptId), getAccountIdWithString(kHolder1Account)).key,
|
||||
xrpl::keylet::mptoken(xrpl::uint192(kMptId), getAccountIdWithString(kHolder2Account)).key
|
||||
};
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObjects(expectedKeys, 30, _)).WillOnce(Return(objs));
|
||||
EXPECT_CALL(*backend_, fetchMPTHolders).Times(0);
|
||||
|
||||
auto const input = boost::json::parse(
|
||||
fmt::format(
|
||||
R"JSON({{
|
||||
"mpt_issuance_id": "{}",
|
||||
"accounts": ["{}", "{}", "{}"]
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kHolder1Account,
|
||||
kHolder2Account,
|
||||
kHolder1Account
|
||||
)
|
||||
);
|
||||
runSpawn([&, this](auto& yield) {
|
||||
auto handler = AnyHandler{MPTHoldersHandler{this->backend_}};
|
||||
auto const output = handler.process(input, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(boost::json::parse(currentOutput), *output.result);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCMPTHoldersHandlerTest, LimitMoreThanMAx)
|
||||
{
|
||||
auto const currentOutput = fmt::format(
|
||||
@@ -741,7 +1319,7 @@ TEST_F(RPCMPTHoldersHandlerTest, LimitMoreThanMAx)
|
||||
"validated": true
|
||||
}})JSON",
|
||||
kMptId,
|
||||
kMptOuT1
|
||||
kMptOut1
|
||||
);
|
||||
|
||||
auto ledgerInfo = createLedgerHeader(kLedgerHash, 30);
|
||||
@@ -750,7 +1328,7 @@ TEST_F(RPCMPTHoldersHandlerTest, LimitMoreThanMAx)
|
||||
ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _))
|
||||
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
auto const mptoken = createMpTokenObject(kHoldeR1Account, xrpl::uint192(kMptId));
|
||||
auto const mptoken = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId));
|
||||
std::vector<Blob> const mpts = {mptoken.getSerializer().peekData()};
|
||||
ON_CALL(*backend_, fetchMPTHolders)
|
||||
.WillByDefault(Return(MPTHoldersAndCursor{.mptokens = mpts, .cursor = {}}));
|
||||
|
||||
@@ -151,8 +151,8 @@ TEST_F(RPCTxTest, ExcessiveLgrRange)
|
||||
});
|
||||
}
|
||||
|
||||
// `transaction` is validated by rpc-spec's uint256 converter, which reports one
|
||||
// format-agnostic message for both a non-string and an unparsable hash.
|
||||
// `transaction` is validated by rpc-spec's uint256 converter, which reports a distinct
|
||||
// message for a non-string value versus an unparsable (but string) hash.
|
||||
TEST_F(RPCTxTest, TransactionNotString)
|
||||
{
|
||||
runSpawn([this](auto yield) {
|
||||
|
||||
@@ -99,7 +99,7 @@ generateTestValuesForParametersTest()
|
||||
"seq": "asdf"
|
||||
})JSON",
|
||||
.expectedError = "malformedRequest",
|
||||
.expectedErrorCode = ClioError::RpcMalformedRequest,
|
||||
.expectedErrorCode = rpc::ClioError::RpcMalformedRequest,
|
||||
.expectedErrorMessage = "Malformed request."
|
||||
},
|
||||
VaultInfoParamTestCaseBundle{
|
||||
@@ -109,7 +109,7 @@ generateTestValuesForParametersTest()
|
||||
"seq": 3
|
||||
})JSON",
|
||||
.expectedError = "malformedRequest",
|
||||
.expectedErrorCode = ClioError::RpcMalformedRequest,
|
||||
.expectedErrorCode = rpc::ClioError::RpcMalformedRequest,
|
||||
.expectedErrorMessage = "OwnerNotHexString"
|
||||
},
|
||||
VaultInfoParamTestCaseBundle{
|
||||
@@ -119,7 +119,7 @@ generateTestValuesForParametersTest()
|
||||
"seq": 3
|
||||
})JSON",
|
||||
.expectedError = "malformedRequest",
|
||||
.expectedErrorCode = ClioError::RpcMalformedRequest,
|
||||
.expectedErrorCode = rpc::ClioError::RpcMalformedRequest,
|
||||
.expectedErrorMessage = "OwnerNotHexString"
|
||||
},
|
||||
VaultInfoParamTestCaseBundle{
|
||||
@@ -128,7 +128,7 @@ generateTestValuesForParametersTest()
|
||||
"vault_id": 3
|
||||
})JSON",
|
||||
.expectedError = "malformedRequest",
|
||||
.expectedErrorCode = ClioError::RpcMalformedRequest,
|
||||
.expectedErrorCode = rpc::ClioError::RpcMalformedRequest,
|
||||
.expectedErrorMessage = "Malformed request."
|
||||
},
|
||||
VaultInfoParamTestCaseBundle{
|
||||
@@ -137,7 +137,7 @@ generateTestValuesForParametersTest()
|
||||
"vault_id": "idk"
|
||||
})JSON",
|
||||
.expectedError = "malformedRequest",
|
||||
.expectedErrorCode = ClioError::RpcMalformedRequest,
|
||||
.expectedErrorCode = rpc::ClioError::RpcMalformedRequest,
|
||||
.expectedErrorMessage = "Malformed request."
|
||||
},
|
||||
VaultInfoParamTestCaseBundle{
|
||||
|
||||
Reference in New Issue
Block a user