From b7c9d9cea0209b3ab8b11d2e6e435369aab941ac Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 26 Mar 2026 17:57:16 +0000 Subject: [PATCH 1/2] feat: Implement VaultList for Lending Protocol --- src/rpc/CMakeLists.txt | 1 + src/rpc/RPCCenter.cpp | 1 + src/rpc/common/impl/HandlerProvider.cpp | 2 + src/rpc/handlers/VaultList.cpp | 200 ++++++ src/rpc/handlers/VaultList.hpp | 126 ++++ tests/unit/CMakeLists.txt | 1 + tests/unit/rpc/handlers/VaultListTests.cpp | 755 +++++++++++++++++++++ 7 files changed, 1086 insertions(+) create mode 100644 src/rpc/handlers/VaultList.cpp create mode 100644 src/rpc/handlers/VaultList.hpp create mode 100644 tests/unit/rpc/handlers/VaultListTests.cpp diff --git a/src/rpc/CMakeLists.txt b/src/rpc/CMakeLists.txt index a23ed71af..8fbd7df56 100644 --- a/src/rpc/CMakeLists.txt +++ b/src/rpc/CMakeLists.txt @@ -56,6 +56,7 @@ target_sources( handlers/TransactionEntry.cpp handlers/Unsubscribe.cpp handlers/VaultInfo.cpp + handlers/VaultList.cpp ) target_link_libraries(clio_rpc PRIVATE clio_util) diff --git a/src/rpc/RPCCenter.cpp b/src/rpc/RPCCenter.cpp index 0ed3ff875..7093f0a97 100644 --- a/src/rpc/RPCCenter.cpp +++ b/src/rpc/RPCCenter.cpp @@ -49,6 +49,7 @@ handledRpcs() "subscribe", "unsubscribe", "vault_info", + "vault_list", "version", // clang-format on }; diff --git a/src/rpc/common/impl/HandlerProvider.cpp b/src/rpc/common/impl/HandlerProvider.cpp index dae6121f2..5a76c2eca 100644 --- a/src/rpc/common/impl/HandlerProvider.cpp +++ b/src/rpc/common/impl/HandlerProvider.cpp @@ -44,6 +44,7 @@ #include "rpc/handlers/Tx.hpp" #include "rpc/handlers/Unsubscribe.hpp" #include "rpc/handlers/VaultInfo.hpp" +#include "rpc/handlers/VaultList.hpp" #include "rpc/handlers/VersionHandler.hpp" #include "util/config/ConfigDefinition.hpp" @@ -109,6 +110,7 @@ ProductionHandlerProvider::ProductionHandlerProvider( {.handler = SubscribeHandler{backend, amendmentCenter, subscriptionManager}}}, {"unsubscribe", {.handler = UnsubscribeHandler{subscriptionManager}}}, {"vault_info", {.handler = VaultInfoHandler{backend}}}, + {"vault_list", {.handler = VaultListHandler{backend}, .isClioOnly = true}}, {"version", {.handler = VersionHandler{config}}}, } { diff --git a/src/rpc/handlers/VaultList.cpp b/src/rpc/handlers/VaultList.cpp new file mode 100644 index 000000000..9440e9409 --- /dev/null +++ b/src/rpc/handlers/VaultList.cpp @@ -0,0 +1,200 @@ +#include "rpc/handlers/VaultList.hpp" + +#include "data/BackendInterface.hpp" +#include "rpc/Errors.hpp" +#include "rpc/JS.hpp" +#include "rpc/RPCHelpers.hpp" +#include "rpc/common/Types.hpp" +#include "util/Assert.hpp" +#include "util/JsonUtils.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace rpc { + +VaultListHandler::VaultListHandler(std::shared_ptr sharedPtrBackend) + : sharedPtrBackend_(std::move(sharedPtrBackend)) +{ +} + +VaultListHandler::Result +VaultListHandler::process(VaultListHandler::Input const& input, Context const& ctx) const +{ + auto const range = sharedPtrBackend_->fetchLedgerRange(); + ASSERT(range.has_value(), "VaultList's ledger range must be available"); + + auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + *sharedPtrBackend_, ctx.yield, input.ledgerHash, input.ledgerIndex, range->maxSequence + ); + + if (not expectedLgrInfo.has_value()) + return Error{expectedLgrInfo.error()}; + + auto const& lgrInfo = *expectedLgrInfo; + + // Parse the MPT Issuance ID from input + auto const mptID = ripple::uint192{input.tokenID.c_str()}; + + // Fetch the MPT Issuance object to validate it exists and get the issuer + auto const issuanceKeylet = ripple::keylet::mptIssuance(mptID).key; + auto const issuanceObject = + sharedPtrBackend_->fetchLedgerObject(issuanceKeylet, lgrInfo.seq, ctx.yield); + + if (!issuanceObject) + return Error{Status{RippledError::rpcOBJECT_NOT_FOUND, "tokenNotFound"}}; + + ripple::STLedgerEntry const issuanceSle{ + ripple::SerialIter{issuanceObject->data(), issuanceObject->size()}, issuanceKeylet + }; + + auto const issuerAccountID = issuanceSle.getAccountID(ripple::sfIssuer); + + // Verify the issuer account exists + auto const accountKeylet = ripple::keylet::account(issuerAccountID); + auto const accountLedgerObject = + sharedPtrBackend_->fetchLedgerObject(accountKeylet.key, lgrInfo.seq, ctx.yield); + + if (!accountLedgerObject) + return Error{Status{RippledError::rpcACT_NOT_FOUND}}; + + // Traverse owned nodes filtering for Vault objects + Output response; + response.tokenID = input.tokenID; + response.ledgerHash = ripple::strHex(lgrInfo.hash); + response.ledgerIndex = lgrInfo.seq; + response.limit = input.limit; + + auto const addToResponse = [&](ripple::SLE&& sle) { + if (sle.getType() != ripple::ltVAULT) + return true; + + boost::json::object summary; + + auto vaultSle = std::move(sle); + + summary[JS(vault_id)] = ripple::strHex(vaultSle.key()); + summary[JS(account)] = ripple::toBase58(vaultSle.getAccountID(ripple::sfAccount)); + summary[JS(owner)] = ripple::toBase58(vaultSle.getAccountID(ripple::sfOwner)); + + // AssetsTotal is an STNumber field; extract via JSON serialization + auto const vaultJson = toBoostJson(vaultSle.getJson(ripple::JsonOptions::none)); + if (vaultJson.as_object().contains("AssetsTotal")) { + summary["total_assets"] = vaultJson.as_object().at("AssetsTotal"); + } else { + summary["total_assets"] = "0"; + } + + // Fetch the share MPT issuance to get total_shares (OutstandingAmount) + auto const issuanceKeylet = ripple::keylet::mptIssuance(vaultSle[ripple::sfShareMPTID]).key; + auto const shareIssuanceObject = + sharedPtrBackend_->fetchLedgerObject(issuanceKeylet, lgrInfo.seq, ctx.yield); + + if (shareIssuanceObject.has_value()) { + ripple::STLedgerEntry const shareSle{ + ripple::SerialIter{shareIssuanceObject->data(), shareIssuanceObject->size()}, + issuanceKeylet + }; + summary["total_shares"] = shareSle.getFieldU64(ripple::sfOutstandingAmount); + } else { + summary["total_shares"] = std::uint64_t{0}; + } + + // Status derived from flags + auto const flags = vaultSle.getFlags(); + // A vault with no special flags set is considered active + summary[JS(status)] = (flags == 0) ? "active" : "modified"; + summary[JS(flags)] = flags; + + response.vaults.push_back(summary); + return true; + }; + + auto const expectedNext = traverseOwnedNodes( + *sharedPtrBackend_, + issuerAccountID, + lgrInfo.seq, + input.limit, + input.marker, + ctx.yield, + addToResponse, + /* nftIncluded =*/false + ); + + if (not expectedNext.has_value()) + return Error{expectedNext.error()}; + + auto const& nextMarker = expectedNext.value(); + if (nextMarker.isNonZero()) + response.marker = nextMarker.toString(); + + return response; +} + +void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + VaultListHandler::Output const& output +) +{ + jv = boost::json::object{ + {"token_id", output.tokenID}, + {JS(ledger_hash), output.ledgerHash}, + {JS(ledger_index), output.ledgerIndex}, + {JS(validated), output.validated}, + {JS(limit), output.limit}, + {"vaults", output.vaults}, + }; + + if (output.marker.has_value()) + jv.as_object()[JS(marker)] = *(output.marker); +} + +VaultListHandler::Input +tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) +{ + auto input = VaultListHandler::Input{}; + auto const& jsonObject = jv.as_object(); + + input.tokenID = boost::json::value_to(jsonObject.at("token_id")); + + if (jsonObject.contains(JS(ledger_hash))) + input.ledgerHash = boost::json::value_to(jsonObject.at(JS(ledger_hash))); + + if (jsonObject.contains(JS(ledger_index))) { + auto const expectedLedgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index))); + if (expectedLedgerIndex.has_value()) + input.ledgerIndex = *expectedLedgerIndex; + } + + if (jsonObject.contains(JS(limit))) + input.limit = util::integralValueAs(jsonObject.at(JS(limit))); + + if (jsonObject.contains(JS(marker))) + input.marker = boost::json::value_to(jsonObject.at(JS(marker))); + + return input; +} + +} // namespace rpc diff --git a/src/rpc/handlers/VaultList.hpp b/src/rpc/handlers/VaultList.hpp new file mode 100644 index 000000000..1bf9fe316 --- /dev/null +++ b/src/rpc/handlers/VaultList.hpp @@ -0,0 +1,126 @@ +#pragma once + +#include "data/BackendInterface.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 +#include +#include +#include + +#include +#include +#include +#include + +namespace rpc { + +/** + * @brief The vault_list command retrieves all Vaults created for a given Token. + * + * Given an MPT Issuance ID (token_id), this handler looks up the token issuer and traverses + * their owner directory to find all Vault ledger objects. For each vault found, a summary is + * returned containing the vault ID, pseudo account, owner, total assets, total shares, status, + * and flags. + */ +class VaultListHandler { + std::shared_ptr sharedPtrBackend_; + +public: + static constexpr auto kLIMIT_MIN = 10; + static constexpr auto kLIMIT_MAX = 400; + static constexpr auto kLIMIT_DEFAULT = 200; + + /** + * @brief A struct to hold the output data of the command + */ + struct Output { + std::string tokenID; + boost::json::array vaults; + std::string ledgerHash; + uint32_t ledgerIndex{}; + std::optional marker; + uint32_t limit{}; + bool validated = true; + }; + + /** + * @brief A struct to hold the input data for the command + */ + struct Input { + std::string tokenID; + std::optional ledgerHash; + std::optional ledgerIndex; + uint32_t limit = kLIMIT_DEFAULT; + std::optional marker; + }; + + using Result = HandlerReturnType; + + /** + * @brief Construct a new VaultListHandler object + * + * @param sharedPtrBackend The backend to use + */ + VaultListHandler(std::shared_ptr sharedPtrBackend); + + /** + * @brief Returns the API specification for the command + * + * @param apiVersion The api version to return the spec for + * @return The spec for the given apiVersion + */ + static RpcSpecConstRef + spec([[maybe_unused]] uint32_t apiVersion) + { + static auto const kRPC_SPEC = RpcSpec{ + {"token_id", + validation::Required{}, + validation::CustomValidators::uint192HexStringValidator}, + {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, + {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, + {JS(limit), + validation::Type{}, + validation::Min(1u), + modifiers::Clamp(kLIMIT_MIN, kLIMIT_MAX)}, + {JS(marker), validation::CustomValidators::accountMarkerValidator}, + }; + + return kRPC_SPEC; + } + + /** + * @brief Process the VaultList command + * + * @param input The input data for the command + * @param ctx The context of the request + * @return The result of the operation + */ + Result + process(Input const& input, Context const& ctx) const; + +private: + /** + * @brief Convert the Output to a JSON object + * + * @param [out] jv The JSON object to convert to + * @param output The output to convert + */ + friend void + tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Output const& output); + + /** + * @brief Convert a JSON object to Input type + * + * @param jv The JSON object to convert + * @return Input parsed from the JSON object + */ + friend Input + tag_invoke(boost::json::value_to_tag, boost::json::value const& jv); +}; + +} // namespace rpc diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index df88b91b9..1b1152356 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -142,6 +142,7 @@ target_sources( rpc/handlers/UnsubscribeTests.cpp rpc/handlers/VersionHandlerTests.cpp rpc/handlers/VaultInfoTests.cpp + rpc/handlers/VaultListTests.cpp rpc/JsonBoolTests.cpp rpc/RPCEngineTests.cpp rpc/RPCHelpersTests.cpp diff --git a/tests/unit/rpc/handlers/VaultListTests.cpp b/tests/unit/rpc/handlers/VaultListTests.cpp new file mode 100644 index 000000000..5171c3abb --- /dev/null +++ b/tests/unit/rpc/handlers/VaultListTests.cpp @@ -0,0 +1,755 @@ +#include "data/Types.hpp" +#include "rpc/Errors.hpp" +#include "rpc/common/AnyHandler.hpp" +#include "rpc/common/Types.hpp" +#include "rpc/handlers/VaultList.hpp" +#include "util/HandlerBaseTestFixture.hpp" +#include "util/NameGenerator.hpp" +#include "util/TestObject.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace rpc; +using namespace data; +using namespace testing; +namespace json = boost::json; + +namespace { + +constexpr auto kACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr auto kACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr auto kLEDGER_HASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; +constexpr auto kINDEX1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"; +constexpr auto kMPT_ID = "000004C463C52827307480341125DA0577DEFC38405B0E3E"; +constexpr auto kASSET_CURRENCY = "USD"; +constexpr auto kASSET_ISSUER = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"; +constexpr uint32_t kSEQ_START = 10; +constexpr uint32_t kSEQ = 30; + +} // namespace + +struct RPCVaultListHandlerTest : HandlerBaseTest { + RPCVaultListHandlerTest() + { + backend_->setRange(kSEQ_START, kSEQ); + } +}; + +// -- Parameter validation tests -- + +struct VaultListParamTestCaseBundle { + std::string testName; + std::string testJson; + std::string expectedError; + std::string expectedErrorMessage; +}; + +struct VaultListParameterTest : RPCVaultListHandlerTest, + WithParamInterface {}; + +static auto +generateTestValuesForParametersTest() +{ + return std::vector{ + { + .testName = "MissingTokenID", + .testJson = R"JSON({})JSON", + .expectedError = "invalidParams", + .expectedErrorMessage = "Required field 'token_id' missing", + }, + { + .testName = "TokenIDInvalidFormat", + .testJson = R"JSON({"token_id": "xxx"})JSON", + .expectedError = "invalidParams", + .expectedErrorMessage = "token_idMalformed", + }, + { + .testName = "TokenIDNotString", + .testJson = R"JSON({"token_id": 123})JSON", + .expectedError = "invalidParams", + .expectedErrorMessage = "token_idNotString", + }, + { + .testName = "NonHexLedgerHash", + .testJson = fmt::format( + R"JSON({{ + "token_id": "{}", + "ledger_hash": "xxx" + }})JSON", + kMPT_ID + ), + .expectedError = "invalidParams", + .expectedErrorMessage = "ledger_hashMalformed", + }, + { + .testName = "NonStringLedgerHash", + .testJson = fmt::format( + R"JSON({{ + "token_id": "{}", + "ledger_hash": 123 + }})JSON", + kMPT_ID + ), + .expectedError = "invalidParams", + .expectedErrorMessage = "ledger_hashNotString", + }, + { + .testName = "InvalidLedgerIndexString", + .testJson = fmt::format( + R"JSON({{ + "token_id": "{}", + "ledger_index": "notvalidated" + }})JSON", + kMPT_ID + ), + .expectedError = "invalidParams", + .expectedErrorMessage = "ledgerIndexMalformed", + }, + }; +} + +INSTANTIATE_TEST_CASE_P( + VaultListGroup, + VaultListParameterTest, + ValuesIn(generateTestValuesForParametersTest()), + tests::util::kNAME_GENERATOR +); + +TEST_P(VaultListParameterTest, InvalidParams) +{ + auto const testBundle = VaultListParameterTest::GetParam(); + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{VaultListHandler{backend_}}; + auto const req = json::parse(testBundle.testJson); + auto const output = handler.process(req, Context{.yield = yield, .apiVersion = 2}); + ASSERT_FALSE(output); + + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), testBundle.expectedError); + EXPECT_EQ(err.at("error_message").as_string(), testBundle.expectedErrorMessage); + }); +} + +TEST_F(RPCVaultListHandlerTest, LedgerNotFound) +{ + EXPECT_CALL(*backend_, fetchLedgerBySequence) + .WillOnce(Return(std::optional{})); + + auto static const kINPUT = json::parse( + fmt::format( + R"JSON({{ + "token_id": "{}", + "ledger_index": "4" + }})JSON", + kMPT_ID + ) + ); + + auto const handler = AnyHandler{VaultListHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + ASSERT_FALSE(output); + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound"); + }); +} + +TEST_F(RPCVaultListHandlerTest, TokenNotFound) +{ + auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kSEQ); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); + + auto const issuanceKk = ripple::keylet::mptIssuance(ripple::uint192(kMPT_ID)).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(issuanceKk, kSEQ, _)).WillOnce(Return(std::nullopt)); + + auto static const kINPUT = json::parse( + fmt::format( + R"JSON({{ + "token_id": "{}" + }})JSON", + kMPT_ID + ) + ); + + auto const handler = AnyHandler{VaultListHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + 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(), "tokenNotFound"); + }); +} + +TEST_F(RPCVaultListHandlerTest, IssuerAccountNotFound) +{ + auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kSEQ); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); + + auto const mptID = ripple::uint192(kMPT_ID); + auto const issuance = createMptIssuanceObject(kACCOUNT, kSEQ, "metadata"); + auto const issuanceKk = ripple::keylet::mptIssuance(mptID).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(issuanceKk, kSEQ, _)) + .WillOnce(Return(issuance.getSerializer().peekData())); + + auto const account = getAccountIdWithString(kACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kSEQ, _)).WillOnce(Return(std::nullopt)); + + auto static const kINPUT = json::parse( + fmt::format( + R"JSON({{ + "token_id": "{}" + }})JSON", + kMPT_ID + ) + ); + + auto const handler = AnyHandler{VaultListHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + ASSERT_FALSE(output); + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "actNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "Account not found."); + }); +} + +TEST_F(RPCVaultListHandlerTest, EmptyResult) +{ + auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kSEQ); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); + + auto const mptID = ripple::uint192(kMPT_ID); + auto const issuance = createMptIssuanceObject(kACCOUNT, kSEQ, "metadata"); + auto const issuanceKk = ripple::keylet::mptIssuance(mptID).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(issuanceKk, kSEQ, _)) + .WillOnce(Return(issuance.getSerializer().peekData())); + + auto const account = getAccountIdWithString(kACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kSEQ, _)) + .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); + + // Owner directory does not exist (no owned objects) + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, kSEQ, _)).WillOnce(Return(std::nullopt)); + + auto static const kINPUT = json::parse( + fmt::format( + R"JSON({{ + "token_id": "{}" + }})JSON", + kMPT_ID + ) + ); + + auto const handler = AnyHandler{VaultListHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + ASSERT_TRUE(output); + EXPECT_EQ(output.result->as_object().at("vaults").as_array().size(), 0); + EXPECT_EQ(output.result->as_object().at("token_id").as_string(), kMPT_ID); + }); +} + +TEST_F(RPCVaultListHandlerTest, SingleVaultListed) +{ + auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kSEQ); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); + + // MPT issuance lookup + auto const mptID = ripple::uint192(kMPT_ID); + auto const issuance = createMptIssuanceObject(kACCOUNT, kSEQ, "metadata", 0, 500); + auto const issuanceKk = ripple::keylet::mptIssuance(mptID).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(issuanceKk, kSEQ, _)) + .WillOnce(Return(issuance.getSerializer().peekData())); + + // Account lookup + auto const account = getAccountIdWithString(kACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kSEQ, _)) + .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); + + // Create vault object + ripple::uint192 const shareMPTID{456}; + ripple::uint256 const prevTxId{2}; + uint32_t const prevTxSeq = 3; + uint64_t const ownerNode = 4; + + auto const vault = createVault( + kACCOUNT, + kACCOUNT2, + kSEQ, + kASSET_CURRENCY, + kASSET_ISSUER, + shareMPTID, + ownerNode, + prevTxId, + prevTxSeq + ); + + auto const vaultKeylet = ripple::keylet::vault(account, kSEQ).key; + + // Owner directory with one entry (the vault) + auto const ownerDir = createOwnerDirLedgerObject({vaultKeylet}, ripple::strHex(vaultKeylet)); + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, kSEQ, _)) + .WillOnce(Return(ownerDir.getSerializer().peekData())); + + // Batch fetch of owned objects returns the vault + std::vector bbs; + bbs.push_back(vault.getSerializer().peekData()); + EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); + + // Share MPT issuance lookup (triggered by the vault summary builder) + auto const shareIssuance = createMptIssuanceObject(kACCOUNT2, kSEQ, std::nullopt, 0, 1000); + auto const shareIssuanceKk = ripple::keylet::mptIssuance(shareMPTID).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(shareIssuanceKk, kSEQ, _)) + .WillOnce(Return(shareIssuance.getSerializer().peekData())); + + auto static const kINPUT = json::parse( + fmt::format( + R"JSON({{ + "token_id": "{}" + }})JSON", + kMPT_ID + ) + ); + + auto const handler = AnyHandler{VaultListHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + ASSERT_TRUE(output); + + auto const& result = output.result->as_object(); + EXPECT_EQ(result.at("token_id").as_string(), kMPT_ID); + EXPECT_EQ(result.at("ledger_index").as_uint64(), kSEQ); + EXPECT_TRUE(result.at("validated").as_bool()); + + auto const& vaults = result.at("vaults").as_array(); + ASSERT_EQ(vaults.size(), 1); + + auto const& vaultSummary = vaults[0].as_object(); + EXPECT_TRUE(vaultSummary.contains("vault_id")); + EXPECT_EQ(vaultSummary.at("account").as_string(), kACCOUNT2); + EXPECT_EQ(vaultSummary.at("owner").as_string(), kACCOUNT); + EXPECT_TRUE(vaultSummary.contains("total_assets")); + EXPECT_EQ(vaultSummary.at("total_shares").as_uint64(), 1000); + EXPECT_EQ(vaultSummary.at("status").as_string(), "active"); + EXPECT_EQ(vaultSummary.at("flags").as_uint64(), 0); + }); +} + +TEST_F(RPCVaultListHandlerTest, NonVaultObjectsFiltered) +{ + auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kSEQ); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); + + // MPT issuance lookup + auto const mptID = ripple::uint192(kMPT_ID); + auto const issuance = createMptIssuanceObject(kACCOUNT, kSEQ, "metadata"); + auto const issuanceKk = ripple::keylet::mptIssuance(mptID).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(issuanceKk, kSEQ, _)) + .WillOnce(Return(issuance.getSerializer().peekData())); + + // Account lookup + auto const account = getAccountIdWithString(kACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kSEQ, _)) + .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); + + // Create a non-vault object (e.g. a RippleState / trust line) + auto const line = createRippleStateLedgerObject( + "USD", kASSET_ISSUER, 100, kACCOUNT, 10, kACCOUNT2, 20, kINDEX1, 123, 0 + ); + + // Owner directory with one entry (the trust line, NOT a vault) + auto const ownerDir = createOwnerDirLedgerObject({ripple::uint256{kINDEX1}}, kINDEX1); + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, kSEQ, _)) + .WillOnce(Return(ownerDir.getSerializer().peekData())); + + std::vector bbs; + bbs.push_back(line.getSerializer().peekData()); + EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); + + auto static const kINPUT = json::parse( + fmt::format( + R"JSON({{ + "token_id": "{}" + }})JSON", + kMPT_ID + ) + ); + + auto const handler = AnyHandler{VaultListHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + ASSERT_TRUE(output); + + auto const& vaults = output.result->as_object().at("vaults").as_array(); + EXPECT_EQ(vaults.size(), 0); + }); +} + +TEST_F(RPCVaultListHandlerTest, MultipleVaultsListed) +{ + auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kSEQ); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); + + // MPT issuance lookup + auto const mptID = ripple::uint192(kMPT_ID); + auto const issuance = createMptIssuanceObject(kACCOUNT, kSEQ, "metadata"); + auto const issuanceKk = ripple::keylet::mptIssuance(mptID).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(issuanceKk, kSEQ, _)) + .WillOnce(Return(issuance.getSerializer().peekData())); + + // Account lookup + auto const account = getAccountIdWithString(kACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kSEQ, _)) + .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); + + // Create two vault objects with different share MPT IDs + ripple::uint192 const shareMPTID1{111}; + ripple::uint192 const shareMPTID2{222}; + ripple::uint256 const prevTxId{2}; + + auto const vault1 = createVault( + kACCOUNT, kACCOUNT2, kSEQ, kASSET_CURRENCY, kASSET_ISSUER, shareMPTID1, 1, prevTxId, 3 + ); + auto const vault2 = createVault( + kACCOUNT, kACCOUNT2, kSEQ + 1, kASSET_CURRENCY, kASSET_ISSUER, shareMPTID2, 2, prevTxId, 4 + ); + + auto const vaultKey1 = ripple::keylet::vault(account, kSEQ).key; + auto const vaultKey2 = ripple::keylet::vault(account, kSEQ + 1).key; + + // Owner directory with two entries + auto const ownerDir = + createOwnerDirLedgerObject({vaultKey1, vaultKey2}, ripple::strHex(vaultKey1)); + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, kSEQ, _)) + .WillOnce(Return(ownerDir.getSerializer().peekData())); + + // Batch fetch returns both vaults + std::vector bbs; + bbs.push_back(vault1.getSerializer().peekData()); + bbs.push_back(vault2.getSerializer().peekData()); + EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); + + // Share MPT issuance lookups for each vault + auto const shareIssuance1 = createMptIssuanceObject(kACCOUNT2, kSEQ, std::nullopt, 0, 100); + auto const shareIssuanceKk1 = ripple::keylet::mptIssuance(shareMPTID1).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(shareIssuanceKk1, kSEQ, _)) + .WillOnce(Return(shareIssuance1.getSerializer().peekData())); + + auto const shareIssuance2 = createMptIssuanceObject(kACCOUNT2, kSEQ + 1, std::nullopt, 0, 200); + auto const shareIssuanceKk2 = ripple::keylet::mptIssuance(shareMPTID2).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(shareIssuanceKk2, kSEQ, _)) + .WillOnce(Return(shareIssuance2.getSerializer().peekData())); + + auto static const kINPUT = json::parse( + fmt::format( + R"JSON({{ + "token_id": "{}" + }})JSON", + kMPT_ID + ) + ); + + auto const handler = AnyHandler{VaultListHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + ASSERT_TRUE(output); + + auto const& vaults = output.result->as_object().at("vaults").as_array(); + ASSERT_EQ(vaults.size(), 2); + + // Both should have the expected summary fields + for (auto const& v : vaults) { + auto const& obj = v.as_object(); + EXPECT_TRUE(obj.contains("vault_id")); + EXPECT_TRUE(obj.contains("account")); + EXPECT_TRUE(obj.contains("owner")); + EXPECT_TRUE(obj.contains("total_assets")); + EXPECT_TRUE(obj.contains("total_shares")); + EXPECT_TRUE(obj.contains("status")); + EXPECT_TRUE(obj.contains("flags")); + } + + EXPECT_EQ(vaults[0].as_object().at("total_shares").as_uint64(), 100); + EXPECT_EQ(vaults[1].as_object().at("total_shares").as_uint64(), 200); + }); +} + +TEST_F(RPCVaultListHandlerTest, ShareIssuanceNotFoundFallsBackToZero) +{ + auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kSEQ); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); + + // MPT issuance lookup + auto const mptID = ripple::uint192(kMPT_ID); + auto const issuance = createMptIssuanceObject(kACCOUNT, kSEQ, "metadata"); + auto const issuanceKk = ripple::keylet::mptIssuance(mptID).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(issuanceKk, kSEQ, _)) + .WillOnce(Return(issuance.getSerializer().peekData())); + + // Account lookup + auto const account = getAccountIdWithString(kACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kSEQ, _)) + .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); + + // Create vault + ripple::uint192 const shareMPTID{789}; + auto const vault = createVault( + kACCOUNT, + kACCOUNT2, + kSEQ, + kASSET_CURRENCY, + kASSET_ISSUER, + shareMPTID, + 1, + ripple::uint256{2}, + 3 + ); + auto const vaultKey = ripple::keylet::vault(account, kSEQ).key; + + // Owner directory + auto const ownerDir = createOwnerDirLedgerObject({vaultKey}, ripple::strHex(vaultKey)); + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, kSEQ, _)) + .WillOnce(Return(ownerDir.getSerializer().peekData())); + + std::vector bbs; + bbs.push_back(vault.getSerializer().peekData()); + EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); + + // Share MPT issuance NOT found + auto const shareIssuanceKk = ripple::keylet::mptIssuance(shareMPTID).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(shareIssuanceKk, kSEQ, _)) + .WillOnce(Return(std::nullopt)); + + auto static const kINPUT = json::parse( + fmt::format( + R"JSON({{ + "token_id": "{}" + }})JSON", + kMPT_ID + ) + ); + + auto const handler = AnyHandler{VaultListHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + ASSERT_TRUE(output); + + auto const& vaults = output.result->as_object().at("vaults").as_array(); + ASSERT_EQ(vaults.size(), 1); + EXPECT_EQ(vaults[0].as_object().at("total_shares").as_uint64(), 0); + }); +} + +TEST_F(RPCVaultListHandlerTest, WithExplicitLedgerIndex) +{ + constexpr uint32_t kEXPLICIT_SEQ = 25; + auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kEXPLICIT_SEQ); + EXPECT_CALL(*backend_, fetchLedgerBySequence(kEXPLICIT_SEQ, _)).WillOnce(Return(ledgerHeader)); + + // MPT issuance lookup + auto const mptID = ripple::uint192(kMPT_ID); + auto const issuance = createMptIssuanceObject(kACCOUNT, kSEQ, "metadata"); + auto const issuanceKk = ripple::keylet::mptIssuance(mptID).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(issuanceKk, kEXPLICIT_SEQ, _)) + .WillOnce(Return(issuance.getSerializer().peekData())); + + auto const account = getAccountIdWithString(kACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kEXPLICIT_SEQ, _)) + .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); + + // Empty owner directory + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, kEXPLICIT_SEQ, _)) + .WillOnce(Return(std::nullopt)); + + auto static const kINPUT = json::parse( + fmt::format( + R"JSON({{ + "token_id": "{}", + "ledger_index": {} + }})JSON", + kMPT_ID, + kEXPLICIT_SEQ + ) + ); + + auto const handler = AnyHandler{VaultListHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + ASSERT_TRUE(output); + EXPECT_EQ(output.result->as_object().at("ledger_index").as_uint64(), kEXPLICIT_SEQ); + }); +} + +TEST_F(RPCVaultListHandlerTest, WithLimitParameter) +{ + auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kSEQ); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); + + auto const mptID = ripple::uint192(kMPT_ID); + auto const issuance = createMptIssuanceObject(kACCOUNT, kSEQ, "metadata"); + auto const issuanceKk = ripple::keylet::mptIssuance(mptID).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(issuanceKk, kSEQ, _)) + .WillOnce(Return(issuance.getSerializer().peekData())); + + auto const account = getAccountIdWithString(kACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kSEQ, _)) + .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, kSEQ, _)).WillOnce(Return(std::nullopt)); + + auto static const kINPUT = json::parse( + fmt::format( + R"JSON({{ + "token_id": "{}", + "limit": 50 + }})JSON", + kMPT_ID + ) + ); + + auto const handler = AnyHandler{VaultListHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + ASSERT_TRUE(output); + // Limit is clamped to [10, 400], 50 is within range so stays 50 + EXPECT_EQ(output.result->as_object().at("limit").as_uint64(), 50); + }); +} + +TEST_F(RPCVaultListHandlerTest, LimitClampedToMax) +{ + auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kSEQ); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); + + auto const mptID = ripple::uint192(kMPT_ID); + auto const issuance = createMptIssuanceObject(kACCOUNT, kSEQ, "metadata"); + auto const issuanceKk = ripple::keylet::mptIssuance(mptID).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(issuanceKk, kSEQ, _)) + .WillOnce(Return(issuance.getSerializer().peekData())); + + auto const account = getAccountIdWithString(kACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kSEQ, _)) + .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, kSEQ, _)).WillOnce(Return(std::nullopt)); + + auto static const kINPUT = json::parse( + fmt::format( + R"JSON({{ + "token_id": "{}", + "limit": 9999 + }})JSON", + kMPT_ID + ) + ); + + auto const handler = AnyHandler{VaultListHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + ASSERT_TRUE(output); + // Limit is clamped to max 400 + EXPECT_EQ(output.result->as_object().at("limit").as_uint64(), 400); + }); +} + +TEST_F(RPCVaultListHandlerTest, VaultWithNonZeroFlagsStatus) +{ + auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kSEQ); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); + + auto const mptID = ripple::uint192(kMPT_ID); + auto const issuance = createMptIssuanceObject(kACCOUNT, kSEQ, "metadata"); + auto const issuanceKk = ripple::keylet::mptIssuance(mptID).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(issuanceKk, kSEQ, _)) + .WillOnce(Return(issuance.getSerializer().peekData())); + + auto const account = getAccountIdWithString(kACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, kSEQ, _)) + .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); + + // Create a vault with non-zero flags + ripple::uint192 const shareMPTID{321}; + auto vault = createVault( + kACCOUNT, + kACCOUNT2, + kSEQ, + kASSET_CURRENCY, + kASSET_ISSUER, + shareMPTID, + 1, + ripple::uint256{2}, + 3 + ); + // Override flags to non-zero + vault.setFieldU32(ripple::sfFlags, 1); + + auto const vaultKey = ripple::keylet::vault(account, kSEQ).key; + + auto const ownerDir = createOwnerDirLedgerObject({vaultKey}, ripple::strHex(vaultKey)); + auto const ownerDirKk = ripple::keylet::ownerDir(account).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, kSEQ, _)) + .WillOnce(Return(ownerDir.getSerializer().peekData())); + + std::vector bbs; + bbs.push_back(vault.getSerializer().peekData()); + EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs)); + + auto const shareIssuance = createMptIssuanceObject(kACCOUNT2, kSEQ, std::nullopt, 0, 50); + auto const shareIssuanceKk = ripple::keylet::mptIssuance(shareMPTID).key; + EXPECT_CALL(*backend_, doFetchLedgerObject(shareIssuanceKk, kSEQ, _)) + .WillOnce(Return(shareIssuance.getSerializer().peekData())); + + auto static const kINPUT = json::parse( + fmt::format( + R"JSON({{ + "token_id": "{}" + }})JSON", + kMPT_ID + ) + ); + + auto const handler = AnyHandler{VaultListHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + ASSERT_TRUE(output); + + auto const& vaults = output.result->as_object().at("vaults").as_array(); + ASSERT_EQ(vaults.size(), 1); + EXPECT_EQ(vaults[0].as_object().at("status").as_string(), "modified"); + EXPECT_EQ(vaults[0].as_object().at("flags").as_uint64(), 1); + }); +} From 91221bd8743e35495f26e87a250bd548ebcd84ab Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 26 Mar 2026 18:30:34 +0000 Subject: [PATCH 2/2] Use kAPI_VERSION --- tests/unit/rpc/handlers/VaultListTests.cpp | 40 +++++++++++++++------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/tests/unit/rpc/handlers/VaultListTests.cpp b/tests/unit/rpc/handlers/VaultListTests.cpp index 5171c3abb..cd9a1a48c 100644 --- a/tests/unit/rpc/handlers/VaultListTests.cpp +++ b/tests/unit/rpc/handlers/VaultListTests.cpp @@ -39,6 +39,7 @@ constexpr auto kASSET_CURRENCY = "USD"; constexpr auto kASSET_ISSUER = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"; constexpr uint32_t kSEQ_START = 10; constexpr uint32_t kSEQ = 30; +constexpr auto kAPI_VERSION = 2; } // namespace @@ -135,7 +136,8 @@ TEST_P(VaultListParameterTest, InvalidParams) runSpawn([&, this](auto yield) { auto const handler = AnyHandler{VaultListHandler{backend_}}; auto const req = json::parse(testBundle.testJson); - auto const output = handler.process(req, Context{.yield = yield, .apiVersion = 2}); + auto const output = + handler.process(req, Context{.yield = yield, .apiVersion = kAPI_VERSION}); ASSERT_FALSE(output); auto const err = rpc::makeError(output.result.error()); @@ -161,7 +163,8 @@ TEST_F(RPCVaultListHandlerTest, LedgerNotFound) auto const handler = AnyHandler{VaultListHandler{backend_}}; runSpawn([&](auto yield) { - auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + auto const output = + handler.process(kINPUT, Context{.yield = yield, .apiVersion = kAPI_VERSION}); ASSERT_FALSE(output); auto const err = rpc::makeError(output.result.error()); EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); @@ -188,7 +191,8 @@ TEST_F(RPCVaultListHandlerTest, TokenNotFound) auto const handler = AnyHandler{VaultListHandler{backend_}}; runSpawn([&](auto yield) { - auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + auto const output = + handler.process(kINPUT, Context{.yield = yield, .apiVersion = kAPI_VERSION}); ASSERT_FALSE(output); auto const err = rpc::makeError(output.result.error()); EXPECT_EQ(err.at("error").as_string(), "objectNotFound"); @@ -222,7 +226,8 @@ TEST_F(RPCVaultListHandlerTest, IssuerAccountNotFound) auto const handler = AnyHandler{VaultListHandler{backend_}}; runSpawn([&](auto yield) { - auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + auto const output = + handler.process(kINPUT, Context{.yield = yield, .apiVersion = kAPI_VERSION}); ASSERT_FALSE(output); auto const err = rpc::makeError(output.result.error()); EXPECT_EQ(err.at("error").as_string(), "actNotFound"); @@ -261,7 +266,8 @@ TEST_F(RPCVaultListHandlerTest, EmptyResult) auto const handler = AnyHandler{VaultListHandler{backend_}}; runSpawn([&](auto yield) { - auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + auto const output = + handler.process(kINPUT, Context{.yield = yield, .apiVersion = kAPI_VERSION}); ASSERT_TRUE(output); EXPECT_EQ(output.result->as_object().at("vaults").as_array().size(), 0); EXPECT_EQ(output.result->as_object().at("token_id").as_string(), kMPT_ID); @@ -334,7 +340,8 @@ TEST_F(RPCVaultListHandlerTest, SingleVaultListed) auto const handler = AnyHandler{VaultListHandler{backend_}}; runSpawn([&](auto yield) { - auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + auto const output = + handler.process(kINPUT, Context{.yield = yield, .apiVersion = kAPI_VERSION}); ASSERT_TRUE(output); auto const& result = output.result->as_object(); @@ -400,7 +407,8 @@ TEST_F(RPCVaultListHandlerTest, NonVaultObjectsFiltered) auto const handler = AnyHandler{VaultListHandler{backend_}}; runSpawn([&](auto yield) { - auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + auto const output = + handler.process(kINPUT, Context{.yield = yield, .apiVersion = kAPI_VERSION}); ASSERT_TRUE(output); auto const& vaults = output.result->as_object().at("vaults").as_array(); @@ -476,7 +484,8 @@ TEST_F(RPCVaultListHandlerTest, MultipleVaultsListed) auto const handler = AnyHandler{VaultListHandler{backend_}}; runSpawn([&](auto yield) { - auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + auto const output = + handler.process(kINPUT, Context{.yield = yield, .apiVersion = kAPI_VERSION}); ASSERT_TRUE(output); auto const& vaults = output.result->as_object().at("vaults").as_array(); @@ -558,7 +567,8 @@ TEST_F(RPCVaultListHandlerTest, ShareIssuanceNotFoundFallsBackToZero) auto const handler = AnyHandler{VaultListHandler{backend_}}; runSpawn([&](auto yield) { - auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + auto const output = + handler.process(kINPUT, Context{.yield = yield, .apiVersion = kAPI_VERSION}); ASSERT_TRUE(output); auto const& vaults = output.result->as_object().at("vaults").as_array(); @@ -603,7 +613,8 @@ TEST_F(RPCVaultListHandlerTest, WithExplicitLedgerIndex) auto const handler = AnyHandler{VaultListHandler{backend_}}; runSpawn([&](auto yield) { - auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + auto const output = + handler.process(kINPUT, Context{.yield = yield, .apiVersion = kAPI_VERSION}); ASSERT_TRUE(output); EXPECT_EQ(output.result->as_object().at("ledger_index").as_uint64(), kEXPLICIT_SEQ); }); @@ -640,7 +651,8 @@ TEST_F(RPCVaultListHandlerTest, WithLimitParameter) auto const handler = AnyHandler{VaultListHandler{backend_}}; runSpawn([&](auto yield) { - auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + auto const output = + handler.process(kINPUT, Context{.yield = yield, .apiVersion = kAPI_VERSION}); ASSERT_TRUE(output); // Limit is clamped to [10, 400], 50 is within range so stays 50 EXPECT_EQ(output.result->as_object().at("limit").as_uint64(), 50); @@ -678,7 +690,8 @@ TEST_F(RPCVaultListHandlerTest, LimitClampedToMax) auto const handler = AnyHandler{VaultListHandler{backend_}}; runSpawn([&](auto yield) { - auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + auto const output = + handler.process(kINPUT, Context{.yield = yield, .apiVersion = kAPI_VERSION}); ASSERT_TRUE(output); // Limit is clamped to max 400 EXPECT_EQ(output.result->as_object().at("limit").as_uint64(), 400); @@ -744,7 +757,8 @@ TEST_F(RPCVaultListHandlerTest, VaultWithNonZeroFlagsStatus) auto const handler = AnyHandler{VaultListHandler{backend_}}; runSpawn([&](auto yield) { - auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + auto const output = + handler.process(kINPUT, Context{.yield = yield, .apiVersion = kAPI_VERSION}); ASSERT_TRUE(output); auto const& vaults = output.result->as_object().at("vaults").as_array();