Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -56,6 +56,7 @@ target_sources(
handlers/TransactionEntry.cpp
handlers/Unsubscribe.cpp
handlers/VaultInfo.cpp
handlers/VaultList.cpp
)

target_link_libraries(clio_rpc PRIVATE clio_util)
1 change: 1 addition & 0 deletions src/rpc/RPCCenter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ handledRpcs()
"subscribe",
"unsubscribe",
"vault_info",
"vault_list",
"version",
// clang-format on
};
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 @@ -44,6 +44,7 @@
#include "rpc/handlers/Tx.hpp"
#include "rpc/handlers/Unsubscribe.hpp"
#include "rpc/handlers/VaultInfo.hpp"
#include "rpc/handlers/VaultList.hpp"
#include "rpc/handlers/VersionHandler.hpp"
#include "util/config/ConfigDefinition.hpp"

Expand Down Expand Up @@ -109,6 +110,7 @@ ProductionHandlerProvider::ProductionHandlerProvider(
{.handler = SubscribeHandler{backend, amendmentCenter, subscriptionManager}}},
{"unsubscribe", {.handler = UnsubscribeHandler{subscriptionManager}}},
{"vault_info", {.handler = VaultInfoHandler{backend}}},
{"vault_list", {.handler = VaultListHandler{backend}, .isClioOnly = true}},
{"version", {.handler = VersionHandler{config}}},
}
{
Expand Down
200 changes: 200 additions & 0 deletions src/rpc/handlers/VaultList.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
#include "rpc/handlers/VaultList.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 "util/JsonUtils.hpp"

#include <boost/json/array.hpp>
#include <boost/json/conversion.hpp>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <boost/json/value_to.hpp>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.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>
#include <utility>

namespace rpc {

VaultListHandler::VaultListHandler(std::shared_ptr<BackendInterface> sharedPtrBackend)
: sharedPtrBackend_(std::move(sharedPtrBackend))
{
}

VaultListHandler::Result
VaultListHandler::process(VaultListHandler::Input const& input, Context const& ctx) const
{
auto const range = sharedPtrBackend_->fetchLedgerRange();
ASSERT(range.has_value(), "VaultList's ledger range must be available");

auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq(
*sharedPtrBackend_, ctx.yield, input.ledgerHash, input.ledgerIndex, range->maxSequence
);

if (not expectedLgrInfo.has_value())
return Error{expectedLgrInfo.error()};

auto const& lgrInfo = *expectedLgrInfo;

// Parse the MPT Issuance ID from input
auto const mptID = ripple::uint192{input.tokenID.c_str()};
Comment on lines +56 to +57
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will check if this is correct way to name/get the id


// Fetch the MPT Issuance object to validate it exists and get the issuer
auto const issuanceKeylet = ripple::keylet::mptIssuance(mptID).key;
auto const issuanceObject =
sharedPtrBackend_->fetchLedgerObject(issuanceKeylet, lgrInfo.seq, ctx.yield);

if (!issuanceObject)
return Error{Status{RippledError::rpcOBJECT_NOT_FOUND, "tokenNotFound"}};

ripple::STLedgerEntry const issuanceSle{
ripple::SerialIter{issuanceObject->data(), issuanceObject->size()}, issuanceKeylet
};

auto const issuerAccountID = issuanceSle.getAccountID(ripple::sfIssuer);

// Verify the issuer account exists
auto const accountKeylet = ripple::keylet::account(issuerAccountID);
auto const accountLedgerObject =
sharedPtrBackend_->fetchLedgerObject(accountKeylet.key, lgrInfo.seq, ctx.yield);

if (!accountLedgerObject)
return Error{Status{RippledError::rpcACT_NOT_FOUND}};

// Traverse owned nodes filtering for Vault objects
Output response;
response.tokenID = input.tokenID;
response.ledgerHash = ripple::strHex(lgrInfo.hash);
response.ledgerIndex = lgrInfo.seq;
response.limit = input.limit;

auto const addToResponse = [&](ripple::SLE&& sle) {
if (sle.getType() != ripple::ltVAULT)
return true;

boost::json::object summary;

auto vaultSle = std::move(sle);

summary[JS(vault_id)] = ripple::strHex(vaultSle.key());
summary[JS(account)] = ripple::toBase58(vaultSle.getAccountID(ripple::sfAccount));
summary[JS(owner)] = ripple::toBase58(vaultSle.getAccountID(ripple::sfOwner));

// AssetsTotal is an STNumber field; extract via JSON serialization
auto const vaultJson = toBoostJson(vaultSle.getJson(ripple::JsonOptions::none));
if (vaultJson.as_object().contains("AssetsTotal")) {
summary["total_assets"] = vaultJson.as_object().at("AssetsTotal");
} else {
summary["total_assets"] = "0";
}

// Fetch the share MPT issuance to get total_shares (OutstandingAmount)
auto const issuanceKeylet = ripple::keylet::mptIssuance(vaultSle[ripple::sfShareMPTID]).key;
auto const shareIssuanceObject =
sharedPtrBackend_->fetchLedgerObject(issuanceKeylet, lgrInfo.seq, ctx.yield);

if (shareIssuanceObject.has_value()) {
ripple::STLedgerEntry const shareSle{
ripple::SerialIter{shareIssuanceObject->data(), shareIssuanceObject->size()},
issuanceKeylet
};
summary["total_shares"] = shareSle.getFieldU64(ripple::sfOutstandingAmount);
} else {
summary["total_shares"] = std::uint64_t{0};
}

// Status derived from flags
auto const flags = vaultSle.getFlags();
// A vault with no special flags set is considered active
summary[JS(status)] = (flags == 0) ? "active" : "modified";
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: No JS for active/modified?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will check how to correctly name them, there is already closed with no JS for the status in the repo

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Tapanito what's the best way to call this status?
The 0 is the default value from the XLS: https://github.com/XRPLF/XRPL-Standards/blob/master/XLS-0066-lending-protocol/README.md#322-fields

But I'm not sure what naming we should choose here

summary[JS(flags)] = flags;

response.vaults.push_back(summary);
return true;
};

auto const expectedNext = traverseOwnedNodes(
*sharedPtrBackend_,
issuerAccountID,
lgrInfo.seq,
input.limit,
input.marker,
ctx.yield,
addToResponse,
/* nftIncluded =*/false
);

if (not expectedNext.has_value())
return Error{expectedNext.error()};

auto const& nextMarker = expectedNext.value();
if (nextMarker.isNonZero())
response.marker = nextMarker.toString();

return response;
}

void
tag_invoke(
boost::json::value_from_tag,
boost::json::value& jv,
VaultListHandler::Output const& output
)
{
jv = boost::json::object{
{"token_id", output.tokenID},
{JS(ledger_hash), output.ledgerHash},
{JS(ledger_index), output.ledgerIndex},
{JS(validated), output.validated},
{JS(limit), output.limit},
{"vaults", output.vaults},
};

if (output.marker.has_value())
jv.as_object()[JS(marker)] = *(output.marker);
}

VaultListHandler::Input
tag_invoke(boost::json::value_to_tag<VaultListHandler::Input>, boost::json::value const& jv)
{
auto input = VaultListHandler::Input{};
auto const& jsonObject = jv.as_object();

input.tokenID = boost::json::value_to<std::string>(jsonObject.at("token_id"));

if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jsonObject.at(JS(ledger_hash)));

if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}

if (jsonObject.contains(JS(limit)))
input.limit = util::integralValueAs<uint32_t>(jsonObject.at(JS(limit)));

if (jsonObject.contains(JS(marker)))
input.marker = boost::json::value_to<std::string>(jsonObject.at(JS(marker)));

return input;
}

} // namespace rpc
126 changes: 126 additions & 0 deletions src/rpc/handlers/VaultList.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#pragma once

