feat: Support Confidential Transfer Fields (#3152)

Add Confidential Transfer Fields to MPToken, MPToken Issuance, and
mpt_holders API
This commit is contained in:
Peter Chen
2026-07-23 15:31:15 -07:00
committed by GitHub
parent 55657fd1e9
commit 24ce768ba8
11 changed files with 412 additions and 7 deletions

View File

@@ -139,8 +139,9 @@ struct Amendments {
REGISTER(fixBatchInnerSigs);
REGISTER(fixCleanup3_1_3);
REGISTER(fixCleanup3_2_0);
// These amendments are added because of libxrpl 3.3.0, but they are not yet supported in Clio.
REGISTER(ConfidentialTransfer);
// These amendments are added because of libxrpl 3.3.0, but they are not yet supported in Clio.
REGISTER(LendingProtocolV1_1);
REGISTER(BatchV1_1);
REGISTER(Sponsor);

View File

@@ -55,6 +55,7 @@ AccountMPTokenIssuancesHandler::addMPTokenIssuance(
setFlag(issuance.mptCanTrade, xrpl::lsfMPTCanTrade);
setFlag(issuance.mptCanTransfer, xrpl::lsfMPTCanTransfer);
setFlag(issuance.mptCanClawback, xrpl::lsfMPTCanClawback);
setFlag(issuance.mptCanHoldConfidentialBalance, xrpl::lsfMPTCanHoldConfidentialBalance);
if (sle.isFieldPresent(xrpl::sfMutableFlags)) {
auto const mutableFlags = sle.getFieldU32(xrpl::sfMutableFlags);
@@ -95,6 +96,17 @@ AccountMPTokenIssuancesHandler::addMPTokenIssuance(
if (sle.isFieldPresent(xrpl::sfDomainID))
issuance.domainID = xrpl::strHex(sle.getFieldH256(xrpl::sfDomainID));
if (sle.isFieldPresent(xrpl::sfConfidentialOutstandingAmount)) {
issuance.confidentialOutstandingAmount =
sle.getFieldU64(xrpl::sfConfidentialOutstandingAmount);
}
if (sle.isFieldPresent(xrpl::sfIssuerEncryptionKey))
issuance.issuerEncryptionKey = xrpl::strHex(sle.getFieldVL(xrpl::sfIssuerEncryptionKey));
if (sle.isFieldPresent(xrpl::sfAuditorEncryptionKey))
issuance.auditorEncryptionKey = xrpl::strHex(sle.getFieldVL(xrpl::sfAuditorEncryptionKey));
issuances.push_back(issuance);
}
@@ -274,6 +286,15 @@ tag_invoke(
setIfPresent("mpt_can_mutate_metadata", issuance.mptCanMutateMetadata);
setIfPresent("mpt_can_mutate_transfer_fee", issuance.mptCanMutateTransferFee);
setIfPresent("mpt_can_hold_confidential_balance", issuance.mptCanHoldConfidentialBalance);
setUint64IfPresent(
"confidential_outstanding_amount",
xrpl::sfConfidentialOutstandingAmount,
issuance.confidentialOutstandingAmount
);
setIfPresent("issuer_encryption_key", issuance.issuerEncryptionKey);
setIfPresent("auditor_encryption_key", issuance.auditorEncryptionKey);
jv = std::move(obj);
}

View File

@@ -71,6 +71,11 @@ public:
std::optional<bool> mptCanMutateCanClawback;
std::optional<bool> mptCanMutateMetadata;
std::optional<bool> mptCanMutateTransferFee;
std::optional<bool> mptCanHoldConfidentialBalance;
std::optional<std::uint64_t> confidentialOutstandingAmount;
std::optional<std::string> issuerEncryptionKey;
std::optional<std::string> auditorEncryptionKey;
};
/**

View File

@@ -52,6 +52,31 @@ AccountMPTokensHandler::addMPToken(std::vector<MPTokenResponse>& mpts, xrpl::SLE
setFlag(token.mptLocked, xrpl::lsfMPTLocked);
setFlag(token.mptAuthorized, xrpl::lsfMPTAuthorized);
if (sle.isFieldPresent(xrpl::sfConfidentialBalanceInbox)) {
token.confidentialBalanceInbox =
xrpl::strHex(sle.getFieldVL(xrpl::sfConfidentialBalanceInbox));
}
if (sle.isFieldPresent(xrpl::sfConfidentialBalanceSpending)) {
token.confidentialBalanceSpending =
xrpl::strHex(sle.getFieldVL(xrpl::sfConfidentialBalanceSpending));
}
if (sle.isFieldPresent(xrpl::sfConfidentialBalanceVersion))
token.confidentialBalanceVersion = sle.getFieldU32(xrpl::sfConfidentialBalanceVersion);
if (sle.isFieldPresent(xrpl::sfIssuerEncryptedBalance)) {
token.issuerEncryptedBalance = xrpl::strHex(sle.getFieldVL(xrpl::sfIssuerEncryptedBalance));
}
if (sle.isFieldPresent(xrpl::sfAuditorEncryptedBalance)) {
token.auditorEncryptedBalance =
xrpl::strHex(sle.getFieldVL(xrpl::sfAuditorEncryptedBalance));
}
if (sle.isFieldPresent(xrpl::sfHolderEncryptionKey))
token.holderEncryptionKey = xrpl::strHex(sle.getFieldVL(xrpl::sfHolderEncryptionKey));
mpts.push_back(token);
}
@@ -203,6 +228,13 @@ tag_invoke(
setIfPresent("mpt_locked", mptoken.mptLocked);
setIfPresent("mpt_authorized", mptoken.mptAuthorized);
setIfPresent(JS(confidential_balance_inbox), mptoken.confidentialBalanceInbox);
setIfPresent(JS(confidential_balance_spending), mptoken.confidentialBalanceSpending);
setIfPresent(JS(confidential_balance_version), mptoken.confidentialBalanceVersion);
setIfPresent(JS(issuer_encrypted_balance), mptoken.issuerEncryptedBalance);
setIfPresent(JS(auditor_encrypted_balance), mptoken.auditorEncryptedBalance);
setIfPresent(JS(holder_encryption_key), mptoken.holderEncryptionKey);
jv = std::move(obj);
}

View File

@@ -49,6 +49,13 @@ public:
std::optional<bool> mptLocked;
std::optional<bool> mptAuthorized;
std::optional<std::string> confidentialBalanceInbox;
std::optional<std::string> confidentialBalanceSpending;
std::optional<uint32_t> confidentialBalanceVersion;
std::optional<std::string> issuerEncryptedBalance;
std::optional<std::string> auditorEncryptedBalance;
std::optional<std::string> holderEncryptionKey;
};
/**

View File

@@ -17,6 +17,7 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STInteger.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/jss.h>
@@ -74,14 +75,50 @@ MPTHoldersHandler::process(MPTHoldersHandler::Input const& input, Context const&
mptJson[JS(account)] = toBase58(sle[xrpl::sfAccount]);
mptJson[JS(flags)] = sle.getFlags();
mptJson["mpt_amount"] = toBoostJson(
mptJson[JS(mpt_amount)] = toBoostJson(
xrpl::STUInt64{xrpl::sfMPTAmount, sle[xrpl::sfMPTAmount]}.getJson(
JsonOptions::Values::None
)
);
mptJson["mptoken_index"] =
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));
}
output.mpts.push_back(mptJson);
}

View File

@@ -1493,7 +1493,10 @@ createMptIssuanceObject(
std::optional<std::uint64_t> maxAmount,
std::optional<std::uint64_t> lockedAmount,
std::optional<std::string_view> domainId,
std::optional<std::uint32_t> mutableFlags
std::optional<std::uint32_t> mutableFlags,
std::optional<std::string_view> issuerEncryptionKey,
std::optional<std::string_view> auditorEncryptionKey,
std::optional<std::uint64_t> confidentialOutstandingAmount
)
{
xrpl::STObject mptIssuance(xrpl::sfLedgerEntry);
@@ -1522,6 +1525,19 @@ createMptIssuanceObject(
mptIssuance.setFieldH256(xrpl::sfDomainID, xrpl::uint256{*domainId});
if (mutableFlags.has_value())
mptIssuance.setFieldU32(xrpl::sfMutableFlags, *mutableFlags);
if (issuerEncryptionKey.has_value()) {
xrpl::Slice const slice(issuerEncryptionKey->data(), issuerEncryptionKey->size());
mptIssuance.setFieldVL(xrpl::sfIssuerEncryptionKey, slice);
}
if (auditorEncryptionKey.has_value()) {
xrpl::Slice const slice(auditorEncryptionKey->data(), auditorEncryptionKey->size());
mptIssuance.setFieldVL(xrpl::sfAuditorEncryptionKey, slice);
}
if (confidentialOutstandingAmount.has_value()) {
mptIssuance.setFieldU64(
xrpl::sfConfidentialOutstandingAmount, *confidentialOutstandingAmount
);
}
return mptIssuance;
}
@@ -1532,7 +1548,13 @@ createMpTokenObject(
xrpl::uint192 issuanceID,
std::uint64_t mptAmount,
std::uint32_t flags,
std::optional<uint64_t> lockedAmount
std::optional<uint64_t> lockedAmount,
std::optional<std::string_view> confidentialBalanceInbox,
std::optional<std::string_view> confidentialBalanceSpending,
std::optional<std::uint32_t> confidentialBalanceVersion,
std::optional<std::string_view> issuerEncryptedBalance,
std::optional<std::string_view> auditorEncryptedBalance,
std::optional<std::string_view> holderEncryptionKey
)
{
xrpl::STObject mptoken(xrpl::sfLedgerEntry);
@@ -1549,6 +1571,23 @@ createMpTokenObject(
if (lockedAmount.has_value())
mptoken.setFieldU64(xrpl::sfLockedAmount, *lockedAmount);
auto const setVL = [&](xrpl::SField const& field,
std::optional<std::string_view> const& value) {
if (value.has_value()) {
xrpl::Slice const slice(value->data(), value->size());
mptoken.setFieldVL(field, slice);
}
};
setVL(xrpl::sfConfidentialBalanceInbox, confidentialBalanceInbox);
setVL(xrpl::sfConfidentialBalanceSpending, confidentialBalanceSpending);
setVL(xrpl::sfIssuerEncryptedBalance, issuerEncryptedBalance);
setVL(xrpl::sfAuditorEncryptedBalance, auditorEncryptedBalance);
setVL(xrpl::sfHolderEncryptionKey, holderEncryptionKey);
if (confidentialBalanceVersion.has_value())
mptoken.setFieldU32(xrpl::sfConfidentialBalanceVersion, *confidentialBalanceVersion);
return mptoken;
}

View File

@@ -466,7 +466,10 @@ createMptIssuanceObject(
std::optional<std::uint64_t> maxAmount = std::nullopt,
std::optional<std::uint64_t> lockedAmount = std::nullopt,
std::optional<std::string_view> domainId = std::nullopt,
std::optional<std::uint32_t> mutableFlags = std::nullopt
std::optional<std::uint32_t> mutableFlags = std::nullopt,
std::optional<std::string_view> issuerEncryptionKey = std::nullopt,
std::optional<std::string_view> auditorEncryptionKey = std::nullopt,
std::optional<std::uint64_t> confidentialOutstandingAmount = std::nullopt
);
[[nodiscard]] xrpl::STObject
@@ -475,7 +478,13 @@ createMpTokenObject(
xrpl::uint192 issuanceID,
std::uint64_t mptAmount = 1,
std::uint32_t flags = 0,
std::optional<uint64_t> lockedAmount = std::nullopt
std::optional<uint64_t> lockedAmount = std::nullopt,
std::optional<std::string_view> confidentialBalanceInbox = std::nullopt,
std::optional<std::string_view> confidentialBalanceSpending = std::nullopt,
std::optional<std::uint32_t> confidentialBalanceVersion = std::nullopt,
std::optional<std::string_view> issuerEncryptedBalance = std::nullopt,
std::optional<std::string_view> auditorEncryptedBalance = std::nullopt,
std::optional<std::string_view> holderEncryptionKey = std::nullopt
);
[[nodiscard]] xrpl::STObject

View File

@@ -15,6 +15,7 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/LedgerHeader.h>
@@ -26,6 +27,7 @@
#include <functional>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
using namespace rpc;
@@ -1080,6 +1082,92 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, MutableFlags)
});
}
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, ConfidentialFields)
{
constexpr auto kIssuerEncryptionKey = "issuer-pubkey";
constexpr auto kAuditorEncryptionKey = "auditor-pubkey";
constexpr uint64_t kConfidentialOutstandingAmount = 42;
auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto const account = getAccountIdWithString(kAccount);
auto const accountKk = xrpl::keylet::account(account).key;
auto const ownerDirKk = xrpl::keylet::ownerDir(account).key;
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _))
.WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
xrpl::STObject const ownerDir =
createOwnerDirLedgerObject({xrpl::uint256{kIssuanceIndeX1}}, kIssuanceIndeX1);
EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
.WillOnce(Return(ownerDir.getSerializer().peekData()));
auto const issuance = createMptIssuanceObject(
kAccount,
1,
std::nullopt,
xrpl::lsfMPTCanHoldConfidentialBalance,
kIssuancE1OutstandingAmount,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
kIssuerEncryptionKey,
kAuditorEncryptionKey,
kConfidentialOutstandingAmount
);
auto const bbs = std::vector<Blob>{issuance.getSerializer().peekData()};
EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
runSpawn([&](auto yield) {
auto const input = boost::json::parse(
fmt::format(
R"JSON({{
"account": "{}"
}})JSON",
kAccount
)
);
auto const correctOutput = fmt::format(
R"JSON({{
"account": "{}",
"ledger_hash": "{}",
"ledger_index": 30,
"validated": true,
"limit": 200,
"mpt_issuances": [
{{
"mpt_issuance_id": "{}",
"issuer": "{}",
"sequence": 1,
"outstanding_amount": "{}",
"mpt_can_hold_confidential_balance": true,
"confidential_outstanding_amount": "{}",
"issuer_encryption_key": "{}",
"auditor_encryption_key": "{}"
}}
]
}})JSON",
kAccount,
kLedgerHash,
kIssuanceIndeX1,
kAccount,
kIssuancE1OutstandingAmount,
kConfidentialOutstandingAmount,
xrpl::strHex(std::string_view{kIssuerEncryptionKey}),
xrpl::strHex(std::string_view{kAuditorEncryptionKey})
);
auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(boost::json::parse(correctOutput), *output.result);
});
}
struct SingleFlagTest {
std::string testName;
uint32_t flag;

View File

@@ -14,6 +14,7 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/LedgerHeader.h>
@@ -24,6 +25,7 @@
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
using namespace rpc;
@@ -813,6 +815,82 @@ TEST_F(RPCAccountMPTokensHandlerTest, EmptyResult)
});
}
TEST_F(RPCAccountMPTokensHandlerTest, ConfidentialFields)
{
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";
auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto const account = getAccountIdWithString(kAccount);
auto const accountKk = xrpl::keylet::account(account).key;
auto const ownerDirKk = xrpl::keylet::ownerDir(account).key;
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _))
.WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
xrpl::STObject const ownerDir =
createOwnerDirLedgerObject({xrpl::uint256{kTokenIndeX1}}, kTokenIndeX1);
EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
.WillOnce(Return(ownerDir.getSerializer().peekData()));
xrpl::STObject const mptoken = createMpTokenObject(
kAccount,
xrpl::uint192(kIssuanceIdHex),
kTokeN1Amount,
0,
std::nullopt,
kConfidentialBalanceInbox,
kConfidentialBalanceSpending,
kConfidentialBalanceVersion,
kIssuerEncryptedBalance,
kAuditorEncryptedBalance,
kHolderEncryptionKey
);
auto const bbs = std::vector<Blob>{mptoken.getSerializer().peekData()};
EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
runSpawn([&, this](auto yield) {
auto const input =
boost::json::parse(fmt::format(R"JSON({{"account": "{}"}})JSON", kAccount));
auto const handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
auto const& mptokens = output.result->as_object().at("mptokens").as_array();
ASSERT_EQ(mptokens.size(), 1);
auto const& token = mptokens[0].as_object();
EXPECT_EQ(
token.at("confidential_balance_inbox").as_string(),
xrpl::strHex(std::string_view{kConfidentialBalanceInbox})
);
EXPECT_EQ(
token.at("confidential_balance_spending").as_string(),
xrpl::strHex(std::string_view{kConfidentialBalanceSpending})
);
EXPECT_EQ(
token.at("confidential_balance_version").as_uint64(), kConfidentialBalanceVersion
);
EXPECT_EQ(
token.at("issuer_encrypted_balance").as_string(),
xrpl::strHex(std::string_view{kIssuerEncryptedBalance})
);
EXPECT_EQ(
token.at("auditor_encrypted_balance").as_string(),
xrpl::strHex(std::string_view{kAuditorEncryptedBalance})
);
EXPECT_EQ(
token.at("holder_encryption_key").as_string(),
xrpl::strHex(std::string_view{kHolderEncryptionKey})
);
});
}
// Regression test: UInt64 amount fields must be serialized as base-10 JSON strings (as xrpld
// does) so that values greater than 2^53 are not silently rounded by JSON parsers backed by
// IEEE-754 doubles. 2^53 itself is still exactly representable as a double, but it must be emitted

View File

@@ -19,6 +19,7 @@
#include <functional>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
using namespace rpc;
@@ -479,6 +480,93 @@ TEST_F(RPCMPTHoldersHandlerTest, CustomAmounts)
});
}
TEST_F(RPCMPTHoldersHandlerTest, ConfidentialFields)
{
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 mpts = {mptoken.getSerializer().peekData()};
ON_CALL(*backend_, fetchMPTHolders)
.WillByDefault(Return(MPTHoldersAndCursor{.mptokens = mpts, .cursor = {}}));
EXPECT_CALL(
*backend_,
fetchMPTHolders(
xrpl::uint192(kMptId), testing::_, testing::Eq(std::nullopt), Const(30), testing::_
)
)
.Times(1);
auto const input = boost::json::parse(
fmt::format(
R"JSON({{
"mpt_issuance_id": "{}"
}})JSON",
kMptId
)
);
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, SpecificLedgerIndex)
{
auto const specificLedger = 20;