From 1e50ec2ac76342edceb0b39cf45cf1f28d32a0ac Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Tue, 25 Mar 2025 17:01:09 -0400 Subject: [PATCH 1/9] save work --- conanfile.py | 2 +- src/etlng/impl/ext/Core.cpp | 1 - src/rpc/handlers/LedgerEntry.cpp | 10 +++++++++- src/rpc/handlers/LedgerEntry.hpp | 17 +++++++++++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/conanfile.py b/conanfile.py index d6e6144ab..35e9b3d7b 100644 --- a/conanfile.py +++ b/conanfile.py @@ -29,7 +29,7 @@ class Clio(ConanFile): 'protobuf/3.21.9', 'grpc/1.50.1', 'openssl/1.1.1v', - 'xrpl/2.4.0', + 'xrpl/2.4.0@my/singleAssetVault', 'zlib/1.3.1', 'libbacktrace/cci.20210118' ] diff --git a/src/etlng/impl/ext/Core.cpp b/src/etlng/impl/ext/Core.cpp index fa2c3c9fa..3049961fd 100644 --- a/src/etlng/impl/ext/Core.cpp +++ b/src/etlng/impl/ext/Core.cpp @@ -23,7 +23,6 @@ #include "etlng/Models.hpp" #include "util/log/Logger.hpp" - #include #include #include diff --git a/src/rpc/handlers/LedgerEntry.cpp b/src/rpc/handlers/LedgerEntry.cpp index c424135aa..53e09d4e6 100644 --- a/src/rpc/handlers/LedgerEntry.cpp +++ b/src/rpc/handlers/LedgerEntry.cpp @@ -185,6 +185,11 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx) ); auto const seq = input.permissionedDomain->at(JS(seq)).as_int64(); key = ripple::keylet::permissionedDomain(*account, seq).key; + } else if (input.vault) { + auto const account = + ripple::parseBase58(boost::json::value_to(input.vault->at(JS(account)))); + auto const seq = input.vault->at(JS(seq)).as_int64(); + key = ripple::keylet::vault(*account, seq).key; } else { // Must specify 1 of the following fields to indicate what type if (ctx.apiVersion == 1) @@ -319,7 +324,8 @@ tag_invoke(boost::json::value_to_tag, boost::json::va {JS(oracle), ripple::ltORACLE}, {JS(credential), ripple::ltCREDENTIAL}, {JS(mptoken), ripple::ltMPTOKEN}, - {JS(permissioned_domain), ripple::ltPERMISSIONED_DOMAIN} + {JS(permissioned_domain), ripple::ltPERMISSIONED_DOMAIN}, + {JS(vault), ripple::ltVAULT} }; auto const parseBridgeFromJson = [](boost::json::value const& bridgeJson) { @@ -408,6 +414,8 @@ tag_invoke(boost::json::value_to_tag, boost::json::va input.mptoken = jv.at(JS(mptoken)).as_object(); } else if (jsonObject.contains(JS(permissioned_domain))) { input.permissionedDomain = jv.at(JS(permissioned_domain)).as_object(); + } else if (jsonObject.contains(JS(vault))) { + input.vault = jv.at(JS(vault)).as_object(); } if (jsonObject.contains("include_deleted")) diff --git a/src/rpc/handlers/LedgerEntry.hpp b/src/rpc/handlers/LedgerEntry.hpp index 9851ad746..be25984a4 100644 --- a/src/rpc/handlers/LedgerEntry.hpp +++ b/src/rpc/handlers/LedgerEntry.hpp @@ -104,6 +104,7 @@ class LedgerEntryHandler { std::optional amm; std::optional mptoken; std::optional permissionedDomain; + std::optional vault; std::optional bridge; std::optional bridgeAccount; std::optional chainClaimId; @@ -392,6 +393,22 @@ class LedgerEntryHandler { }, }, }}}, + {JS(vault), + meta::WithCustomError{ + validation::Type{}, Status(ClioError::RpcMalformedRequest) + }, + meta::IfType{meta::Section{ + {JS(seq), + meta::WithCustomError{validation::Required{}, Status(ClioError::RpcMalformedRequest)}, + meta::WithCustomError{validation::Type{}, Status(ClioError::RpcMalformedRequest)}}, + { + JS(account), + meta::WithCustomError{validation::Required{}, Status(ClioError::RpcMalformedRequest)}, + meta::WithCustomError{ + validation::CustomValidators::accountBase58Validator, Status(ClioError::RpcMalformedAddress) + }, + }, + }}}, {JS(ledger), check::Deprecated{}}, {"include_deleted", validation::Type{}}, }; From 0fd18fdc7a724fe1fe9ff0d2b41a9437a50850e4 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Thu, 27 Mar 2025 18:24:15 -0400 Subject: [PATCH 2/9] feat: Add Support for Single Asset Vault --- src/data/AmendmentCenter.hpp | 1 + src/rpc/RPCHelpers.hpp | 40 +++++++ src/rpc/handlers/LedgerData.cpp | 4 + src/rpc/handlers/LedgerEntry.cpp | 4 +- src/rpc/handlers/LedgerEntry.hpp | 5 +- src/util/LedgerUtils.hpp | 1 + tests/common/util/TestObject.cpp | 37 ++++++ tests/common/util/TestObject.hpp | 13 ++ tests/unit/rpc/handlers/LedgerEntryTests.cpp | 119 +++++++++++++++++++ tests/unit/util/LedgerUtilsTests.cpp | 2 + 10 files changed, 223 insertions(+), 3 deletions(-) diff --git a/src/data/AmendmentCenter.hpp b/src/data/AmendmentCenter.hpp index 1aac59548..f5e50c754 100644 --- a/src/data/AmendmentCenter.hpp +++ b/src/data/AmendmentCenter.hpp @@ -137,6 +137,7 @@ struct Amendments { REGISTER(fixInvalidTxFlags); REGISTER(fixFrozenLPTokenTransfer); REGISTER(DeepFreeze); + REGISTER(SingleAssetVault); // Obsolete but supported by libxrpl REGISTER(CryptoConditionsSuite); diff --git a/src/rpc/RPCHelpers.hpp b/src/rpc/RPCHelpers.hpp index 040a211a0..d84bf5637 100644 --- a/src/rpc/RPCHelpers.hpp +++ b/src/rpc/RPCHelpers.hpp @@ -28,6 +28,7 @@ #include "data/BackendInterface.hpp" #include "data/Types.hpp" #include "rpc/Errors.hpp" +#include "rpc/JS.hpp" #include "rpc/common/Types.hpp" #include "util/JsonUtils.hpp" #include "util/log/Logger.hpp" @@ -41,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -49,9 +51,12 @@ #include #include #include +#include #include +#include #include #include +#include #include #include #include @@ -59,9 +64,11 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -841,4 +848,37 @@ getDeliveredAmount( uint32_t date ); +/** + * @brief Get the delivered amount + * + * @param txn The transaction + * @param meta The metadata + * @param ledgerSequence The sequence + * @param date The date of the ledger + * @return The delivered amount or std::nullopt if not available + */ +template +inline void +supplementJson( + BackendInterface const& backend, + ripple::STLedgerEntry const& vault, + boost::json::object& entry, + std::uint32_t ledgerSequence, + boost::asio::yield_context yield +) +{ + auto const share = vault.at(ripple::sfMPTokenIssuanceID); + auto const sleIssuance = backend.fetchLedgerObject(ripple::keylet::mptIssuance(share).key, ledgerSequence, yield); + if (!sleIssuance) + return; + + // blob to sle + ripple::STLedgerEntry const sle{ + ripple::SerialIter{sleIssuance->data(), sleIssuance->size()}, ripple::keylet::mptIssuance(share).key + }; + if (sle.empty()) + return; + entry.at(JS(ShareTotal)) = sle.getFieldU64(ripple::sfOutstandingAmount); +}; + } // namespace rpc diff --git a/src/rpc/handlers/LedgerData.cpp b/src/rpc/handlers/LedgerData.cpp index 0b6d396e4..5a5d1f7a2 100644 --- a/src/rpc/handlers/LedgerData.cpp +++ b/src/rpc/handlers/LedgerData.cpp @@ -141,6 +141,10 @@ LedgerDataHandler::process(Input input, Context const& ctx) const entry[JS(index)] = ripple::to_string(sle.key()); output.states.push_back(std::move(entry)); } else { + boost::json::object entry; + if (sle.getType() == ripple::ltVAULT) + supplementJson(*sharedPtrBackend_, sle, entry, lgrInfo.seq, ctx.yield); + output.states.push_back(toJson(sle)); } } diff --git a/src/rpc/handlers/LedgerEntry.cpp b/src/rpc/handlers/LedgerEntry.cpp index 53e09d4e6..e4e15d475 100644 --- a/src/rpc/handlers/LedgerEntry.cpp +++ b/src/rpc/handlers/LedgerEntry.cpp @@ -187,7 +187,7 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx) key = ripple::keylet::permissionedDomain(*account, seq).key; } else if (input.vault) { auto const account = - ripple::parseBase58(boost::json::value_to(input.vault->at(JS(account)))); + ripple::parseBase58(boost::json::value_to(input.vault->at(JS(owner)))); auto const seq = input.vault->at(JS(seq)).as_int64(); key = ripple::keylet::vault(*account, seq).key; } else { @@ -236,6 +236,8 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx) output.nodeBinary = ripple::strHex(*ledgerObject); } else { output.node = toJson(sle); + if (input.expectedType == ripple::ltVAULT) + supplementJson(*sharedPtrBackend_, sle, output.node.value(), lgrInfo.seq, ctx.yield); } return output; diff --git a/src/rpc/handlers/LedgerEntry.hpp b/src/rpc/handlers/LedgerEntry.hpp index be25984a4..b550d1b44 100644 --- a/src/rpc/handlers/LedgerEntry.hpp +++ b/src/rpc/handlers/LedgerEntry.hpp @@ -397,15 +397,16 @@ class LedgerEntryHandler { meta::WithCustomError{ validation::Type{}, Status(ClioError::RpcMalformedRequest) }, + meta::IfType{kMALFORMED_REQUEST_HEX_STRING_VALIDATOR}, meta::IfType{meta::Section{ {JS(seq), meta::WithCustomError{validation::Required{}, Status(ClioError::RpcMalformedRequest)}, meta::WithCustomError{validation::Type{}, Status(ClioError::RpcMalformedRequest)}}, { - JS(account), + JS(owner), meta::WithCustomError{validation::Required{}, Status(ClioError::RpcMalformedRequest)}, meta::WithCustomError{ - validation::CustomValidators::accountBase58Validator, Status(ClioError::RpcMalformedAddress) + validation::CustomValidators::accountBase58Validator, Status(ClioError::RpcMalformedOwner) }, }, }}}, diff --git a/src/util/LedgerUtils.hpp b/src/util/LedgerUtils.hpp index 2c1916e0d..11f43ab20 100644 --- a/src/util/LedgerUtils.hpp +++ b/src/util/LedgerUtils.hpp @@ -114,6 +114,7 @@ class LedgerTypes { LedgerTypeAttribute::accountOwnedLedgerType(JS(did), ripple::ltDID), LedgerTypeAttribute::accountOwnedLedgerType(JS(oracle), ripple::ltORACLE), LedgerTypeAttribute::accountOwnedLedgerType(JS(credential), ripple::ltCREDENTIAL), + LedgerTypeAttribute::accountOwnedLedgerType(JS(vault), ripple::ltVAULT), LedgerTypeAttribute::chainLedgerType(JS(nunl), ripple::ltNEGATIVE_UNL), LedgerTypeAttribute::deletionBlockerLedgerType(JS(mpt_issuance), ripple::ltMPTOKEN_ISSUANCE), LedgerTypeAttribute::deletionBlockerLedgerType(JS(mptoken), ripple::ltMPTOKEN), diff --git a/tests/common/util/TestObject.cpp b/tests/common/util/TestObject.cpp index f04149e66..78266fb2f 100644 --- a/tests/common/util/TestObject.cpp +++ b/tests/common/util/TestObject.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -1609,3 +1610,39 @@ createAuthCredentialArray(std::vector issuer, std::vector issuer, std::vector credType); + +[[nodiscard]] ripple::STObject +createVault( + std::string_view accountId, + std::string_view ledgerIndex, + ripple::LedgerIndex seq, + std::string_view assetCurrency, + std::string_view assetIssuer, + ripple::uint192 issuanceID, + uint64_t ownerNode, + ripple::uint256 previousTxId, + uint32_t previousTxSeq +); diff --git a/tests/unit/rpc/handlers/LedgerEntryTests.cpp b/tests/unit/rpc/handlers/LedgerEntryTests.cpp index 632759c51..6f36556d8 100644 --- a/tests/unit/rpc/handlers/LedgerEntryTests.cpp +++ b/tests/unit/rpc/handlers/LedgerEntryTests.cpp @@ -2193,6 +2193,76 @@ generateTestValuesForParametersTest() .expectedError = "malformedRequest", .expectedErrorMessage = "Malformed request.", }, + ParamTestCaseBundle{ + .testName = "InvalidVault_Type", + .testJson = + R"json({ + "vault": 0 + })json", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "InvalidVault_NotHex", + .testJson = + R"json({ + "vault": "invalid_hex" + })json", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "MissingOwner", + .testJson = + R"json({ + "vault": { "seq": 1 } + })json", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + + ParamTestCaseBundle{ + .testName = "MissingSeq", + .testJson = + R"json({ + "vault": { "owner": "abcd" } + })json", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "SeqNotInteger", + .testJson = + R"json({ + "vault": { + "owner": "abcd", + "seq": "notAnInteger" + }})json", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "InvalidOwnerFormat", + .testJson = + R"json({ + "vault" : { + "owner": "abcd", + "seq": 10 + }})json", + .expectedError = "malformedOwner", + .expectedErrorMessage = "Malformed owner.", + }, + ParamTestCaseBundle{ + .testName = "BothOwnerAndSeqInvalid", + .testJson = + R"json({ + "vault" : { + "owner": "abcd", + "seq": -200 + }})json", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + } }; } @@ -2957,6 +3027,55 @@ generateTestValuesForNormalPathTest() ripple::keylet::permissionedDomain(ripple::parseBase58(kACCOUNT).value(), kRANGE_MAX) .key, .mockedEntity = createPermissionedDomainObject(kACCOUNT, kINDEX1, kRANGE_MAX, 0, ripple::uint256{0}, 0) + }, + NormalPathTestBundle{ + .testName = "CreateVaultObjectByHexString", + .testJson = fmt::format( + R"json({{ + "binary": true, + "vault": "{}" + }})json", + kINDEX1 + ), + .expectedIndex = ripple::uint256(kINDEX1), + .mockedEntity = createVault( + kACCOUNT, + kINDEX1, + kRANGE_MAX, + "XRP", + ripple::toBase58(ripple::xrpAccount()), + ripple::makeMptID(2, getAccountIdWithString(kACCOUNT)), + 0, + ripple::uint256{0}, + 0 + ) + }, + NormalPathTestBundle{ + .testName = "CreateVaultObjectByAccount", + .testJson = fmt::format( + R"json({{ + "binary": true, + "vault": {{ + "owner": "{}", + "seq": {} + }} + }})json", + kACCOUNT, + kRANGE_MAX + ), + .expectedIndex = + ripple::keylet::vault(ripple::parseBase58(kACCOUNT).value(), kRANGE_MAX).key, + .mockedEntity = createVault( + kACCOUNT, + kINDEX1, + kRANGE_MAX, + "XRP", + ripple::toBase58(ripple::xrpAccount()), + ripple::makeMptID(2, getAccountIdWithString(kACCOUNT)), + 0, + ripple::uint256{0}, + 0 + ) } }; } diff --git a/tests/unit/util/LedgerUtilsTests.cpp b/tests/unit/util/LedgerUtilsTests.cpp index 98a8a8d49..7f7cc9aaa 100644 --- a/tests/unit/util/LedgerUtilsTests.cpp +++ b/tests/unit/util/LedgerUtilsTests.cpp @@ -57,6 +57,7 @@ TEST(LedgerUtilsTests, LedgerObjectTypeList) JS(permissioned_domain), JS(oracle), JS(credential), + JS(vault), JS(nunl) }; @@ -91,6 +92,7 @@ TEST(LedgerUtilsTests, AccountOwnedTypeList) JS(mpt_issuance), JS(mptoken), JS(permissioned_domain), + JS(vault) }; static_assert(std::size(kCORRECT_TYPES) == kACCOUNT_OWNED.size()); From c9c2c98e05cd2db503710d1fd3a75f8978283d70 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Mon, 31 Mar 2025 15:37:03 -0400 Subject: [PATCH 3/9] add test for Supplement Json method --- src/rpc/RPCHelpers.hpp | 14 ++-- src/rpc/handlers/LedgerData.cpp | 4 +- src/rpc/handlers/LedgerEntry.cpp | 2 +- tests/unit/rpc/RPCHelpersTests.cpp | 72 ++++++++++++++++++++ tests/unit/rpc/handlers/LedgerDataTests.cpp | 58 ++++++++++++++++ tests/unit/rpc/handlers/LedgerEntryTests.cpp | 58 ++++++++++++++++ 6 files changed, 199 insertions(+), 9 deletions(-) diff --git a/src/rpc/RPCHelpers.hpp b/src/rpc/RPCHelpers.hpp index d84bf5637..89c1a8702 100644 --- a/src/rpc/RPCHelpers.hpp +++ b/src/rpc/RPCHelpers.hpp @@ -849,13 +849,14 @@ getDeliveredAmount( ); /** - * @brief Get the delivered amount + * @brief Supplements a JSON representation of a ltVAULT ledger entry by looking up its associated MPT issuance entry + * and adding the SharesTotal field * - * @param txn The transaction - * @param meta The metadata + * @param backend The backend to use + * @param vault The vault ledger entry + * @param entry The entry object * @param ledgerSequence The sequence - * @param date The date of the ledger - * @return The delivered amount or std::nullopt if not available + * @param yield The coroutine context */ template inline void @@ -878,7 +879,8 @@ supplementJson( }; if (sle.empty()) return; - entry.at(JS(ShareTotal)) = sle.getFieldU64(ripple::sfOutstandingAmount); + + entry[JS(ShareTotal)] = sle.getFieldU64(ripple::sfOutstandingAmount); }; } // namespace rpc diff --git a/src/rpc/handlers/LedgerData.cpp b/src/rpc/handlers/LedgerData.cpp index 5a5d1f7a2..e7f7ccd05 100644 --- a/src/rpc/handlers/LedgerData.cpp +++ b/src/rpc/handlers/LedgerData.cpp @@ -141,11 +141,11 @@ LedgerDataHandler::process(Input input, Context const& ctx) const entry[JS(index)] = ripple::to_string(sle.key()); output.states.push_back(std::move(entry)); } else { - boost::json::object entry; + auto entry = toJson(sle); if (sle.getType() == ripple::ltVAULT) supplementJson(*sharedPtrBackend_, sle, entry, lgrInfo.seq, ctx.yield); - output.states.push_back(toJson(sle)); + output.states.push_back(entry); } } } diff --git a/src/rpc/handlers/LedgerEntry.cpp b/src/rpc/handlers/LedgerEntry.cpp index e4e15d475..56780760d 100644 --- a/src/rpc/handlers/LedgerEntry.cpp +++ b/src/rpc/handlers/LedgerEntry.cpp @@ -236,7 +236,7 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx) output.nodeBinary = ripple::strHex(*ledgerObject); } else { output.node = toJson(sle); - if (input.expectedType == ripple::ltVAULT) + if (input.vault) supplementJson(*sharedPtrBackend_, sle, output.node.value(), lgrInfo.seq, ctx.yield); } diff --git a/tests/unit/rpc/RPCHelpersTests.cpp b/tests/unit/rpc/RPCHelpersTests.cpp index cfc0c7e03..c724d5909 100644 --- a/tests/unit/rpc/RPCHelpersTests.cpp +++ b/tests/unit/rpc/RPCHelpersTests.cpp @@ -33,7 +33,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -43,7 +45,9 @@ #include #include #include +#include #include +#include #include #include @@ -1076,6 +1080,74 @@ TEST_F(RPCHelpersTest, AccountHoldsLPTokenUnfrozen) ctx_.run(); } +TEST_F(RPCHelpersTest, SupplementJson_ValidVaultEntry) +{ + boost::json::object entry; + + auto const vault = createVault( + kACCOUNT, + kINDEX1, + 30, + "XRP", + ripple::toBase58(ripple::xrpAccount()), + ripple::makeMptID(30, getAccountIdWithString(kACCOUNT)), + 0, + ripple::uint256{1}, + 0 + ); + + auto const vaultKey = ripple::keylet::vault(ripple::parseBase58(kACCOUNT).value(), 30).key; + + auto const issuance = createMptIssuanceObject(kACCOUNT, 30, "metadata"); + ripple::uint256 issuanceKey = + ripple::keylet::mptIssuance(ripple::makeMptID(30, getAccountIdWithString(kACCOUNT))).key; + + ripple::STLedgerEntry const sle{ + ripple::SerialIter{vault.getSerializer().peekData().data(), vault.getSerializer().peekData().size()}, vaultKey + }; + + EXPECT_CALL(*backend_, doFetchLedgerObject(issuanceKey, testing::_, testing::_)) + .WillOnce(Return(issuance.getSerializer().peekData())); + + runSpawn([&](boost::asio::yield_context yield) { + supplementJson(*backend_, sle, entry, 100, yield); + EXPECT_EQ(boost::json::value_to(entry.at(JS(ShareTotal))), 0); + }); +} + +TEST_F(RPCHelpersTest, SupplementJson_MissingIssuanceEntry) +{ + boost::json::object entry; + + auto const vault = createVault( + kACCOUNT, + kINDEX1, + 30, + "XRP", + ripple::toBase58(ripple::xrpAccount()), + ripple::makeMptID(30, getAccountIdWithString(kACCOUNT)), + 0, + ripple::uint256{1}, + 0 + ); + + auto const vaultKey = ripple::keylet::vault(ripple::parseBase58(kACCOUNT).value(), 30).key; + + ripple::uint256 issuanceKey = + ripple::keylet::mptIssuance(ripple::makeMptID(30, getAccountIdWithString(kACCOUNT))).key; + + ripple::STLedgerEntry const sle{ + ripple::SerialIter{vault.getSerializer().peekData().data(), vault.getSerializer().peekData().size()}, vaultKey + }; + + EXPECT_CALL(*backend_, doFetchLedgerObject(issuanceKey, testing::_, testing::_)).WillOnce(Return(std::nullopt)); + + runSpawn([&](boost::asio::yield_context yield) { + supplementJson(*backend_, sle, entry, 100, yield); + EXPECT_TRUE(entry.empty()); // Expect no changes to entry + }); +} + struct IsAdminCmdParamTestCaseBundle { std::string testName; std::string method; diff --git a/tests/unit/rpc/handlers/LedgerDataTests.cpp b/tests/unit/rpc/handlers/LedgerDataTests.cpp index 6fe3c9b5e..d4c4ce951 100644 --- a/tests/unit/rpc/handlers/LedgerDataTests.cpp +++ b/tests/unit/rpc/handlers/LedgerDataTests.cpp @@ -870,3 +870,61 @@ TEST(RPCLedgerDataHandlerSpecTest, DeprecatedFields) EXPECT_EQ(warning.at("id").as_int64(), static_cast(rpc::WarningCode::WarnRpcDeprecated)); EXPECT_NE(warning.at("message").as_string().find("Field 'ledger' is deprecated."), std::string::npos) << warning; } + +TEST_F(RPCLedgerDataHandlerTest, JsonFetchVaultLedgerData) +{ + EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); + ON_CALL(*backend_, fetchLedgerBySequence(kRANGE_MAX, _)) + .WillByDefault(Return(createLedgerHeader(kLEDGER_HASH, kRANGE_MAX))); + + std::vector bbs; + EXPECT_CALL(*backend_, doFetchSuccessorKey).Times(1); + ON_CALL(*backend_, doFetchSuccessorKey(_, kRANGE_MAX, _)).WillByDefault(Return(ripple::uint256{kINDEX2})); + + auto const vault = createVault( + kACCOUNT, + kINDEX1, + 30, + "XRP", + ripple::toBase58(ripple::xrpAccount()), + ripple::makeMptID(30, getAccountIdWithString(kACCOUNT)), + 0, + ripple::uint256{1}, + 0 + ); + + bbs.push_back(vault.getSerializer().peekData()); + + ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1); + + auto const issuance = createMptIssuanceObject(kACCOUNT, 30, "metadata"); + ripple::uint256 issuanceKey = + ripple::keylet::mptIssuance(ripple::makeMptID(30, getAccountIdWithString(kACCOUNT))).key; + + EXPECT_CALL(*backend_, doFetchLedgerObject(issuanceKey, testing::_, testing::_)) + .WillOnce(Return(issuance.getSerializer().peekData())); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{LedgerDataHandler{backend_}}; + auto const req = json::parse(R"({ + "limit":1, + "type":"vault" + })"); + + auto output = handler.process(req, Context{yield}); + ASSERT_TRUE(output); + EXPECT_TRUE(output.result->as_object().contains("ledger")); + EXPECT_EQ(output.result->as_object().at("state").as_array().size(), 1); + EXPECT_EQ(output.result->as_object().at("marker").as_string(), kINDEX2); + EXPECT_EQ(output.result->as_object().at("ledger_hash").as_string(), kLEDGER_HASH); + EXPECT_EQ(output.result->as_object().at("ledger_index").as_uint64(), kRANGE_MAX); + + auto const& objects = output.result->as_object().at("state").as_array(); + EXPECT_EQ(objects.front().at("LedgerEntryType").as_string(), "Vault"); + EXPECT_EQ(objects.front().at("LedgerEntryType").as_string(), "Vault"); + + auto const& firstEntry = objects[0].as_object(); + ASSERT_TRUE(firstEntry.contains("ShareTotal")); + }); +} diff --git a/tests/unit/rpc/handlers/LedgerEntryTests.cpp b/tests/unit/rpc/handlers/LedgerEntryTests.cpp index 6f36556d8..d724dc9eb 100644 --- a/tests/unit/rpc/handlers/LedgerEntryTests.cpp +++ b/tests/unit/rpc/handlers/LedgerEntryTests.cpp @@ -44,8 +44,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -3164,6 +3166,62 @@ TEST_F(RPCLedgerEntryTest, BinaryFalse) }); } +TEST_F(RPCLedgerEntryTest, Vault_BinaryFalse) +{ + // return valid ledgerHeader + auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kRANGE_MAX); + EXPECT_CALL(*backend_, fetchLedgerBySequence(kRANGE_MAX, _)).WillRepeatedly(Return(ledgerHeader)); + + boost::json::object entry; + + auto const vault = createVault( + kACCOUNT, + kINDEX1, + kRANGE_MAX, + "XRP", + ripple::toBase58(ripple::xrpAccount()), + ripple::makeMptID(30, getAccountIdWithString(kACCOUNT)), + 0, + ripple::uint256{1}, + 0 + ); + + auto const vaultKey = + ripple::keylet::vault(ripple::parseBase58(kACCOUNT).value(), kRANGE_MAX).key; + + auto const issuance = createMptIssuanceObject(kACCOUNT, kRANGE_MAX, "metadata"); + ripple::uint256 issuanceKey = + ripple::keylet::mptIssuance(ripple::makeMptID(kRANGE_MAX, getAccountIdWithString(kACCOUNT))).key; + + ripple::STLedgerEntry const sle{ + ripple::SerialIter{vault.getSerializer().peekData().data(), vault.getSerializer().peekData().size()}, vaultKey + }; + + EXPECT_CALL(*backend_, doFetchLedgerObject(vaultKey, testing::_, testing::_)) + .WillOnce(Return(vault.getSerializer().peekData())); + + EXPECT_CALL(*backend_, doFetchLedgerObject(issuanceKey, testing::_, testing::_)) + .WillOnce(Return(issuance.getSerializer().peekData())); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{LedgerEntryHandler{backend_}}; + auto const req = json::parse(fmt::format( + R"({{ + "binary": false, + "vault": {{ + "owner": "{}", + "seq": {} + }} + }})", + kACCOUNT, + kRANGE_MAX + )); + auto const output = handler.process(req, Context{yield}); + ASSERT_TRUE(output); + EXPECT_EQ(boost::json::value_to(output.result->at("node").at("ShareTotal")), 0); + }); +} + TEST_F(RPCLedgerEntryTest, UnexpectedLedgerType) { // return valid ledgerHeader From d733294efd95e4b9c982fb59a007a70696a7acf7 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Wed, 9 Apr 2025 09:24:24 -0700 Subject: [PATCH 4/9] add handler + tests --- src/rpc/CMakeLists.txt | 1 + src/rpc/RPCHelpers.hpp | 2 +- src/rpc/common/Validators.cpp | 16 ++ src/rpc/common/Validators.hpp | 7 + src/rpc/common/impl/HandlerProvider.cpp | 2 + src/rpc/handlers/VaultInfo.cpp | 122 +++++++++++++ src/rpc/handlers/VaultInfo.hpp | 111 +++++++++++ tests/common/util/TestObject.cpp | 6 +- tests/unit/CMakeLists.txt | 1 + tests/unit/rpc/RPCHelpersTests.cpp | 2 +- tests/unit/rpc/handlers/LedgerDataTests.cpp | 2 +- tests/unit/rpc/handlers/LedgerEntryTests.cpp | 2 +- tests/unit/rpc/handlers/VaultInfoTests.cpp | 183 +++++++++++++++++++ 13 files changed, 450 insertions(+), 7 deletions(-) create mode 100644 src/rpc/handlers/VaultInfo.cpp create mode 100644 src/rpc/handlers/VaultInfo.hpp create mode 100644 tests/unit/rpc/handlers/VaultInfoTests.cpp diff --git a/src/rpc/CMakeLists.txt b/src/rpc/CMakeLists.txt index 557e843d6..b6d7c5b4b 100644 --- a/src/rpc/CMakeLists.txt +++ b/src/rpc/CMakeLists.txt @@ -46,6 +46,7 @@ target_sources( handlers/Subscribe.cpp handlers/TransactionEntry.cpp handlers/Unsubscribe.cpp + handlers/VaultInfo.cpp ) target_link_libraries(clio_rpc PRIVATE clio_util) diff --git a/src/rpc/RPCHelpers.hpp b/src/rpc/RPCHelpers.hpp index 89c1a8702..2d4d7ff4c 100644 --- a/src/rpc/RPCHelpers.hpp +++ b/src/rpc/RPCHelpers.hpp @@ -880,7 +880,7 @@ supplementJson( if (sle.empty()) return; - entry[JS(ShareTotal)] = sle.getFieldU64(ripple::sfOutstandingAmount); + entry[JS(SharesTotal)] = sle.getFieldU64(ripple::sfOutstandingAmount); }; } // namespace rpc diff --git a/src/rpc/common/Validators.cpp b/src/rpc/common/Validators.cpp index 58fd005be..0f30c0d86 100644 --- a/src/rpc/common/Validators.cpp +++ b/src/rpc/common/Validators.cpp @@ -330,4 +330,20 @@ CustomValidator CustomValidators::authorizeCredentialValidator = return MaybeError{}; }}; +CustomValidator CustomValidators::vaultObjectValidator = + CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { + if (!value.is_object()) + return Error{Status{ClioError::RpcMalformedRequest, std::string(key) + " NotString"}}; + + auto const& vaultObj = value.as_object(); + if (!vaultObj.contains("owner") || !vaultObj.contains("seq") || vaultObj.at("owner").is_string() || + vaultObj.at("seq").is_uint64()) + return Error{Status{ClioError::RpcMalformedRequest}}; + + if (ripple::parseBase58(std::string{vaultObj.at("owner").as_string()})) + return Error{ClioError::RpcMalformedOwner}; + + return MaybeError{}; + }}; + } // namespace rpc::validation diff --git a/src/rpc/common/Validators.hpp b/src/rpc/common/Validators.hpp index ad23fdbab..311f12e45 100644 --- a/src/rpc/common/Validators.hpp +++ b/src/rpc/common/Validators.hpp @@ -571,6 +571,13 @@ struct CustomValidators final { * Used by AuthorizeCredentialValidator in deposit_preauth. */ static CustomValidator credentialTypeValidator; + + /** + * @brief Provides a validator for validating vault object. + * + * Used by vaultInfo handler. + */ + static CustomValidator vaultObjectValidator; }; /** diff --git a/src/rpc/common/impl/HandlerProvider.cpp b/src/rpc/common/impl/HandlerProvider.cpp index cf4485cc2..0b2438487 100644 --- a/src/rpc/common/impl/HandlerProvider.cpp +++ b/src/rpc/common/impl/HandlerProvider.cpp @@ -59,6 +59,7 @@ #include "rpc/handlers/TransactionEntry.hpp" #include "rpc/handlers/Tx.hpp" #include "rpc/handlers/Unsubscribe.hpp" +#include "rpc/handlers/VaultInfo.hpp" #include "rpc/handlers/VersionHandler.hpp" #include "util/newconfig/ConfigDefinition.hpp" @@ -112,6 +113,7 @@ ProductionHandlerProvider::ProductionHandlerProvider( {"tx", {.handler = TxHandler{backend, etl}}}, {"subscribe", {.handler = SubscribeHandler{backend, amendmentCenter, subscriptionManager}}}, {"unsubscribe", {.handler = UnsubscribeHandler{subscriptionManager}}}, + {"vault_info", {.handler = VaultInfoHandler{backend}}}, {"version", {.handler = VersionHandler{config}}}, } { diff --git a/src/rpc/handlers/VaultInfo.cpp b/src/rpc/handlers/VaultInfo.cpp new file mode 100644 index 000000000..d23c2a16c --- /dev/null +++ b/src/rpc/handlers/VaultInfo.cpp @@ -0,0 +1,122 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2025, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "rpc/handlers/VaultInfo.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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace rpc { + +VaultInfoHandler::VaultInfoHandler(std::shared_ptr const& backend) : sharedPtrBackend_{backend} +{ +} + +VaultInfoHandler::Result +VaultInfoHandler::process(VaultInfoHandler::Input input, Context const& ctx) const +{ + auto const range = sharedPtrBackend_->fetchLedgerRange(); + ASSERT(range.has_value(), "AccountInfo's ledger range must be available"); + + auto const lgrInfoOrStatus = getLedgerHeaderFromHashOrSeq( + *sharedPtrBackend_, ctx.yield, std::nullopt, input.vaultObj.ledgerIndex, range->maxSequence + ); + + if (auto const status = std::get_if(&lgrInfoOrStatus)) + return Error{*status}; + + auto const lgrInfo = std::get(lgrInfoOrStatus); + + // Extract the vault owner and construct a keylet for the vault object + auto const accountStr = input.vaultObj.owner; + auto const accountID = accountFromStringStrict(accountStr); + auto const accountKeylet = ripple::keylet::account(*accountID); + + // Fetch the account ledger object + auto const accountLedgerObject = sharedPtrBackend_->fetchLedgerObject(accountKeylet.key, lgrInfo.seq, ctx.yield); + + if (!accountLedgerObject) + return Error{Status{RippledError::rpcACT_NOT_FOUND}}; + + // use account to get vault ledger object + ripple::STLedgerEntry const sle{ + ripple::SerialIter{accountLedgerObject->data(), accountLedgerObject->size()}, accountKeylet.key + }; + + auto const vaultKeylet = ripple::keylet::vault(*accountID, input.vaultObj.ledgerIndex); + + // Fetch the vault object + auto const vaultLedgerObject = sharedPtrBackend_->fetchLedgerObject(vaultKeylet.key, lgrInfo.seq, ctx.yield); + + if (!vaultLedgerObject) + return Error{Status{"entryNotFound"}}; + + ripple::STLedgerEntry const vaultSle{ + ripple::SerialIter{vaultLedgerObject->data(), vaultLedgerObject->size()}, vaultKeylet.key + }; + + // Prepare output + return Output(vaultSle, lgrInfo.seq); +} + +void +tag_invoke(boost::json::value_from_tag, boost::json::value& jv, VaultInfoHandler::Output const& output) +{ + jv = boost::json::object{ + {JS(ledger_index), output.ledgerIndex}, {JS(validated), output.validated}, {JS(vault), toJson(output.vault)} + }; +} + +VaultInfoHandler::Input +tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) +{ + auto input = VaultInfoHandler::Input{}; + auto const& jsonObject = jv.as_object(); + + // vault guarentees to exist from spec + auto const& vaultJson = jsonObject.at(JS(vault)).as_object(); + input.vaultObj = VaultInfoHandler::VaultInfoResponse{ + .owner = std::string{vaultJson.at(JS(owner)).as_string()}, + .ledgerIndex = static_cast(vaultJson.at(JS(seq)).as_uint64()) + }; + return input; +} + +} // namespace rpc diff --git a/src/rpc/handlers/VaultInfo.hpp b/src/rpc/handlers/VaultInfo.hpp new file mode 100644 index 000000000..3b8fa8024 --- /dev/null +++ b/src/rpc/handlers/VaultInfo.hpp @@ -0,0 +1,111 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2025, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include "data/BackendInterface.hpp" +#include "rpc/JS.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 { + +class VaultInfoHandler { + std::shared_ptr sharedPtrBackend_; + +public: + VaultInfoHandler(std::shared_ptr const& backend); + + struct VaultInfoResponse { + std::string owner; + uint32_t ledgerIndex; + }; + + /** + * @brief A struct to hold the input data for the command + */ + struct Input { + VaultInfoResponse vaultObj; + }; + + struct Output { + ripple::STLedgerEntry vault; + uint32_t ledgerIndex{}; + bool validated = true; + }; + + using Result = HandlerReturnType; + + /** + * @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{{JS(vault), validation::Required{}, validation::CustomValidators::vaultObjectValidator}}; + + return kRPC_SPEC; + } + + /** + * @brief Process the VaultInfo 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 input, Context const& ctx) const; + +private: + /** + * @brief Convert the Output to a JSON object + * + * @param 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/common/util/TestObject.cpp b/tests/common/util/TestObject.cpp index 78266fb2f..aa590284f 100644 --- a/tests/common/util/TestObject.cpp +++ b/tests/common/util/TestObject.cpp @@ -1635,10 +1635,10 @@ createVault( vault.setFieldIssue(ripple::sfAsset, ripple::STIssue{ripple::sfAsset, getIssue(assetCurrency, assetIssuer)}); vault[ripple::sfMPTokenIssuanceID] = issuanceID; - vault.setFieldNumber(ripple::sfAssetTotal, ripple::STNumber{ripple::sfAssetTotal, 300}); - vault.setFieldNumber(ripple::sfAssetAvailable, ripple::STNumber{ripple::sfAssetAvailable, 300}); + vault.setFieldNumber(ripple::sfAssetsTotal, ripple::STNumber{ripple::sfAssetsTotal, 300}); + vault.setFieldNumber(ripple::sfAssetsAvailable, ripple::STNumber{ripple::sfAssetsAvailable, 300}); vault.setFieldNumber(ripple::sfLossUnrealized, ripple::STNumber{ripple::sfLossUnrealized, 0}); - vault.setFieldNumber(ripple::sfAssetTotal, ripple::STNumber{ripple::sfAssetTotal, 300}); + vault.setFieldNumber(ripple::sfAssetsTotal, ripple::STNumber{ripple::sfAssetsTotal, 300}); vault.setFieldU8(ripple::sfWithdrawalPolicy, 200); vault.setFieldU32(ripple::sfFlags, 0); diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 1f97643fe..fc06f427e 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -122,6 +122,7 @@ target_sources( rpc/handlers/TxTests.cpp rpc/handlers/UnsubscribeTests.cpp rpc/handlers/VersionHandlerTests.cpp + rpc/handlers/VaultInfoTests.cpp rpc/JsonBoolTests.cpp rpc/RPCEngineTests.cpp rpc/RPCHelpersTests.cpp diff --git a/tests/unit/rpc/RPCHelpersTests.cpp b/tests/unit/rpc/RPCHelpersTests.cpp index c724d5909..ab778d664 100644 --- a/tests/unit/rpc/RPCHelpersTests.cpp +++ b/tests/unit/rpc/RPCHelpersTests.cpp @@ -1111,7 +1111,7 @@ TEST_F(RPCHelpersTest, SupplementJson_ValidVaultEntry) runSpawn([&](boost::asio::yield_context yield) { supplementJson(*backend_, sle, entry, 100, yield); - EXPECT_EQ(boost::json::value_to(entry.at(JS(ShareTotal))), 0); + EXPECT_EQ(boost::json::value_to(entry.at(JS(SharesTotal))), 0); }); } diff --git a/tests/unit/rpc/handlers/LedgerDataTests.cpp b/tests/unit/rpc/handlers/LedgerDataTests.cpp index d4c4ce951..78fb3d8e4 100644 --- a/tests/unit/rpc/handlers/LedgerDataTests.cpp +++ b/tests/unit/rpc/handlers/LedgerDataTests.cpp @@ -925,6 +925,6 @@ TEST_F(RPCLedgerDataHandlerTest, JsonFetchVaultLedgerData) EXPECT_EQ(objects.front().at("LedgerEntryType").as_string(), "Vault"); auto const& firstEntry = objects[0].as_object(); - ASSERT_TRUE(firstEntry.contains("ShareTotal")); + ASSERT_TRUE(firstEntry.contains("SharesTotal")); }); } diff --git a/tests/unit/rpc/handlers/LedgerEntryTests.cpp b/tests/unit/rpc/handlers/LedgerEntryTests.cpp index d724dc9eb..53075b3d9 100644 --- a/tests/unit/rpc/handlers/LedgerEntryTests.cpp +++ b/tests/unit/rpc/handlers/LedgerEntryTests.cpp @@ -3218,7 +3218,7 @@ TEST_F(RPCLedgerEntryTest, Vault_BinaryFalse) )); auto const output = handler.process(req, Context{yield}); ASSERT_TRUE(output); - EXPECT_EQ(boost::json::value_to(output.result->at("node").at("ShareTotal")), 0); + EXPECT_EQ(boost::json::value_to(output.result->at("node").at("SharesTotal")), 0); }); } diff --git a/tests/unit/rpc/handlers/VaultInfoTests.cpp b/tests/unit/rpc/handlers/VaultInfoTests.cpp new file mode 100644 index 000000000..d515cb3f9 --- /dev/null +++ b/tests/unit/rpc/handlers/VaultInfoTests.cpp @@ -0,0 +1,183 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2025, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "rpc/Errors.hpp" +#include "rpc/common/AnyHandler.hpp" +#include "rpc/common/Types.hpp" +#include "rpc/handlers/VaultInfo.hpp" +#include "util/HandlerBaseTestFixture.hpp" +#include "util/MockAmendmentCenter.hpp" +#include "util/NameGenerator.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace rpc; +using namespace data; +using namespace testing; +namespace json = boost::json; + +struct RPCVaultInfoHandlerTest : HandlerBaseTest { + RPCVaultInfoHandlerTest() + { + backend_->setRange(10, 30); + } + +protected: + StrictMockAmendmentCenterSharedPtr mockAmendmentCenterPtr_; +}; + +struct VaultInfoParamTestCaseBundle { + std::string testName; + std::string testJson; + std::string expectedError; + std::string expectedErrorMessage; +}; + +struct VaultInfoParameterTest : RPCVaultInfoHandlerTest, WithParamInterface {}; + +static auto +generateTestValuesForParametersTest() +{ + return std::vector{ + VaultInfoParamTestCaseBundle{ + .testName = "MissingVaultField", + .testJson = R"({ + "method": "vault_info", + "params": [{ + "idk" : "idk" + }] + })", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Required field vault missing" + }, + VaultInfoParamTestCaseBundle{ + .testName = "MissingOwnerInVault", + .testJson = R"({ + "method": "vault_info", + "params": [ + { + "vault": { + "seq": 4 + } + } + ] + })", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request." + }, + VaultInfoParamTestCaseBundle{ + .testName = "MissingSeqInVault", + .testJson = R"({ + "method": "vault_info", + "params": [ + { + "vault": { + "owner": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh" + } + } + ] + })", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request." + }, + VaultInfoParamTestCaseBundle{ + .testName = "SeqNotAnInteger", + .testJson = R"({ + "method": "vault_info", + "params": [ + { + "vault": { + "owner": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "seq": "asdf" + } + } + ] + })", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request." + }, + VaultInfoParamTestCaseBundle{ + .testName = "OwnerNotAString", + .testJson = R"({ + "method": "vault_info", + "params": [ + { + "vault": { + "owner": true, + "seq": 3 + + } + } + ] + })", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request." + }, + VaultInfoParamTestCaseBundle{ + .testName = "OwnerNotAHexString", + .testJson = R"({ + "method": "vault_info", + "params": [ + { + "vault": { + "owner": "asdf", + "seq": 3 + + } + } + ] + })", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request." + } + }; +} + +INSTANTIATE_TEST_CASE_P( + RPCAccountInfoGroup1, + VaultInfoParameterTest, + ValuesIn(generateTestValuesForParametersTest()), + tests::util::kNAME_GENERATOR +); + +TEST_P(VaultInfoParameterTest, InvalidParams) +{ + auto const testBundle = VaultInfoParameterTest::GetParam(); + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{VaultInfoHandler{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); + }); +} From c1c2ec1bb02fcdbc73f832e559571dbc367390d7 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Wed, 9 Apr 2025 13:29:38 -0700 Subject: [PATCH 5/9] more test --- src/rpc/common/Validators.cpp | 7 +-- src/rpc/handlers/VaultInfo.cpp | 2 +- tests/unit/rpc/handlers/VaultInfoTests.cpp | 63 +++++++++++++++++++++- 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/rpc/common/Validators.cpp b/src/rpc/common/Validators.cpp index 0f30c0d86..6845c4dc8 100644 --- a/src/rpc/common/Validators.cpp +++ b/src/rpc/common/Validators.cpp @@ -336,11 +336,12 @@ CustomValidator CustomValidators::vaultObjectValidator = return Error{Status{ClioError::RpcMalformedRequest, std::string(key) + " NotString"}}; auto const& vaultObj = value.as_object(); - if (!vaultObj.contains("owner") || !vaultObj.contains("seq") || vaultObj.at("owner").is_string() || - vaultObj.at("seq").is_uint64()) + + if (!vaultObj.contains("owner") || !vaultObj.contains("seq") || !vaultObj.at("owner").is_string() || + !vaultObj.at("seq").is_int64()) return Error{Status{ClioError::RpcMalformedRequest}}; - if (ripple::parseBase58(std::string{vaultObj.at("owner").as_string()})) + if (!ripple::parseBase58(std::string{vaultObj.at("owner").as_string()})) return Error{ClioError::RpcMalformedOwner}; return MaybeError{}; diff --git a/src/rpc/handlers/VaultInfo.cpp b/src/rpc/handlers/VaultInfo.cpp index d23c2a16c..32a4a157b 100644 --- a/src/rpc/handlers/VaultInfo.cpp +++ b/src/rpc/handlers/VaultInfo.cpp @@ -114,7 +114,7 @@ tag_invoke(boost::json::value_to_tag, boost::json::valu auto const& vaultJson = jsonObject.at(JS(vault)).as_object(); input.vaultObj = VaultInfoHandler::VaultInfoResponse{ .owner = std::string{vaultJson.at(JS(owner)).as_string()}, - .ledgerIndex = static_cast(vaultJson.at(JS(seq)).as_uint64()) + .ledgerIndex = static_cast(vaultJson.at(JS(seq)).as_int64()) }; return input; } diff --git a/tests/unit/rpc/handlers/VaultInfoTests.cpp b/tests/unit/rpc/handlers/VaultInfoTests.cpp index d515cb3f9..73caf5ac4 100644 --- a/tests/unit/rpc/handlers/VaultInfoTests.cpp +++ b/tests/unit/rpc/handlers/VaultInfoTests.cpp @@ -24,6 +24,7 @@ #include "util/HandlerBaseTestFixture.hpp" #include "util/MockAmendmentCenter.hpp" #include "util/NameGenerator.hpp" +#include "util/TestObject.hpp" #include #include @@ -35,6 +36,7 @@ #include #include +#include #include #include @@ -43,10 +45,21 @@ using namespace data; using namespace testing; namespace json = boost::json; +namespace { + +constexpr auto kACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr auto kLEDGER_HASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; +constexpr auto kINDEX1 = "ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890"; +constexpr auto kSEQ = 30; +constexpr auto kASSET_CURRENCY = "XRP"; +constexpr auto kASSET_ISSUER = "rrrrrrrrrrrrrrrrrrrrrhoLvTp"; + +} // namespace + struct RPCVaultInfoHandlerTest : HandlerBaseTest { RPCVaultInfoHandlerTest() { - backend_->setRange(10, 30); + backend_->setRange(10, kSEQ); } protected: @@ -181,3 +194,51 @@ TEST_P(VaultInfoParameterTest, InvalidParams) EXPECT_EQ(err.at("error_message").as_string(), testBundle.expectedErrorMessage); }); } + +TEST_F(RPCVaultInfoHandlerTest, ValidVaultObjectQuery) +{ + auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kSEQ); + EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); + ON_CALL(*backend_, fetchLedgerBySequence).WillByDefault(Return(ledgerHeader)); + + // Vault params + ripple::uint192 issuanceID{1}; + ripple::uint256 prevTxId{2}; + uint32_t prevTxSeq = 3; + uint64_t ownerNode = 4; + + // Mock vault object + auto const vault = createVault( + kACCOUNT, kINDEX1, kSEQ, kASSET_CURRENCY, kASSET_ISSUER, issuanceID, ownerNode, prevTxId, prevTxSeq + ); + + auto const accountRoot = createAccountRootObject(kACCOUNT, 0, 5, 200, 2, kINDEX1, 2); + auto const account = getAccountIdWithString(kACCOUNT); + auto const accountKeylet = ripple::keylet::account(account).key; + auto const vaultKeylet = ripple::keylet::vault(account, kSEQ).key; + + ON_CALL(*backend_, doFetchLedgerObject(accountKeylet, kSEQ, _)) + .WillByDefault(Return(accountRoot.getSerializer().peekData())); + std::cout << accountKeylet << std::endl; + // Return serialized vault object + EXPECT_CALL(*backend_, doFetchLedgerObject(vaultKeylet, _, _)).WillOnce(Return(vault.getSerializer().peekData())); + + // Input JSON using vault object + auto static const kINPUT = boost::json::parse(fmt::format( + R"({{ + "vault": {{ + "owner": "{}", + "seq": {} + }} + }})", + kACCOUNT, + kSEQ + )); + + // Run the handler + auto const handler = AnyHandler{VaultInfoHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + ASSERT_TRUE(output); + }); +} From 6a4573ebf39544d52dd558db7b9afcca373d23b2 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Thu, 10 Apr 2025 17:20:01 -0700 Subject: [PATCH 6/9] more tests --- src/rpc/handlers/VaultInfo.cpp | 3 +- src/rpc/handlers/VaultInfo.hpp | 16 ++++++- tests/common/util/TestObject.cpp | 8 ++-- tests/common/util/TestObject.hpp | 3 +- tests/unit/rpc/RPCHelpersTests.cpp | 2 + tests/unit/rpc/handlers/LedgerDataTests.cpp | 1 + tests/unit/rpc/handlers/LedgerEntryTests.cpp | 3 ++ tests/unit/rpc/handlers/VaultInfoTests.cpp | 50 ++++++++++++++++---- 8 files changed, 70 insertions(+), 16 deletions(-) diff --git a/src/rpc/handlers/VaultInfo.cpp b/src/rpc/handlers/VaultInfo.cpp index 32a4a157b..ae5463caa 100644 --- a/src/rpc/handlers/VaultInfo.cpp +++ b/src/rpc/handlers/VaultInfo.cpp @@ -45,7 +45,8 @@ namespace rpc { -VaultInfoHandler::VaultInfoHandler(std::shared_ptr const& backend) : sharedPtrBackend_{backend} +VaultInfoHandler::VaultInfoHandler(std::shared_ptr const& sharedPtrBackend) + : sharedPtrBackend_{sharedPtrBackend} { } diff --git a/src/rpc/handlers/VaultInfo.hpp b/src/rpc/handlers/VaultInfo.hpp index 3b8fa8024..8c7387247 100644 --- a/src/rpc/handlers/VaultInfo.hpp +++ b/src/rpc/handlers/VaultInfo.hpp @@ -37,12 +37,23 @@ namespace rpc { +/** + * @brief The vault_info command retrieves information about a vault, currency, shares etc. + */ class VaultInfoHandler { std::shared_ptr sharedPtrBackend_; public: - VaultInfoHandler(std::shared_ptr const& backend); + /** + * @brief Construct a new VaultInfo object + * + * @param sharedPtrBackend The backend to use + */ + VaultInfoHandler(std::shared_ptr const& sharedPtrBackend); + /** + * @brief A struct to hold the data of vault object + */ struct VaultInfoResponse { std::string owner; uint32_t ledgerIndex; @@ -55,6 +66,9 @@ class VaultInfoHandler { VaultInfoResponse vaultObj; }; + /** + * @brief A struct to hold the output data for the command + */ struct Output { ripple::STLedgerEntry vault; uint32_t ledgerIndex{}; diff --git a/tests/common/util/TestObject.cpp b/tests/common/util/TestObject.cpp index aa590284f..aef21e5c7 100644 --- a/tests/common/util/TestObject.cpp +++ b/tests/common/util/TestObject.cpp @@ -1613,7 +1613,8 @@ createAuthCredentialArray(std::vector issuer, std::vector issuer, std::vector(kACCOUNT).value(), kRANGE_MAX).key, .mockedEntity = createVault( + kACCOUNT, kACCOUNT, kINDEX1, kRANGE_MAX, @@ -3175,6 +3177,7 @@ TEST_F(RPCLedgerEntryTest, Vault_BinaryFalse) boost::json::object entry; auto const vault = createVault( + kACCOUNT, kACCOUNT, kINDEX1, kRANGE_MAX, diff --git a/tests/unit/rpc/handlers/VaultInfoTests.cpp b/tests/unit/rpc/handlers/VaultInfoTests.cpp index 73caf5ac4..5a5a31eaf 100644 --- a/tests/unit/rpc/handlers/VaultInfoTests.cpp +++ b/tests/unit/rpc/handlers/VaultInfoTests.cpp @@ -48,7 +48,7 @@ namespace json = boost::json; namespace { constexpr auto kACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; -constexpr auto kLEDGER_HASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; +constexpr auto kACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; constexpr auto kINDEX1 = "ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890"; constexpr auto kSEQ = 30; constexpr auto kASSET_CURRENCY = "XRP"; @@ -197,9 +197,42 @@ TEST_P(VaultInfoParameterTest, InvalidParams) TEST_F(RPCVaultInfoHandlerTest, ValidVaultObjectQuery) { - auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kSEQ); - EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); - ON_CALL(*backend_, fetchLedgerBySequence).WillByDefault(Return(ledgerHeader)); + auto const expectedOutput = fmt::format( + R"({{ + "ledger_index": 30, + "validated": true, + "vault": {{ + "Account": "{}", + "Asset": {{ + "currency": "{}" + }}, + "AssetsAvailable": "300", + "AssetsTotal": "300", + "Flags": 0, + "LedgerEntryType": "Vault", + "LedgerIndex": "{}", + "LossUnrealized": "0", + "MPTokenIssuanceID": "000000000000000000000000000000000000000000000001", + "Owner": "{}", + "OwnerNode": "4", + "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000002", + "PreviousTxnLgrSeq": 3, + "Sequence": 30, + "Share": {{ + "mpt_issuance_id": "000000000000000000000000000000000000000000000001" + }}, + "WithdrawalPolicy": 200, + "index": "1B7BB49E0663E073D1C3EF989271F89E290AAF2D67CEE85F18E2CC76D168F694" + }} + }})", + kACCOUNT2, + kASSET_CURRENCY, + kINDEX1, + kACCOUNT + ); + + auto const ledgerHeader = createLedgerHeader(kINDEX1, kSEQ); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); // Vault params ripple::uint192 issuanceID{1}; @@ -209,19 +242,17 @@ TEST_F(RPCVaultInfoHandlerTest, ValidVaultObjectQuery) // Mock vault object auto const vault = createVault( - kACCOUNT, kINDEX1, kSEQ, kASSET_CURRENCY, kASSET_ISSUER, issuanceID, ownerNode, prevTxId, prevTxSeq + kACCOUNT, kACCOUNT2, kINDEX1, kSEQ, kASSET_CURRENCY, kASSET_ISSUER, issuanceID, ownerNode, prevTxId, prevTxSeq ); - auto const accountRoot = createAccountRootObject(kACCOUNT, 0, 5, 200, 2, kINDEX1, 2); + auto const accountRoot = createAccountRootObject(kACCOUNT, 0, kSEQ, 200, 2, kINDEX1, 2); auto const account = getAccountIdWithString(kACCOUNT); auto const accountKeylet = ripple::keylet::account(account).key; auto const vaultKeylet = ripple::keylet::vault(account, kSEQ).key; ON_CALL(*backend_, doFetchLedgerObject(accountKeylet, kSEQ, _)) .WillByDefault(Return(accountRoot.getSerializer().peekData())); - std::cout << accountKeylet << std::endl; - // Return serialized vault object - EXPECT_CALL(*backend_, doFetchLedgerObject(vaultKeylet, _, _)).WillOnce(Return(vault.getSerializer().peekData())); + ON_CALL(*backend_, doFetchLedgerObject(vaultKeylet, _, _)).WillByDefault(Return(vault.getSerializer().peekData())); // Input JSON using vault object auto static const kINPUT = boost::json::parse(fmt::format( @@ -240,5 +271,6 @@ TEST_F(RPCVaultInfoHandlerTest, ValidVaultObjectQuery) runSpawn([&](auto yield) { auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); ASSERT_TRUE(output); + EXPECT_EQ(*output.result, json::parse(expectedOutput)); }); } From 28050013b435da534f8e308bbb367747c49ec1e3 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Fri, 25 Apr 2025 16:37:28 -0400 Subject: [PATCH 7/9] save work for now --- src/rpc/RPCHelpers.hpp | 35 ---------- src/rpc/handlers/LedgerData.cpp | 6 +- src/rpc/handlers/LedgerEntry.cpp | 2 - src/rpc/handlers/VaultInfo.cpp | 58 ++++++++++++---- src/rpc/handlers/VaultInfo.hpp | 20 +++--- tests/common/util/TestObject.cpp | 4 +- tests/common/util/TestObject.hpp | 2 +- tests/unit/rpc/RPCHelpersTests.cpp | 70 -------------------- tests/unit/rpc/handlers/LedgerDataTests.cpp | 59 ----------------- tests/unit/rpc/handlers/LedgerEntryTests.cpp | 17 ++--- tests/unit/rpc/handlers/VaultInfoTests.cpp | 57 +++++++--------- 11 files changed, 87 insertions(+), 243 deletions(-) diff --git a/src/rpc/RPCHelpers.hpp b/src/rpc/RPCHelpers.hpp index 45ca45507..518d02d9b 100644 --- a/src/rpc/RPCHelpers.hpp +++ b/src/rpc/RPCHelpers.hpp @@ -850,39 +850,4 @@ getDeliveredAmount( uint32_t date ); -/** - * @brief Supplements a JSON representation of a ltVAULT ledger entry by looking up its associated MPT issuance entry - * and adding the SharesTotal field - * - * @param backend The backend to use - * @param vault The vault ledger entry - * @param entry The entry object - * @param ledgerSequence The sequence - * @param yield The coroutine context - */ -template -inline void -supplementJson( - BackendInterface const& backend, - ripple::STLedgerEntry const& vault, - boost::json::object& entry, - std::uint32_t ledgerSequence, - boost::asio::yield_context yield -) -{ - auto const share = vault.at(ripple::sfMPTokenIssuanceID); - auto const sleIssuance = backend.fetchLedgerObject(ripple::keylet::mptIssuance(share).key, ledgerSequence, yield); - if (!sleIssuance) - return; - - // blob to sle - ripple::STLedgerEntry const sle{ - ripple::SerialIter{sleIssuance->data(), sleIssuance->size()}, ripple::keylet::mptIssuance(share).key - }; - if (sle.empty()) - return; - - entry[JS(SharesTotal)] = sle.getFieldU64(ripple::sfOutstandingAmount); -}; - } // namespace rpc diff --git a/src/rpc/handlers/LedgerData.cpp b/src/rpc/handlers/LedgerData.cpp index e7f7ccd05..0b6d396e4 100644 --- a/src/rpc/handlers/LedgerData.cpp +++ b/src/rpc/handlers/LedgerData.cpp @@ -141,11 +141,7 @@ LedgerDataHandler::process(Input input, Context const& ctx) const entry[JS(index)] = ripple::to_string(sle.key()); output.states.push_back(std::move(entry)); } else { - auto entry = toJson(sle); - if (sle.getType() == ripple::ltVAULT) - supplementJson(*sharedPtrBackend_, sle, entry, lgrInfo.seq, ctx.yield); - - output.states.push_back(entry); + output.states.push_back(toJson(sle)); } } } diff --git a/src/rpc/handlers/LedgerEntry.cpp b/src/rpc/handlers/LedgerEntry.cpp index 56780760d..bde7cf607 100644 --- a/src/rpc/handlers/LedgerEntry.cpp +++ b/src/rpc/handlers/LedgerEntry.cpp @@ -236,8 +236,6 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx) output.nodeBinary = ripple::strHex(*ledgerObject); } else { output.node = toJson(sle); - if (input.vault) - supplementJson(*sharedPtrBackend_, sle, output.node.value(), lgrInfo.seq, ctx.yield); } return output; diff --git a/src/rpc/handlers/VaultInfo.cpp b/src/rpc/handlers/VaultInfo.cpp index ae5463caa..be48f6337 100644 --- a/src/rpc/handlers/VaultInfo.cpp +++ b/src/rpc/handlers/VaultInfo.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -50,14 +51,32 @@ VaultInfoHandler::VaultInfoHandler(std::shared_ptr const& shar { } +static std::expected +parseVaultField(VaultInfoHandler::Input const& input) +{ + auto const hasVaultId = input.vaultID.has_value(); + auto const hasOwner = input.owner.has_value(); + auto const hasSeq = input.ledgerIndex.has_value(); + + // valid vault_info has input of either vault_id or owner & sequence + if ((hasVaultId && !hasOwner && !hasSeq) || (!hasVaultId && hasOwner && hasSeq)) + return {}; + + return std::unexpected{ClioError::RpcMalformedRequest}; +} + VaultInfoHandler::Result VaultInfoHandler::process(VaultInfoHandler::Input input, Context const& ctx) const { + // vault info input must either have owner and sequence, or vault_id only. + if (auto const res = parseVaultField(input); !res.has_value()) + return Error{res.error()}; + auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "AccountInfo's ledger range must be available"); auto const lgrInfoOrStatus = getLedgerHeaderFromHashOrSeq( - *sharedPtrBackend_, ctx.yield, std::nullopt, input.vaultObj.ledgerIndex, range->maxSequence + *sharedPtrBackend_, ctx.yield, std::nullopt, input.ledgerIndex, range->maxSequence ); if (auto const status = std::get_if(&lgrInfoOrStatus)) @@ -66,7 +85,7 @@ VaultInfoHandler::process(VaultInfoHandler::Input input, Context const& ctx) con auto const lgrInfo = std::get(lgrInfoOrStatus); // Extract the vault owner and construct a keylet for the vault object - auto const accountStr = input.vaultObj.owner; + auto const accountStr = *input.owner; auto const accountID = accountFromStringStrict(accountStr); auto const accountKeylet = ripple::keylet::account(*accountID); @@ -77,22 +96,32 @@ VaultInfoHandler::process(VaultInfoHandler::Input input, Context const& ctx) con return Error{Status{RippledError::rpcACT_NOT_FOUND}}; // use account to get vault ledger object - ripple::STLedgerEntry const sle{ + ripple::STLedgerEntry const sleObject{ ripple::SerialIter{accountLedgerObject->data(), accountLedgerObject->size()}, accountKeylet.key }; - auto const vaultKeylet = ripple::keylet::vault(*accountID, input.vaultObj.ledgerIndex); + auto const vaultKeylet = ripple::keylet::vault(*accountID, *input.ledgerIndex); // Fetch the vault object auto const vaultLedgerObject = sharedPtrBackend_->fetchLedgerObject(vaultKeylet.key, lgrInfo.seq, ctx.yield); - if (!vaultLedgerObject) - return Error{Status{"entryNotFound"}}; - ripple::STLedgerEntry const vaultSle{ ripple::SerialIter{vaultLedgerObject->data(), vaultLedgerObject->size()}, vaultKeylet.key }; + std::cout << ripple::keylet::mptIssuance(vaultSle[ripple::sfShareMPTID]).key; + + auto const issuanceObject = !vaultLedgerObject.has_value() + ? std::nullopt + : sharedPtrBackend_->fetchLedgerObject( + ripple::keylet::mptIssuance(vaultSle[ripple::sfShareMPTID]).key, lgrInfo.seq, ctx.yield + ); + + if (!vaultLedgerObject || !issuanceObject) + return Error{Status{"entryNotFound"}}; + + // todo: vaultSLE + // Prepare output return Output(vaultSle, lgrInfo.seq); } @@ -111,12 +140,15 @@ tag_invoke(boost::json::value_to_tag, boost::json::valu auto input = VaultInfoHandler::Input{}; auto const& jsonObject = jv.as_object(); - // vault guarentees to exist from spec - auto const& vaultJson = jsonObject.at(JS(vault)).as_object(); - input.vaultObj = VaultInfoHandler::VaultInfoResponse{ - .owner = std::string{vaultJson.at(JS(owner)).as_string()}, - .ledgerIndex = static_cast(vaultJson.at(JS(seq)).as_int64()) - }; + if (jsonObject.contains(JS(owner))) + input.owner = jsonObject.at(JS(owner)).as_string(); + + if (jsonObject.contains(JS(seq))) + input.ledgerIndex = static_cast(jsonObject.at(JS(seq)).as_int64()); + + if (jsonObject.contains(JS(vault_id))) + input.vaultID = jsonObject.at(JS(vault_id)).as_string(); + return input; } diff --git a/src/rpc/handlers/VaultInfo.hpp b/src/rpc/handlers/VaultInfo.hpp index 8c7387247..738679cb7 100644 --- a/src/rpc/handlers/VaultInfo.hpp +++ b/src/rpc/handlers/VaultInfo.hpp @@ -33,6 +33,7 @@ #include #include +#include #include namespace rpc { @@ -51,19 +52,13 @@ class VaultInfoHandler { */ VaultInfoHandler(std::shared_ptr const& sharedPtrBackend); - /** - * @brief A struct to hold the data of vault object - */ - struct VaultInfoResponse { - std::string owner; - uint32_t ledgerIndex; - }; - /** * @brief A struct to hold the input data for the command */ struct Input { - VaultInfoResponse vaultObj; + std::optional vaultID; + std::optional owner; + std::optional ledgerIndex; }; /** @@ -86,8 +81,11 @@ class VaultInfoHandler { static RpcSpecConstRef spec([[maybe_unused]] uint32_t apiVersion) { - static auto const kRPC_SPEC = - RpcSpec{{JS(vault), validation::Required{}, validation::CustomValidators::vaultObjectValidator}}; + static auto const kRPC_SPEC = RpcSpec{ + {JS(vault_id), validation::CustomValidators::uint256HexStringValidator}, + {JS(owner), validation::CustomValidators::accountBase58Validator}, + {JS(seq), validation::Type{}} + }; return kRPC_SPEC; } diff --git a/tests/common/util/TestObject.cpp b/tests/common/util/TestObject.cpp index aef21e5c7..b2199aceb 100644 --- a/tests/common/util/TestObject.cpp +++ b/tests/common/util/TestObject.cpp @@ -1619,7 +1619,7 @@ createVault( ripple::LedgerIndex seq, std::string_view assetCurrency, std::string_view assetIssuer, - ripple::uint192 issuanceID, + ripple::uint192 shareMPTID, uint64_t ownerNode, ripple::uint256 previousTxId, uint32_t previousTxSeq @@ -1635,7 +1635,7 @@ createVault( vault.setFieldU32(ripple::sfPreviousTxnLgrSeq, previousTxSeq); vault.setFieldIssue(ripple::sfAsset, ripple::STIssue{ripple::sfAsset, getIssue(assetCurrency, assetIssuer)}); - vault[ripple::sfMPTokenIssuanceID] = issuanceID; + vault[ripple::sfShareMPTID] = shareMPTID; vault.setFieldNumber(ripple::sfAssetsTotal, ripple::STNumber{ripple::sfAssetsTotal, 300}); vault.setFieldNumber(ripple::sfAssetsAvailable, ripple::STNumber{ripple::sfAssetsAvailable, 300}); vault.setFieldNumber(ripple::sfLossUnrealized, ripple::STNumber{ripple::sfLossUnrealized, 0}); diff --git a/tests/common/util/TestObject.hpp b/tests/common/util/TestObject.hpp index b72e472b2..c1c2864b2 100644 --- a/tests/common/util/TestObject.hpp +++ b/tests/common/util/TestObject.hpp @@ -521,7 +521,7 @@ createVault( ripple::LedgerIndex seq, std::string_view assetCurrency, std::string_view assetIssuer, - ripple::uint192 issuanceID, + ripple::uint192 shareMPTID, uint64_t ownerNode, ripple::uint256 previousTxId, uint32_t previousTxSeq diff --git a/tests/unit/rpc/RPCHelpersTests.cpp b/tests/unit/rpc/RPCHelpersTests.cpp index 16d977710..58e105f3b 100644 --- a/tests/unit/rpc/RPCHelpersTests.cpp +++ b/tests/unit/rpc/RPCHelpersTests.cpp @@ -1086,76 +1086,6 @@ TEST_F(RPCHelpersTest, AccountHoldsLPTokenUnfrozen) ctx_.run(); } -TEST_F(RPCHelpersTest, SupplementJson_ValidVaultEntry) -{ - boost::json::object entry; - - auto const vault = createVault( - kACCOUNT, - kACCOUNT, - kINDEX1, - 30, - "XRP", - ripple::toBase58(ripple::xrpAccount()), - ripple::makeMptID(30, getAccountIdWithString(kACCOUNT)), - 0, - ripple::uint256{1}, - 0 - ); - - auto const vaultKey = ripple::keylet::vault(ripple::parseBase58(kACCOUNT).value(), 30).key; - - auto const issuance = createMptIssuanceObject(kACCOUNT, 30, "metadata"); - ripple::uint256 issuanceKey = - ripple::keylet::mptIssuance(ripple::makeMptID(30, getAccountIdWithString(kACCOUNT))).key; - - ripple::STLedgerEntry const sle{ - ripple::SerialIter{vault.getSerializer().peekData().data(), vault.getSerializer().peekData().size()}, vaultKey - }; - - EXPECT_CALL(*backend_, doFetchLedgerObject(issuanceKey, testing::_, testing::_)) - .WillOnce(Return(issuance.getSerializer().peekData())); - - runSpawn([&](boost::asio::yield_context yield) { - supplementJson(*backend_, sle, entry, 100, yield); - EXPECT_EQ(boost::json::value_to(entry.at(JS(SharesTotal))), 0); - }); -} - -TEST_F(RPCHelpersTest, SupplementJson_MissingIssuanceEntry) -{ - boost::json::object entry; - - auto const vault = createVault( - kACCOUNT, - kACCOUNT, - kINDEX1, - 30, - "XRP", - ripple::toBase58(ripple::xrpAccount()), - ripple::makeMptID(30, getAccountIdWithString(kACCOUNT)), - 0, - ripple::uint256{1}, - 0 - ); - - auto const vaultKey = ripple::keylet::vault(ripple::parseBase58(kACCOUNT).value(), 30).key; - - ripple::uint256 issuanceKey = - ripple::keylet::mptIssuance(ripple::makeMptID(30, getAccountIdWithString(kACCOUNT))).key; - - ripple::STLedgerEntry const sle{ - ripple::SerialIter{vault.getSerializer().peekData().data(), vault.getSerializer().peekData().size()}, vaultKey - }; - - EXPECT_CALL(*backend_, doFetchLedgerObject(issuanceKey, testing::_, testing::_)).WillOnce(Return(std::nullopt)); - - runSpawn([&](boost::asio::yield_context yield) { - supplementJson(*backend_, sle, entry, 100, yield); - EXPECT_TRUE(entry.empty()); // Expect no changes to entry - }); -} - struct IsAdminCmdParamTestCaseBundle { std::string testName; std::string method; diff --git a/tests/unit/rpc/handlers/LedgerDataTests.cpp b/tests/unit/rpc/handlers/LedgerDataTests.cpp index 3574dd868..6fe3c9b5e 100644 --- a/tests/unit/rpc/handlers/LedgerDataTests.cpp +++ b/tests/unit/rpc/handlers/LedgerDataTests.cpp @@ -870,62 +870,3 @@ TEST(RPCLedgerDataHandlerSpecTest, DeprecatedFields) EXPECT_EQ(warning.at("id").as_int64(), static_cast(rpc::WarningCode::WarnRpcDeprecated)); EXPECT_NE(warning.at("message").as_string().find("Field 'ledger' is deprecated."), std::string::npos) << warning; } - -TEST_F(RPCLedgerDataHandlerTest, JsonFetchVaultLedgerData) -{ - EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); - ON_CALL(*backend_, fetchLedgerBySequence(kRANGE_MAX, _)) - .WillByDefault(Return(createLedgerHeader(kLEDGER_HASH, kRANGE_MAX))); - - std::vector bbs; - EXPECT_CALL(*backend_, doFetchSuccessorKey).Times(1); - ON_CALL(*backend_, doFetchSuccessorKey(_, kRANGE_MAX, _)).WillByDefault(Return(ripple::uint256{kINDEX2})); - - auto const vault = createVault( - kACCOUNT, - kACCOUNT, - kINDEX1, - 30, - "XRP", - ripple::toBase58(ripple::xrpAccount()), - ripple::makeMptID(30, getAccountIdWithString(kACCOUNT)), - 0, - ripple::uint256{1}, - 0 - ); - - bbs.push_back(vault.getSerializer().peekData()); - - ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(bbs)); - EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1); - - auto const issuance = createMptIssuanceObject(kACCOUNT, 30, "metadata"); - ripple::uint256 issuanceKey = - ripple::keylet::mptIssuance(ripple::makeMptID(30, getAccountIdWithString(kACCOUNT))).key; - - EXPECT_CALL(*backend_, doFetchLedgerObject(issuanceKey, testing::_, testing::_)) - .WillOnce(Return(issuance.getSerializer().peekData())); - - runSpawn([&, this](auto yield) { - auto const handler = AnyHandler{LedgerDataHandler{backend_}}; - auto const req = json::parse(R"({ - "limit":1, - "type":"vault" - })"); - - auto output = handler.process(req, Context{yield}); - ASSERT_TRUE(output); - EXPECT_TRUE(output.result->as_object().contains("ledger")); - EXPECT_EQ(output.result->as_object().at("state").as_array().size(), 1); - EXPECT_EQ(output.result->as_object().at("marker").as_string(), kINDEX2); - EXPECT_EQ(output.result->as_object().at("ledger_hash").as_string(), kLEDGER_HASH); - EXPECT_EQ(output.result->as_object().at("ledger_index").as_uint64(), kRANGE_MAX); - - auto const& objects = output.result->as_object().at("state").as_array(); - EXPECT_EQ(objects.front().at("LedgerEntryType").as_string(), "Vault"); - EXPECT_EQ(objects.front().at("LedgerEntryType").as_string(), "Vault"); - - auto const& firstEntry = objects[0].as_object(); - ASSERT_TRUE(firstEntry.contains("SharesTotal")); - }); -} diff --git a/tests/unit/rpc/handlers/LedgerEntryTests.cpp b/tests/unit/rpc/handlers/LedgerEntryTests.cpp index ba1f04239..07c2c58ea 100644 --- a/tests/unit/rpc/handlers/LedgerEntryTests.cpp +++ b/tests/unit/rpc/handlers/LedgerEntryTests.cpp @@ -3047,7 +3047,7 @@ generateTestValuesForNormalPathTest() kRANGE_MAX, "XRP", ripple::toBase58(ripple::xrpAccount()), - ripple::makeMptID(2, getAccountIdWithString(kACCOUNT)), + ripple::uint192(0), 0, ripple::uint256{0}, 0 @@ -3075,7 +3075,7 @@ generateTestValuesForNormalPathTest() kRANGE_MAX, "XRP", ripple::toBase58(ripple::xrpAccount()), - ripple::makeMptID(2, getAccountIdWithString(kACCOUNT)), + ripple::uint192(0), 0, ripple::uint256{0}, 0 @@ -3183,7 +3183,7 @@ TEST_F(RPCLedgerEntryTest, Vault_BinaryFalse) kRANGE_MAX, "XRP", ripple::toBase58(ripple::xrpAccount()), - ripple::makeMptID(30, getAccountIdWithString(kACCOUNT)), + ripple::uint192(0), 0, ripple::uint256{1}, 0 @@ -3192,10 +3192,6 @@ TEST_F(RPCLedgerEntryTest, Vault_BinaryFalse) auto const vaultKey = ripple::keylet::vault(ripple::parseBase58(kACCOUNT).value(), kRANGE_MAX).key; - auto const issuance = createMptIssuanceObject(kACCOUNT, kRANGE_MAX, "metadata"); - ripple::uint256 issuanceKey = - ripple::keylet::mptIssuance(ripple::makeMptID(kRANGE_MAX, getAccountIdWithString(kACCOUNT))).key; - ripple::STLedgerEntry const sle{ ripple::SerialIter{vault.getSerializer().peekData().data(), vault.getSerializer().peekData().size()}, vaultKey }; @@ -3203,9 +3199,6 @@ TEST_F(RPCLedgerEntryTest, Vault_BinaryFalse) EXPECT_CALL(*backend_, doFetchLedgerObject(vaultKey, testing::_, testing::_)) .WillOnce(Return(vault.getSerializer().peekData())); - EXPECT_CALL(*backend_, doFetchLedgerObject(issuanceKey, testing::_, testing::_)) - .WillOnce(Return(issuance.getSerializer().peekData())); - runSpawn([&, this](auto yield) { auto const handler = AnyHandler{LedgerEntryHandler{backend_}}; auto const req = json::parse(fmt::format( @@ -3221,7 +3214,9 @@ TEST_F(RPCLedgerEntryTest, Vault_BinaryFalse) )); auto const output = handler.process(req, Context{yield}); ASSERT_TRUE(output); - EXPECT_EQ(boost::json::value_to(output.result->at("node").at("SharesTotal")), 0); + + EXPECT_EQ(output.result->at("node").at("Owner").as_string(), kACCOUNT); + EXPECT_EQ(output.result->at("node").at("Sequence").as_int64(), kRANGE_MAX); }); } diff --git a/tests/unit/rpc/handlers/VaultInfoTests.cpp b/tests/unit/rpc/handlers/VaultInfoTests.cpp index 5a5a31eaf..51d9a908c 100644 --- a/tests/unit/rpc/handlers/VaultInfoTests.cpp +++ b/tests/unit/rpc/handlers/VaultInfoTests.cpp @@ -88,7 +88,7 @@ generateTestValuesForParametersTest() }] })", .expectedError = "malformedRequest", - .expectedErrorMessage = "Required field vault missing" + .expectedErrorMessage = "Malformed request." }, VaultInfoParamTestCaseBundle{ .testName = "MissingOwnerInVault", @@ -96,9 +96,7 @@ generateTestValuesForParametersTest() "method": "vault_info", "params": [ { - "vault": { - "seq": 4 - } + "seq": 4 } ] })", @@ -111,9 +109,7 @@ generateTestValuesForParametersTest() "method": "vault_info", "params": [ { - "vault": { - "owner": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh" - } + "owner": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh" } ] })", @@ -126,10 +122,8 @@ generateTestValuesForParametersTest() "method": "vault_info", "params": [ { - "vault": { - "owner": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", - "seq": "asdf" - } + "owner": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "seq": "asdf" } ] })", @@ -142,11 +136,8 @@ generateTestValuesForParametersTest() "method": "vault_info", "params": [ { - "vault": { - "owner": true, - "seq": 3 - - } + "owner": true, + "seq": 3 } ] })", @@ -159,11 +150,8 @@ generateTestValuesForParametersTest() "method": "vault_info", "params": [ { - "vault": { - "owner": "asdf", - "seq": 3 - - } + "owner": "asdf", + "seq": 3 } ] })", @@ -174,7 +162,7 @@ generateTestValuesForParametersTest() } INSTANTIATE_TEST_CASE_P( - RPCAccountInfoGroup1, + RPCVaultInfoGroup, VaultInfoParameterTest, ValuesIn(generateTestValuesForParametersTest()), tests::util::kNAME_GENERATOR @@ -195,7 +183,7 @@ TEST_P(VaultInfoParameterTest, InvalidParams) }); } -TEST_F(RPCVaultInfoHandlerTest, ValidVaultObjectQuery) +TEST_F(RPCVaultInfoHandlerTest, ValidVaultObjectQueryByOwnerAndSeq) { auto const expectedOutput = fmt::format( R"({{ @@ -212,15 +200,12 @@ TEST_F(RPCVaultInfoHandlerTest, ValidVaultObjectQuery) "LedgerEntryType": "Vault", "LedgerIndex": "{}", "LossUnrealized": "0", - "MPTokenIssuanceID": "000000000000000000000000000000000000000000000001", "Owner": "{}", "OwnerNode": "4", "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000002", "PreviousTxnLgrSeq": 3, "Sequence": 30, - "Share": {{ - "mpt_issuance_id": "000000000000000000000000000000000000000000000001" - }}, + "ShareMPTID":"00000000000000000000000000000000000000000000007B", "WithdrawalPolicy": 200, "index": "1B7BB49E0663E073D1C3EF989271F89E290AAF2D67CEE85F18E2CC76D168F694" }} @@ -235,32 +220,34 @@ TEST_F(RPCVaultInfoHandlerTest, ValidVaultObjectQuery) EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); // Vault params - ripple::uint192 issuanceID{1}; + ripple::uint192 mptSharesID{123}; ripple::uint256 prevTxId{2}; uint32_t prevTxSeq = 3; uint64_t ownerNode = 4; // Mock vault object auto const vault = createVault( - kACCOUNT, kACCOUNT2, kINDEX1, kSEQ, kASSET_CURRENCY, kASSET_ISSUER, issuanceID, ownerNode, prevTxId, prevTxSeq + kACCOUNT, kACCOUNT2, kINDEX1, kSEQ, kASSET_CURRENCY, kASSET_ISSUER, mptSharesID, ownerNode, prevTxId, prevTxSeq ); auto const accountRoot = createAccountRootObject(kACCOUNT, 0, kSEQ, 200, 2, kINDEX1, 2); auto const account = getAccountIdWithString(kACCOUNT); auto const accountKeylet = ripple::keylet::account(account).key; auto const vaultKeylet = ripple::keylet::vault(account, kSEQ).key; + auto const mptIssuance = ripple::keylet::mptIssuance(mptSharesID).key; + std::cout << mptIssuance << std::endl; ON_CALL(*backend_, doFetchLedgerObject(accountKeylet, kSEQ, _)) .WillByDefault(Return(accountRoot.getSerializer().peekData())); - ON_CALL(*backend_, doFetchLedgerObject(vaultKeylet, _, _)).WillByDefault(Return(vault.getSerializer().peekData())); + ON_CALL(*backend_, doFetchLedgerObject(vaultKeylet, kSEQ, _)) + .WillByDefault(Return(vault.getSerializer().peekData())); + ON_CALL(*backend_, doFetchLedgerObject(mptIssuance, kSEQ, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); // Input JSON using vault object auto static const kINPUT = boost::json::parse(fmt::format( R"({{ - "vault": {{ - "owner": "{}", - "seq": {} - }} + "owner": "{}", + "seq": {} }})", kACCOUNT, kSEQ @@ -271,6 +258,8 @@ TEST_F(RPCVaultInfoHandlerTest, ValidVaultObjectQuery) runSpawn([&](auto yield) { auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); ASSERT_TRUE(output); + std::cout << boost::json::serialize(*output.result) << std::endl; + EXPECT_EQ(*output.result, json::parse(expectedOutput)); }); } From 0037eba04f8efba8071fc3964a2acd7adb5ff7ea Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Tue, 29 Apr 2025 12:12:53 -0400 Subject: [PATCH 8/9] finish vault --- src/rpc/Errors.cpp | 2 +- src/rpc/Errors.hpp | 1 + src/rpc/common/Validators.cpp | 17 - src/rpc/common/Validators.hpp | 7 - src/rpc/handlers/LedgerEntry.cpp | 6 +- src/rpc/handlers/VaultInfo.cpp | 92 +++-- src/rpc/handlers/VaultInfo.hpp | 18 +- src/web/impl/ErrorHandling.hpp | 1 + src/web/ng/impl/ErrorHandling.cpp | 1 + tests/common/util/TestObject.cpp | 2 - tests/common/util/TestObject.hpp | 1 - tests/unit/rpc/handlers/LedgerEntryTests.cpp | 3 - tests/unit/rpc/handlers/TxTests.cpp | 2 +- tests/unit/rpc/handlers/VaultInfoTests.cpp | 338 ++++++++++++++----- 14 files changed, 343 insertions(+), 148 deletions(-) diff --git a/src/rpc/Errors.cpp b/src/rpc/Errors.cpp index 2609fc881..a8b6e721f 100644 --- a/src/rpc/Errors.cpp +++ b/src/rpc/Errors.cpp @@ -89,7 +89,7 @@ getErrorInfo(ClioError code) {.code = ClioError::RpcMalformedAuthorizedCredentials, .error = "malformedAuthorizedCredentials", .message = "Malformed authorized credentials."}, - + {.code = ClioError::RpcEntryNotFound, .error = "entryNotFound", .message = "Entry Not Found."}, // special system errors {.code = ClioError::RpcInvalidApiVersion, .error = JS(invalid_API_version), .message = "Invalid API version."}, {.code = ClioError::RpcCommandIsMissing, diff --git a/src/rpc/Errors.hpp b/src/rpc/Errors.hpp index 4ea7a4ebc..77546455e 100644 --- a/src/rpc/Errors.hpp +++ b/src/rpc/Errors.hpp @@ -43,6 +43,7 @@ enum class ClioError { RpcFieldNotFoundTransaction = 5006, RpcMalformedOracleDocumentId = 5007, RpcMalformedAuthorizedCredentials = 5008, + RpcEntryNotFound = 5009, // special system errors start with 6000 RpcInvalidApiVersion = 6000, diff --git a/src/rpc/common/Validators.cpp b/src/rpc/common/Validators.cpp index 6845c4dc8..58fd005be 100644 --- a/src/rpc/common/Validators.cpp +++ b/src/rpc/common/Validators.cpp @@ -330,21 +330,4 @@ CustomValidator CustomValidators::authorizeCredentialValidator = return MaybeError{}; }}; -CustomValidator CustomValidators::vaultObjectValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_object()) - return Error{Status{ClioError::RpcMalformedRequest, std::string(key) + " NotString"}}; - - auto const& vaultObj = value.as_object(); - - if (!vaultObj.contains("owner") || !vaultObj.contains("seq") || !vaultObj.at("owner").is_string() || - !vaultObj.at("seq").is_int64()) - return Error{Status{ClioError::RpcMalformedRequest}}; - - if (!ripple::parseBase58(std::string{vaultObj.at("owner").as_string()})) - return Error{ClioError::RpcMalformedOwner}; - - return MaybeError{}; - }}; - } // namespace rpc::validation diff --git a/src/rpc/common/Validators.hpp b/src/rpc/common/Validators.hpp index 311f12e45..ad23fdbab 100644 --- a/src/rpc/common/Validators.hpp +++ b/src/rpc/common/Validators.hpp @@ -571,13 +571,6 @@ struct CustomValidators final { * Used by AuthorizeCredentialValidator in deposit_preauth. */ static CustomValidator credentialTypeValidator; - - /** - * @brief Provides a validator for validating vault object. - * - * Used by vaultInfo handler. - */ - static CustomValidator vaultObjectValidator; }; /** diff --git a/src/rpc/handlers/LedgerEntry.cpp b/src/rpc/handlers/LedgerEntry.cpp index bde7cf607..273d4a558 100644 --- a/src/rpc/handlers/LedgerEntry.cpp +++ b/src/rpc/handlers/LedgerEntry.cpp @@ -213,13 +213,13 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx) if (!ledgerObject || ledgerObject->empty()) { if (not input.includeDeleted) - return Error{Status{"entryNotFound"}}; + return Error{Status{ClioError::RpcEntryNotFound}}; auto const deletedSeq = sharedPtrBackend_->fetchLedgerObjectSeq(key, lgrInfo.seq, ctx.yield); if (!deletedSeq) - return Error{Status{"entryNotFound"}}; + return Error{Status{ClioError::RpcEntryNotFound}}; ledgerObject = sharedPtrBackend_->fetchLedgerObject(key, deletedSeq.value() - 1, ctx.yield); if (!ledgerObject || ledgerObject->empty()) - return Error{Status{"entryNotFound"}}; + return Error{Status{ClioError::RpcEntryNotFound}}; output.deletedLedgerIndex = deletedSeq; } diff --git a/src/rpc/handlers/VaultInfo.cpp b/src/rpc/handlers/VaultInfo.cpp index be48f6337..1c741411b 100644 --- a/src/rpc/handlers/VaultInfo.cpp +++ b/src/rpc/handlers/VaultInfo.cpp @@ -29,10 +29,13 @@ #include #include #include +#include #include #include +#include #include #include +#include #include #include #include @@ -42,7 +45,6 @@ #include #include #include -#include namespace rpc { @@ -56,9 +58,9 @@ parseVaultField(VaultInfoHandler::Input const& input) { auto const hasVaultId = input.vaultID.has_value(); auto const hasOwner = input.owner.has_value(); - auto const hasSeq = input.ledgerIndex.has_value(); + auto const hasSeq = input.tnxSequence.has_value(); - // valid vault_info has input of either vault_id or owner & sequence + // Only valid combinations: (vaultID) or (owner + ledgerIndex) if ((hasVaultId && !hasOwner && !hasSeq) || (!hasVaultId && hasOwner && hasSeq)) return {}; @@ -73,7 +75,7 @@ VaultInfoHandler::process(VaultInfoHandler::Input input, Context const& ctx) con return Error{res.error()}; auto const range = sharedPtrBackend_->fetchLedgerRange(); - ASSERT(range.has_value(), "AccountInfo's ledger range must be available"); + ASSERT(range.has_value(), "VaultInfo's ledger range must be available"); auto const lgrInfoOrStatus = getLedgerHeaderFromHashOrSeq( *sharedPtrBackend_, ctx.yield, std::nullopt, input.ledgerIndex, range->maxSequence @@ -84,53 +86,69 @@ VaultInfoHandler::process(VaultInfoHandler::Input input, Context const& ctx) con auto const lgrInfo = std::get(lgrInfoOrStatus); - // Extract the vault owner and construct a keylet for the vault object - auto const accountStr = *input.owner; - auto const accountID = accountFromStringStrict(accountStr); - auto const accountKeylet = ripple::keylet::account(*accountID); + // Extract the vault keylet based on input + auto const vaultKeylet = [&]() -> std::expected { + if (input.owner && input.tnxSequence) { + auto const accountStr = *input.owner; + auto const accountID = accountFromStringStrict(accountStr); - // Fetch the account ledger object - auto const accountLedgerObject = sharedPtrBackend_->fetchLedgerObject(accountKeylet.key, lgrInfo.seq, ctx.yield); + // checks that account exists + { + auto const accountKeylet = ripple::keylet::account(*accountID); + auto const accountLedgerObject = + sharedPtrBackend_->fetchLedgerObject(accountKeylet.key, lgrInfo.seq, ctx.yield); - if (!accountLedgerObject) - return Error{Status{RippledError::rpcACT_NOT_FOUND}}; + if (!accountLedgerObject) + return std::unexpected{Status{ClioError::RpcEntryNotFound}}; + } - // use account to get vault ledger object - ripple::STLedgerEntry const sleObject{ - ripple::SerialIter{accountLedgerObject->data(), accountLedgerObject->size()}, accountKeylet.key - }; + return ripple::keylet::vault(*accountID, *input.tnxSequence); + } + ripple::uint256 nodeIndex; + if (nodeIndex.parseHex(*input.vaultID)) + return ripple::keylet::vault(nodeIndex); + + return std::unexpected{Status{ClioError::RpcEntryNotFound}}; + }(); + + if (!vaultKeylet.has_value()) + return Error{vaultKeylet.error()}; - auto const vaultKeylet = ripple::keylet::vault(*accountID, *input.ledgerIndex); + // Fetch the vault object and it's associated issuance ID + auto const vaultLedgerObject = + sharedPtrBackend_->fetchLedgerObject(vaultKeylet.value().key, lgrInfo.seq, ctx.yield); - // Fetch the vault object - auto const vaultLedgerObject = sharedPtrBackend_->fetchLedgerObject(vaultKeylet.key, lgrInfo.seq, ctx.yield); + if (!vaultLedgerObject) + return Error{Status{ClioError::RpcEntryNotFound, "vault object not found."}}; ripple::STLedgerEntry const vaultSle{ - ripple::SerialIter{vaultLedgerObject->data(), vaultLedgerObject->size()}, vaultKeylet.key + ripple::SerialIter{vaultLedgerObject->data(), vaultLedgerObject->size()}, vaultKeylet.value().key }; - std::cout << ripple::keylet::mptIssuance(vaultSle[ripple::sfShareMPTID]).key; + auto const issuanceKeylet = ripple::keylet::mptIssuance(vaultSle[ripple::sfShareMPTID]).key; + auto const issuanceObject = sharedPtrBackend_->fetchLedgerObject(issuanceKeylet, lgrInfo.seq, ctx.yield); - auto const issuanceObject = !vaultLedgerObject.has_value() - ? std::nullopt - : sharedPtrBackend_->fetchLedgerObject( - ripple::keylet::mptIssuance(vaultSle[ripple::sfShareMPTID]).key, lgrInfo.seq, ctx.yield - ); + if (!issuanceObject) + return Error{Status{ClioError::RpcEntryNotFound, "issuance object not found."}}; - if (!vaultLedgerObject || !issuanceObject) - return Error{Status{"entryNotFound"}}; + ripple::STLedgerEntry const issuanceSle{ + ripple::SerialIter{issuanceObject->data(), issuanceObject->size()}, issuanceKeylet + }; - // todo: vaultSLE + // put issuance object into "shares" field of vault object + Output response; + response.vault = toBoostJson(vaultSle.getJson(ripple::JsonOptions::none)); + response.vault.as_object()[JS(shares)] = toBoostJson(issuanceSle.getJson(ripple::JsonOptions::none)); + response.ledgerIndex = lgrInfo.seq; - // Prepare output - return Output(vaultSle, lgrInfo.seq); + return response; } void tag_invoke(boost::json::value_from_tag, boost::json::value& jv, VaultInfoHandler::Output const& output) { jv = boost::json::object{ - {JS(ledger_index), output.ledgerIndex}, {JS(validated), output.validated}, {JS(vault), toJson(output.vault)} + {JS(ledger_index), output.ledgerIndex}, {JS(validated), output.validated}, {JS(vault), output.vault} }; } @@ -144,11 +162,19 @@ tag_invoke(boost::json::value_to_tag, boost::json::valu input.owner = jsonObject.at(JS(owner)).as_string(); if (jsonObject.contains(JS(seq))) - input.ledgerIndex = static_cast(jsonObject.at(JS(seq)).as_int64()); + input.tnxSequence = static_cast(jsonObject.at(JS(seq)).as_int64()); if (jsonObject.contains(JS(vault_id))) input.vaultID = jsonObject.at(JS(vault_id)).as_string(); + if (jsonObject.contains(JS(ledger_index))) { + if (!jsonObject.at(JS(ledger_index)).is_string()) { + input.ledgerIndex = jsonObject.at(JS(ledger_index)).as_int64(); + } else if (jsonObject.at(JS(ledger_index)).as_string() != "validated") { + input.ledgerIndex = std::stoi(jsonObject.at(JS(ledger_index)).as_string().c_str()); + } + } + return input; } diff --git a/src/rpc/handlers/VaultInfo.hpp b/src/rpc/handlers/VaultInfo.hpp index 738679cb7..d1dc01aeb 100644 --- a/src/rpc/handlers/VaultInfo.hpp +++ b/src/rpc/handlers/VaultInfo.hpp @@ -20,7 +20,9 @@ #pragma once #include "data/BackendInterface.hpp" +#include "rpc/Errors.hpp" #include "rpc/JS.hpp" +#include "rpc/common/MetaProcessors.hpp" #include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" #include "rpc/common/Validators.hpp" @@ -58,6 +60,7 @@ class VaultInfoHandler { struct Input { std::optional vaultID; std::optional owner; + std::optional tnxSequence; std::optional ledgerIndex; }; @@ -65,7 +68,7 @@ class VaultInfoHandler { * @brief A struct to hold the output data for the command */ struct Output { - ripple::STLedgerEntry vault; + boost::json::value vault; uint32_t ledgerIndex{}; bool validated = true; }; @@ -82,9 +85,16 @@ class VaultInfoHandler { spec([[maybe_unused]] uint32_t apiVersion) { static auto const kRPC_SPEC = RpcSpec{ - {JS(vault_id), validation::CustomValidators::uint256HexStringValidator}, - {JS(owner), validation::CustomValidators::accountBase58Validator}, - {JS(seq), validation::Type{}} + {JS(vault_id), + meta::WithCustomError{ + validation::CustomValidators::uint256HexStringValidator, Status(ClioError::RpcMalformedRequest) + }}, + {JS(owner), + meta::WithCustomError{ + validation::CustomValidators::accountBase58Validator, + Status(ClioError::RpcMalformedRequest, "OwnerNotHexString") + }}, + {JS(seq), meta::WithCustomError{validation::Type{}, Status(ClioError::RpcMalformedRequest)}} }; return kRPC_SPEC; diff --git a/src/web/impl/ErrorHandling.hpp b/src/web/impl/ErrorHandling.hpp index e35126ec4..644041a74 100644 --- a/src/web/impl/ErrorHandling.hpp +++ b/src/web/impl/ErrorHandling.hpp @@ -91,6 +91,7 @@ class ErrorHelper { case rpc::ClioError::RpcFieldNotFoundTransaction: case rpc::ClioError::RpcMalformedOracleDocumentId: case rpc::ClioError::RpcMalformedAuthorizedCredentials: + case rpc::ClioError::RpcEntryNotFound: case rpc::ClioError::EtlConnectionError: case rpc::ClioError::EtlRequestError: case rpc::ClioError::EtlRequestTimeout: diff --git a/src/web/ng/impl/ErrorHandling.cpp b/src/web/ng/impl/ErrorHandling.cpp index f8c244289..1b58a62e5 100644 --- a/src/web/ng/impl/ErrorHandling.cpp +++ b/src/web/ng/impl/ErrorHandling.cpp @@ -105,6 +105,7 @@ ErrorHelper::makeError(rpc::Status const& err) const case rpc::ClioError::RpcFieldNotFoundTransaction: case rpc::ClioError::RpcMalformedOracleDocumentId: case rpc::ClioError::RpcMalformedAuthorizedCredentials: + case rpc::ClioError::RpcEntryNotFound: case rpc::ClioError::EtlConnectionError: case rpc::ClioError::EtlRequestError: case rpc::ClioError::EtlRequestTimeout: diff --git a/tests/common/util/TestObject.cpp b/tests/common/util/TestObject.cpp index b2199aceb..e11aa3b72 100644 --- a/tests/common/util/TestObject.cpp +++ b/tests/common/util/TestObject.cpp @@ -1615,7 +1615,6 @@ ripple::STObject createVault( std::string_view owner, std::string_view account, - std::string_view ledgerIndex, ripple::LedgerIndex seq, std::string_view assetCurrency, std::string_view assetIssuer, @@ -1626,7 +1625,6 @@ createVault( ) { auto vault = ripple::STObject(ripple::sfLedgerEntry); - vault.setFieldH256(ripple::sfLedgerIndex, ripple::uint256(ledgerIndex)); vault.setAccountID(ripple::sfOwner, getAccountIdWithString(owner)); vault.setAccountID(ripple::sfAccount, getAccountIdWithString(account)); vault.setFieldU32(ripple::sfSequence, seq); diff --git a/tests/common/util/TestObject.hpp b/tests/common/util/TestObject.hpp index c1c2864b2..67da8a09b 100644 --- a/tests/common/util/TestObject.hpp +++ b/tests/common/util/TestObject.hpp @@ -517,7 +517,6 @@ createAuthCredentialArray(std::vector issuer, std::vector #include #include +#include #include #include #include #include +#include #include #include @@ -53,6 +56,7 @@ constexpr auto kINDEX1 = "ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF constexpr auto kSEQ = 30; constexpr auto kASSET_CURRENCY = "XRP"; constexpr auto kASSET_ISSUER = "rrrrrrrrrrrrrrrrrrrrrhoLvTp"; +constexpr auto kVAULT_ID = "61B03A6F8CEBD3AF9D8F696C3D0A9A9F0493B34BF6B5D93CF0BC009E6BA75303"; } // namespace @@ -80,12 +84,9 @@ generateTestValuesForParametersTest() { return std::vector{ VaultInfoParamTestCaseBundle{ - .testName = "MissingVaultField", + .testName = "RandomField", .testJson = R"({ - "method": "vault_info", - "params": [{ - "idk" : "idk" - }] + "idk" : "idk" })", .expectedError = "malformedRequest", .expectedErrorMessage = "Malformed request." @@ -93,12 +94,7 @@ generateTestValuesForParametersTest() VaultInfoParamTestCaseBundle{ .testName = "MissingOwnerInVault", .testJson = R"({ - "method": "vault_info", - "params": [ - { - "seq": 4 - } - ] + "seq": 4 })", .expectedError = "malformedRequest", .expectedErrorMessage = "Malformed request." @@ -106,12 +102,7 @@ generateTestValuesForParametersTest() VaultInfoParamTestCaseBundle{ .testName = "MissingSeqInVault", .testJson = R"({ - "method": "vault_info", - "params": [ - { - "owner": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh" - } - ] + "owner": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh" })", .expectedError = "malformedRequest", .expectedErrorMessage = "Malformed request." @@ -119,13 +110,8 @@ generateTestValuesForParametersTest() VaultInfoParamTestCaseBundle{ .testName = "SeqNotAnInteger", .testJson = R"({ - "method": "vault_info", - "params": [ - { - "owner": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", - "seq": "asdf" - } - ] + "owner": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "seq": "asdf" })", .expectedError = "malformedRequest", .expectedErrorMessage = "Malformed request." @@ -133,30 +119,49 @@ generateTestValuesForParametersTest() VaultInfoParamTestCaseBundle{ .testName = "OwnerNotAString", .testJson = R"({ - "method": "vault_info", - "params": [ - { - "owner": true, - "seq": 3 - } - ] + "owner": true, + "seq": 3 })", .expectedError = "malformedRequest", - .expectedErrorMessage = "Malformed request." + .expectedErrorMessage = "OwnerNotHexString" }, VaultInfoParamTestCaseBundle{ .testName = "OwnerNotAHexString", .testJson = R"({ - "method": "vault_info", - "params": [ - { - "owner": "asdf", - "seq": 3 - } - ] + "owner": "asdf", + "seq": 3 + })", + .expectedError = "malformedRequest", + .expectedErrorMessage = "OwnerNotHexString" + }, + VaultInfoParamTestCaseBundle{ + .testName = "vaultIDNotString", + .testJson = R"({ + "vault_id": 3 })", .expectedError = "malformedRequest", .expectedErrorMessage = "Malformed request." + }, + VaultInfoParamTestCaseBundle{ + .testName = "vaultIDNotHex256", + .testJson = R"({ + "vault_id": "idk" + })", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request." + }, + VaultInfoParamTestCaseBundle{ + .testName = "vaultIDWithOwner", + .testJson = fmt::format( + R"({{ + "vault_id": "{}", + "owner": "{}" + }})", + kVAULT_ID, + kACCOUNT + ), + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request." } }; } @@ -183,39 +188,136 @@ TEST_P(VaultInfoParameterTest, InvalidParams) }); } -TEST_F(RPCVaultInfoHandlerTest, ValidVaultObjectQueryByOwnerAndSeq) +TEST_F(RPCVaultInfoHandlerTest, InputHasOwnerButNotFoundResultsInError) { - auto const expectedOutput = fmt::format( + auto const ledgerHeader = createLedgerHeader(kINDEX1, kSEQ); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); + + // Input JSON using vault object + auto static const kINPUT = boost::json::parse(fmt::format( R"({{ - "ledger_index": 30, - "validated": true, - "vault": {{ - "Account": "{}", - "Asset": {{ - "currency": "{}" - }}, - "AssetsAvailable": "300", - "AssetsTotal": "300", - "Flags": 0, - "LedgerEntryType": "Vault", - "LedgerIndex": "{}", - "LossUnrealized": "0", - "Owner": "{}", - "OwnerNode": "4", - "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000002", - "PreviousTxnLgrSeq": 3, - "Sequence": 30, - "ShareMPTID":"00000000000000000000000000000000000000000000007B", - "WithdrawalPolicy": 200, - "index": "1B7BB49E0663E073D1C3EF989271F89E290AAF2D67CEE85F18E2CC76D168F694" - }} - }})", - kACCOUNT2, - kASSET_CURRENCY, - kINDEX1, + "owner": "{}", + "seq": 3 + }})", kACCOUNT + )); + + // Run the handler + auto const handler = AnyHandler{VaultInfoHandler{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(), "entryNotFound"); + }); +} + +TEST_F(RPCVaultInfoHandlerTest, VaultIDFailsVaultDeserializationReturnsEntryNotFound) +{ + auto const ledgerHeader = createLedgerHeader(kINDEX1, kSEQ); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); + + // Mock: vault_id exists, but data is not a valid vault object + ripple::uint256 vaultKey = ripple::uint256{kVAULT_ID}; + ON_CALL(*backend_, doFetchLedgerObject(vaultKey, kSEQ, _)) + .WillByDefault(Return(std::nullopt)); // intentionally invalid vault + + auto const kINPUT = boost::json::parse(fmt::format( + R"({{ + "vault_id": "{}" + }})", + kVAULT_ID + )); + + auto const handler = AnyHandler{VaultInfoHandler{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(), "entryNotFound"); + }); +} + +TEST_F(RPCVaultInfoHandlerTest, MissingIssuanceObject) +{ + auto const ledgerHeader = createLedgerHeader(kINDEX1, kSEQ); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); + + ripple::uint192 mptSharesID{123}; + ripple::uint256 prevTxId{2}; + uint32_t prevTxSeq = 3; + uint64_t ownerNode = 4; + + auto const vault = createVault( + kACCOUNT, kACCOUNT2, kSEQ, kASSET_CURRENCY, kASSET_ISSUER, mptSharesID, ownerNode, prevTxId, prevTxSeq ); + auto const vaultKeylet = ripple::keylet::vault(ripple::uint256{kVAULT_ID}).key; + auto const mptIssuance = ripple::keylet::mptIssuance(mptSharesID).key; + + ON_CALL(*backend_, doFetchLedgerObject(vaultKeylet, kSEQ, _)) + .WillByDefault(Return(vault.getSerializer().peekData())); + ON_CALL(*backend_, doFetchLedgerObject(mptIssuance, kSEQ, _)) + .WillByDefault(Return(std::nullopt)); // Missing issuance + + auto static const kINPUT = boost::json::parse(fmt::format( + R"({{ + "vault_id": "{}" + }})", + kVAULT_ID + )); + + auto const handler = AnyHandler{VaultInfoHandler{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(), "entryNotFound"); + }); +} + +TEST_F(RPCVaultInfoHandlerTest, ValidVaultObjectQueryByVaultID) +{ + constexpr auto kEXPECTED_OUTPUT = + R"({ + "ledger_index": 30, + "validated": true, + "vault": { + "Account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Asset": { + "currency": "XRP" + }, + "AssetsAvailable": "300", + "AssetsTotal": "300", + "Flags": 0, + "LedgerEntryType": "Vault", + "LossUnrealized": "0", + "Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "OwnerNode": "4", + "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000002", + "PreviousTxnLgrSeq": 3, + "Sequence": 30, + "ShareMPTID": "00000000000000000000000000000000000000000000007B", + "WithdrawalPolicy": 200, + "index": "61B03A6F8CEBD3AF9D8F696C3D0A9A9F0493B34BF6B5D93CF0BC009E6BA75303", + "shares": { + "Flags": 0, + "Issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "LedgerEntryType": "MPTokenIssuance", + "MPTokenMetadata": "6D65746164617461", + "MaximumAmount": "0", + "OutstandingAmount": "0", + "OwnerNode": "0", + "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000", + "PreviousTxnLgrSeq": 0, + "Sequence": 30, + "index": "87658CA4D4D7A50EE99E632055FE7A879CD9A331880AC21D538FA6E4032804E3", + "mpt_issuance_id": "0000001E4B4E9C06F24296074F7BC48F92A97916C6DC5EA9" + } + } + })"; + auto const ledgerHeader = createLedgerHeader(kINDEX1, kSEQ); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); @@ -227,28 +329,114 @@ TEST_F(RPCVaultInfoHandlerTest, ValidVaultObjectQueryByOwnerAndSeq) // Mock vault object auto const vault = createVault( - kACCOUNT, kACCOUNT2, kINDEX1, kSEQ, kASSET_CURRENCY, kASSET_ISSUER, mptSharesID, ownerNode, prevTxId, prevTxSeq + kACCOUNT, kACCOUNT2, kSEQ, kASSET_CURRENCY, kASSET_ISSUER, mptSharesID, ownerNode, prevTxId, prevTxSeq ); + // Set up keylet based on vaultID + auto const issuance = createMptIssuanceObject(kACCOUNT, kSEQ, "metadata"); + auto const vaultKeylet = ripple::keylet::vault(ripple::uint256{kVAULT_ID}).key; + auto const mptIssuance = ripple::keylet::mptIssuance(mptSharesID).key; + + ON_CALL(*backend_, doFetchLedgerObject(ripple::uint256{kVAULT_ID}, kSEQ, _)) + .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + ON_CALL(*backend_, doFetchLedgerObject(vaultKeylet, kSEQ, _)) + .WillByDefault(Return(vault.getSerializer().peekData())); + ON_CALL(*backend_, doFetchLedgerObject(mptIssuance, kSEQ, _)) + .WillByDefault(Return(issuance.getSerializer().peekData())); + + // Input JSON using vault_id + auto static const kINPUT = boost::json::parse(fmt::format( + R"({{ + "vault_id": "{}" + }})", + kVAULT_ID + )); + + // Run the handler + auto const handler = AnyHandler{VaultInfoHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); + ASSERT_TRUE(output); + EXPECT_EQ(*output.result, json::parse(kEXPECTED_OUTPUT)); + }); +} + +TEST_F(RPCVaultInfoHandlerTest, ValidVaultObjectQueryByOwnerAndSeq) +{ + constexpr auto kEXPECTED_OUTPUT = + R"({ + "ledger_index": 30, + "validated": true, + "vault": { + "Account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Asset": { + "currency": "XRP" + }, + "AssetsAvailable": "300", + "AssetsTotal": "300", + "Flags": 0, + "LedgerEntryType": "Vault", + "LossUnrealized": "0", + "Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "OwnerNode": "4", + "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000002", + "PreviousTxnLgrSeq": 3, + "Sequence": 30, + "ShareMPTID": "00000000000000000000000000000000000000000000007B", + "WithdrawalPolicy": 200, + "index": "1B7BB49E0663E073D1C3EF989271F89E290AAF2D67CEE85F18E2CC76D168F694", + "shares": { + "Flags": 0, + "Issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "LedgerEntryType": "MPTokenIssuance", + "MPTokenMetadata": "6D65746164617461", + "MaximumAmount": "0", + "OutstandingAmount": "0", + "OwnerNode": "0", + "PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000", + "PreviousTxnLgrSeq": 0, + "Sequence": 30, + "index": "87658CA4D4D7A50EE99E632055FE7A879CD9A331880AC21D538FA6E4032804E3", + "mpt_issuance_id": "0000001E4B4E9C06F24296074F7BC48F92A97916C6DC5EA9" + } + } + })"; + + auto const ledgerHeader = createLedgerHeader(kINDEX1, kSEQ); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); + + // Vault params + ripple::uint192 mptSharesID{123}; + ripple::uint256 prevTxId{2}; + uint32_t prevTxSeq = 3; + uint64_t ownerNode = 4; + + // Mock vault object + auto const vault = createVault( + kACCOUNT, kACCOUNT2, kSEQ, kASSET_CURRENCY, kASSET_ISSUER, mptSharesID, ownerNode, prevTxId, prevTxSeq + ); + + auto const issuance = createMptIssuanceObject(kACCOUNT, kSEQ, "metadata"); + auto const accountRoot = createAccountRootObject(kACCOUNT, 0, kSEQ, 200, 2, kINDEX1, 2); auto const account = getAccountIdWithString(kACCOUNT); auto const accountKeylet = ripple::keylet::account(account).key; auto const vaultKeylet = ripple::keylet::vault(account, kSEQ).key; auto const mptIssuance = ripple::keylet::mptIssuance(mptSharesID).key; - std::cout << mptIssuance << std::endl; ON_CALL(*backend_, doFetchLedgerObject(accountKeylet, kSEQ, _)) .WillByDefault(Return(accountRoot.getSerializer().peekData())); ON_CALL(*backend_, doFetchLedgerObject(vaultKeylet, kSEQ, _)) .WillByDefault(Return(vault.getSerializer().peekData())); - ON_CALL(*backend_, doFetchLedgerObject(mptIssuance, kSEQ, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + ON_CALL(*backend_, doFetchLedgerObject(mptIssuance, kSEQ, _)) + .WillByDefault(Return(issuance.getSerializer().peekData())); // Input JSON using vault object auto static const kINPUT = boost::json::parse(fmt::format( R"({{ - "owner": "{}", - "seq": {} - }})", + "owner": "{}", + "seq": {} + }})", kACCOUNT, kSEQ )); @@ -258,8 +446,6 @@ TEST_F(RPCVaultInfoHandlerTest, ValidVaultObjectQueryByOwnerAndSeq) runSpawn([&](auto yield) { auto const output = handler.process(kINPUT, Context{.yield = yield, .apiVersion = 2}); ASSERT_TRUE(output); - std::cout << boost::json::serialize(*output.result) << std::endl; - - EXPECT_EQ(*output.result, json::parse(expectedOutput)); + EXPECT_EQ(*output.result, json::parse(kEXPECTED_OUTPUT)); }); } From c9a2d686e6a67cf82c0ff6247b0f1e19cd62e49d Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Tue, 29 Apr 2025 12:23:41 -0400 Subject: [PATCH 9/9] Trigger pre-commit hook