diff --git a/src/data/AmendmentCenter.hpp b/src/data/AmendmentCenter.hpp index bd13559fc..5599a45ef 100644 --- a/src/data/AmendmentCenter.hpp +++ b/src/data/AmendmentCenter.hpp @@ -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); diff --git a/src/rpc/handlers/AccountMPTokenIssuances.cpp b/src/rpc/handlers/AccountMPTokenIssuances.cpp index 14413dc48..b5acc7de9 100644 --- a/src/rpc/handlers/AccountMPTokenIssuances.cpp +++ b/src/rpc/handlers/AccountMPTokenIssuances.cpp @@ -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); } diff --git a/src/rpc/handlers/AccountMPTokenIssuances.hpp b/src/rpc/handlers/AccountMPTokenIssuances.hpp index 1ba32f7e4..e26fba853 100644 --- a/src/rpc/handlers/AccountMPTokenIssuances.hpp +++ b/src/rpc/handlers/AccountMPTokenIssuances.hpp @@ -71,6 +71,11 @@ public: std::optional mptCanMutateCanClawback; std::optional mptCanMutateMetadata; std::optional mptCanMutateTransferFee; + + std::optional mptCanHoldConfidentialBalance; + std::optional confidentialOutstandingAmount; + std::optional issuerEncryptionKey; + std::optional auditorEncryptionKey; }; /** diff --git a/src/rpc/handlers/AccountMPTokens.cpp b/src/rpc/handlers/AccountMPTokens.cpp index 6978ea389..f5381cee2 100644 --- a/src/rpc/handlers/AccountMPTokens.cpp +++ b/src/rpc/handlers/AccountMPTokens.cpp @@ -52,6 +52,31 @@ AccountMPTokensHandler::addMPToken(std::vector& 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); } diff --git a/src/rpc/handlers/AccountMPTokens.hpp b/src/rpc/handlers/AccountMPTokens.hpp index c7d3a5246..c165ef2a7 100644 --- a/src/rpc/handlers/AccountMPTokens.hpp +++ b/src/rpc/handlers/AccountMPTokens.hpp @@ -49,6 +49,13 @@ public: std::optional mptLocked; std::optional mptAuthorized; + + std::optional confidentialBalanceInbox; + std::optional confidentialBalanceSpending; + std::optional confidentialBalanceVersion; + std::optional issuerEncryptedBalance; + std::optional auditorEncryptedBalance; + std::optional holderEncryptionKey; }; /** diff --git a/src/rpc/handlers/MPTHolders.cpp b/src/rpc/handlers/MPTHolders.cpp index 12037f28a..ba67302ca 100644 --- a/src/rpc/handlers/MPTHolders.cpp +++ b/src/rpc/handlers/MPTHolders.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -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); } diff --git a/tests/common/util/TestObject.cpp b/tests/common/util/TestObject.cpp index 3e6be2b27..19fe2d95e 100644 --- a/tests/common/util/TestObject.cpp +++ b/tests/common/util/TestObject.cpp @@ -1493,7 +1493,10 @@ createMptIssuanceObject( std::optional maxAmount, std::optional lockedAmount, std::optional domainId, - std::optional mutableFlags + std::optional mutableFlags, + std::optional issuerEncryptionKey, + std::optional auditorEncryptionKey, + std::optional 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 lockedAmount + std::optional lockedAmount, + std::optional confidentialBalanceInbox, + std::optional confidentialBalanceSpending, + std::optional confidentialBalanceVersion, + std::optional issuerEncryptedBalance, + std::optional auditorEncryptedBalance, + std::optional 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 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; } diff --git a/tests/common/util/TestObject.hpp b/tests/common/util/TestObject.hpp index aa0b8b88f..280f8798c 100644 --- a/tests/common/util/TestObject.hpp +++ b/tests/common/util/TestObject.hpp @@ -466,7 +466,10 @@ createMptIssuanceObject( std::optional maxAmount = std::nullopt, std::optional lockedAmount = std::nullopt, std::optional domainId = std::nullopt, - std::optional mutableFlags = std::nullopt + std::optional mutableFlags = std::nullopt, + std::optional issuerEncryptionKey = std::nullopt, + std::optional auditorEncryptionKey = std::nullopt, + std::optional 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 lockedAmount = std::nullopt + std::optional lockedAmount = std::nullopt, + std::optional confidentialBalanceInbox = std::nullopt, + std::optional confidentialBalanceSpending = std::nullopt, + std::optional confidentialBalanceVersion = std::nullopt, + std::optional issuerEncryptedBalance = std::nullopt, + std::optional auditorEncryptedBalance = std::nullopt, + std::optional holderEncryptionKey = std::nullopt ); [[nodiscard]] xrpl::STObject diff --git a/tests/unit/rpc/handlers/AccountMPTokenIssuancesTests.cpp b/tests/unit/rpc/handlers/AccountMPTokenIssuancesTests.cpp index 4ec2575ff..638dbc99d 100644 --- a/tests/unit/rpc/handlers/AccountMPTokenIssuancesTests.cpp +++ b/tests/unit/rpc/handlers/AccountMPTokenIssuancesTests.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +27,7 @@ #include #include #include +#include #include 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{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; diff --git a/tests/unit/rpc/handlers/AccountMPTokensTests.cpp b/tests/unit/rpc/handlers/AccountMPTokensTests.cpp index 8c56dbd8a..9ecba45e3 100644 --- a/tests/unit/rpc/handlers/AccountMPTokensTests.cpp +++ b/tests/unit/rpc/handlers/AccountMPTokensTests.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,7 @@ #include #include #include +#include #include 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{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 diff --git a/tests/unit/rpc/handlers/MPTHoldersTests.cpp b/tests/unit/rpc/handlers/MPTHoldersTests.cpp index 48f1ee958..9d4321a59 100644 --- a/tests/unit/rpc/handlers/MPTHoldersTests.cpp +++ b/tests/unit/rpc/handlers/MPTHoldersTests.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include 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 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;