diff --git a/conanfile.py b/conanfile.py index 6d97d1afe..210e9bdef 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/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/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/Errors.cpp b/src/rpc/Errors.cpp index b57f85bf7..285fd33f9 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 e3735a86c..0bff5d26e 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/RPCHelpers.hpp b/src/rpc/RPCHelpers.hpp index a2c830517..67b15ab97 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/Taggable.hpp" @@ -42,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -50,9 +52,12 @@ #include #include #include +#include #include +#include #include #include +#include #include #include #include @@ -60,9 +65,11 @@ #include #include #include +#include #include #include #include +#include #include #include diff --git a/src/rpc/common/impl/HandlerProvider.cpp b/src/rpc/common/impl/HandlerProvider.cpp index c05e5cd57..ba0b9cb78 100644 --- a/src/rpc/common/impl/HandlerProvider.cpp +++ b/src/rpc/common/impl/HandlerProvider.cpp @@ -60,6 +60,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" @@ -113,6 +114,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/LedgerEntry.cpp b/src/rpc/handlers/LedgerEntry.cpp index 7139002db..3694b53c1 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(owner)))); + 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) @@ -208,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; } @@ -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..b550d1b44 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,23 @@ class LedgerEntryHandler { }, }, }}}, + {JS(vault), + 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(owner), + meta::WithCustomError{validation::Required{}, Status(ClioError::RpcMalformedRequest)}, + meta::WithCustomError{ + validation::CustomValidators::accountBase58Validator, Status(ClioError::RpcMalformedOwner) + }, + }, + }}}, {JS(ledger), check::Deprecated{}}, {"include_deleted", validation::Type{}}, }; diff --git a/src/rpc/handlers/VaultInfo.cpp b/src/rpc/handlers/VaultInfo.cpp new file mode 100644 index 000000000..1c741411b --- /dev/null +++ b/src/rpc/handlers/VaultInfo.cpp @@ -0,0 +1,181 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include + +namespace rpc { + +VaultInfoHandler::VaultInfoHandler(std::shared_ptr const& sharedPtrBackend) + : sharedPtrBackend_{sharedPtrBackend} +{ +} + +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.tnxSequence.has_value(); + + // Only valid combinations: (vaultID) or (owner + ledgerIndex) + 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(), "VaultInfo's ledger range must be available"); + + auto const lgrInfoOrStatus = getLedgerHeaderFromHashOrSeq( + *sharedPtrBackend_, ctx.yield, std::nullopt, input.ledgerIndex, range->maxSequence + ); + + if (auto const status = std::get_if(&lgrInfoOrStatus)) + return Error{*status}; + + auto const lgrInfo = std::get(lgrInfoOrStatus); + + // 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); + + // 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 std::unexpected{Status{ClioError::RpcEntryNotFound}}; + } + + 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()}; + + // Fetch the vault object and it's associated issuance ID + auto const vaultLedgerObject = + sharedPtrBackend_->fetchLedgerObject(vaultKeylet.value().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.value().key + }; + + auto const issuanceKeylet = ripple::keylet::mptIssuance(vaultSle[ripple::sfShareMPTID]).key; + auto const issuanceObject = sharedPtrBackend_->fetchLedgerObject(issuanceKeylet, lgrInfo.seq, ctx.yield); + + if (!issuanceObject) + return Error{Status{ClioError::RpcEntryNotFound, "issuance object not found."}}; + + ripple::STLedgerEntry const issuanceSle{ + ripple::SerialIter{issuanceObject->data(), issuanceObject->size()}, issuanceKeylet + }; + + // 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; + + 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), 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(); + + if (jsonObject.contains(JS(owner))) + input.owner = jsonObject.at(JS(owner)).as_string(); + + if (jsonObject.contains(JS(seq))) + 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; +} + +} // namespace rpc diff --git a/src/rpc/handlers/VaultInfo.hpp b/src/rpc/handlers/VaultInfo.hpp new file mode 100644 index 000000000..d1dc01aeb --- /dev/null +++ b/src/rpc/handlers/VaultInfo.hpp @@ -0,0 +1,133 @@ +//------------------------------------------------------------------------------ +/* + 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/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" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace rpc { + +/** + * @brief The vault_info command retrieves information about a vault, currency, shares etc. + */ +class VaultInfoHandler { + std::shared_ptr sharedPtrBackend_; + +public: + /** + * @brief Construct a new VaultInfo object + * + * @param sharedPtrBackend The backend to use + */ + VaultInfoHandler(std::shared_ptr const& sharedPtrBackend); + + /** + * @brief A struct to hold the input data for the command + */ + struct Input { + std::optional vaultID; + std::optional owner; + std::optional tnxSequence; + std::optional ledgerIndex; + }; + + /** + * @brief A struct to hold the output data for the command + */ + struct Output { + boost::json::value 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_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; + } + + /** + * @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/src/util/LedgerUtils.hpp b/src/util/LedgerUtils.hpp index 6e01e3e0e..2c7180be4 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/src/web/impl/ErrorHandling.hpp b/src/web/impl/ErrorHandling.hpp index d200810c6..42fe6b6aa 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 6e9a0540f..23836191e 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 f04149e66..e11aa3b72 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,37 @@ createAuthCredentialArray(std::vector issuer, std::vector issuer, std::vector credType); + +[[nodiscard]] ripple::STObject +createVault( + std::string_view owner, + std::string_view account, + ripple::LedgerIndex seq, + std::string_view assetCurrency, + std::string_view assetIssuer, + ripple::uint192 shareMPTID, + uint64_t ownerNode, + ripple::uint256 previousTxId, + uint32_t previousTxSeq +); diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 0b9444fef..6722df684 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -128,6 +128,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 fd8df9dd8..8771c6c0f 100644 --- a/tests/unit/rpc/RPCHelpersTests.cpp +++ b/tests/unit/rpc/RPCHelpersTests.cpp @@ -49,7 +49,9 @@ #include #include #include +#include #include +#include #include #include diff --git a/tests/unit/rpc/handlers/LedgerEntryTests.cpp b/tests/unit/rpc/handlers/LedgerEntryTests.cpp index b11c34b19..cffeab026 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 @@ -2193,6 +2195,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 +3029,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, + kACCOUNT, + kRANGE_MAX, + "XRP", + ripple::toBase58(ripple::xrpAccount()), + ripple::uint192(0), + 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, + kACCOUNT, + kRANGE_MAX, + "XRP", + ripple::toBase58(ripple::xrpAccount()), + ripple::uint192(0), + 0, + ripple::uint256{0}, + 0 + ) } }; } @@ -3045,6 +3166,57 @@ 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, + kACCOUNT, + kRANGE_MAX, + "XRP", + ripple::toBase58(ripple::xrpAccount()), + ripple::uint192(0), + 0, + ripple::uint256{1}, + 0 + ); + + auto const vaultKey = + ripple::keylet::vault(ripple::parseBase58(kACCOUNT).value(), kRANGE_MAX).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())); + + 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(output.result->at("node").at("Owner").as_string(), kACCOUNT); + EXPECT_EQ(output.result->at("node").at("Sequence").as_int64(), kRANGE_MAX); + }); +} + TEST_F(RPCLedgerEntryTest, UnexpectedLedgerType) { // return valid ledgerHeader diff --git a/tests/unit/rpc/handlers/TxTests.cpp b/tests/unit/rpc/handlers/TxTests.cpp index 6815bcdbf..57bea26ed 100644 --- a/tests/unit/rpc/handlers/TxTests.cpp +++ b/tests/unit/rpc/handlers/TxTests.cpp @@ -834,7 +834,7 @@ TEST_F(RPCTxTest, CTIDNotMatch) ASSERT_FALSE(output); auto const err = rpc::makeError(output.result.error()); - EXPECT_EQ(err.at("error").as_string(), "unknown"); + EXPECT_EQ(err.at("error").as_string(), "wrongNetwork"); EXPECT_EQ(err.at("error_code").as_uint64(), rpc::RippledError::rpcWRONG_NETWORK); EXPECT_EQ( err.at("error_message").as_string(), diff --git a/tests/unit/rpc/handlers/VaultInfoTests.cpp b/tests/unit/rpc/handlers/VaultInfoTests.cpp new file mode 100644 index 000000000..d5deaf008 --- /dev/null +++ b/tests/unit/rpc/handlers/VaultInfoTests.cpp @@ -0,0 +1,451 @@ +//------------------------------------------------------------------------------ +/* + 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 "data/Types.hpp" +#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 "util/TestObject.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace rpc; +using namespace data; +using namespace testing; +namespace json = boost::json; + +namespace { + +constexpr auto kACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr auto kACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr auto kINDEX1 = "ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890"; +constexpr auto kSEQ = 30; +constexpr auto kASSET_CURRENCY = "XRP"; +constexpr auto kASSET_ISSUER = "rrrrrrrrrrrrrrrrrrrrrhoLvTp"; +constexpr auto kVAULT_ID = "61B03A6F8CEBD3AF9D8F696C3D0A9A9F0493B34BF6B5D93CF0BC009E6BA75303"; + +} // namespace + +struct RPCVaultInfoHandlerTest : HandlerBaseTest { + RPCVaultInfoHandlerTest() + { + backend_->setRange(10, kSEQ); + } + +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 = "RandomField", + .testJson = R"({ + "idk" : "idk" + })", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request." + }, + VaultInfoParamTestCaseBundle{ + .testName = "MissingOwnerInVault", + .testJson = R"({ + "seq": 4 + })", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request." + }, + VaultInfoParamTestCaseBundle{ + .testName = "MissingSeqInVault", + .testJson = R"({ + "owner": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh" + })", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request." + }, + VaultInfoParamTestCaseBundle{ + .testName = "SeqNotAnInteger", + .testJson = R"({ + "owner": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "seq": "asdf" + })", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request." + }, + VaultInfoParamTestCaseBundle{ + .testName = "OwnerNotAString", + .testJson = R"({ + "owner": true, + "seq": 3 + })", + .expectedError = "malformedRequest", + .expectedErrorMessage = "OwnerNotHexString" + }, + VaultInfoParamTestCaseBundle{ + .testName = "OwnerNotAHexString", + .testJson = R"({ + "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." + } + }; +} + +INSTANTIATE_TEST_CASE_P( + RPCVaultInfoGroup, + 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); + }); +} + +TEST_F(RPCVaultInfoHandlerTest, InputHasOwnerButNotFoundResultsInError) +{ + 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"({{ + "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)); + + // 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 + ); + + // 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; + + 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(issuance.getSerializer().peekData())); + + // Input JSON using vault object + auto static const kINPUT = boost::json::parse(fmt::format( + R"({{ + "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); + EXPECT_EQ(*output.result, json::parse(kEXPECTED_OUTPUT)); + }); +} 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());