mirror of
https://github.com/XRPLF/clio.git
synced 2025-11-25 14:15:53 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -43,6 +43,7 @@ enum class ClioError {
|
||||
RpcFieldNotFoundTransaction = 5006,
|
||||
RpcMalformedOracleDocumentId = 5007,
|
||||
RpcMalformedAuthorizedCredentials = 5008,
|
||||
RpcEntryNotFound = 5009,
|
||||
|
||||
// special system errors start with 6000
|
||||
RpcInvalidApiVersion = 6000,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}}},
|
||||
}
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
191
src/rpc/handlers/VaultInfo.cpp
Normal file
191
src/rpc/handlers/VaultInfo.cpp
Normal 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
|
||||
134
src/rpc/handlers/VaultInfo.hpp
Normal file
134
src/rpc/handlers/VaultInfo.hpp
Normal 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
|
||||
@@ -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),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
450
tests/unit/rpc/handlers/VaultInfoTests.cpp
Normal file
450
tests/unit/rpc/handlers/VaultInfoTests.cpp
Normal 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));
|
||||
});
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user