Skip to content

feat: Support single asset vault #1979

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Jun 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1e50ec2
save work
PeterChen13579 Mar 25, 2025
0fd18fd
feat: Add Support for Single Asset Vault
PeterChen13579 Mar 27, 2025
c9c2c98
add test for Supplement Json method
PeterChen13579 Mar 31, 2025
d733294
add handler + tests
PeterChen13579 Apr 9, 2025
c83fcd7
Merge branch 'develop' into SupportSingleAssetVault
PeterChen13579 Apr 23, 2025
ee676aa
Merge branch 'develop' into SupportSingleAssetVault
PeterChen13579 Apr 25, 2025
c1c2ec1
more test
PeterChen13579 Apr 9, 2025
6a4573e
more tests
PeterChen13579 Apr 11, 2025
2805001
save work for now
PeterChen13579 Apr 25, 2025
0037eba
finish vault
PeterChen13579 Apr 29, 2025
fdc024b
Merge branch 'develop' into SupportSingleAssetVault
PeterChen13579 Apr 29, 2025
c9a2d68
Trigger pre-commit hook
PeterChen13579 Apr 29, 2025
5f1aafd
Merge branch 'develop' into SupportSingleAssetVault
PeterChen13579 May 16, 2025
9ecda27
fix ut and doxygen
PeterChen13579 May 16, 2025
7b72774
Merge remote-tracking branch 'upstream/develop' into SupportSingleAss…
PeterChen13579 May 30, 2025
f512e28
more test coverage
PeterChen13579 Jun 2, 2025
07cc14f
Merge branch 'develop' into SupportSingleAssetVault
kuznetsss Jun 23, 2025
2354bf8
Remove duplicated amendment
kuznetsss Jun 23, 2025
bf99e5f
Fix build
kuznetsss Jun 23, 2025
320faf2
Merge branch 'develop' into SupportSingleAssetVault
kuznetsss Jun 27, 2025
ff8f2c5
Improve tests
kuznetsss Jun 27, 2025
6b44d46
One more JSON
kuznetsss Jun 27, 2025
980d07d
Merge branch 'develop' into SupportSingleAssetVault
kuznetsss Jun 27, 2025
b0076ef
Fix review comments
kuznetsss Jun 27, 2025
f04da55
Fix review comments
kuznetsss Jun 27, 2025
517847d
Update tests/unit/rpc/handlers/LedgerEntryTests.cpp
kuznetsss Jun 27, 2025
1a068ec
Update tests/unit/rpc/handlers/VaultInfoTests.cpp
kuznetsss Jun 27, 2025
99803fb
Update tests/unit/rpc/handlers/LedgerEntryTests.cpp
kuznetsss Jun 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/rpc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion src/rpc/Errors.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/rpc/Errors.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ enum class ClioError {
RpcFieldNotFoundTransaction = 5006,
RpcMalformedOracleDocumentId = 5007,
RpcMalformedAuthorizedCredentials = 5008,
RpcEntryNotFound = 5009,

// special system errors start with 6000
RpcInvalidApiVersion = 6000,
Expand Down
3 changes: 3 additions & 0 deletions src/rpc/RPCCenter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -64,7 +65,9 @@ handledRpcs()
"tx",
"subscribe",
"unsubscribe",
"vault_info",
"version",
// clang-format on
};
return kHANDLED_RPCS;
}
Expand Down
7 changes: 7 additions & 0 deletions src/rpc/RPCHelpers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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>
Expand All @@ -50,19 +52,24 @@
#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>
#include <xrpl/protocol/STObject.h>
#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>
Expand Down
2 changes: 2 additions & 0 deletions src/rpc/common/impl/HandlerProvider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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}}},
}
{
Expand Down
14 changes: 11 additions & 3 deletions src/rpc/handlers/LedgerEntry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@
);
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))));
Expand Down Expand Up @@ -214,13 +219,13 @@

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;
}

Expand Down Expand Up @@ -326,6 +331,7 @@
{JS(credential), ripple::ltCREDENTIAL},
{JS(mptoken), ripple::ltMPTOKEN},
{JS(permissioned_domain), ripple::ltPERMISSIONED_DOMAIN},
{JS(vault), ripple::ltVAULT},

Check warning on line 334 in src/rpc/handlers/LedgerEntry.cpp

View check run for this annotation

Codecov / codecov/patch

src/rpc/handlers/LedgerEntry.cpp#L334

Added line #L334 was not covered by tests
{JS(delegate), ripple::ltDELEGATE}
};

Expand Down Expand Up @@ -415,6 +421,8 @@
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();
}
Expand Down
18 changes: 18 additions & 0 deletions src/rpc/handlers/LedgerEntry.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class LedgerEntryHandler {
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;
Expand Down Expand Up @@ -393,6 +394,23 @@ class LedgerEntryHandler {
},
},
}}},
{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)
Expand Down
191 changes: 191 additions & 0 deletions src/rpc/handlers/VaultInfo.cpp
Original file line number Diff line number Diff line change
@@ -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> {

Check warning on line 98 in src/rpc/handlers/VaultInfo.cpp

View check run for this annotation

Codecov / codecov/patch

src/rpc/handlers/VaultInfo.cpp#L98

Added line #L98 was not covered by tests
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}};

Check warning on line 119 in src/rpc/handlers/VaultInfo.cpp

View check run for this annotation

Codecov / codecov/patch

src/rpc/handlers/VaultInfo.cpp#L119

Added line #L119 was not covered by tests
}();

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;
}

Check warning on line 189 in src/rpc/handlers/VaultInfo.cpp

View check run for this annotation

Codecov / codecov/patch

src/rpc/handlers/VaultInfo.cpp#L189

Added line #L189 was not covered by tests

} // namespace rpc
Loading