feat: Support single asset vault (#1979)

fixes #1921

---------

Co-authored-by: Sergey Kuznetsov <skuznetsov@ripple.com>
Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
This commit is contained in:
Peter Chen
2025-06-27 07:27:34 -07:00
committed by GitHub
parent d97f19ba1d
commit 371237487b
21 changed files with 1061 additions and 5 deletions

View File

@@ -51,6 +51,7 @@ target_sources(
handlers/Subscribe.cpp
handlers/TransactionEntry.cpp
handlers/Unsubscribe.cpp
handlers/VaultInfo.cpp
)
target_link_libraries(clio_rpc PRIVATE clio_util)

View File

@@ -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,

View File

@@ -43,6 +43,7 @@ enum class ClioError {
RpcFieldNotFoundTransaction = 5006,
RpcMalformedOracleDocumentId = 5007,
RpcMalformedAuthorizedCredentials = 5008,
RpcEntryNotFound = 5009,
// special system errors start with 6000
RpcInvalidApiVersion = 6000,

View File

@@ -30,6 +30,7 @@ std::unordered_set<std::string_view> const&
handledRpcs()
{
static std::unordered_set<std::string_view> const kHANDLED_RPCS = {
// clang-format off
"account_channels",
"account_currencies",
"account_info",
@@ -64,7 +65,9 @@ handledRpcs()
"tx",
"subscribe",
"unsubscribe",
"vault_info",
"version",
// clang-format on
};
return kHANDLED_RPCS;
}

View File

@@ -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 <boost/regex/v5/regex_fwd.hpp>
#include <boost/regex/v5/regex_match.hpp>
#include <fmt/core.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/AccountID.h>
@@ -50,9 +52,12 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STBase.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -60,9 +65,11 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TxMeta.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/jss.h>
#include <chrono>
#include <cstddef>

View File

@@ -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/config/ConfigDefinition.hpp"
@@ -114,6 +115,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}}},
}
{

View File

@@ -184,6 +184,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<ripple::AccountID>(boost::json::value_to<std::string>(input.vault->at(JS(owner))));
auto const seq = input.vault->at(JS(seq)).as_int64();
key = ripple::keylet::vault(*account, seq).key;
} else if (input.delegate) {
auto const account =
ripple::parseBase58<ripple::AccountID>(boost::json::value_to<std::string>(input.delegate->at(JS(account))));
@@ -214,13 +219,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;
}
@@ -326,6 +331,7 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
{JS(credential), ripple::ltCREDENTIAL},
{JS(mptoken), ripple::ltMPTOKEN},
{JS(permissioned_domain), ripple::ltPERMISSIONED_DOMAIN},
{JS(vault), ripple::ltVAULT},
{JS(delegate), ripple::ltDELEGATE}
};
@@ -415,6 +421,8 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, 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();
} else if (jsonObject.contains(JS(delegate))) {
input.delegate = jv.at(JS(delegate)).as_object();
}

View File

