diff --git a/API-CHANGELOG.md b/API-CHANGELOG.md index 5115eee0855..a3c06399cf5 100644 --- a/API-CHANGELOG.md +++ b/API-CHANGELOG.md @@ -26,6 +26,14 @@ In `api_version: 2`, the `signer_lists` field [will be moved](#modifications-to- The `network_id` field was added in the `server_info` response in version 1.5.0 (2019), but it was not returned in [reporting mode](https://xrpl.org/rippled-server-modes.html#reporting-mode). +## Unreleased + +### Additions + +Additions are intended to be non-breaking (because they are purely additive). + +- `server_definitions`: A new RPC that generates a `definitions.json`-like output that can be used in XRPL libraries. + ## XRP Ledger version 1.12.0 [Version 1.12.0](https://github.com/XRPLF/rippled/releases/tag/1.12.0) was released on Sep 6, 2023. @@ -46,27 +54,27 @@ Additions are intended to be non-breaking (because they are purely additive). - `Account`: The issuer of the asset being clawed back. Must also be the sender of the transaction. - `Amount`: The amount being clawed back, with the `Amount.issuer` being the token holder's address. - Adds [AMM](https://github.com/XRPLF/XRPL-Standards/discussions/78) ([#4294](https://github.com/XRPLF/rippled/pull/4294), [#4626](https://github.com/XRPLF/rippled/pull/4626)) feature: - - Adds `amm_info` API to retrieve AMM information for a given tokens pair. - - Adds `AMMCreate` transaction type to create `AMM` instance. - - Adds `AMMDeposit` transaction type to deposit funds into `AMM` instance. - - Adds `AMMWithdraw` transaction type to withdraw funds from `AMM` instance. - - Adds `AMMVote` transaction type to vote for the trading fee of `AMM` instance. - - Adds `AMMBid` transaction type to bid for the Auction Slot of `AMM` instance. - - Adds `AMMDelete` transaction type to delete `AMM` instance. - - Adds `sfAMMID` to `AccountRoot` to indicate that the account is `AMM`'s account. `AMMID` is used to fetch `ltAMM`. - - Adds `lsfAMMNode` `TrustLine` flag to indicate that one side of the `TrustLine` is `AMM` account. - - Adds `tfLPToken`, `tfSingleAsset`, `tfTwoAsset`, `tfOneAssetLPToken`, `tfLimitLPToken`, `tfTwoAssetIfEmpty`, - `tfWithdrawAll`, `tfOneAssetWithdrawAll` which allow a trader to specify different fields combination - for `AMMDeposit` and `AMMWithdraw` transactions. - - Adds new transaction result codes: - - tecUNFUNDED_AMM: insufficient balance to fund AMM. The account does not have funds for liquidity provision. - - tecAMM_BALANCE: AMM has invalid balance. Calculated balances greater than the current pool balances. - - tecAMM_FAILED: AMM transaction failed. Fails due to a processing failure. - - tecAMM_INVALID_TOKENS: AMM invalid LP tokens. Invalid input values, format, or calculated values. - - tecAMM_EMPTY: AMM is in empty state. Transaction expects AMM in non-empty state (LP tokens > 0). - - tecAMM_NOT_EMPTY: AMM is not in empty state. Transaction expects AMM in empty state (LP tokens == 0). - - tecAMM_ACCOUNT: AMM account. Clawback of AMM account. - - tecINCOMPLETE: Some work was completed, but more submissions required to finish. AMMDelete partially deletes the trustlines. + - Adds `amm_info` API to retrieve AMM information for a given tokens pair. + - Adds `AMMCreate` transaction type to create `AMM` instance. + - Adds `AMMDeposit` transaction type to deposit funds into `AMM` instance. + - Adds `AMMWithdraw` transaction type to withdraw funds from `AMM` instance. + - Adds `AMMVote` transaction type to vote for the trading fee of `AMM` instance. + - Adds `AMMBid` transaction type to bid for the Auction Slot of `AMM` instance. + - Adds `AMMDelete` transaction type to delete `AMM` instance. + - Adds `sfAMMID` to `AccountRoot` to indicate that the account is `AMM`'s account. `AMMID` is used to fetch `ltAMM`. + - Adds `lsfAMMNode` `TrustLine` flag to indicate that one side of the `TrustLine` is `AMM` account. + - Adds `tfLPToken`, `tfSingleAsset`, `tfTwoAsset`, `tfOneAssetLPToken`, `tfLimitLPToken`, `tfTwoAssetIfEmpty`, + `tfWithdrawAll`, `tfOneAssetWithdrawAll` which allow a trader to specify different fields combination + for `AMMDeposit` and `AMMWithdraw` transactions. + - Adds new transaction result codes: + - tecUNFUNDED_AMM: insufficient balance to fund AMM. The account does not have funds for liquidity provision. + - tecAMM_BALANCE: AMM has invalid balance. Calculated balances greater than the current pool balances. + - tecAMM_FAILED: AMM transaction failed. Fails due to a processing failure. + - tecAMM_INVALID_TOKENS: AMM invalid LP tokens. Invalid input values, format, or calculated values. + - tecAMM_EMPTY: AMM is in empty state. Transaction expects AMM in non-empty state (LP tokens > 0). + - tecAMM_NOT_EMPTY: AMM is not in empty state. Transaction expects AMM in empty state (LP tokens == 0). + - tecAMM_ACCOUNT: AMM account. Clawback of AMM account. + - tecINCOMPLETE: Some work was completed, but more submissions required to finish. AMMDelete partially deletes the trustlines. ## XRP Ledger version 1.11.0 @@ -118,6 +126,7 @@ Changes below this point are in development. At the time of writing, this version is expected to be introduced in `rippled` version 2.0. Currently (prior to the release of 2.0), it is available as a "beta" version, meaning it can be enabled with a config setting in `rippled.cfg`: + ``` [beta_rpc_api] 1 diff --git a/src/ripple/app/main/Main.cpp b/src/ripple/app/main/Main.cpp index a998640c01c..2ddb771b1e1 100644 --- a/src/ripple/app/main/Main.cpp +++ b/src/ripple/app/main/Main.cpp @@ -169,6 +169,7 @@ printHelp(const po::options_description& desc) " peer_reservations_list\n" " ripple ...\n" " ripple_path_find []\n" + " server_definitions []\n" " server_info [counters]\n" " server_state [counters]\n" " sign [offline]\n" diff --git a/src/ripple/net/impl/RPCCall.cpp b/src/ripple/net/impl/RPCCall.cpp index 1242fdc2a68..b52545960cc 100644 --- a/src/ripple/net/impl/RPCCall.cpp +++ b/src/ripple/net/impl/RPCCall.cpp @@ -1194,6 +1194,20 @@ class RPCParser return jvRequest; } + // server_definitions [hash] + Json::Value + parseServerDefinitions(Json::Value const& jvParams) + { + Json::Value jvRequest{Json::objectValue}; + + if (jvParams.size() == 1) + { + jvRequest[jss::hash] = jvParams[0u].asString(); + } + + return jvRequest; + } + // server_info [counters] Json::Value parseServerInfo(Json::Value const& jvParams) @@ -1255,6 +1269,7 @@ class RPCParser {"channel_verify", &RPCParser::parseChannelVerify, 4, 4}, {"connect", &RPCParser::parseConnect, 1, 2}, {"consensus_info", &RPCParser::parseAsIs, 0, 0}, + {"crawl_shards", &RPCParser::parseAsIs, 0, 2}, {"deposit_authorized", &RPCParser::parseDepositAuthorized, 2, 3}, {"download_shard", &RPCParser::parseDownloadShard, 2, -1}, {"feature", &RPCParser::parseFeature, 0, 2}, @@ -1292,14 +1307,14 @@ class RPCParser 1}, {"peer_reservations_list", &RPCParser::parseAsIs, 0, 0}, {"ripple_path_find", &RPCParser::parseRipplePathFind, 1, 2}, + {"server_definitions", &RPCParser::parseServerDefinitions, 0, 1}, + {"server_info", &RPCParser::parseServerInfo, 0, 1}, + {"server_state", &RPCParser::parseServerInfo, 0, 1}, {"sign", &RPCParser::parseSignSubmit, 2, 3}, {"sign_for", &RPCParser::parseSignFor, 3, 4}, + {"stop", &RPCParser::parseAsIs, 0, 0}, {"submit", &RPCParser::parseSignSubmit, 1, 3}, {"submit_multisigned", &RPCParser::parseSubmitMultiSigned, 1, 1}, - {"server_info", &RPCParser::parseServerInfo, 0, 1}, - {"server_state", &RPCParser::parseServerInfo, 0, 1}, - {"crawl_shards", &RPCParser::parseAsIs, 0, 2}, - {"stop", &RPCParser::parseAsIs, 0, 0}, {"transaction_entry", &RPCParser::parseTransactionEntry, 2, 2}, {"tx", &RPCParser::parseTx, 1, 4}, {"tx_account", &RPCParser::parseTxAccount, 1, 7}, diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index fe045d001d5..5d7acb12383 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -52,43 +52,65 @@ class STInteger; class STXChainBridge; class STVector256; -enum SerializedTypeID { - // special types - STI_UNKNOWN = -2, - STI_NOTPRESENT = 0, - - // // types (common) - STI_UINT16 = 1, - STI_UINT32 = 2, - STI_UINT64 = 3, - STI_UINT128 = 4, - STI_UINT256 = 5, - STI_AMOUNT = 6, - STI_VL = 7, - STI_ACCOUNT = 8, - // 9-13 are reserved - STI_OBJECT = 14, - STI_ARRAY = 15, - - // types (uncommon) - STI_UINT8 = 16, - STI_UINT160 = 17, - STI_PATHSET = 18, - STI_VECTOR256 = 19, - STI_UINT96 = 20, - STI_UINT192 = 21, - STI_UINT384 = 22, - STI_UINT512 = 23, - STI_ISSUE = 24, - STI_XCHAIN_BRIDGE = 25, - - // high level types - // cannot be serialized inside other types - STI_TRANSACTION = 10001, - STI_LEDGERENTRY = 10002, - STI_VALIDATION = 10003, - STI_METADATA = 10004, -}; +#pragma push_macro("XMACRO") +#undef XMACRO + +#define XMACRO(STYPE) \ + /* special types */ \ + STYPE(STI_UNKNOWN, -2) \ + STYPE(STI_NOTPRESENT, 0) \ + STYPE(STI_UINT16, 1) \ + \ + /* types (common) */ \ + STYPE(STI_UINT32, 2) \ + STYPE(STI_UINT64, 3) \ + STYPE(STI_UINT128, 4) \ + STYPE(STI_UINT256, 5) \ + STYPE(STI_AMOUNT, 6) \ + STYPE(STI_VL, 7) \ + STYPE(STI_ACCOUNT, 8) \ + \ + /* 9-13 are reserved */ \ + STYPE(STI_OBJECT, 14) \ + STYPE(STI_ARRAY, 15) \ + \ + /* types (uncommon) */ \ + STYPE(STI_UINT8, 16) \ + STYPE(STI_UINT160, 17) \ + STYPE(STI_PATHSET, 18) \ + STYPE(STI_VECTOR256, 19) \ + STYPE(STI_UINT96, 20) \ + STYPE(STI_UINT192, 21) \ + STYPE(STI_UINT384, 22) \ + STYPE(STI_UINT512, 23) \ + STYPE(STI_ISSUE, 24) \ + STYPE(STI_XCHAIN_BRIDGE, 25) \ + \ + /* high-level types */ \ + /* cannot be serialized inside other types */ \ + STYPE(STI_TRANSACTION, 10001) \ + STYPE(STI_LEDGERENTRY, 10002) \ + STYPE(STI_VALIDATION, 10003) \ + STYPE(STI_METADATA, 10004) + +#pragma push_macro("TO_ENUM") +#undef TO_ENUM +#pragma push_macro("TO_MAP") +#undef TO_MAP + +#define TO_ENUM(name, value) name = value, +#define TO_MAP(name, value) {#name, value}, + +enum SerializedTypeID { XMACRO(TO_ENUM) }; + +static std::map const sTypeMap = {XMACRO(TO_MAP)}; + +#undef XMACRO +#undef TO_ENUM + +#pragma pop_macro("XMACRO") +#pragma pop_macro("TO_ENUM") +#pragma pop_macro("TO_MAP") // constexpr inline int @@ -266,6 +288,12 @@ class SField static int compare(const SField& f1, const SField& f2); + static std::map const& + getKnownCodeToField() + { + return knownCodeToField; + } + private: static int num; static std::map knownCodeToField; diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index f832fe24bce..23d4fb3ef00 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -26,6 +26,7 @@ #include #include #include +#include namespace ripple { @@ -644,6 +645,11 @@ isTecClaim(TER x) return ((x) >= tecCLAIM); } +std::unordered_map< + TERUnderlyingType, + std::pair> const& +transResults(); + bool transResultInfo(TER code, std::string& token, std::string& text); diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index f48bef5232d..1c2db3feb3b 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -20,13 +20,10 @@ #include #include #include -#include namespace ripple { -namespace detail { - -static std::unordered_map< +std::unordered_map< TERUnderlyingType, std::pair> const& transResults() @@ -225,12 +222,10 @@ transResults() return results; } -} // namespace detail - bool transResultInfo(TER code, std::string& token, std::string& text) { - auto& results = detail::transResults(); + auto& results = transResults(); auto const r = results.find(TERtoInt(code)); @@ -264,7 +259,7 @@ std::optional transCode(std::string const& token) { static auto const results = [] { - auto& byTer = detail::transResults(); + auto& byTer = transResults(); auto range = boost::make_iterator_range(byTer.begin(), byTer.end()); auto tRange = boost::adaptors::transform(range, [](auto const& r) { return std::make_pair(r.second.first, r.first); diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index e2800dc80a0..4a8f80b8c7b 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -321,6 +321,8 @@ JSS(fee_level); // out: AccountInfo JSS(fee_mult_max); // in: TransactionSign JSS(fee_ref); // out: NetworkOPs, DEPRECATED JSS(fetch_pack); // out: NetworkOPs +JSS(FIELDS); // out: RPC server_definitions + // matches definitions.json format JSS(first); // out: rpc/Version JSS(firstSequence); // out: NodeToShardStatus JSS(firstShardIndex); // out: NodeToShardStatus @@ -365,6 +367,12 @@ JSS(invalid_API_version); // out: Many, when a request has an invalid JSS(io_latency_ms); // out: NetworkOPs JSS(ip); // in: Connect, out: OverlayImpl JSS(is_burned); // out: nft_info (clio) +JSS(isSerialized); // out: RPC server_definitions + // matches definitions.json format +JSS(isSigningField); // out: RPC server_definitions + // matches definitions.json format +JSS(isVLEncoded); // out: RPC server_definitions + // matches definitions.json format JSS(issuer); // in: RipplePathFind, Subscribe, // Unsubscribe, BookOffers // out: STPathSet, STAmount @@ -404,6 +412,8 @@ JSS(ledger_index_min); // in, out: AccountTx* JSS(ledger_max); // in, out: AccountTx* JSS(ledger_min); // in, out: AccountTx* JSS(ledger_time); // out: NetworkOPs +JSS(LEDGER_ENTRY_TYPES); // out: RPC server_definitions + // matches definitions.json format JSS(levels); // LogLevels JSS(limit); // in/out: AccountTx*, AccountOffers, // AccountLines, AccountObjects @@ -490,6 +500,7 @@ JSS(node_written_bytes); // out: GetCounts JSS(node_writes_duration_us); // out: GetCounts JSS(node_write_retries); // out: GetCounts JSS(node_writes_delayed); // out::GetCounts +JSS(nth); // out: RPC server_definitions JSS(obligations); // out: GatewayBalances JSS(offer); // in: LedgerEntry JSS(offers); // out: NetworkOPs, AccountOffers, Subscribe @@ -649,6 +660,12 @@ JSS(transaction); // in: Tx JSS(transaction_hash); // out: RCLCxPeerPos, LedgerToJson JSS(transactions); // out: LedgerToJson, // in: AccountTx*, Unsubscribe +JSS(TRANSACTION_RESULTS); // out: RPC server_definitions + // matches definitions.json format +JSS(TRANSACTION_TYPES); // out: RPC server_definitions + // matches definitions.json format +JSS(TYPES); // out: RPC server_definitions + // matches definitions.json format JSS(transfer_rate); // out: nft_info (clio) JSS(transitions); // out: NetworkOPs JSS(treenode_cache_size); // out: GetCounts @@ -680,7 +697,7 @@ JSS(txr_not_enabled_cnt); // out: peers with tx reduce-relay disabled count JSS(txr_missing_tx_freq); // out: missing tx frequency average JSS(txs); // out: TxHistory JSS(type); // in: AccountObjects - // out: NetworkOPs + // out: NetworkOPs, RPC server_definitions // OverlayImpl, Logic JSS(type_hex); // out: STPathSet JSS(unl); // out: UnlList diff --git a/src/ripple/rpc/handlers/Handlers.h b/src/ripple/rpc/handlers/Handlers.h index 367e715ce1f..ba93be54513 100644 --- a/src/ripple/rpc/handlers/Handlers.h +++ b/src/ripple/rpc/handlers/Handlers.h @@ -127,6 +127,8 @@ doPeerReservationsList(RPC::JsonContext&); Json::Value doRipplePathFind(RPC::JsonContext&); Json::Value +doServerDefinitions(RPC::JsonContext&); +Json::Value doServerInfo(RPC::JsonContext&); // for humans Json::Value doServerState(RPC::JsonContext&); // for machines diff --git a/src/ripple/rpc/handlers/ServerInfo.cpp b/src/ripple/rpc/handlers/ServerInfo.cpp index aad3d9988e3..2637c3d51be 100644 --- a/src/ripple/rpc/handlers/ServerInfo.cpp +++ b/src/ripple/rpc/handlers/ServerInfo.cpp @@ -22,13 +22,302 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include -#include + +#include + +#include namespace ripple { +namespace detail { + +class ServerDefinitions +{ +private: + std::string + // translate e.g. STI_LEDGERENTRY to LedgerEntry + translate(std::string const& inp); + + uint256 defsHash_; + Json::Value defs_; + +public: + ServerDefinitions(); + + bool + hashMatches(uint256 hash) const + { + return defsHash_ == hash; + } + + Json::Value const& + get() const + { + return defs_; + } +}; + +std::string +ServerDefinitions::translate(std::string const& inp) +{ + auto replace = [&](char const* oldStr, char const* newStr) -> std::string { + std::string out = inp; + boost::replace_all(out, oldStr, newStr); + return out; + }; + + auto contains = [&](char const* s) -> bool { + return inp.find(s) != std::string::npos; + }; + + if (contains("UINT")) + { + if (contains("256") || contains("160") || contains("128")) + return replace("UINT", "Hash"); + else + return replace("UINT", "UInt"); + } + + std::unordered_map replacements{ + {"OBJECT", "STObject"}, + {"ARRAY", "STArray"}, + {"ACCOUNT", "AccountID"}, + {"LEDGERENTRY", "LedgerEntry"}, + {"NOTPRESENT", "NotPresent"}, + {"PATHSET", "PathSet"}, + {"VL", "Blob"}, + {"XCHAIN_BRIDGE", "XChainBridge"}, + }; + + if (auto const& it = replacements.find(inp); it != replacements.end()) + { + return it->second; + } + + std::string out; + size_t pos = 0; + std::string inpToProcess = inp; + + // convert snake_case to CamelCase + for (;;) + { + pos = inpToProcess.find("_"); + if (pos == std::string::npos) + pos = inpToProcess.size(); + std::string token = inpToProcess.substr(0, pos); + if (token.size() > 1) + { + boost::algorithm::to_lower(token); + token.data()[0] -= ('a' - 'A'); + out += token; + } + else + out += token; + if (pos == inpToProcess.size()) + break; + inpToProcess = inpToProcess.substr(pos + 1); + } + return out; +}; + +ServerDefinitions::ServerDefinitions() : defs_{Json::objectValue} +{ + // populate SerializedTypeID names and values + defs_[jss::TYPES] = Json::objectValue; + + defs_[jss::TYPES]["Done"] = -1; + std::map typeMap{{-1, "Done"}}; + for (auto const& [rawName, typeValue] : sTypeMap) + { + std::string typeName = + translate(std::string(rawName).substr(4) /* remove STI_ */); + defs_[jss::TYPES][typeName] = typeValue; + typeMap[typeValue] = typeName; + } + + // populate LedgerEntryType names and values + defs_[jss::LEDGER_ENTRY_TYPES] = Json::objectValue; + defs_[jss::LEDGER_ENTRY_TYPES][jss::Invalid] = -1; + + for (auto const& f : LedgerFormats::getInstance()) + { + defs_[jss::LEDGER_ENTRY_TYPES][f.getName()] = f.getType(); + } + + // populate SField serialization data + defs_[jss::FIELDS] = Json::arrayValue; + + uint32_t i = 0; + { + Json::Value a = Json::arrayValue; + a[0U] = "Generic"; + Json::Value v = Json::objectValue; + v[jss::nth] = 0; + v[jss::isVLEncoded] = false; + v[jss::isSerialized] = false; + v[jss::isSigningField] = false; + v[jss::type] = "Unknown"; + a[1U] = v; + defs_[jss::FIELDS][i++] = a; + } + + { + Json::Value a = Json::arrayValue; + a[0U] = "Invalid"; + Json::Value v = Json::objectValue; + v[jss::nth] = -1; + v[jss::isVLEncoded] = false; + v[jss::isSerialized] = false; + v[jss::isSigningField] = false; + v[jss::type] = "Unknown"; + a[1U] = v; + defs_[jss::FIELDS][i++] = a; + } + + { + Json::Value a = Json::arrayValue; + a[0U] = "ObjectEndMarker"; + Json::Value v = Json::objectValue; + v[jss::nth] = 1; + v[jss::isVLEncoded] = false; + v[jss::isSerialized] = true; + v[jss::isSigningField] = true; + v[jss::type] = "STObject"; + a[1U] = v; + defs_[jss::FIELDS][i++] = a; + } + + { + Json::Value a = Json::arrayValue; + a[0U] = "ArrayEndMarker"; + Json::Value v = Json::objectValue; + v[jss::nth] = 1; + v[jss::isVLEncoded] = false; + v[jss::isSerialized] = true; + v[jss::isSigningField] = true; + v[jss::type] = "STArray"; + a[1U] = v; + defs_[jss::FIELDS][i++] = a; + } + + { + Json::Value a = Json::arrayValue; + a[0U] = "taker_gets_funded"; + Json::Value v = Json::objectValue; + v[jss::nth] = 258; + v[jss::isVLEncoded] = false; + v[jss::isSerialized] = false; + v[jss::isSigningField] = false; + v[jss::type] = "Amount"; + a[1U] = v; + defs_[jss::FIELDS][i++] = a; + } + + { + Json::Value a = Json::arrayValue; + a[0U] = "taker_pays_funded"; + Json::Value v = Json::objectValue; + v[jss::nth] = 259; + v[jss::isVLEncoded] = false; + v[jss::isSerialized] = false; + v[jss::isSigningField] = false; + v[jss::type] = "Amount"; + a[1U] = v; + defs_[jss::FIELDS][i++] = a; + } + + for (auto const& [code, f] : ripple::SField::getKnownCodeToField()) + { + if (f->fieldName == "") + continue; + + Json::Value innerObj = Json::objectValue; + + uint32_t type = f->fieldType; + + innerObj[jss::nth] = f->fieldValue; + + // whether the field is variable-length encoded + // this means that the length is included before the content + innerObj[jss::isVLEncoded] = + (type == 7U /* Blob */ || type == 8U /* AccountID */ || + type == 19U /* Vector256 */); + + // whether the field is included in serialization + innerObj[jss::isSerialized] = + (type < 10000 && f->fieldName != "hash" && + f->fieldName != "index"); /* hash, index, TRANSACTION, + LEDGER_ENTRY, VALIDATION, METADATA */ + + // whether the field is included in serialization when signing + innerObj[jss::isSigningField] = f->shouldInclude(false); + + innerObj[jss::type] = typeMap[type]; + + Json::Value innerArray = Json::arrayValue; + innerArray[0U] = f->fieldName; + innerArray[1U] = innerObj; + + defs_[jss::FIELDS][i++] = innerArray; + } + + // populate TER code names and values + defs_[jss::TRANSACTION_RESULTS] = Json::objectValue; + + for (auto const& [code, terInfo] : transResults()) + { + defs_[jss::TRANSACTION_RESULTS][terInfo.first] = code; + } + + // populate TxType names and values + defs_[jss::TRANSACTION_TYPES] = Json::objectValue; + defs_[jss::TRANSACTION_TYPES][jss::Invalid] = -1; + for (auto const& f : TxFormats::getInstance()) + { + defs_[jss::TRANSACTION_TYPES][f.getName()] = f.getType(); + } + + // generate hash + { + const std::string out = Json::FastWriter().write(defs_); + defsHash_ = ripple::sha512Half(ripple::Slice{out.data(), out.size()}); + defs_[jss::hash] = to_string(defsHash_); + } +} + +} // namespace detail + +Json::Value +doServerDefinitions(RPC::JsonContext& context) +{ + auto& params = context.params; + + uint256 hash; + if (params.isMember(jss::hash)) + { + if (!params[jss::hash].isString() || + !hash.parseHex(params[jss::hash].asString())) + return RPC::invalid_field_error(jss::hash); + } + + static const detail::ServerDefinitions defs{}; + if (defs.hashMatches(hash)) + { + Json::Value jv = Json::objectValue; + jv[jss::hash] = to_string(hash); + return jv; + } + return defs.get(); +} + Json::Value doServerInfo(RPC::JsonContext& context) { diff --git a/src/ripple/rpc/impl/Handler.cpp b/src/ripple/rpc/impl/Handler.cpp index dd898ee8722..b69d2608b0e 100644 --- a/src/ripple/rpc/impl/Handler.cpp +++ b/src/ripple/rpc/impl/Handler.cpp @@ -80,6 +80,7 @@ Handler const handlerArray[]{ {"channel_verify", byRef(&doChannelVerify), Role::USER, NO_CONDITION}, {"connect", byRef(&doConnect), Role::ADMIN, NO_CONDITION}, {"consensus_info", byRef(&doConsensusInfo), Role::ADMIN, NO_CONDITION}, + {"crawl_shards", byRef(&doCrawlShards), Role::ADMIN, NO_CONDITION}, {"deposit_authorized", byRef(&doDepositAuthorized), Role::USER, @@ -139,17 +140,20 @@ Handler const handlerArray[]{ Role::ADMIN, NO_CONDITION}, {"ripple_path_find", byRef(&doRipplePathFind), Role::USER, NO_CONDITION}, + {"server_definitions", + byRef(&doServerDefinitions), + Role::USER, + NO_CONDITION}, + {"server_info", byRef(&doServerInfo), Role::USER, NO_CONDITION}, + {"server_state", byRef(&doServerState), Role::USER, NO_CONDITION}, {"sign", byRef(&doSign), Role::USER, NO_CONDITION}, {"sign_for", byRef(&doSignFor), Role::USER, NO_CONDITION}, + {"stop", byRef(&doStop), Role::ADMIN, NO_CONDITION}, {"submit", byRef(&doSubmit), Role::USER, NEEDS_CURRENT_LEDGER}, {"submit_multisigned", byRef(&doSubmitMultiSigned), Role::USER, NEEDS_CURRENT_LEDGER}, - {"server_info", byRef(&doServerInfo), Role::USER, NO_CONDITION}, - {"server_state", byRef(&doServerState), Role::USER, NO_CONDITION}, - {"crawl_shards", byRef(&doCrawlShards), Role::ADMIN, NO_CONDITION}, - {"stop", byRef(&doStop), Role::ADMIN, NO_CONDITION}, {"transaction_entry", byRef(&doTransactionEntry), Role::USER, NO_CONDITION}, {"tx", byRef(&doTxJson), Role::USER, NEEDS_NETWORK_CONNECTION}, {"tx_history", byRef(&doTxHistory), Role::USER, NO_CONDITION}, diff --git a/src/test/rpc/ServerInfo_test.cpp b/src/test/rpc/ServerInfo_test.cpp index a69483cb130..1d78f1cc36b 100644 --- a/src/test/rpc/ServerInfo_test.cpp +++ b/src/test/rpc/ServerInfo_test.cpp @@ -79,6 +79,8 @@ admin = 127.0.0.1 void testServerInfo() { + testcase("server_info"); + using namespace test::jtx; { @@ -148,10 +150,104 @@ admin = 127.0.0.1 } } + void + testServerDefinitions() + { + testcase("server_definitions"); + + using namespace test::jtx; + + { + Env env(*this); + auto const result = env.rpc("server_definitions"); + BEAST_EXPECT(!result[jss::result].isMember(jss::error)); + BEAST_EXPECT(result[jss::result][jss::status] == "success"); + BEAST_EXPECT(result[jss::result].isMember(jss::FIELDS)); + BEAST_EXPECT(result[jss::result].isMember(jss::LEDGER_ENTRY_TYPES)); + BEAST_EXPECT( + result[jss::result].isMember(jss::TRANSACTION_RESULTS)); + BEAST_EXPECT(result[jss::result].isMember(jss::TRANSACTION_TYPES)); + BEAST_EXPECT(result[jss::result].isMember(jss::TYPES)); + BEAST_EXPECT(result[jss::result].isMember(jss::hash)); + + // test a random element of each result + // (testing the whole output would be difficult to maintain) + + { + auto const firstField = result[jss::result][jss::FIELDS][0u]; + BEAST_EXPECT(firstField[0u].asString() == "Generic"); + BEAST_EXPECT( + firstField[1][jss::isSerialized].asBool() == false); + BEAST_EXPECT( + firstField[1][jss::isSigningField].asBool() == false); + BEAST_EXPECT(firstField[1][jss::isVLEncoded].asBool() == false); + BEAST_EXPECT(firstField[1][jss::nth].asUInt() == 0); + BEAST_EXPECT(firstField[1][jss::type].asString() == "Unknown"); + } + + BEAST_EXPECT( + result[jss::result][jss::LEDGER_ENTRY_TYPES]["AccountRoot"] + .asUInt() == 97); + BEAST_EXPECT( + result[jss::result][jss::TRANSACTION_RESULTS]["tecDIR_FULL"] + .asUInt() == 121); + BEAST_EXPECT( + result[jss::result][jss::TRANSACTION_TYPES]["Payment"] + .asUInt() == 0); + BEAST_EXPECT( + result[jss::result][jss::TYPES]["AccountID"].asUInt() == 8); + } + + // test providing the same hash + { + Env env(*this); + auto const firstResult = env.rpc("server_definitions"); + auto const hash = firstResult[jss::result][jss::hash].asString(); + auto const hashParam = + std::string("{ ") + "\"hash\": \"" + hash + "\"}"; + + auto const result = + env.rpc("json", "server_definitions", hashParam); + BEAST_EXPECT(!result[jss::result].isMember(jss::error)); + BEAST_EXPECT(result[jss::result][jss::status] == "success"); + BEAST_EXPECT(!result[jss::result].isMember(jss::FIELDS)); + BEAST_EXPECT( + !result[jss::result].isMember(jss::LEDGER_ENTRY_TYPES)); + BEAST_EXPECT( + !result[jss::result].isMember(jss::TRANSACTION_RESULTS)); + BEAST_EXPECT(!result[jss::result].isMember(jss::TRANSACTION_TYPES)); + BEAST_EXPECT(!result[jss::result].isMember(jss::TYPES)); + BEAST_EXPECT(result[jss::result].isMember(jss::hash)); + } + + // test providing a different hash + { + Env env(*this); + std::string const hash = + "54296160385A27154BFA70A239DD8E8FD4CC2DB7BA32D970BA3A5B132CF749" + "D1"; + auto const hashParam = + std::string("{ ") + "\"hash\": \"" + hash + "\"}"; + + auto const result = + env.rpc("json", "server_definitions", hashParam); + BEAST_EXPECT(!result[jss::result].isMember(jss::error)); + BEAST_EXPECT(result[jss::result][jss::status] == "success"); + BEAST_EXPECT(result[jss::result].isMember(jss::FIELDS)); + BEAST_EXPECT(result[jss::result].isMember(jss::LEDGER_ENTRY_TYPES)); + BEAST_EXPECT( + result[jss::result].isMember(jss::TRANSACTION_RESULTS)); + BEAST_EXPECT(result[jss::result].isMember(jss::TRANSACTION_TYPES)); + BEAST_EXPECT(result[jss::result].isMember(jss::TYPES)); + BEAST_EXPECT(result[jss::result].isMember(jss::hash)); + } + } + void run() override { testServerInfo(); + testServerDefinitions(); } };