diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index e563c31d50..c363de7d89 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -450,6 +450,7 @@ JSS(node_reads_hit); // out: GetCounts JSS(node_reads_total); // out: GetCounts JSS(node_reads_duration_us); // out: GetCounts JSS(node_size); // out: server_info +JSS(nodes); // out: VaultInfo JSS(nodestore); // out: GetCounts JSS(node_writes); // out: GetCounts JSS(node_written_bytes); // out: GetCounts diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 352d07f9e5..33ac33e2cc 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -802,19 +802,17 @@ class Vault_test : public beast::unit_test::suite auto const vaultAccount = // [&env, key = keylet.key, this]() -> AccountID { - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::vault] = strHex(key); - auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); + auto jvVault = env.rpc("vault_info", strHex(key)); BEAST_EXPECT( - jvVault[jss::result][jss::node][jss::SharesTotal] == "100"); + jvVault[jss::result][jss::nodes][0u][sfAssetsTotal] == "100"); BEAST_EXPECT( - jvVault[jss::result][jss::node][sfAssetsTotal.fieldName] == + jvVault[jss::result][jss::nodes][1u][sfOutstandingAmount] == "100"); // Vault pseudo-account return parseBase58( - jvVault[jss::result][jss::node][jss::Account].asString()) + jvVault[jss::result][jss::nodes][0u][jss::Account] + .asString()) .value(); }(); @@ -843,15 +841,11 @@ class Vault_test : public beast::unit_test::suite { testcase("nontransferable shares balance check"); - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::vault] = strHex(keylet.key); - auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); - + auto jvVault = env.rpc("vault_info", strHex(keylet.key)); BEAST_EXPECT( - jvVault[jss::result][jss::node][jss::SharesTotal] == "50"); + jvVault[jss::result][jss::nodes][0u][sfAssetsTotal] == "50"); BEAST_EXPECT( - jvVault[jss::result][jss::node][sfAssetsTotal.fieldName] == + jvVault[jss::result][jss::nodes][1u][sfOutstandingAmount] == "50"); } @@ -1383,52 +1377,66 @@ class Vault_test : public beast::unit_test::suite }(); auto const check = [&, keylet = keylet, sle = sleVault, this]( - Json::Value const& node) { - BEAST_EXPECT(node.isObject()); + Json::Value const& vault, + Json::Value const& issuance = Json::nullValue) { + BEAST_EXPECT(vault.isObject()); - auto checkString = - [&node](SField const& field, std::string expected) -> bool { + constexpr auto checkString = + [](auto& node, SField const& field, std::string v) -> bool { return node.isMember(field.fieldName) && node[field.fieldName].isString() && - node[field.fieldName] == expected; + node[field.fieldName] == v; }; - auto checkObject = - [&node](SField const& field, Json::Value expected) -> bool { + constexpr auto checkObject = + [](auto& node, SField const& field, Json::Value v) -> bool { return node.isMember(field.fieldName) && node[field.fieldName].isObject() && - node[field.fieldName] == expected; + node[field.fieldName] == v; }; - auto checkInt = [&node](SField const& field, int expected) -> bool { + constexpr auto checkInt = + [](auto& node, SField const& field, int v) -> bool { return node.isMember(field.fieldName) && ((node[field.fieldName].isInt() && - node[field.fieldName] == Json::Int(expected)) || + node[field.fieldName] == Json::Int(v)) || (node[field.fieldName].isUInt() && - node[field.fieldName] == Json::UInt(expected))); + node[field.fieldName] == Json::UInt(v))); }; - BEAST_EXPECT(node["LedgerEntryType"].asString() == "Vault"); - BEAST_EXPECT(node[jss::index].asString() == strHex(keylet.key)); - BEAST_EXPECT(checkInt(sfFlags, 0)); + BEAST_EXPECT(vault["LedgerEntryType"].asString() == "Vault"); + BEAST_EXPECT(vault[jss::index].asString() == strHex(keylet.key)); + BEAST_EXPECT(checkInt(vault, sfFlags, 0)); // Ignore all other standard fields, this test doesn't care - BEAST_EXPECT(checkString(sfAccount, toBase58(sle->at(sfAccount)))); - BEAST_EXPECT(checkObject(sfAsset, to_json(sle->at(sfAsset)))); - BEAST_EXPECT(checkString(sfAssetsAvailable, "50")); - BEAST_EXPECT(checkString(sfAssetsMaximum, "1000")); - BEAST_EXPECT(checkString(sfAssetsTotal, "50")); - BEAST_EXPECT(checkString(sfLossUnrealized, "0")); BEAST_EXPECT( - checkString(sfShareMPTID, strHex(sle->at(sfShareMPTID)))); - BEAST_EXPECT(checkString(sfOwner, toBase58(owner.id()))); - BEAST_EXPECT(checkInt(sfSequence, sequence)); + checkString(vault, sfAccount, toBase58(sle->at(sfAccount)))); BEAST_EXPECT( - checkInt(sfWithdrawalPolicy, vaultStrategyFirstComeFirstServe)); + checkObject(vault, sfAsset, to_json(sle->at(sfAsset)))); + BEAST_EXPECT(checkString(vault, sfAssetsAvailable, "50")); + BEAST_EXPECT(checkString(vault, sfAssetsMaximum, "1000")); + BEAST_EXPECT(checkString(vault, sfAssetsTotal, "50")); + BEAST_EXPECT(checkString(vault, sfLossUnrealized, "0")); - // This field is injected in RPC::supplementJson - BEAST_EXPECT( - node.isMember(jss::SharesTotal) && - node[jss::SharesTotal].isString() && - node[jss::SharesTotal] == "50"); + auto const strShareID = strHex(sle->at(sfShareMPTID)); + BEAST_EXPECT(checkString(vault, sfShareMPTID, strShareID)); + BEAST_EXPECT(checkString(vault, sfOwner, toBase58(owner.id()))); + BEAST_EXPECT(checkInt(vault, sfSequence, sequence)); + BEAST_EXPECT(checkInt( + vault, sfWithdrawalPolicy, vaultStrategyFirstComeFirstServe)); + + if (issuance.isObject()) + { + BEAST_EXPECT( + issuance["LedgerEntryType"].asString() == + "MPTokenIssuance"); + BEAST_EXPECT( + issuance[jss::mpt_issuance_id].asString() == strShareID); + BEAST_EXPECT(checkInt(issuance, sfSequence, 1)); + BEAST_EXPECT(checkInt( + issuance, + sfFlags, + int(lsfMPTCanEscrow | lsfMPTCanTrade | lsfMPTCanTransfer))); + BEAST_EXPECT(checkString(issuance, sfOutstandingAmount, "50")); + } }; { @@ -1537,6 +1545,74 @@ class Vault_test : public beast::unit_test::suite BEAST_EXPECT(jv[jss::result][jss::state].size() == 1); check(jv[jss::result][jss::state][0u]); } + + { + testcase("RPC vault_info command line"); + Json::Value jv = + env.rpc("vault_info", strHex(keylet.key), "validated"); + + BEAST_EXPECT(!jv[jss::result].isMember(jss::error)); + BEAST_EXPECT(jv[jss::result].isMember(jss::nodes)); + BEAST_EXPECT(jv[jss::result][jss::nodes].isArray()); + BEAST_EXPECT(jv[jss::result].isMember(jss::directory)); + BEAST_EXPECT(jv[jss::result][jss::directory][jss::vault] == 0); + BEAST_EXPECT( + jv[jss::result][jss::directory][jss::mpt_issuance] == 1); + check( + jv[jss::result][jss::nodes][0u], + jv[jss::result][jss::nodes][1u]); + } + + { + testcase("RPC vault_info json by owner and sequence"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault][jss::owner] = owner.human(); + jvParams[jss::vault][jss::seq] = sequence; + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + + BEAST_EXPECT(!jv[jss::result].isMember(jss::error)); + BEAST_EXPECT(jv[jss::result].isMember(jss::nodes)); + BEAST_EXPECT(jv[jss::result][jss::nodes].isArray()); + BEAST_EXPECT(jv[jss::result].isMember(jss::directory)); + BEAST_EXPECT(jv[jss::result][jss::directory][jss::vault] == 0); + BEAST_EXPECT( + jv[jss::result][jss::directory][jss::mpt_issuance] == 1); + check( + jv[jss::result][jss::nodes][0u], + jv[jss::result][jss::nodes][1u]); + } + + { + testcase("RPC vault_info command line invalid index"); + Json::Value jv = env.rpc("vault_info", "0", "validated"); + BEAST_EXPECT(jv[jss::error].asString() == "invalidParams"); + } + + { + testcase("RPC vault_info json invalid index"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault] = 0; + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "malformedRequest"); + } + + { + testcase("RPC vault_info command line invalid index"); + Json::Value jv = + env.rpc("vault_info", strHex(uint256(42)), "validated"); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "entryNotFound"); + } + + { + testcase("RPC vault_info command line invalid ledger"); + Json::Value jv = env.rpc("vault_info", strHex(keylet.key), "0"); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "lgrNotFound"); + } } public: diff --git a/src/xrpld/net/detail/RPCCall.cpp b/src/xrpld/net/detail/RPCCall.cpp index 814ea362e4..1d6c904ab1 100644 --- a/src/xrpld/net/detail/RPCCall.cpp +++ b/src/xrpld/net/detail/RPCCall.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -862,6 +863,25 @@ private: return jvRequest; } + Json::Value + parseVault(Json::Value const& jvParams) + { + std::string strVaultID = jvParams[0u].asString(); + uint256 id = beast::zero; + if (!id.parseHex(strVaultID)) + return rpcError(rpcINVALID_PARAMS); + if (id == beast::zero) + return rpcError(rpcINVALID_PARAMS); + + Json::Value jvRequest(Json::objectValue); + jvRequest[jss::vault] = strVaultID; + + if (jvParams.size() > 1) + jvParseLedger(jvRequest, jvParams[1u].asString()); + + return jvRequest; + } + // peer_reservations_add [] Json::Value parsePeerReservationsAdd(Json::Value const& jvParams) @@ -1208,6 +1228,7 @@ public: {"account_offers", &RPCParser::parseAccountItems, 1, 4}, {"account_tx", &RPCParser::parseAccountTransactions, 1, 8}, {"amm_info", &RPCParser::parseAsIs, 1, 2}, + {"vault_info", &RPCParser::parseVault, 1, 2}, {"book_changes", &RPCParser::parseLedgerId, 1, 1}, {"book_offers", &RPCParser::parseBookOffers, 2, 7}, {"can_delete", &RPCParser::parseCanDelete, 0, 1}, diff --git a/src/xrpld/rpc/detail/Handler.cpp b/src/xrpld/rpc/detail/Handler.cpp index abdfa921bb..c2ffb1c125 100644 --- a/src/xrpld/rpc/detail/Handler.cpp +++ b/src/xrpld/rpc/detail/Handler.cpp @@ -93,6 +93,7 @@ Handler const handlerArray[]{ {"account_offers", byRef(&doAccountOffers), Role::USER, NO_CONDITION}, {"account_tx", byRef(&doAccountTxJson), Role::USER, NO_CONDITION}, {"amm_info", byRef(&doAMMInfo), Role::USER, NO_CONDITION}, + {"vault_info", byRef(&doVaultInfo), Role::USER, NO_CONDITION}, {"blacklist", byRef(&doBlackList), Role::ADMIN, NO_CONDITION}, {"book_changes", byRef(&doBookChanges), Role::USER, NO_CONDITION}, {"book_offers", byRef(&doBookOffers), Role::USER, NO_CONDITION}, diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index 7136c22e1b..ee246e0e19 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -149,26 +149,6 @@ isRelatedToAccount( return false; } -template <> -void -supplementJson( - ReadView const& view, - std::shared_ptr const& vault, - Json::Value& node) -{ - XRPL_ASSERT( - vault->getType() == ltVAULT, - "ripple::RPC::supplementJson : matching type"); - - auto const share = vault->at(sfShareMPTID); - auto const sleIssuance = view.read(keylet::mptIssuance(share)); - if (!sleIssuance) - return; - - node[jss::SharesTotal] = - Number(sleIssuance->getFieldU64(sfOutstandingAmount)); -} - bool getAccountObjects( ReadView const& ledger, @@ -320,10 +300,7 @@ getAccountObjects( if (!typeFilter.has_value() || typeMatchesFilter(typeFilter.value(), sleNode->getType())) { - auto& entry = - jvObjects.append(sleNode->getJson(JsonOptions::none)); - if (sleNode->getType() == ltVAULT) - RPC::supplementJson(ledger, sleNode, entry); + jvObjects.append(sleNode->getJson(JsonOptions::none)); } if (++i == mlimit) @@ -1163,5 +1140,37 @@ getLedgerByContext(RPC::JsonContext& context) return RPC::make_error( rpcNOT_READY, "findCreate failed to return an inbound ledger"); } + +std::optional +parseVault(Json::Value const& params, Json::Value& jvResult) +{ + if (!params.isObject()) + { + uint256 uNodeIndex; + if (!uNodeIndex.parseHex(params.asString())) + { + jvResult[jss::error] = "malformedRequest"; + return std::nullopt; + } + return uNodeIndex; + } + + if (!params.isMember(jss::owner) || !params.isMember(jss::seq) || + !params[jss::seq].isIntegral()) + { + jvResult[jss::error] = "malformedRequest"; + return std::nullopt; + } + + auto const id = parseBase58(params[jss::owner].asString()); + if (!id) + { + jvResult[jss::error] = "malformedOwner"; + return std::nullopt; + } + + return keylet::vault(*id, params[jss::seq].asUInt()).key; +} + } // namespace RPC } // namespace ripple diff --git a/src/xrpld/rpc/detail/RPCHelpers.h b/src/xrpld/rpc/detail/RPCHelpers.h index ef3f3acb4d..da12304177 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.h +++ b/src/xrpld/rpc/detail/RPCHelpers.h @@ -270,29 +270,12 @@ keypairForSignature( Json::Value& error, unsigned int apiVersion = apiVersionIfUnspecified); -/** - * Supplement JSON of a ledger object with additional data read from a view - * - * To be specialized per ledger object type as needed; any specialization of - * this function must be declared below and defined in RPCHelpers.cpp - * - * The general template is never used, hence it is only declared but not defined - */ -template -inline void -supplementJson( - ReadView const&, - std::shared_ptr const& sle, - Json::Value&); - -template <> -void -supplementJson( - ReadView const& view, - std::shared_ptr const& vault, - Json::Value& node); +// Used by both VaultInfo and LedgerEntry +std::optional +parseVault(Json::Value const& params, Json::Value& jvResult); } // namespace RPC + } // namespace ripple #endif diff --git a/src/xrpld/rpc/handlers/Handlers.h b/src/xrpld/rpc/handlers/Handlers.h index 12e493576b..b76cbea8cd 100644 --- a/src/xrpld/rpc/handlers/Handlers.h +++ b/src/xrpld/rpc/handlers/Handlers.h @@ -166,6 +166,8 @@ Json::Value doValidatorListSites(RPC::JsonContext&); Json::Value doValidatorInfo(RPC::JsonContext&); +Json::Value +doVaultInfo(RPC::JsonContext&); } // namespace ripple #endif diff --git a/src/xrpld/rpc/handlers/LedgerData.cpp b/src/xrpld/rpc/handlers/LedgerData.cpp index f94f8b482e..7bd50cc1e5 100644 --- a/src/xrpld/rpc/handlers/LedgerData.cpp +++ b/src/xrpld/rpc/handlers/LedgerData.cpp @@ -124,8 +124,6 @@ doLedgerData(RPC::JsonContext& context) { Json::Value& entry = nodes.append(sle->getJson(JsonOptions::none)); - if (sle->getType() == ltVAULT) - RPC::supplementJson(*lpLedger, sle, entry); entry[jss::index] = to_string(sle->key()); } } diff --git a/src/xrpld/rpc/handlers/LedgerEntry.cpp b/src/xrpld/rpc/handlers/LedgerEntry.cpp index 9c4a4b2bbb..bd43209124 100644 --- a/src/xrpld/rpc/handlers/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/LedgerEntry.cpp @@ -856,32 +856,7 @@ parsePermissionedDomains(Json::Value const& pd, Json::Value& jvResult) static std::optional parseVault(Json::Value const& params, Json::Value& jvResult) { - if (!params.isObject()) - { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; - } - - if (!params.isMember(jss::owner) || !params.isMember(jss::seq) || - !params[jss::seq].isIntegral()) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - auto const id = parseBase58(params[jss::owner].asString()); - if (!id) - { - jvResult[jss::error] = "malformedOwner"; - return std::nullopt; - } - - return keylet::vault(*id, params[jss::seq].asUInt()).key; + return RPC::parseVault(params, jvResult); } using FunctionType = @@ -1033,9 +1008,6 @@ doLedgerEntry(RPC::JsonContext& context) else { jvResult[jss::node] = sleNode->getJson(JsonOptions::none); - if (sleNode->getType() == ltVAULT) - RPC::supplementJson( - *lpLedger, sleNode, jvResult[jss::node]); jvResult[jss::index] = to_string(uNodeIndex); } diff --git a/src/xrpld/rpc/handlers/VaultInfo.cpp b/src/xrpld/rpc/handlers/VaultInfo.cpp new file mode 100644 index 0000000000..ff8a5c6091 --- /dev/null +++ b/src/xrpld/rpc/handlers/VaultInfo.cpp @@ -0,0 +1,95 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or 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 +#include + +#include +#include +#include +#include +#include +#include + +namespace ripple { + +Json::Value +doVaultInfo(RPC::JsonContext& context) +{ + std::shared_ptr lpLedger; + auto jvResult = RPC::lookupLedger(lpLedger, context); + + if (!lpLedger) + return jvResult; + + auto const uNodeIndex = + RPC::parseVault(context.params[jss::vault], jvResult) + .value_or(beast::zero); + if (uNodeIndex == beast::zero) + { + jvResult[jss::error] = "malformedRequest"; + return jvResult; + } + + bool const isBinary = context.params[jss::binary].asBool(); + + auto const sleVault = lpLedger->read(keylet::vault(uNodeIndex)); + auto const sleIssuance = sleVault == nullptr // + ? nullptr + : lpLedger->read(keylet::mptIssuance(sleVault->at(sfShareMPTID))); + if (!sleVault || !sleIssuance) + { + jvResult[jss::error] = "entryNotFound"; + return jvResult; + } + + Json::Value directory = Json::objectValue; + // Some directory positions in nodes are hardcoded below, because the + // order of writing these is hardcoded, but it may not stay like this + // forever. If a given type can have any number of nodes, use an array + // rather than a number + directory[jss::vault] = 0; + directory[jss::mpt_issuance] = 1; + + Json::Value nodes = Json::arrayValue; + if (!isBinary) + { + auto& vault = nodes.append(Json::objectValue); + vault = sleVault->getJson(JsonOptions::none); + + auto& issuance = nodes.append(Json::objectValue); + issuance = sleIssuance->getJson(JsonOptions::none); + } + else + { + auto& vault = nodes.append(Json::objectValue); + vault[jss::data] = serializeHex(*sleVault); + vault[jss::index] = to_string(sleVault->key()); + + auto& issuance = nodes.append(Json::objectValue); + issuance[jss::data] = serializeHex(*sleIssuance); + issuance[jss::index] = to_string(sleIssuance->key()); + } + + jvResult[jss::directory] = directory; + jvResult[jss::nodes] = nodes; + return jvResult; +} + +} // namespace ripple