@@ -104,6 +104,7 @@ public:
std::optional<boost::json::object> amm;
std::optional<boost::json::object> mptoken;
std::optional<boost::json::object> permissionedDomain;
std::optional<boost::json::object> vault;
std::optional<ripple::STXChainBridge> bridge;
std::optional<std::string> bridgeAccount;
std::optional<uint32_t> chainClaimId;
@@ -393,6 +394,23 @@ public:
},
},
}}},
{JS(vault),
meta::WithCustomError{
validation::Type<std::string, boost::json::object>{}, Status(ClioError::RpcMalformedRequest)
},
meta::IfType<std::string>{kMALFORMED_REQUEST_HEX_STRING_VALIDATOR},
meta::IfType<boost::json::object>{meta::Section{
{JS(seq),
meta::WithCustomError{validation::Required{}, Status(ClioError::RpcMalformedRequest)},
meta::WithCustomError{validation::Type<uint32_t>{}, Status(ClioError::RpcMalformedRequest)}},
{
JS(owner),
meta::WithCustomError{validation::Required{}, Status(ClioError::RpcMalformedRequest)},
meta::WithCustomError{
validation::CustomValidators::accountBase58Validator, Status(ClioError::RpcMalformedOwner)
},
},
}}},
{JS(delegate),
meta::WithCustomError{
validation::Type<std::string, boost::json::object>{}, Status(ClioError::RpcMalformedRequest)

View File

@@ -0,0 +1,191 @@
//------------------------------------------------------------------------------
/*
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 <boost/json/conversion.hpp>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STBase.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/jss.h>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
namespace rpc {
namespace {
/**
* @brief Ensures that the input contains either a `vaultID` alone, or both `owner` and `tnxSequence`.
* Any other combination is considered malformed.
*
* @param input The input object containing optional fields for the vault request.
* @return Returns true if the input is valid, false otherwise.
*/
bool
validate(VaultInfoHandler::Input const& input)
{
bool const hasVaultId = input.vaultID.has_value();
bool const hasOwner = input.owner.has_value();
bool const hasSeq = input.tnxSequence.has_value();
// Only valid combinations: (vaultID) or (owner + ledgerIndex)
// NOLINTNEXTLINE(readability-simplify-boolean-expr)
return (hasVaultId && !hasOwner && !hasSeq) || (!hasVaultId && hasOwner && hasSeq);
}
} // namespace
VaultInfoHandler::VaultInfoHandler(std::shared_ptr<BackendInterface> const& sharedPtrBackend)
: sharedPtrBackend_{sharedPtrBackend}
{
}
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 (not validate(input))
return Error{ClioError::RpcMalformedRequest};
auto const range = sharedPtrBackend_->fetchLedgerRange();
ASSERT(range.has_value(), "VaultInfo's ledger range must be available");
auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq(
*sharedPtrBackend_, ctx.yield, std::nullopt, input.ledgerIndex, range->maxSequence
);
if (not expectedLgrInfo.has_value())
return Error{expectedLgrInfo.error()};
auto const& lgrInfo = *expectedLgrInfo;
// Extract the vault keylet based on input
auto const vaultKeylet = [&]() -> std::expected<ripple::Keylet, Status> {
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 (not 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 (not 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 (not 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
// follows same logic as rippled:
// https://github.com/XRPLF/rippled/pull/5224/files#diff-6cb544622c7942261f097d628f61f1c1fcf34a1bcfd954aedbada4238fc28f69R107
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<VaultInfoHandler::Input>, 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<uint32_t>(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 (not 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

View File

@@ -0,0 +1,134 @@
//------------------------------------------------------------------------------
/*
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 <boost/json/conversion.hpp>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/jss.h>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
namespace rpc {
/**
* @brief The vault_info command retrieves information about a vault, currency, shares etc.
*/
class VaultInfoHandler {
std::shared_ptr<BackendInterface> sharedPtrBackend_;
public:
/**
* @brief Construct a new VaultInfo object
*
* @param sharedPtrBackend The backend to use
*/
VaultInfoHandler(std::shared_ptr<BackendInterface> const& sharedPtrBackend);
/**
* @brief A struct to hold the input data for the command
*/
struct Input {
std::optional<std::string> vaultID;
std::optional<std::string> owner;
std::optional<uint32_t> tnxSequence;
std::optional<uint32_t> 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<Output>;
/**
* @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<uint32_t>{}, Status(ClioError::RpcMalformedRequest)}},
{JS(ledger_index), validation::CustomValidators::ledgerIndexValidator},
};
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<Input>, boost::json::value const& jv);
};
} // namespace rpc

View File

@@ -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),

View File

@@ -91,6 +91,7 @@ public:
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:

View File

@@ -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:

View File

@@ -41,6 +41,7 @@
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STIssue.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STVector256.h>
#include <xrpl/protocol/TER.h>
@@ -1642,3 +1643,37 @@ createAuthCredentialArray(std::vector<std::string_view> issuer, std::vector<std:
}
return arr;
}
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
)
{
auto vault = ripple::STObject(ripple::sfLedgerEntry);
vault.setAccountID(ripple::sfOwner, getAccountIdWithString(owner));
vault.setAccountID(ripple::sfAccount, getAccountIdWithString(account));
vault.setFieldU32(ripple::sfSequence, seq);
vault.setFieldU64(ripple::sfOwnerNode, ownerNode);
vault.setFieldH256(ripple::sfPreviousTxnID, previousTxId);
vault.setFieldU32(ripple::sfPreviousTxnLgrSeq, previousTxSeq);
vault.setFieldIssue(ripple::sfAsset, ripple::STIssue{ripple::sfAsset, getIssue(assetCurrency, assetIssuer)});
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});
vault.setFieldU8(ripple::sfWithdrawalPolicy, 200);
vault.setFieldU32(ripple::sfFlags, 0);
vault.setFieldU16(ripple::sfLedgerEntryType, ripple::ltVAULT);
return vault;
}

View File

@@ -524,3 +524,16 @@ createCredentialObject(
[[nodiscard]] ripple::STArray
createAuthCredentialArray(std::vector<std::string_view> issuer, std::vector<std::string_view> 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
);

View File

@@ -132,6 +132,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

View File

@@ -49,7 +49,9 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>

View File

@@ -47,6 +47,7 @@
#include "rpc/handlers/ServerInfo.hpp"
#include "rpc/handlers/Subscribe.hpp"
#include "rpc/handlers/TransactionEntry.hpp"
#include "rpc/handlers/VaultInfo.hpp"
#include "util/Assert.hpp"
#include "util/HandlerBaseTestFixture.hpp"
#include "util/MockAmendmentCenter.hpp"
@@ -79,6 +80,7 @@ static constexpr auto kAMM_ACCOUNT = "rLcS7XL6nxRAi7JcbJcn1Na179oF3vdfbh";
static constexpr auto kACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
static constexpr auto kNFT_ID = "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004";
static constexpr auto kCURRENCY = "0158415500000000C1F76FF6ECB0BAC600000000";
static constexpr auto kVAULT_ID = "61B03A6F8CEBD3AF9D8F696C3D0A9A9F0493B34BF6B5D93CF0BC009E6BA75303";
using AnyHandlerType = Types<
AccountChannelsHandler,
@@ -109,7 +111,8 @@ using AnyHandlerType = Types<
NoRippleCheckHandler,
TestServerInfoHandler,
SubscribeHandler,
TransactionEntryHandler>;
TransactionEntryHandler,
VaultInfoHandler>;
template <typename HandlerType>
struct AllHandlersAssertTest : common::util::WithMockAssert,
@@ -255,6 +258,16 @@ createInput<SubscribeHandler>()
return input;
}
template <>
VaultInfoHandler::Input
createInput<VaultInfoHandler>()
{
VaultInfoHandler::Input input{};
input.vaultID = kVAULT_ID;
return input;
}
TYPED_TEST_CASE(AllHandlersAssertTest, AnyHandlerType);
TYPED_TEST(AllHandlersAssertTest, NoRangeAvailable)

View File

@@ -44,8 +44,10 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STXChainBridge.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/UintTypes.h>
#include <cstdint>
@@ -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.",
},
ParamTestCaseBundle{
.testName = "Delegate_InvalidType",
.testJson = R"JSON({"delegate": 123})JSON",
@@ -3058,6 +3130,55 @@ generateTestValuesForNormalPathTest()
.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<ripple::AccountID>(kACCOUNT).value(), kRANGE_MAX).key,
.mockedEntity = createVault(
kACCOUNT,
kACCOUNT,
kRANGE_MAX,
"XRP",
ripple::toBase58(ripple::xrpAccount()),
ripple::uint192(0),
0,
ripple::uint256{0},
0
)
},
NormalPathTestBundle{
.testName = "DelegateViaStringIndex",
.testJson = fmt::format(
@@ -3174,6 +3295,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<ripple::AccountID>(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"JSON({{
"binary": false,
"vault": {{
"owner": "{}",
"seq": {}
}}
}})JSON",
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

View File

@@ -0,0 +1,450 @@
//------------------------------------------------------------------------------
/*
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 <boost/json/parse.hpp>
#include <boost/json/value.hpp>
#include <fmt/core.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
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<VaultInfoParamTestCaseBundle> {};
static auto
generateTestValuesForParametersTest()
{
return std::vector<VaultInfoParamTestCaseBundle>{
VaultInfoParamTestCaseBundle{
.testName = "RandomField",
.testJson = R"JSON({
"idk": "idk"
})JSON",
.expectedError = "malformedRequest",
.expectedErrorMessage = "Malformed request."
},
VaultInfoParamTestCaseBundle{
.testName = "MissingOwnerInVault",
.testJson = R"JSON({
"seq": 4
})JSON",
.expectedError = "malformedRequest",
.expectedErrorMessage = "Malformed request."
},
VaultInfoParamTestCaseBundle{
.testName = "MissingSeqInVault",
.testJson = R"JSON({
"owner": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"
})JSON",
.expectedError = "malformedRequest",
.expectedErrorMessage = "Malformed request."
},
VaultInfoParamTestCaseBundle{
.testName = "SeqNotAnInteger",
.testJson = R"JSON({
"owner": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"seq": "asdf"
})JSON",
.expectedError = "malformedRequest",
.expectedErrorMessage = "Malformed request."
},
VaultInfoParamTestCaseBundle{
.testName = "OwnerNotAString",
.testJson = R"JSON({
"owner": true,
"seq": 3
})JSON",
.expectedError = "malformedRequest",
.expectedErrorMessage = "OwnerNotHexString"
},
VaultInfoParamTestCaseBundle{
.testName = "OwnerNotAHexString",
.testJson = R"JSON({
"owner": "asdf",
"seq": 3
})JSON",
.expectedError = "malformedRequest",
.expectedErrorMessage = "OwnerNotHexString"
},
VaultInfoParamTestCaseBundle{
.testName = "vaultIDNotString",
.testJson = R"JSON({
"vault_id": 3
})JSON",
.expectedError = "malformedRequest",
.expectedErrorMessage = "Malformed request."
},
VaultInfoParamTestCaseBundle{
.testName = "vaultIDNotHex256",
.testJson = R"JSON({
"vault_id": "idk"
})JSON",
.expectedError = "malformedRequest",
.expectedErrorMessage = "Malformed request."
},
VaultInfoParamTestCaseBundle{
.testName = "vaultIDWithOwner",
.testJson = fmt::format(
R"JSON({{
"vault_id": "{}",
"owner": "{}"
}})JSON",
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"JSON({{
"owner": "{}",
"seq": 3
}})JSON",
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};
EXPECT_CALL(*backend_, doFetchLedgerObject(vaultKey, kSEQ, _))
.WillOnce(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;
EXPECT_CALL(*backend_, doFetchLedgerObject(vaultKeylet, kSEQ, _))
.WillOnce(Return(vault.getSerializer().peekData()));
EXPECT_CALL(*backend_, doFetchLedgerObject(mptIssuance, kSEQ, _))
.WillOnce(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"JSON({
"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"
}
}
})JSON";
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;
EXPECT_CALL(*backend_, doFetchLedgerObject(vaultKeylet, kSEQ, _))
.WillOnce(Return(vault.getSerializer().peekData()));
EXPECT_CALL(*backend_, doFetchLedgerObject(mptIssuance, kSEQ, _))
.WillOnce(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"JSON({
"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"
}
}
})JSON";
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;
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKeylet, kSEQ, _))
.WillOnce(Return(accountRoot.getSerializer().peekData()));
EXPECT_CALL(*backend_, doFetchLedgerObject(vaultKeylet, kSEQ, _))
.WillOnce(Return(vault.getSerializer().peekData()));
EXPECT_CALL(*backend_, doFetchLedgerObject(mptIssuance, kSEQ, _))
.WillOnce(Return(issuance.getSerializer().peekData()));
// Input JSON using vault object
auto static const kINPUT = boost::json::parse(fmt::format(
R"JSON({{
"owner": "{}",
"seq": {},
"ledger_index": 30
}})JSON",
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));
});
}

View File

@@ -57,6 +57,7 @@ TEST(LedgerUtilsTests, LedgerObjectTypeList)
JS(permissioned_domain),
JS(oracle),
JS(credential),
JS(vault),
JS(nunl),
JS(delegate)
};
@@ -92,6 +93,7 @@ TEST(LedgerUtilsTests, AccountOwnedTypeList)
JS(mpt_issuance),
JS(mptoken),
JS(permissioned_domain),
JS(vault),
JS(delegate)
};