#include "data/BackendInterface.hpp"
#include "rpc/JS.hpp"
#include "rpc/common/Modifiers.hpp"
#include "rpc/common/Specs.hpp"
#include "rpc/common/Types.hpp"
#include "rpc/common/Validators.hpp"

#include <boost/json/array.hpp>
#include <boost/json/conversion.hpp>
#include <boost/json/value.hpp>
#include <xrpl/protocol/jss.h>

#include <cstdint>
#include <memory>
#include <optional>
#include <string>

namespace rpc {

/**
* @brief The vault_list command retrieves all Vaults created for a given Token.
*
* Given an MPT Issuance ID (token_id), this handler looks up the token issuer and traverses
* their owner directory to find all Vault ledger objects. For each vault found, a summary is
* returned containing the vault ID, pseudo account, owner, total assets, total shares, status,
* and flags.
*/
class VaultListHandler {
std::shared_ptr<BackendInterface> sharedPtrBackend_;

public:
static constexpr auto kLIMIT_MIN = 10;
static constexpr auto kLIMIT_MAX = 400;
static constexpr auto kLIMIT_DEFAULT = 200;

/**
* @brief A struct to hold the output data of the command
*/
struct Output {
std::string tokenID;
boost::json::array vaults;
std::string ledgerHash;
uint32_t ledgerIndex{};
std::optional<std::string> marker;
uint32_t limit{};
bool validated = true;
};

/**
* @brief A struct to hold the input data for the command
*/
struct Input {
std::string tokenID;
std::optional<std::string> ledgerHash;
std::optional<uint32_t> ledgerIndex;
uint32_t limit = kLIMIT_DEFAULT;
std::optional<std::string> marker;
};

using Result = HandlerReturnType<Output>;

/**
* @brief Construct a new VaultListHandler object
*
* @param sharedPtrBackend The backend to use
*/
VaultListHandler(std::shared_ptr<BackendInterface> sharedPtrBackend);

/**
* @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{
{"token_id",
validation::Required{},
validation::CustomValidators::uint192HexStringValidator},
{JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator},
{JS(ledger_index), validation::CustomValidators::ledgerIndexValidator},
{JS(limit),
validation::Type<uint32_t>{},
validation::Min(1u),
modifiers::Clamp<int32_t>(kLIMIT_MIN, kLIMIT_MAX)},
{JS(marker), validation::CustomValidators::accountMarkerValidator},
};

return kRPC_SPEC;
}

/**
* @brief Process the VaultList 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 const& input, Context const& ctx) const;

private:
/**
* @brief Convert the Output to a JSON object
*
* @param [out] 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
1 change: 1 addition & 0 deletions tests/unit/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ target_sources(
rpc/handlers/UnsubscribeTests.cpp
rpc/handlers/VersionHandlerTests.cpp
rpc/handlers/VaultInfoTests.cpp
rpc/handlers/VaultListTests.cpp
rpc/JsonBoolTests.cpp
rpc/RPCEngineTests.cpp
rpc/RPCHelpersTests.cpp
Expand Down
Loading
Loading