diff --git a/conan.lock b/conan.lock index 1e7c54e73..ba3034e03 100644 --- a/conan.lock +++ b/conan.lock @@ -3,8 +3,8 @@ "requires": [ "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708", "xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1782392402.420688", - "xrpl-rpc-spec/0.1.7#774d2f93c4b48a1523d8d5a94a2b082a%1787768955.574105", - "xrpl/3.4.0-rc1#19678cbb46117ef8a669558ad19d6a1f%1789050823.813662", + "xrpl-rpc-spec/0.1.17#ddfbc1f89da7797b6506fb9ee9488e17%1790136770.803908", + "xrpl/3.4.0#e06b127b3ba92806a43fc7a53fc05fd3%1789577552.219154", "sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1782392403.185447", "spdlog/1.17.0#bcbaaf7147bda6ad24ffbd1ac3d7142c%1782736610.443882", "soci/4.0.3#e726491a03468795453f7c83fc924a96%1782392402.679521", diff --git a/conanfile.py b/conanfile.py index 0404b34bb..339b0ade5 100644 --- a/conanfile.py +++ b/conanfile.py @@ -17,8 +17,8 @@ class ClioConan(ConanFile): "fmt/12.1.0", "libbacktrace/cci.20210118", "spdlog/1.17.0", - "xrpl-rpc-spec/0.1.7", - "xrpl/3.4.0-rc1", + "xrpl-rpc-spec/0.1.17", + "xrpl/3.4.0", ] default_options = { diff --git a/src/data/AmendmentCenter.hpp b/src/data/AmendmentCenter.hpp index a76ca202b..784f0755c 100644 --- a/src/data/AmendmentCenter.hpp +++ b/src/data/AmendmentCenter.hpp @@ -140,15 +140,10 @@ struct Amendments { REGISTER(fixCleanup3_1_3); REGISTER(fixCleanup3_2_0); REGISTER(ConfidentialTransfer); - - // These amendments are added because of libxrpl 3.3.0, but they are not yet supported in Clio. REGISTER(LendingProtocolV1_1); REGISTER(BatchV1_1); REGISTER(Sponsor); REGISTER(fixCleanup3_3_0); - - // These amendments are added because of libxrpl 3.4.0, but they are not necessarily supported - // in Clio yet REGISTER(fixCleanup3_4_0); // Obsolete but supported by libxrpl diff --git a/src/rpc/CMakeLists.txt b/src/rpc/CMakeLists.txt index 73dbcade0..2daf99925 100644 --- a/src/rpc/CMakeLists.txt +++ b/src/rpc/CMakeLists.txt @@ -16,9 +16,6 @@ target_sources( CredentialHelpers.cpp Counters.cpp WorkQueue.cpp - common/Specs.cpp - common/Validators.cpp - common/MetaProcessors.cpp common/impl/APIVersionParser.cpp common/impl/HandlerProvider.cpp common/impl/HandlerRegistry.cpp @@ -61,4 +58,7 @@ target_sources( handlers/VaultInfo.cpp ) +rpcspec_generate_instantiations(OUT_VAR rpcspec_instantiations) +target_sources(clio_rpc PRIVATE ${rpcspec_instantiations}) + target_link_libraries(clio_rpc PUBLIC clio_util clio_data rpcspec::rpcspec) diff --git a/src/rpc/CredentialHelpers.cpp b/src/rpc/CredentialHelpers.cpp index f89f904f1..be54bb244 100644 --- a/src/rpc/CredentialHelpers.cpp +++ b/src/rpc/CredentialHelpers.cpp @@ -5,7 +5,6 @@ #include #include -#include #include #include #include @@ -27,9 +26,9 @@ #include #include #include -#include #include #include +#include namespace rpc::credentials { @@ -99,7 +98,7 @@ parseAuthorizeCredentials(boost::json::array const& jv) std::expected fetchCredentialArray( - std::optional const& credID, + std::vector const& credIDs, xrpl::AccountID const& srcAcc, BackendInterface const& backend, xrpl::LedgerHeader const& info, @@ -107,21 +106,11 @@ fetchCredentialArray( ) { xrpl::STArray authCreds; - std::unordered_set elems; - for (auto const& elem : *credID) { // NOLINT(bugprone-unchecked-optional-access) - ASSERT( - elem.is_string(), "should already be checked in validators.hpp that elem is a string." - ); - - if (elems.contains(elem.as_string())) + std::unordered_set seen; + for (auto const& credHash : credIDs) { + if (seen.contains(credHash)) return Error{Status{RippledError::RpcBadCredentials, "duplicates in credentials."}}; - elems.insert(elem.as_string()); - - xrpl::uint256 credHash; - ASSERT( - credHash.parseHex(boost::json::value_to(elem)), - "should already be checked in validators.hpp that elem is a uint256 hex" - ); + seen.insert(credHash); auto const credKeylet = xrpl::keylet::credential(credHash).key; auto const credLedgerObject = backend.fetchLedgerObject(credKeylet, info.seq, yield); diff --git a/src/rpc/CredentialHelpers.hpp b/src/rpc/CredentialHelpers.hpp index 1cf27e652..58cefd946 100644 --- a/src/rpc/CredentialHelpers.hpp +++ b/src/rpc/CredentialHelpers.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,7 @@ #include #include #include +#include namespace rpc::credentials { @@ -52,7 +54,7 @@ parseAuthorizeCredentials(boost::json::array const& jv); /** * @brief Get Array of Credential objects * - * @param credID Array of CredentialID's to parse + * @param credIDs The credential IDs to look up * @param srcAcc The Source Account * @param backend backend interface * @param info The ledger header @@ -61,7 +63,7 @@ parseAuthorizeCredentials(boost::json::array const& jv); */ std::expected fetchCredentialArray( - std::optional const& credID, + std::vector const& credIDs, xrpl::AccountID const& srcAcc, BackendInterface const& backend, xrpl::LedgerHeader const& info, diff --git a/src/rpc/README.md b/src/rpc/README.md index 717a173e4..424b9daa6 100644 --- a/src/rpc/README.md +++ b/src/rpc/README.md @@ -4,27 +4,74 @@ The RPC subsystem is where the common framework for handling incoming JSON requests is implemented. +Request validation and deserialisation are **not** defined here. They live in the shared +[`xrpl-rpc-spec`](https://github.com/XRPLF/rpc-spec) library, consumed as ``, so that +one spec per RPC method can be shared verbatim between Clio and xrpld. Clio's job is to supply the +handler body and the response shape. + ## Components See the [common](https://github.com/XRPLF/clio/blob/develop/src/rpc/common) subfolder. - **AnyHandler**: The type-erased wrapper that allows for storing different handlers in one map/vector. -- **RpcSpec/FieldSpec**: The RPC specification classes, used to specify how incoming JSON is to be validated before it's parsed and passed on to individual handler implementations. -- **Validators/Modifiers**: A bunch of supported validators and modifiers that can be specified as requirements for each `FieldSpec` to make up the final `RpcSpec` of any given RPC handler. +- **Concepts**: The `SomeHandler` concept and its parts, which define what a handler must provide. +- **HandlerRegistry**: The table mapping a method name to its handler factory, plus whether the + method is Clio-only (and therefore never forwarded to xrpld). + +From the spec library: + +- **`rpc::spec::HandlerFor`**: Base class supplying the static `parseInput` and `spec` + entry points a handler needs. It resolves the versioned spec for `Input` through ADL, so a + handler names only its `Input` type. +- **`rpcspec/handlers//Types.hpp`**: The strongly-typed `Input` struct for a method + (`xrpl::AccountID`, `xrpl::uint256`, `LedgerSpecifier`, ... rather than `std::string`). +- **`rpcspec/handlers//Spec.hpp`**: The consteval spec declaring that method's fields, + their validators and their converters. ## Implementing a handler -See [tests/unit/rpc](https://github.com/XRPLF/clio/tree/develop/tests/unit/rpc) for examples. +See the existing handlers in [src/rpc/handlers](https://github.com/XRPLF/clio/tree/develop/src/rpc/handlers) +for examples; `NFTInfo` is a small one. -Handlers need to fulfil the requirements specified by the `SomeHandler` concept (see `rpc/common/Concepts.hpp`): +Handlers need to fulfil the requirements specified by the `SomeHandler` concept (see +`rpc/common/Concepts.hpp`): -- Expose types: - - `Input` - The POD struct which acts as input for the handler +- Derive from `rpc::spec::HandlerFor::Input>`. This supplies: - - `Output` - The POD struct which acts as output of a valid handler invocation + - `Input` — the strongly-typed input struct, owned by the spec library rather than declared here -- Have a `spec(uint32_t)` member function returning a const reference to an `RpcSpec` describing the JSON input for the specified API version. + - `static parseInput(boost::json::value const&, uint32_t apiVersion)` — validates and + deserialises in one pass, returning `std::expected` -- Have a `process(Input)` member function that operates on `Input` POD and returns `HandlerReturnType` + - `static spec(uint32_t apiVersion)` — returns a type-erased `rpc::spec::RpcSpecView` -- Implement `value_from` and `value_to` support using `tag_invoke` as per `boost::json` documentation for these functions. + If the method takes no input at all, skip the base class and expose only `process(Context const&)`. + +- Expose an `Output` POD struct which acts as output of a valid handler invocation. + +- Have a `process(Input const&, Context const&)` member function returning + `HandlerReturnType`. Cross-field checks that cannot be expressed in the spec belong here. + +- Implement `value_from` support for `Output` using `tag_invoke` as per `boost::json` + documentation. A `value_to` for `Input` is **not** needed — the spec deserialises it. + +- Register the method in `rpc/common/impl/HandlerRegistry.cpp`. + +If the method has no spec yet, add `Types.hpp` and `Spec.hpp` for it in the spec library first. + +> [!IMPORTANT] +> Do not hand-write `template struct rpc::spec::HandlerFor<...>;`, and do not include +> `` or `` from a handler. The explicit +> instantiations are generated by `rpcspec_generate_instantiations()` in `src/rpc/CMakeLists.txt`, +> one translation unit per method that has a `Spec.hpp`. A useful side effect is that every spec is +> compiled by Clio's build, so spec-side mistakes surface here. + +## Error messages + +A method's error codes and messages are part of its wire contract: clients parse them, so an +altered `error`, `error_code` or `error_message` is a breaking change. If a test disagrees with the +code, fix the spec rather than the expectation. + +Clio and xrpld do not always word the same failure identically. The spec library expresses those +differences per server rather than unifying early — `ifServerClio()` / `ifServerXrpld()` for +whole validators, and the `RPCSPEC_IS_CLIO` / `RPCSPEC_IS_XRPLD` macros for anything finer. diff --git a/src/rpc/RPCHelpers.cpp b/src/rpc/RPCHelpers.cpp index f6777f003..b8d36f5e2 100644 --- a/src/rpc/RPCHelpers.cpp +++ b/src/rpc/RPCHelpers.cpp @@ -569,12 +569,12 @@ getLedgerHeaderFromLedgerSpecifier( return *maybeLgrInfo; } - if (resolved.isShortcut()) { - auto const shortcut = std::get(resolved.value); - ASSERT( - shortcut == rpc::spec::LedgerShortcut::Validated, - "current/closed ledgers must be forwarded before dispatch" - ); + // `current` and `closed` name ledgers Clio does not hold. Forwarding diverts them for the + // methods xrpld can answer; Clio-only methods are not forwarded, so they reach here. + if (resolved.isShortcut() and + std::get(resolved.value) != + rpc::spec::LedgerShortcut::Validated) { + return std::unexpected{Status{RippledError::RpcInvalidParams, "ledgerIndexMalformed"}}; } auto const ledgerSequence = resolved.isSequence() ? std::get(resolved.value) : maxSeq; @@ -1677,7 +1677,7 @@ parseBook(boost::json::object const& request) } if (payCurrency == getCurrency && payIssuer == getIssuer) - return std::unexpected{Status{RippledError::RpcBadMarket, "badMarket"}}; + return std::unexpected{Status{RippledError::RpcBadMarket}}; std::optional domainID; if (request.contains("domain")) { @@ -1784,47 +1784,4 @@ toJsonWithBinaryTx(data::TransactionAndMetadata const& txnPlusMeta, std::uint32_ return obj; } -std::optional -parseDelegateType(boost::json::value const& delegateType) -{ - if (not delegateType.is_string()) - return {}; - - auto const& type = delegateType.as_string(); - - if (type == JS(authorizer)) - return DelegateFilter::Role::Authorizer; - if (type == JS(actor)) - return DelegateFilter::Role::Actor; - - return {}; -} - -std::optional -parseDelegateFilter(boost::json::object const& delegateObject) -{ - if (!delegateObject.contains(JS(delegate_filter))) - return {}; - - auto const& filterVal = delegateObject.at(JS(delegate_filter)); - if (!filterVal.is_string()) - return {}; - - auto const delegateTypeOpt = parseDelegateType(filterVal.as_string()); - if (!delegateTypeOpt.has_value()) - return {}; - - std::optional counterParty; - if (delegateObject.contains(JS(counter_party))) { - auto const& counterpartyVal = delegateObject.at(JS(counter_party)); - - if (!counterpartyVal.is_string()) - return {}; - - counterParty = counterpartyVal.as_string(); - } - - return DelegateFilter{*delegateTypeOpt, std::move(counterParty)}; -} - } // namespace rpc diff --git a/src/rpc/RPCHelpers.hpp b/src/rpc/RPCHelpers.hpp index 0e10dfbf0..d9a61c971 100644 --- a/src/rpc/RPCHelpers.hpp +++ b/src/rpc/RPCHelpers.hpp @@ -313,8 +313,12 @@ getLedgerHeaderFromHashOrSeq( * only serves validated data. An unspecified ledger resolves via * @c LedgerSpecifier::resolved(), which the spec library fixes to @c validated for Clio. * - * @c current and @c closed cannot reach here: @ref specifiesCurrentOrClosedLedger forwards - * those upstream before dispatch. + * @c current and @c closed name ledgers Clio does not hold, and yield @c invalidParams with + * @c ledgerIndexMalformed. + * @ref specifiesCurrentOrClosedLedger diverts such requests to xrpld before dispatch, but only + * for methods xrpld can answer: @c ForwardingProxy::shouldForward returns early for Clio-only + * methods, so those arrive here with the shortcut intact. The spec does not reject the two + * shortcuts either, since xrpld needs them. * * @param backend The backend to use * @param yield The coroutine context @@ -930,22 +934,4 @@ getDeliveredAmount( uint32_t date ); -/** - * @brief Parse the delegate type from a JSON value - * - * @param delegateType The JSON value containing the delegate type string - * @return The parsed delegate type or std::nullopt if the input is invalid or not a string - */ -std::optional -parseDelegateType(boost::json::value const& delegateType); - -/** - * @brief Parse a delegate filter object from JSON - * - * @param delegateObject The JSON object containing the delegate filter input from user - * @return The constructed DelegateFilter or std::nullopt if parsing fails - */ -std::optional -parseDelegateFilter(boost::json::object const& delegateObject); - } // namespace rpc diff --git a/src/rpc/common/Checkers.hpp b/src/rpc/common/Checkers.hpp deleted file mode 100644 index 05ca7e103..000000000 --- a/src/rpc/common/Checkers.hpp +++ /dev/null @@ -1,120 +0,0 @@ -#pragma once - -#include "rpc/Errors.hpp" -#include "rpc/common/ValidationHelpers.hpp" - -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace rpc::check { -/** - * @brief Warning that checks can return - */ -struct Warning { - /** - * @brief Construct a new Warning object - * - * @param code The warning code - * @param message The warning message - */ - Warning(WarningCode code, std::string message) - : warningCode(code), extraMessage(std::move(message)) - { - } - - bool - operator==(Warning const& other) const = default; - - WarningCode warningCode; - std::string extraMessage; -}; -using Warnings = std::vector; - -/** - * @brief Check for a deprecated fields - */ -template -class Deprecated; - -/** - * @brief Check if a field is deprecated - */ -template <> -class Deprecated<> final { -public: - /** - * @brief Check if a field is deprecated - * - * @param value The json value to check - * @param key The key to check - * @return A warning if the field is deprecated or std::nullopt otherwise - */ - [[nodiscard]] static std::optional - check(boost::json::value const& value, std::string_view key) - { - if (value.is_object() and value.as_object().contains(key)) { - return Warning{ - WarningCode::WarnRpcDeprecated, fmt::format("Field '{}' is deprecated.", key) - }; - } - return std::nullopt; - } -}; - -/** - * @brief Check if a value of a field is deprecated - * @tparam T The type of the field - */ -template -class Deprecated final { - T value_; - -public: - /** - * @brief Construct a new Deprecated object - * - * @param val The value that is deprecated - */ - Deprecated(T val) : value_(std::move(val)) - { - } - - /** - * @brief Check if a value of a field is deprecated - * - * @param value The json value to check - * @param key The key to check - * @return A warning if the field is deprecated or std::nullopt otherwise - */ - [[nodiscard]] std::optional - check(boost::json::value const& value, std::string_view key) const - { - if (value.is_object() and value.as_object().contains(key) and - validation::checkType(value.as_object().at(key))) { - using boost::json::value_to; - auto const res = value_to(value.as_object().at(key)); - if (value_ == res) { - return Warning{ - WarningCode::WarnRpcDeprecated, - fmt::format("Value '{}' for field '{}' is deprecated", value_, key) - }; - } - } - return std::nullopt; - } -}; - -/** - * @brief Deduction guide for Deprecated - */ -template -Deprecated(T&&...) -> Deprecated; - -} // namespace rpc::check diff --git a/src/rpc/common/Concepts.hpp b/src/rpc/common/Concepts.hpp index df7be4d52..05dceaf30 100644 --- a/src/rpc/common/Concepts.hpp +++ b/src/rpc/common/Concepts.hpp @@ -1,7 +1,6 @@ #pragma once #include "rpc/Errors.hpp" -#include "rpc/common/Checkers.hpp" #include "rpc/common/Types.hpp" #include @@ -18,38 +17,6 @@ namespace rpc { -struct RpcSpec; - -/** - * @brief Specifies what a requirement used with @ref rpc::FieldSpec must provide. - */ -template -concept SomeRequirement = requires(T a, boost::json::value lval) { - { a.verify(lval, std::string{}) } -> std::same_as; -}; - -/** - * @brief Specifies what a modifier used with @ref rpc::FieldSpec must provide. - */ -template -concept SomeModifier = requires(T a, boost::json::value lval) { - { a.modify(lval, std::string{}) } -> std::same_as; -}; - -/** - * @brief Specifies what a check used with @ref rpc::FieldSpec must provide. - */ -template -concept SomeCheck = requires(T a, boost::json::value lval) { - { a.check(lval, std::string{}) } -> std::same_as>; -}; - -/** - * @brief The requirements of a processor to be used with @ref rpc::FieldSpec. - */ -template -concept SomeProcessor = (SomeRequirement or SomeModifier); - /** * @brief A process function that expects both some Input and a Context. */ @@ -67,25 +34,12 @@ concept SomeContextProcessWithoutInput = requires(T a, T::Output out, Context co { a.process(ctx) } -> std::same_as>; }; -/** - * @brief Specifies what a Handler with Input must provide. - */ -template -concept SomeHandlerWithInput = requires(T a, uint32_t version) { - { a.spec(version) } -> std::same_as; -} and SomeContextProcessWithInput and boost::json::has_value_to::value; - /** * @brief Specifies what a Handler validated by the shared consteval spec must provide. * * Such a handler inherits @c rpc::spec::HandlerFor from the spec library, which * supplies a static @c parseInput (validate and deserialise in one pass) and a static - * @c spec returning a type-erased @c RpcSpecView. Presence of @c parseInput is what - * selects this path over @c SomeHandlerWithInput. - * - * The two input paths are mutually exclusive by construction: a legacy handler returns - * @c RpcSpec @c const& from a non-static @c spec and needs a @c value_to for its Input, - * neither of which holds here. @c kIsSingleInputPath asserts that below. + * @c spec returning a type-erased @c RpcSpecView. */ template concept SomeHandlerWithTypedInput = requires(uint32_t version, boost::json::value jv) { @@ -100,21 +54,11 @@ concept SomeHandlerWithTypedInput = requires(uint32_t version, boost::json::valu template concept SomeHandlerWithoutInput = SomeContextProcessWithoutInput; -/** - * @brief True when @p T does not straddle the legacy and typed input paths. - * - * Guards the @c if @c constexpr chain in @c DefaultProcessor - were a handler to satisfy - * both, the dispatch order alone would silently decide which spec ran. - */ -template -constexpr bool kIsSingleInputPath = not(SomeHandlerWithInput and SomeHandlerWithTypedInput); - /** * @brief Specifies what a Handler type must provide. */ template -concept SomeHandler = - (SomeHandlerWithInput or SomeHandlerWithTypedInput or SomeHandlerWithoutInput) and +concept SomeHandler = (SomeHandlerWithTypedInput or SomeHandlerWithoutInput) and boost::json::has_value_from::value; } // namespace rpc diff --git a/src/rpc/common/JsonBool.hpp b/src/rpc/common/JsonBool.hpp deleted file mode 100644 index 6ec80883c..000000000 --- a/src/rpc/common/JsonBool.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include - -namespace rpc { - -/** - * @brief A wrapper around bool that allows conversion from any JSON value. - * - */ -using JsonBool = rpc::spec::JsonBool; - -} // namespace rpc diff --git a/src/rpc/common/MetaProcessors.cpp b/src/rpc/common/MetaProcessors.cpp deleted file mode 100644 index 6b6155d25..000000000 --- a/src/rpc/common/MetaProcessors.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#include "rpc/common/MetaProcessors.hpp" - -#include "rpc/common/Types.hpp" - -#include -#include - -#include - -namespace rpc::meta { - -[[nodiscard]] MaybeError -Section::verify(boost::json::value& value, std::string_view key) const -{ - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - auto& res = value.as_object().at(key); - - // if it is not a json object, let other validators fail - if (!res.is_object()) - return {}; - - for (auto const& spec : specs_) { - if (auto const ret = spec.process(res); not ret) - return Error{ret.error()}; - } - - return {}; -} - -[[nodiscard]] MaybeError -ValidateArrayAt::verify(boost::json::value& value, std::string_view key) const -{ - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - if (not value.as_object().at(key).is_array()) - return Error{Status{RippledError::RpcInvalidParams}}; - - auto& arr = value.as_object().at(key).as_array(); - if (idx_ >= arr.size()) - return Error{Status{RippledError::RpcInvalidParams}}; - - auto& res = arr.at(idx_); - for (auto const& spec : specs_) { - if (auto const ret = spec.process(res); not ret) - return Error{ret.error()}; - } - - return {}; -} - -} // namespace rpc::meta diff --git a/src/rpc/common/MetaProcessors.hpp b/src/rpc/common/MetaProcessors.hpp deleted file mode 100644 index 09cf383a8..000000000 --- a/src/rpc/common/MetaProcessors.hpp +++ /dev/null @@ -1,224 +0,0 @@ -#pragma once - -#include "rpc/Errors.hpp" -#include "rpc/common/Concepts.hpp" -#include "rpc/common/Specs.hpp" -#include "rpc/common/Types.hpp" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace rpc::meta { - -/** - * @brief A meta-processor that acts as a spec for a sub-object/section. - */ -class Section final { - std::vector specs_; - -public: - /** - * @brief Construct new section validator from a list of specs. - * - * @param specs List of specs @ref FieldSpec - */ - explicit Section(std::initializer_list specs) : specs_{specs} - { - } - - /** - * @brief Verify that the JSON value representing the section is valid according to the given - * specs. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the section from the outer object - * @return Possibly an error - */ - [[nodiscard]] MaybeError - verify(boost::json::value& value, std::string_view key) const; -}; - -/** - * @brief A meta-processor that specifies a list of specs to run against the object at the given - * index in the array. - */ -class ValidateArrayAt final { - std::size_t idx_; - std::vector specs_; - -public: - /** - * @brief Constructs a processor that validates the specified element of a JSON array. - * - * @param idx The index inside the array to validate - * @param specs The specifications to validate against - */ - ValidateArrayAt(std::size_t idx, std::initializer_list specs) - : idx_{idx}, specs_{specs} - { - } - - /** - * @brief Verify that the JSON array element at given index is valid according the stored specs. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the array from the outer object - * @return Possibly an error - */ - [[nodiscard]] MaybeError - verify(boost::json::value& value, std::string_view key) const; -}; - -/** - * @brief A meta-processor that specifies a list of requirements to run against when the type - * matches the template parameter. - */ -template -class IfType final { -public: - /** - * @brief Constructs a validator that validates the specs if the type matches. - * @param requirements The requirements to validate against - */ - template - explicit IfType(Requirements&&... requirements) - : processor_( - [... r = std::forward( - requirements - )](boost::json::value& j, std::string_view key) -> MaybeError { - std::optional firstFailure = std::nullopt; - - // the check logic is the same as fieldspec - ( - [&j, &key, &firstFailure, req = &r]() { - if (firstFailure) - return; - - if (auto const res = req->verify(j, key); not res) - firstFailure = res.error(); - }(), - ...); - - if (firstFailure) - return Error{*firstFailure}; - - return {}; - } - ) - { - } - - IfType(IfType const&) = default; - IfType(IfType&&) = default; - - /** - * @brief Verify that the element is valid according to the stored requirements when type - * matches. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the element from the outer object - * @return Possibly an error - */ - [[nodiscard]] MaybeError - verify(boost::json::value& value, std::string_view key) const - { - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - if (not rpc::validation::checkType(value.as_object().at(key))) - return {}; // ignore if type does not match - - return processor_(value, key); - } - -private: - std::function processor_; -}; - -/** - * @brief A meta-processor that wraps a validator and produces a custom error in case the wrapped - * validator fails. - */ -template - requires SomeRequirement or SomeModifier -class WithCustomError final { - RequirementOrModifierType reqOrModifier_; - Status error_; - -public: - /** - * @brief Constructs a validator that calls the given validator `req` and returns a custom error - * `err` in case `req` fails. - * - * @param reqOrModifier The requirement to validate against - * @param err The custom error to return in case `req` fails - */ - WithCustomError(RequirementOrModifierType reqOrModifier, Status err) - : reqOrModifier_{std::move(reqOrModifier)}, error_{std::move(err)} - { - } - - /** - * @brief Runs the stored validator and produces a custom error if the wrapped validator fails. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the element from the outer object - * @return Possibly an error - */ - [[nodiscard]] MaybeError - verify(boost::json::value const& value, std::string_view key) const - requires SomeRequirement - { - if (auto const res = reqOrModifier_.verify(value, key); not res) - return Error{error_}; - - return {}; - } - - /** - * @brief Runs the stored validator and produces a custom error if the wrapped validator fails. - * This is an overload for the requirement which can modify the value. Such as IfType. - * - * @param value The JSON value representing the outer object, this value can be modified by the - * requirement inside - * @param key The key used to retrieve the element from the outer object - * @return Possibly an error - */ - [[nodiscard]] MaybeError - verify(boost::json::value& value, std::string_view key) const - requires SomeRequirement - { - if (auto const res = reqOrModifier_.verify(value, key); not res) - return Error{error_}; - - return {}; - } - - /** - * @brief Runs the stored modifier and produces a custom error if the wrapped modifier fails. - * - * @param value The JSON value representing the outer object. This value can be modified by the - * modifier. - * @param key The key used to retrieve the element from the outer object - * @return Possibly an error - */ - MaybeError - modify(boost::json::value& value, std::string_view key) const - requires SomeModifier - - { - if (auto const res = reqOrModifier_.modify(value, key); not res) - return Error{error_}; - return {}; - } -}; - -} // namespace rpc::meta diff --git a/src/rpc/common/Modifiers.hpp b/src/rpc/common/Modifiers.hpp deleted file mode 100644 index 0b621220b..000000000 --- a/src/rpc/common/Modifiers.hpp +++ /dev/null @@ -1,161 +0,0 @@ -#pragma once - -#include "rpc/Errors.hpp" -#include "rpc/common/Types.hpp" -#include "util/JsonUtils.hpp" - -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace rpc::modifiers { - -/** - * @brief Clamp value between min and max. - */ -template -class Clamp final { - Type min_; - Type max_; - -public: - /** - * @brief Construct the modifier storing min and max values. - * - * @param min - * @param max - */ - explicit Clamp(Type min, Type max) : min_{min}, max_{max} - { - } - - /** - * @brief Clamp the value to stored min and max values. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the modified value from the outer object - * @return Possibly an error - */ - [[nodiscard]] MaybeError - modify(boost::json::value& value, std::string_view key) const - { - using boost::json::value_to; - - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - // clamp to min_ and max_ - auto const oldValue = value_to(value.as_object().at(key)); - value.as_object()[key] = std::clamp(oldValue, min_, max_); - - return {}; - } -}; - -/** - * @brief Convert input string to lower case. - * - * Note: the conversion is only performed if the input value is a string. - */ -struct ToLower final { - /** - * @brief Update the input string to lower case. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the modified value from the outer object - * @return Possibly an error - */ - [[nodiscard]] static MaybeError - modify(boost::json::value& value, std::string_view key) - { - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - if (not value.as_object().at(key).is_string()) - return {}; // ignore for non-string types - - value.as_object()[key] = - util::toLower(boost::json::value_to(value.as_object().at(key))); - return {}; - } -}; - -/** - * @brief Convert input string to integer. - * - * Note: the conversion is only performed if the input value is a string. - */ -struct ToNumber final { - /** - * @brief Update the input string to integer if it can be converted to integer by stoi. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the modified value from the outer object - * @return Possibly an error - */ - [[nodiscard]] static MaybeError - modify(boost::json::value& value, std::string_view key) - { - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - if (not value.as_object().at(key).is_string()) - return {}; // ignore for non-string types - - auto const strInt = boost::json::value_to(value.as_object().at(key)); - if (strInt.contains('.')) - return Error{Status{RippledError::RpcInvalidParams}}; // maybe a float - - try { - value.as_object()[key] = std::stoi(strInt); - } catch (std::exception& e) { - return Error{Status{RippledError::RpcInvalidParams}}; - } - return {}; - } -}; - -/** - * @brief Customised modifier allowing user define how to modify input in provided callable. - */ -class CustomModifier final { - std::function modifier_; - -public: - /** - * @brief Constructs a custom modifier from any supported callable. - * - * @tparam Fn The type of callable - * @param fn The callable/function object - */ - template - requires std::invocable - explicit CustomModifier(Fn&& fn) : modifier_{std::forward(fn)} - { - } - - /** - * @brief Modify the JSON value according to the custom modifier function stored. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return Any compatible user-provided error if modify/verify failed; otherwise no error is - * returned - */ - [[nodiscard]] MaybeError - modify(boost::json::value& value, std::string_view key) const - { - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - return modifier_(value.as_object().at(key), key); - }; -}; - -} // namespace rpc::modifiers diff --git a/src/rpc/common/Specs.cpp b/src/rpc/common/Specs.cpp deleted file mode 100644 index ab0919265..000000000 --- a/src/rpc/common/Specs.cpp +++ /dev/null @@ -1,63 +0,0 @@ -#include "rpc/common/Specs.hpp" - -#include "rpc/common/Checkers.hpp" -#include "rpc/common/Types.hpp" - -#include -#include -#include - -#include -#include -#include -#include - -namespace rpc { - -[[nodiscard]] MaybeError -FieldSpec::process(boost::json::value& value) const -{ - return processor_(value); -} - -[[nodiscard]] check::Warnings -FieldSpec::check(boost::json::value const& value) const -{ - return checker_(value); -} - -[[nodiscard]] MaybeError -RpcSpec::process(boost::json::value& value) const -{ - for (auto const& field : fields_) { - if (auto ret = field.process(value); not ret) - return Error{ret.error()}; - } - - return {}; -} - -[[nodiscard]] boost::json::array -RpcSpec::check(boost::json::value const& value) const -{ - std::unordered_map> warnings; - for (auto const& field : fields_) { - auto fieldWarnings = field.check(value); - for (auto& fw : fieldWarnings) { - warnings[fw.warningCode].push_back(std::move(fw.extraMessage)); - } - } - - boost::json::array result; - for (auto const& [code, messages] : warnings) { - auto warningObject = makeWarning(code); - auto& warningMessage = warningObject["message"].as_string(); - for (auto const& message : messages) { - warningMessage.append(" ").append(message); - } - result.push_back(std::move(warningObject)); - } - return result; -} - -} // namespace rpc diff --git a/src/rpc/common/Specs.hpp b/src/rpc/common/Specs.hpp deleted file mode 100644 index 5a5e6eaa7..000000000 --- a/src/rpc/common/Specs.hpp +++ /dev/null @@ -1,132 +0,0 @@ -#pragma once - -#include "rpc/common/Checkers.hpp" -#include "rpc/common/Concepts.hpp" -#include "rpc/common/Types.hpp" -#include "rpc/common/impl/Factories.hpp" - -#include -#include - -#include -#include -#include -#include - -namespace rpc { - -/** - * @brief Represents a Specification for one field of an RPC command. - */ -struct FieldSpec final { - /** - * @brief Construct a field specification out of a set of processors. - * - * @tparam Processors The types of processors - * @param key The key in a JSON object that the field validates - * @param processors The processors, each of them have to fulfil the @ref rpc::SomeProcessor - * concept - */ - template - FieldSpec(std::string const& key, Processors&&... processors) - : processor_{ - impl::makeFieldProcessor(key, std::forward(processors)...) - } - , checker_{impl::kEmptyFieldChecker} - { - } - - /** - * @brief Construct a field specification out of a set of checkers. - * - * @tparam Checks The types of checkers - * @param key The key in a JSON object that the field validates - * @param checks The checks, each of them have to fulfil the @ref rpc::SomeCheck concept - */ - template - FieldSpec(std::string const& key, Checks&&... checks) - : processor_{impl::kEmptyFieldProcessor} - , checker_{impl::makeFieldChecker(key, std::forward(checks)...)} - { - } - - /** - * @brief Processes the passed JSON value using the stored processors. - * - * @param value The JSON value to validate and/or modify - * @return Nothing on success; Status on error - */ - [[nodiscard]] MaybeError - process(boost::json::value& value) const; - - /** - * @brief Checks the passed JSON value using the stored checkers. - * - * @param value The JSON value to validate - * @return A vector of warnings (empty if no warnings) - */ - [[nodiscard]] check::Warnings - check(boost::json::value const& value) const; - -private: - impl::FieldSpecProcessor processor_; - impl::FieldChecker checker_; -}; - -/** - * @brief Represents a Specification of an entire RPC command. - * - * Note: this should really be all constexpr and handlers would expose - * static constexpr RpcSpec spec instead. Maybe some day in the future. - */ -struct RpcSpec final { - /** - * @brief Construct a full RPC request specification. - * - * @param fields The fields of the RPC specification @ref FieldSpec - */ - RpcSpec(std::initializer_list fields) : fields_{fields} - { - } - - /** - * @brief Construct a full RPC request specification from another spec and additional fields. - * - * @param other The other spec to copy fields from - * @param additionalFields The additional fields to add to the spec - */ - RpcSpec(RpcSpec const& other, std::initializer_list additionalFields) - : fields_{other.fields_} - { - for (auto& f : additionalFields) - fields_.push_back(f); - } - - /** - * @brief Processes the passed JSON value using the stored field specs. - * - * @param value The JSON value to validate and/or modify - * @return Nothing on success; Status on error - */ - [[nodiscard]] MaybeError - process(boost::json::value& value) const; - - /** - * @brief Checks the passed JSON value using the stored field specs. - * - * @param value The JSON value to validate - * @return JSON array of warnings (empty if no warnings) - */ - [[nodiscard]] boost::json::array - check(boost::json::value const& value) const; - -private: - std::vector fields_; -}; - -/** - * @brief An alias for a const reference to @ref RpcSpec. - */ -using RpcSpecConstRef = RpcSpec const&; - -} // namespace rpc diff --git a/src/rpc/common/Types.hpp b/src/rpc/common/Types.hpp index d8697f5af..396d14d10 100644 --- a/src/rpc/common/Types.hpp +++ b/src/rpc/common/Types.hpp @@ -14,7 +14,6 @@ #include #include -#include #include #include @@ -175,35 +174,6 @@ struct AccountCursor { } }; -/** - * @brief A delegate object used filter account_tx by specific delegate accounts - */ -struct DelegateFilter { - /** - * @brief A delegate type used in delegate filter - */ - enum class Role : std::uint8_t { - Actor, /**< This account is the *active* sender, acting on behalf of another party. - * e.g., Account A in "A sends payment to B on behalf of C." */ - - Authorizer /**< This account is the *passive* party whose funds are being moved from. - * e.g., Account C in "A sends payment to B on behalf of C." */ - }; - - /** - * @brief Construct a delegate filter - * @param role The delegate type to filter by - * @param counterParty The optional counterparty to further narrow the filter by - */ - explicit DelegateFilter(Role role, std::optional counterParty = std::nullopt) - : delegateType(role), counterParty(std::move(counterParty)) - { - } - - Role delegateType; - std::optional counterParty; -}; - /** * @brief Convert an empty output to a JSON object * diff --git a/src/rpc/common/ValidationHelpers.hpp b/src/rpc/common/ValidationHelpers.hpp deleted file mode 100644 index 0c143b43e..000000000 --- a/src/rpc/common/ValidationHelpers.hpp +++ /dev/null @@ -1,117 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace rpc::validation { -namespace impl { - -template -void -clampAs(boost::json::value& value) -{ - if (value.is_uint64()) { - auto const valueUint = value.as_uint64(); - if (valueUint > static_cast(std::numeric_limits::max())) - value = std::numeric_limits::max(); - } else if (value.is_int64()) { - auto const valueInt = value.as_int64(); - if (valueInt > static_cast(std::numeric_limits::max())) - value = std::numeric_limits::max(); - } -} - -template -void -clampAs(boost::json::value& value) -{ - if (value.is_uint64()) { - auto const valueUint = value.as_uint64(); - if (valueUint > static_cast(std::numeric_limits::max())) - value = std::numeric_limits::max(); - } else if (value.is_int64()) { - auto const valueInt = value.as_int64(); - if (valueInt > static_cast(std::numeric_limits::max())) { - value = std::numeric_limits::max(); - } else if (valueInt < static_cast(std::numeric_limits::min())) { - value = std::numeric_limits::min(); - } - } -} - -} // namespace impl - -/** - * @brief Check that the type is the same as what was expected. - * - * @tparam Expected The expected type that value should be convertible to - * @param value The json value to check the type of - * @return true if convertible; false otherwise - */ -template -[[nodiscard]] bool -checkType(boost::json::value const& value) -{ - auto hasError = false; - if constexpr (std::is_same_v) { - if (not value.is_bool()) - hasError = true; - } else if constexpr (std::is_same_v) { - if (not value.is_string()) - hasError = true; - } else if constexpr (std::is_same_v or std::is_same_v) { - if (not value.is_double()) - hasError = true; - } else if constexpr (std::is_same_v) { - if (not value.is_array()) - hasError = true; - } else if constexpr (std::is_same_v) { - if (not value.is_object()) - hasError = true; - } else if constexpr ( - std::is_convertible_v or std::is_convertible_v - ) { - if (not value.is_int64() && not value.is_uint64()) - hasError = true; - // if the type specified is unsigned, it should not be negative - if constexpr (std::is_unsigned_v) { - if (value.is_int64() and value.as_int64() < 0) - hasError = true; - } - } - - return not hasError; -} - -/** - * @brief Check that the type is the same as what was expected optionally clamping it into range. - * - * This is used to automatically clamp the value into the range available to the specified type. It - * is needed in order to avoid Min, Max and other validators throw "not exact" error from Boost.Json - * library if the value does not fit in the specified type. - * - * @tparam Expected The expected type that value should be convertible to - * @param value The json value to check the type of - * @return true if convertible; false otherwise - */ -template -[[nodiscard]] bool -checkTypeAndClamp(boost::json::value& value) -{ - if (not checkType(value)) - return false; // fails basic type check - - if constexpr (std::is_integral_v and not std::is_same_v) - impl::clampAs(value); - - return true; -} - -} // namespace rpc::validation diff --git a/src/rpc/common/Validators.cpp b/src/rpc/common/Validators.cpp deleted file mode 100644 index bc31299fa..000000000 --- a/src/rpc/common/Validators.cpp +++ /dev/null @@ -1,451 +0,0 @@ -#include "rpc/common/Validators.hpp" - -#include "rpc/JS.hpp" -#include "rpc/RPCHelpers.hpp" -#include "rpc/common/Types.hpp" -#include "util/AccountUtils.hpp" -#include "util/TimeUtils.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace rpc::validation { - -[[nodiscard]] MaybeError -Required::verify(boost::json::value const& value, std::string_view key) -{ - if (not value.is_object() or not value.as_object().contains(key)) { - return Error{Status{ - RippledError::RpcInvalidParams, "Required field '" + std::string{key} + "' missing" - }}; - } - - return {}; -} - -[[nodiscard]] MaybeError -TimeFormatValidator::verify(boost::json::value const& value, std::string_view key) const -{ - using boost::json::value_to; - - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - if (not value.as_object().at(key).is_string()) - return Error{Status{RippledError::RpcInvalidParams}}; - - auto const ret = - util::systemTpFromUtcStr(value_to(value.as_object().at(key)), format_); - if (!ret) - return Error{Status{RippledError::RpcInvalidParams}}; - - return {}; -} - -[[nodiscard]] MaybeError -CustomValidator::verify(boost::json::value const& value, std::string_view key) const -{ - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - return validator_(value.as_object().at(key), key); -} - -[[nodiscard]] bool -checkIsU32Numeric(std::string_view sv) -{ - uint32_t unused = 0; - auto [_, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), unused); - - return ec == std::errc(); -} - -CustomValidator CustomValidators::uint160HexStringValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - return makeHexStringValidator(value, key); - }}; - -CustomValidator CustomValidators::uint192HexStringValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - return makeHexStringValidator(value, key); - }}; - -CustomValidator CustomValidators::uint256HexStringValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - return makeHexStringValidator(value, key); - }}; - -CustomValidator CustomValidators::ledgerIndexValidator = - CustomValidator{[](boost::json::value const& value, std::string_view /* key */) -> MaybeError { - auto err = Error{Status{RippledError::RpcInvalidParams, "ledgerIndexMalformed"}}; - - if (!value.is_string() && !(value.is_uint64() || value.is_int64())) - return err; - - if (value.is_string() && value.as_string() != "validated" && - !checkIsU32Numeric(boost::json::value_to(value))) - return err; - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::ledgerTypeValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_string()) { - return Error{Status{ - RippledError::RpcInvalidParams, fmt::format("Invalid field '{}', not string.", key) - }}; - } - - auto const type = - rpc::spec::ledgerEntryTypeFromStr(boost::json::value_to(value)); - if (type == xrpl::ltANY) { - return Error{ - Status{RippledError::RpcInvalidParams, fmt::format("Invalid field '{}'.", key)} - }; - } - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::accountValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_string()) - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotString"}}; - - // TODO: we are using accountFromStringStrict from RPCHelpers, after we - // remove all old handler, this function can be moved to here - if (!accountFromStringStrict(boost::json::value_to(value))) - return Error{Status{RippledError::RpcActMalformed, std::string(key) + "Malformed"}}; - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::accountBase58Validator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_string()) - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotString"}}; - - auto const account = - util::parseBase58Wrapper(boost::json::value_to(value)); - if (!account || account->isZero()) - return Error{Status{ClioError::RpcMalformedAddress}}; - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::accountMarkerValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_string()) - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotString"}}; - - // TODO: we are using parseAccountCursor from RPCHelpers, after we - // remove all old handler, this function can be moved to here - if (!parseAccountCursor(boost::json::value_to(value))) { - // align with the current error message - return Error{Status{RippledError::RpcInvalidParams, "Malformed cursor."}}; - } - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::accountTypeValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_string()) { - return Error{Status{ - RippledError::RpcInvalidParams, fmt::format("Invalid field '{}', not string.", key) - }}; - } - - auto const type = - rpc::spec::accountOwnedLedgerTypeFromStr(boost::json::value_to(value)); - if (type == xrpl::ltANY) { - return Error{ - Status{RippledError::RpcInvalidParams, fmt::format("Invalid field '{}'.", key)} - }; - } - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::currencyValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_string()) - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotString"}}; - - auto const currencyStr = boost::json::value_to(value); - if (currencyStr.empty()) - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "IsEmpty"}}; - - xrpl::Currency currency; - if (!xrpl::toCurrency(currency, currencyStr)) - return Error{Status{ClioError::RpcMalformedCurrency, "malformedCurrency"}}; - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::issuerValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_string()) - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotString"}}; - - xrpl::AccountID issuer; - - // TODO: need to align with the error - if (!xrpl::toIssuer(issuer, boost::json::value_to(value))) { - return Error{Status{ - RippledError::RpcInvalidParams, fmt::format("Invalid field '{}', bad issuer.", key) - }}; - } - - if (issuer == xrpl::noAccount()) { - return Error{Status{ - RippledError::RpcInvalidParams, - fmt::format("Invalid field '{}', bad issuer account one.", key) - }}; - } - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::bookTakerValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_object()) - return {}; // type is validated separately by Type - - auto const& obj = value.as_object(); - bool const hasCurrency = obj.contains("currency"); - bool const hasMptId = obj.contains("mpt_issuance_id"); - - if (!hasCurrency && !hasMptId) { - return Error{Status{ - RippledError::RpcInvalidParams, fmt::format("Missing field '{}.currency'.", key) - }}; - } - - if (hasMptId && (hasCurrency || obj.contains("issuer"))) { - return Error{ - Status{RippledError::RpcInvalidParams, fmt::format("Invalid field '{}'.", key)} - }; - } - - // Wrong type -> invalidParams (xrpld's validateTakerJSON), checked here before the - // per-field validators so they can own bad *values* -> dst/srcAmtMalformed. - if ((hasCurrency && !obj.at(JS(currency)).is_string()) || - (hasMptId && !obj.at(JS(mpt_issuance_id)).is_string())) { - return Error{Status{ - RippledError::RpcInvalidParams, - fmt::format("Invalid field '{}.currency', not string.", key) - }}; - } - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::subscribeStreamValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_array()) - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotArray"}}; - - static std::unordered_set const kValidStreams = { - "ledger", - "transactions", - "transactions_proposed", - "book_changes", - "manifests", - "validations" - }; - - static std::unordered_set const kNotSupportStreams = { - "peer_status", "consensus", "server" - }; - for (auto const& v : value.as_array()) { - if (!v.is_string()) - return Error{Status{RippledError::RpcInvalidParams, "streamNotString"}}; - - if (kNotSupportStreams.contains(boost::json::value_to(v))) - return Error{Status{RippledError::RpcNotSupported}}; - - if (not kValidStreams.contains(boost::json::value_to(v))) - return Error{Status{RippledError::RpcStreamMalformed}}; - } - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::subscribeAccountsValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_array()) - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotArray"}}; - - if (value.as_array().empty()) - return Error{Status{RippledError::RpcActMalformed, std::string(key) + " malformed."}}; - - for (auto const& v : value.as_array()) { - auto obj = boost::json::object(); - auto const keyItem = std::string(key) + "'sItem"; - - obj[keyItem] = v; - - if (auto err = accountValidator.verify(obj, keyItem); !err) - return err; - } - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::currencyIssueValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (not value.is_object()) - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotObject"}}; - - try { - parseIssue(value.as_object()); - } catch (std::runtime_error const&) { - return Error{Status{ClioError::RpcMalformedRequest}}; - } - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::credentialTypeValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (not value.is_string()) { - return Error{Status{ - ClioError::RpcMalformedAuthorizedCredentials, std::string(key) + " NotString" - }}; - } - - auto const& credTypeHex = xrpl::strUnHex(value.as_string()); - if (!credTypeHex.has_value()) { - return Error{Status{ - ClioError::RpcMalformedAuthorizedCredentials, std::string(key) + " NotHexString" - }}; - } - - if (credTypeHex->empty()) { - return Error{ - Status{ClioError::RpcMalformedAuthorizedCredentials, std::string(key) + " is empty"} - }; - } - - if (credTypeHex->size() > xrpl::kMaxCredentialTypeLength) { - return Error{Status{ - ClioError::RpcMalformedAuthorizedCredentials, - std::string(key) + " greater than max length" - }}; - } - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::authorizeCredentialValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (not value.is_array()) - return Error{Status{ClioError::RpcMalformedRequest, std::string(key) + " not array"}}; - - auto const& authCred = value.as_array(); - if (authCred.empty()) { - return Error{Status{ - ClioError::RpcMalformedAuthorizedCredentials, - fmt::format("Requires at least one element in authorized_credentials array.") - }}; - } - - if (authCred.size() > xrpl::kMaxCredentialsArraySize) { - return Error{Status{ - ClioError::RpcMalformedAuthorizedCredentials, - fmt::format( - "Max {} number of credentials in authorized_credentials array", - xrpl::kMaxCredentialsArraySize - ) - }}; - } - - for (auto const& credObj : value.as_array()) { - if (!credObj.is_object()) { - return Error{Status{ - ClioError::RpcMalformedAuthorizedCredentials, - "authorized_credentials elements in array are not objects." - }}; - } - auto const& obj = credObj.as_object(); - - if (!obj.contains("issuer")) { - return Error{Status{ - ClioError::RpcMalformedAuthorizedCredentials, - "Field 'Issuer' is required but missing." - }}; - } - - // don't want to change issuer error message to be about credentials - if (!accountBase58Validator.verify(credObj, "issuer")) { - return Error{ - Status{ClioError::RpcMalformedAuthorizedCredentials, "issuer NotString"} - }; - } - - if (!obj.contains("credential_type")) { - return Error{Status{ - ClioError::RpcMalformedAuthorizedCredentials, - "Field 'CredentialType' is required but missing." - }}; - } - - if (auto const err = credentialTypeValidator.verify(credObj, "credential_type"); !err) - return err; - } - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::delegateValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (not value.is_object()) - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotObject"}}; - - auto const& delegate = value.as_object(); - if (!delegate.contains(JS(delegate_filter))) { - return Error{Status{ - RippledError::RpcInvalidParams, "Field 'delegate_filter' is required but missing." - }}; - } - - if (!parseDelegateType(delegate.at(JS(delegate_filter))).has_value()) { - return Error{Status{ - RippledError::RpcInvalidParams, - "Field 'delegate_filter' value must be 'actor' or 'authorizer'." - }}; - } - - if (delegate.contains(JS(counter_party)) && - !accountValidator.verify(delegate, JS(counter_party))) { - return Error{Status{ - RippledError::RpcActMalformed, - "Field 'counter_party' value must be a valid account." - }}; - } - - return MaybeError{}; - }}; - -} // namespace rpc::validation diff --git a/src/rpc/common/Validators.hpp b/src/rpc/common/Validators.hpp deleted file mode 100644 index 7b8afce90..000000000 --- a/src/rpc/common/Validators.hpp +++ /dev/null @@ -1,654 +0,0 @@ -#pragma once - -#include "rpc/Errors.hpp" -#include "rpc/common/Types.hpp" -#include "rpc/common/ValidationHelpers.hpp" - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace rpc::validation { - -/** - * @brief A validator that simply requires a field to be present. - */ -struct Required final { - /** - * @brief Verify that the JSON value is present and not null. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return An error if validation failed; otherwise no error is returned - */ - [[nodiscard]] static MaybeError - verify(boost::json::value const& value, std::string_view key); -}; - -/** - * @brief A validator that forbids a field to be present. - * - * If there is a value provided, it will forbid the field only when the value equals. - * If there is no value provided, it will forbid the field when the field shows up. - */ -template -class NotSupported; - -/** - * @brief A specialized NotSupported validator that forbids a field to be present when the value - * equals the given value. - */ -template -class NotSupported final { - T value_; - -public: - /** - * @brief Constructs a new NotSupported validator. - * - * @param val The value to store and verify against - */ - NotSupported(T val) : value_(val) - { - } - - /** - * @brief Verify whether the field is supported or not. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcNotSupported` if the value matched; otherwise no error is returned - */ - [[nodiscard]] MaybeError - verify(boost::json::value const& value, std::string_view key) const - { - if (value.is_object() and value.as_object().contains(key)) { - using boost::json::value_to; - auto const res = value_to(value.as_object().at(key)); - if (value_ == res) { - return Error{Status{ - RippledError::RpcNotSupported, - fmt::format("Not supported field '{}'s value '{}'", std::string{key}, res) - }}; - } - } - return {}; - } -}; - -/** - * @brief A specialized NotSupported validator that forbids a field to be present. - */ -template <> -class NotSupported<> final { -public: - /** - * @brief Verify whether the field is supported or not. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcNotSupported` if the field is found; otherwise no error is - * returned - */ - [[nodiscard]] static MaybeError - verify(boost::json::value const& value, std::string_view key) - { - if (value.is_object() and value.as_object().contains(key)) { - return Error{Status{ - RippledError::RpcNotSupported, "Not supported field '" + std::string{key} + '\'' - }}; - } - - return {}; - } -}; - -/** - * @brief Deduction guide to avoid having to specify the template arguments. - */ -template -NotSupported(T&&... t) -> NotSupported; - -/** - * @brief Validates that the type of the value is one of the given types. - */ -template -struct Type final { - /** - * @brief Verify that the JSON value is (one) of specified type(s). - * @note The value itself can only change for integral types and only if the value is outside of - * the range of the expected integer type (see checkTypeAndClamp). - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is - * returned - */ - [[nodiscard]] MaybeError - verify(boost::json::value& value, std::string_view key) const - { - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. If field is supposed to exist, let 'required' fail instead - - auto& res = value.as_object().at(key); - auto const convertible = (checkTypeAndClamp(res) || ...); - - if (not convertible) - return Error{Status{RippledError::RpcInvalidParams}}; - - return {}; - } -}; - -/** - * @brief Validate that value is between specified min and max. - */ -template -class Between final { - Type min_; - Type max_; - -public: - /** - * @brief Construct the validator storing min and max values. - * - * @param min - * @param max - */ - explicit Between(Type min, Type max) : min_{min}, max_{max} - { - } - - /** - * @brief Verify that the JSON value is within a certain range. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is - * returned - */ - [[nodiscard]] MaybeError - verify(boost::json::value const& value, std::string_view key) const - { - using boost::json::value_to; - - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - auto const res = value_to(value.as_object().at(key)); - - // TODO: may want a way to make this code more generic (e.g. use a free - // function that can be overridden for this comparison) - if (res < min_ || res > max_) - return Error{Status{RippledError::RpcInvalidParams}}; - - return {}; - } -}; - -/** - * @brief Validate that value is equal or greater than the specified min. - */ -template -class Min final { - Type min_; - -public: - /** - * @brief Construct the validator storing min value. - * - * @param min - */ - explicit Min(Type min) : min_{min} - { - } - - /** - * @brief Verify that the JSON value is not smaller than min - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is - * returned - */ - [[nodiscard]] MaybeError - verify(boost::json::value const& value, std::string_view key) const - { - using boost::json::value_to; - - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - auto const res = value_to(value.as_object().at(key)); - - if (res < min_) - return Error{Status{RippledError::RpcInvalidParams}}; - - return {}; - } -}; - -/** - * @brief Validate that value is not greater than max. - */ -template -class Max final { - Type max_; - -public: - /** - * @brief Construct the validator storing max value. - * - * @param max - */ - explicit Max(Type max) : max_{max} - { - } - - /** - * @brief Verify that the JSON value is not greater than max. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is - * returned - */ - [[nodiscard]] MaybeError - verify(boost::json::value const& value, std::string_view key) const - { - using boost::json::value_to; - - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - auto const res = value_to(value.as_object().at(key)); - - if (res > max_) - return Error{Status{RippledError::RpcInvalidParams}}; - - return {}; - } -}; - -/** - * @brief Validate that value can be converted to time according to the given format. - */ -class TimeFormatValidator final { - std::string format_; - -public: - /** - * @brief Construct the validator storing format value. - * - * @param format The format to use for time conversion - */ - explicit TimeFormatValidator(std::string format) : format_{std::move(format)} - { - } - - /** - * @brief Verify that the JSON value is valid formatted time. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is - * returned - */ - [[nodiscard]] MaybeError - verify(boost::json::value const& value, std::string_view key) const; -}; - -/** - * @brief Validates that the value is equal to the one passed in. - */ -template -class EqualTo final { - Type original_; - -public: - /** - * @brief Construct the validator with stored original value. - * - * @param original The original value to store - */ - explicit EqualTo(Type original) : original_{std::move(original)} - { - } - - /** - * @brief Verify that the JSON value is equal to the stored original. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is - * returned - */ - [[nodiscard]] MaybeError - verify(boost::json::value const& value, std::string_view key) const - { - using boost::json::value_to; - - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - auto const res = value_to(value.as_object().at(key)); - if (res != original_) - return Error{Status{RippledError::RpcInvalidParams}}; - - return {}; - } -}; - -/** - * @brief Deduction guide to help disambiguate what it means to EqualTo a "string" without - * specifying the type. - */ -EqualTo(char const*) -> EqualTo; - -/** - * @brief Validates that the value is one of the values passed in. - */ -template -class OneOf final { - std::vector options_; - -public: - /** - * @brief Construct the validator with stored options of initializer list. - * - * @param options The list of allowed options - */ - explicit OneOf(std::initializer_list options) : options_{options} - { - } - - /** - * @brief Construct the validator with stored options of other container. - * - * @param begin,end the range to copy the elements from - */ - template - explicit OneOf(InputIt begin, InputIt end) : options_{begin, end} - { - } - - /** - * @brief Verify that the JSON value is one of the stored options. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is - * returned - */ - [[nodiscard]] MaybeError - verify(boost::json::value const& value, std::string_view key) const - { - using boost::json::value_to; - - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - auto const res = value_to(value.as_object().at(key)); - if (std::find(std::begin(options_), std::end(options_), res) == std::end(options_)) { - return Error{ - Status{RippledError::RpcInvalidParams, fmt::format("Invalid field '{}'.", key)} - }; - } - - return {}; - } -}; - -/** - * @brief Deduction guide to help disambiguate what it means to OneOf a few "strings" without - * specifying the type. - */ -OneOf(std::initializer_list) -> OneOf; - -/** - * @brief A meta-validator that allows to specify a custom validation function. - */ -class CustomValidator final { - std::function validator_; - -public: - /** - * @brief Constructs a custom validator from any supported callable. - * - * @tparam Fn The type of callable - * @param fn The callable/function object - */ - template - requires std::invocable - explicit CustomValidator(Fn&& fn) : validator_{std::forward(fn)} - { - } - - /** - * @brief Verify that the JSON value is valid according to the custom validation function - * stored. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return Any compatible user-provided error if validation failed; otherwise no error is - * returned - */ - [[nodiscard]] MaybeError - verify(boost::json::value const& value, std::string_view key) const; -}; - -/** - * @brief Helper function to check if input value is an uint32 number or not. - * - * @param sv The input value as a string_view - * @return true if the string can be converted to a uint32; false otherwise - */ -[[nodiscard]] bool -checkIsU32Numeric(std::string_view sv); - -template - requires( - std::is_same_v || std::is_same_v || - std::is_same_v - ) -MaybeError -makeHexStringValidator(boost::json::value const& value, std::string_view key) -{ - if (!value.is_string()) - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotString"}}; - - HexType parsedInt; - if (!parsedInt.parseHex(value.as_string().c_str())) - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "Malformed"}}; - - return MaybeError{}; -} - -/** - * @brief A group of custom validation functions - */ -struct CustomValidators final { - /** - * @brief Provides a commonly used validator for ledger index. - * - * LedgerIndex must be a string or an int. If the specified LedgerIndex is a string, its value - * must be either "validated" or a valid integer value represented as a string. - */ - static CustomValidator ledgerIndexValidator; - - /** - * @brief Provides a validator for ledger type. - * - * A type accepts canonical names of ledger entry types (case insensitive) or short names. - * Used by ledger_data. - */ - static CustomValidator ledgerTypeValidator; - - /** - * @brief Provides a commonly used validator for accounts. - * - * Account must be a string and the converted public key is valid. - */ - static CustomValidator accountValidator; - - /** - * @brief Provides a commonly used validator for accounts. - * - * Account must be a string and can convert to base58. - */ - static CustomValidator accountBase58Validator; - - /** - * @brief Provides a commonly used validator for markers. - * - * A marker is composed of a comma-separated index and a start hint. - * The former will be read as hex, and the latter can be cast to uint64. - */ - static CustomValidator accountMarkerValidator; - - /** - * @brief Provides a validator for account type. - * - * A type accepts canonical names of owned ledger entry types (case insensitive) or short names. - * Used by account_objects. - */ - static CustomValidator accountTypeValidator; - - /** - * @brief Provides a commonly used validator for uint160(AccountID) hex string. - * - * It must be a string and also a decodable hex. - * AccountID uses this validator. - */ - static CustomValidator uint160HexStringValidator; - - /** - * @brief Provides a commonly used validator for uint192 hex string. - * - * It must be a string and also a decodable hex. - * MPTIssuanceID uses this validator. - */ - static CustomValidator uint192HexStringValidator; - - /** - * @brief Provides a commonly used validator for uint256 hex string. - * - * It must be a string and also a decodable hex. - * Transaction index, ledger hash all use this validator. - */ - static CustomValidator uint256HexStringValidator; - - /** - * @brief Provides a commonly used validator for currency, including standard currency code and - * token code. - */ - static CustomValidator currencyValidator; - - /** - * @brief Provides a commonly used validator for issuer type. - * - * It must be a hex string or base58 string. - */ - static CustomValidator issuerValidator; - - /** - * @brief Provides a validator for validating streams used in subscribe/unsubscribe. - */ - static CustomValidator subscribeStreamValidator; - - /** - * @brief Provides a validator for validating accounts used in subscribe/unsubscribe. - */ - static CustomValidator subscribeAccountsValidator; - - /** - * @brief Validates an asset (xrpl::Issue). - * - * Used by amm_info. - */ - static CustomValidator currencyIssueValidator; - - /** - * @brief Validates a book taker object (`taker_gets`/`taker_pays`). - * - * The object must specify an asset as either a `currency` (optionally with an `issuer`) or an - * `mpt_issuance_id`, but not both, and `mpt_issuance_id` must not be combined with `issuer`. - * Mirrors `xrpld`'s `validateTakerJSON`: a missing asset yields `Missing field - * '.currency'.`, conflicting fields yield `Invalid field ''.`, and a present but - * non-string `currency`/`mpt_issuance_id` yields `Invalid field '.currency', not - * string.` (all `invalidParams`). The field name is taken from the validated key. - * - * Used by book_offers. - */ - static CustomValidator bookTakerValidator; - - /** - * @brief Provides a validator for validating authorized_credentials json array. - * - * Used by deposit_preauth. - */ - static CustomValidator authorizeCredentialValidator; - - /** - * @brief Provides a validator for validating credential_type. - * - * Used by AuthorizeCredentialValidator in deposit_preauth and by the credential - * object lookup in ledger_entry. - */ - static CustomValidator credentialTypeValidator; - - /** - * @brief Provides a validator for validating filtering by delegation. - * - * Used by account_tx if user wants to filter by delegation. - */ - static CustomValidator delegateValidator; -}; - -/** - * @brief Validates that the elements of the array is of type Hex256 uint - */ -struct Hex256ItemType final { - /** - * @brief Validates given the prerequisite that the type of the json value is an array, - * verifies all values within the array is of uint256 hash - * - * @param value the value to verify - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is - * returned - */ - [[nodiscard]] static MaybeError - verify(boost::json::value const& value, std::string_view key) - { - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. If field is supposed to exist, let 'required' fail instead - - auto const& res = value.as_object().at(key); - - // loop through each item in the array and make sure it is uint256 hex string - for (auto const& elem : res.as_array()) { - xrpl::uint256 num; - if (!elem.is_string() || !num.parseHex(elem.as_string())) { - return Error{ - Status{RippledError::RpcInvalidParams, "Item is not a valid uint256 type."} - }; - } - } - return {}; - } -}; - -} // namespace rpc::validation diff --git a/src/rpc/common/impl/Factories.hpp b/src/rpc/common/impl/Factories.hpp deleted file mode 100644 index 9980178fd..000000000 --- a/src/rpc/common/impl/Factories.hpp +++ /dev/null @@ -1,86 +0,0 @@ -#pragma once - -#include "rpc/Errors.hpp" -#include "rpc/common/Checkers.hpp" -#include "rpc/common/Concepts.hpp" -#include "rpc/common/Types.hpp" -#include "util/UnsupportedType.hpp" - -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace rpc::impl { - -using FieldSpecProcessor = std::function; - -static FieldSpecProcessor const kEmptyFieldProcessor = [](boost::json::value&) -> MaybeError { - return {}; -}; - -template -[[nodiscard]] FieldSpecProcessor -makeFieldProcessor(std::string const& key, Processors&&... procs) -{ - return [key, ... proc = std::forward(procs)](boost::json::value& j) -> MaybeError { - std::optional firstFailure = std::nullopt; - - // This expands in order of Requirements and stops evaluating after first failure which is - // stored in `firstFailure` and can be checked later on to see whether the verification - // failed as a whole or not. - ( - [&j, &key, &firstFailure, req = &proc]() { - if (firstFailure) - return; // already failed earlier - skip - - if constexpr (SomeRequirement) { - if (auto const res = req->verify(j, key); not res) - firstFailure = res.error(); - } else if constexpr (SomeModifier) { - if (auto const res = req->modify(j, key); not res) - firstFailure = res.error(); - } else { - static_assert(util::Unsupported); - } - }(), - ...); - - if (firstFailure) - return std::unexpected{std::move(firstFailure).value()}; - - return {}; - }; -} - -using FieldChecker = std::function; - -static FieldChecker const kEmptyFieldChecker = [](boost::json::value const&) -> check::Warnings { - return {}; -}; - -template -[[nodiscard]] FieldChecker -makeFieldChecker(std::string const& key, Checks&&... checks) -{ - return [key, - ... checks = - std::forward(checks)](boost::json::value const& j) -> check::Warnings { - check::Warnings warnings; - // This expands in order of Checks and collects all warnings into a WarningsCollection - ( - [&j, &key, &warnings, req = &checks]() { - if (auto res = req->check(j, key); res) - warnings.push_back(*std::move(res)); - }(), - ...); - return warnings; - }; -} - -} // namespace rpc::impl diff --git a/src/rpc/common/impl/Processors.hpp b/src/rpc/common/impl/Processors.hpp index 1604af1d2..e9595d136 100644 --- a/src/rpc/common/impl/Processors.hpp +++ b/src/rpc/common/impl/Processors.hpp @@ -20,20 +20,12 @@ struct DefaultProcessor final { ) const { using boost::json::value_from; - using boost::json::value_to; static_assert( - kIsSingleInputPath, - "handler satisfies both the legacy and the typed input path; dispatch would be " - "decided by the order of the branches below rather than by the handler" - ); - static_assert( - SomeHandlerWithTypedInput or SomeHandlerWithInput or - SomeHandlerWithoutInput, + SomeHandlerWithTypedInput or SomeHandlerWithoutInput, "handler matches none of the branches below" ); - // New `rpc-spec`-based handler if constexpr (SomeHandlerWithTypedInput) { auto input = HandlerType::parseInput(value, ctx.apiVersion); auto warnings = rpc::spec::toJsonArray(HandlerType::spec(ctx.apiVersion).check(value)); @@ -49,26 +41,6 @@ struct DefaultProcessor final { return ReturnType{value_from(std::move(ret).value()), std::move(warnings)}; } - if constexpr (SomeHandlerWithInput) { - // Old spec-based handler: first we run validation against specified API version - // TODO: This will be eventually removed once fully migraded to new rpc-spec system. - auto const spec = handler.spec(ctx.apiVersion); - auto warnings = spec.check(value); - auto input = value; // copy here, spec require mutable data - - if (auto const ret = spec.process(input); not ret.has_value()) - return ReturnType{Error{ret.error()}, std::move(warnings)}; // forward Status - - auto const inData = value_to(input); - auto ret = handler.process(inData, ctx); - - // real handler is given expected Input, not json - if (not ret.has_value()) - return ReturnType{Error{std::move(ret).error()}, std::move(warnings)}; - - return ReturnType{value_from(std::move(ret).value()), std::move(warnings)}; - } - if constexpr (SomeHandlerWithoutInput) { // no input to pass, ignore the value auto const ret = handler.process(ctx); diff --git a/src/rpc/filters/TransactionFilter.hpp b/src/rpc/filters/TransactionFilter.hpp index ffe541cbc..de27a1b3b 100644 --- a/src/rpc/filters/TransactionFilter.hpp +++ b/src/rpc/filters/TransactionFilter.hpp @@ -3,15 +3,22 @@ #include "data/Types.hpp" #include "rpc/common/Types.hpp" +#include #include #include namespace rpc { +/** + * @brief The delegate filter used to narrow account_tx by delegation. + */ +using DelegateFilter = rpc::spec::handlers::account_tx::DelegateFilter; + /** * @brief Interface for filtering transactions. */ +// TODO: consider moving to rpc-spec later on class TransactionFilter { public: /** diff --git a/src/rpc/filters/impl/DelegateTransactionsFilter.cpp b/src/rpc/filters/impl/DelegateTransactionsFilter.cpp index d960a497a..a4719d8ba 100644 --- a/src/rpc/filters/impl/DelegateTransactionsFilter.cpp +++ b/src/rpc/filters/impl/DelegateTransactionsFilter.cpp @@ -1,7 +1,6 @@ #include "rpc/filters/impl/DelegateTransactionsFilter.hpp" #include "data/Types.hpp" -#include "rpc/common/Types.hpp" #include "rpc/filters/TransactionFilter.hpp" #include diff --git a/src/rpc/handlers/AMMInfo.cpp b/src/rpc/handlers/AMMInfo.cpp index 685c253c3..6d955cd91 100644 --- a/src/rpc/handlers/AMMInfo.cpp +++ b/src/rpc/handlers/AMMInfo.cpp @@ -4,18 +4,13 @@ #include "rpc/AMMHelpers.hpp" #include "rpc/JS.hpp" #include "rpc/RPCHelpers.hpp" -#include "rpc/common/MetaProcessors.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include #include #include -#include #include #include #include @@ -34,10 +29,7 @@ #include #include -#include -#include #include -#include #include namespace { @@ -80,11 +72,10 @@ AMMInfoHandler::process(AMMInfoHandler::Input const& input, Context const& ctx) auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "AMMInfo's ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -228,67 +219,6 @@ AMMInfoHandler::process(AMMInfoHandler::Input const& input, Context const& ctx) return response; } -RpcSpecConstRef -AMMInfoHandler::spec([[maybe_unused]] uint32_t apiVersion) -{ - static auto const kStringIssueValidator = validation::CustomValidator{ - [](boost::json::value const& value, std::string_view key) -> MaybeError { - if (not value.is_string()) { - return Error{ - Status{RippledError::RpcInvalidParams, std::string(key) + "NotString"} - }; - } - - try { - xrpl::issueFromJson(boost::json::value_to(value)); - } catch (std::runtime_error const&) { - return Error{Status{RippledError::RpcIssueMalformed}}; - } - - return MaybeError{}; - } - }; - - static auto const kRpcSpec = RpcSpec{ - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(asset), - meta::WithCustomError{ - validation::Type{}, - Status(RippledError::RpcIssueMalformed) - }, - meta::IfType{kStringIssueValidator}, - meta::IfType{ - meta::WithCustomError{ - validation::CustomValidators::currencyIssueValidator, - Status(RippledError::RpcIssueMalformed) - }, - }}, - {JS(asset2), - meta::WithCustomError{ - validation::Type{}, - Status(RippledError::RpcIssueMalformed) - }, - meta::IfType{kStringIssueValidator}, - meta::IfType{ - meta::WithCustomError{ - validation::CustomValidators::currencyIssueValidator, - Status(RippledError::RpcIssueMalformed) - }, - }}, - {JS(amm_account), - meta::WithCustomError{ - validation::CustomValidators::accountValidator, Status(RippledError::RpcActMalformed) - }}, - {JS(account), - meta::WithCustomError{ - validation::CustomValidators::accountValidator, Status(RippledError::RpcActMalformed) - }}, - }; - - return kRpcSpec; -} - void tag_invoke( boost::json::value_from_tag, @@ -324,39 +254,4 @@ tag_invoke( }; } -AMMInfoHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = AMMInfoHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(jv.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(asset))) - input.issue1 = parseIssue(jsonObject.at(JS(asset)).as_object()); - - if (jsonObject.contains(JS(asset2))) - input.issue2 = parseIssue(jsonObject.at(JS(asset2)).as_object()); - - if (jsonObject.contains(JS(account))) { - input.accountID = - accountFromStringStrict(boost::json::value_to(jsonObject.at(JS(account)))); - } - - if (jsonObject.contains(JS(amm_account))) { - input.ammAccount = accountFromStringStrict( - boost::json::value_to(jsonObject.at(JS(amm_account))) - ); - } - - return input; -} - } // namespace rpc diff --git a/src/rpc/handlers/AMMInfo.hpp b/src/rpc/handlers/AMMInfo.hpp index abcfea87c..bb35374dc 100644 --- a/src/rpc/handlers/AMMInfo.hpp +++ b/src/rpc/handlers/AMMInfo.hpp @@ -2,18 +2,19 @@ #include "data/AmendmentCenterInterface.hpp" #include "data/BackendInterface.hpp" -#include "rpc/common/Specs.hpp" +#include "rpc/common/Types.hpp" #include #include #include -#include -#include +#include +#include #include #include #include #include +#include namespace rpc { @@ -22,7 +23,7 @@ namespace rpc { * * For more info see: https://xrpl.org/amm_info.html */ -class AMMInfoHandler { +class AMMInfoHandler : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; std::shared_ptr amendmentCenter_; @@ -47,18 +48,6 @@ public: bool validated = true; }; - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::optional accountID; - std::optional ammAccount; - xrpl::Issue issue1 = xrpl::noIssue(); - xrpl::Issue issue2 = xrpl::noIssue(); - std::optional ledgerHash; - std::optional ledgerIndex; - }; - using Result = HandlerReturnType; /** @@ -75,15 +64,6 @@ public: { } - /** - * @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); - /** * @brief Process the AMMInfo command * @@ -103,15 +83,6 @@ private: */ 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, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/AccountChannels.cpp b/src/rpc/handlers/AccountChannels.cpp index 9a2ae4e09..c905003f0 100644 --- a/src/rpc/handlers/AccountChannels.cpp +++ b/src/rpc/handlers/AccountChannels.cpp @@ -4,12 +4,10 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include #include -#include #include #include #include @@ -23,7 +21,6 @@ #include #include -#include #include #include #include @@ -74,11 +71,10 @@ AccountChannelsHandler::process( { auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "AccountChannel's ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -86,20 +82,15 @@ AccountChannelsHandler::process( return Error{expectedLgrInfo.error()}; auto const& lgrInfo = *expectedLgrInfo; - auto const accountID = accountFromStringStrict(input.account); + auto const& accountID = input.account; auto const accountLedgerObject = sharedPtrBackend_->fetchLedgerObject( - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - xrpl::keylet::account(*accountID).key, - lgrInfo.seq, - ctx.yield + xrpl::keylet::account(accountID).key, lgrInfo.seq, ctx.yield ); if (!accountLedgerObject) return Error{Status{RippledError::RpcActNotFound}}; - auto const destAccountID = input.destinationAccount - ? accountFromStringStrict(*input.destinationAccount) - : std::optional{}; + auto const& destAccountID = input.destinationAccount; Output response; auto const addToResponse = [&](xrpl::SLE const sle) { @@ -113,7 +104,7 @@ AccountChannelsHandler::process( auto const expectedNext = traverseOwnedNodes( *sharedPtrBackend_, - *accountID, // NOLINT(bugprone-unchecked-optional-access) + accountID, lgrInfo.seq, input.limit, input.marker, @@ -124,7 +115,7 @@ AccountChannelsHandler::process( if (not expectedNext.has_value()) return Error{expectedNext.error()}; - response.account = input.account; + response.account = xrpl::to_string(accountID); response.limit = input.limit; response.ledgerHash = xrpl::strHex(lgrInfo.hash); response.ledgerIndex = lgrInfo.seq; @@ -136,37 +127,6 @@ AccountChannelsHandler::process( return response; } -AccountChannelsHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = AccountChannelsHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - input.account = boost::json::value_to(jv.at(JS(account))); - - if (jsonObject.contains(JS(limit))) - input.limit = util::integralValueAs(jv.at(JS(limit))); - - if (jsonObject.contains(JS(marker))) - input.marker = boost::json::value_to(jv.at(JS(marker))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(jv.at(JS(ledger_hash))); - - if (jsonObject.contains(JS(destination_account))) { - input.destinationAccount = - boost::json::value_to(jv.at(JS(destination_account))); - } - - if (jsonObject.contains(JS(ledger_index))) { - auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index))); - if (expectedLedgerIndex.has_value()) - input.ledgerIndex = *expectedLedgerIndex; - } - - return input; -} - void tag_invoke( boost::json::value_from_tag, diff --git a/src/rpc/handlers/AccountChannels.hpp b/src/rpc/handlers/AccountChannels.hpp index f3b72d2c3..d80f59245 100644 --- a/src/rpc/handlers/AccountChannels.hpp +++ b/src/rpc/handlers/AccountChannels.hpp @@ -1,21 +1,19 @@ #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 #include +#include +#include #include -#include #include #include #include #include +#include #include namespace rpc { @@ -27,14 +25,15 @@ namespace rpc { * * For more details see: https://xrpl.org/account_channels.html */ -class AccountChannelsHandler { +class AccountChannelsHandler + : public rpc::spec::HandlerFor { // dependencies std::shared_ptr const sharedPtrBackend_; public: - static constexpr auto kLimitMin = 10; - static constexpr auto kLimitMax = 400; - static constexpr auto kLimitDefault = 200; + static constexpr auto kLimitMin = rpc::spec::handlers::account_channels::kLimitMin; + static constexpr auto kLimitMax = rpc::spec::handlers::account_channels::kLimitMax; + static constexpr auto kLimitDefault = rpc::spec::handlers::account_channels::kLimitDefault; /** * @brief A struct to hold data for one channel response @@ -70,18 +69,6 @@ public: std::optional marker; }; - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::string account; - std::optional destinationAccount; - std::optional ledgerHash; - std::optional ledgerIndex; - uint32_t limit = kLimitDefault; - std::optional marker; - }; - using Result = HandlerReturnType; /** @@ -94,32 +81,6 @@ public: { } - /** - * @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 kRpcSpec = RpcSpec{ - {JS(account), validation::Required{}, validation::CustomValidators::accountValidator}, - {JS(destination_account), - validation::Type{}, - validation::CustomValidators::accountValidator}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(limit), - validation::Type{}, - validation::Min(1u), - modifiers::Clamp{kLimitMin, kLimitMax}}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(marker), validation::CustomValidators::accountMarkerValidator}, - }; - - return kRpcSpec; - } - /** * @brief Process the AccountChannels command * @@ -143,15 +104,6 @@ private: 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, boost::json::value const& jv); - /** * @brief Convert the ChannelResponse to a JSON object * diff --git a/src/rpc/handlers/AccountCurrencies.cpp b/src/rpc/handlers/AccountCurrencies.cpp index a72ebc3e9..913fb373c 100644 --- a/src/rpc/handlers/AccountCurrencies.cpp +++ b/src/rpc/handlers/AccountCurrencies.cpp @@ -4,11 +4,9 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include -#include #include #include #include @@ -33,11 +31,10 @@ AccountCurrenciesHandler::process( { auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "AccountCurrencies' ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -45,13 +42,10 @@ AccountCurrenciesHandler::process( return Error{expectedLgrInfo.error()}; auto const& lgrInfo = *expectedLgrInfo; - auto const accountID = accountFromStringStrict(input.account); + auto const& accountID = input.account; auto const accountLedgerObject = sharedPtrBackend_->fetchLedgerObject( - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - xrpl::keylet::account(*accountID).key, - lgrInfo.seq, - ctx.yield + xrpl::keylet::account(accountID).key, lgrInfo.seq, ctx.yield ); if (!accountLedgerObject) return Error{Status{RippledError::RpcActNotFound}}; @@ -88,7 +82,7 @@ AccountCurrenciesHandler::process( // traverse all owned nodes, limit->max, marker->empty traverseOwnedNodes( *sharedPtrBackend_, - *accountID, // NOLINT(bugprone-unchecked-optional-access) + accountID, lgrInfo.seq, std::numeric_limits::max(), {}, @@ -120,24 +114,4 @@ tag_invoke( }; } -AccountCurrenciesHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = AccountCurrenciesHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - input.account = boost::json::value_to(jv.at(JS(account))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(jv.at(JS(ledger_hash))); - - if (jsonObject.contains(JS(ledger_index))) { - auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index))); - if (expectedLedgerIndex.has_value()) - input.ledgerIndex = *expectedLedgerIndex; - } - - return input; -} - } // namespace rpc diff --git a/src/rpc/handlers/AccountCurrencies.hpp b/src/rpc/handlers/AccountCurrencies.hpp index f8e4441f2..eecac4fff 100644 --- a/src/rpc/handlers/AccountCurrencies.hpp +++ b/src/rpc/handlers/AccountCurrencies.hpp @@ -1,19 +1,15 @@ #pragma once #include "data/BackendInterface.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/Checkers.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include #include -#include +#include +#include #include #include -#include #include #include @@ -25,7 +21,8 @@ namespace rpc { * * For more details see: https://xrpl.org/account_currencies.html */ -class AccountCurrenciesHandler { +class AccountCurrenciesHandler + : public rpc::spec::HandlerFor { // dependencies std::shared_ptr sharedPtrBackend_; @@ -42,15 +39,6 @@ public: bool validated = true; }; - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::string account; - std::optional ledgerHash; - std::optional ledgerIndex; - }; - using Result = HandlerReturnType; /** @@ -63,26 +51,6 @@ public: { } - /** - * @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 kRpcSpec = RpcSpec{ - {JS(account), validation::Required{}, validation::CustomValidators::accountValidator}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {"account_index", check::Deprecated{}}, - {JS(strict), check::Deprecated{}} - }; - - return kRpcSpec; - } - /** * @brief Process the AccountCurrencies command * @@ -102,15 +70,6 @@ private: */ 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, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/AccountInfo.cpp b/src/rpc/handlers/AccountInfo.cpp index 6c246568e..5b7951f38 100644 --- a/src/rpc/handlers/AccountInfo.cpp +++ b/src/rpc/handlers/AccountInfo.cpp @@ -3,16 +3,13 @@ #include "data/AmendmentCenter.hpp" #include "rpc/JS.hpp" #include "rpc/RPCHelpers.hpp" -#include "rpc/common/JsonBool.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include #include #include -#include #include #include #include @@ -46,11 +43,10 @@ AccountInfoHandler::process(AccountInfoHandler::Input const& input, Context cons auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "AccountInfo's ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -58,10 +54,8 @@ AccountInfoHandler::process(AccountInfoHandler::Input const& input, Context cons return Error{expectedLgrInfo.error()}; auto const& lgrInfo = *expectedLgrInfo; - auto const accountStr = input.account.value_or(input.ident.value_or("")); - auto const accountID = accountFromStringStrict(accountStr); - auto const accountKeylet = - xrpl::keylet::account(*accountID); // NOLINT(bugprone-unchecked-optional-access) + auto const accountID = input.account ? *input.account : *input.ident; + auto const accountKeylet = xrpl::keylet::account(accountID); auto const accountLedgerObject = sharedPtrBackend_->fetchLedgerObject(accountKeylet.key, lgrInfo.seq, ctx.yield); @@ -99,8 +93,7 @@ AccountInfoHandler::process(AccountInfoHandler::Input const& input, Context cons if (input.signerLists) { // We put the SignerList in an array because of an anticipated // future when we support multiple signer lists on one account. - auto const signersKey = - xrpl::keylet::signerList(*accountID); // NOLINT(bugprone-unchecked-optional-access) + auto const signersKey = xrpl::keylet::signerList(accountID); // This code will need to be revisited if in the future we // support multiple SignerLists on one account. @@ -203,31 +196,4 @@ tag_invoke( } } -AccountInfoHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = AccountInfoHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - if (jsonObject.contains(JS(ident))) - input.ident = boost::json::value_to(jsonObject.at(JS(ident))); - - if (jsonObject.contains(JS(account))) - input.account = boost::json::value_to(jsonObject.at(JS(account))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(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(signer_lists))) - input.signerLists = boost::json::value_to(jsonObject.at(JS(signer_lists))); - - return input; -} - } // namespace rpc diff --git a/src/rpc/handlers/AccountInfo.hpp b/src/rpc/handlers/AccountInfo.hpp index 091859b0e..08196c054 100644 --- a/src/rpc/handlers/AccountInfo.hpp +++ b/src/rpc/handlers/AccountInfo.hpp @@ -2,17 +2,13 @@ #include "data/AmendmentCenterInterface.hpp" #include "data/BackendInterface.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/Checkers.hpp" -#include "rpc/common/JsonBool.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include #include +#include +#include #include -#include #include #include @@ -28,7 +24,7 @@ namespace rpc { * * For more details see: https://xrpl.org/account_info.html */ -class AccountInfoHandler { +class AccountInfoHandler : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; std::shared_ptr amendmentCenter_; @@ -49,20 +45,6 @@ public: bool validated = true; }; - /** - * @brief A struct to hold the input data for the command - * - * `queue` is not available in Reporting mode - * `ident` is deprecated, keep it for now, in line with rippled - */ - struct Input { - std::optional account; - std::optional ident; - std::optional ledgerHash; - std::optional ledgerIndex; - JsonBool signerLists{false}; - }; - using Result = HandlerReturnType; /** @@ -79,31 +61,6 @@ public: { } - /** - * @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 kRpcSpecV1 = RpcSpec{ - {JS(account), validation::CustomValidators::accountValidator}, - {JS(ident), validation::CustomValidators::accountValidator}, - {JS(ident), check::Deprecated{}}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(ledger), check::Deprecated{}}, - {JS(strict), check::Deprecated{}} - }; - - static auto const kRpcSpec = - RpcSpec{kRpcSpecV1, {{JS(signer_lists), validation::Type{}}}}; - - return apiVersion == 1 ? kRpcSpecV1 : kRpcSpec; - } - /** * @brief Process the AccountInfo command * @@ -123,15 +80,6 @@ private: */ 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, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/AccountLines.cpp b/src/rpc/handlers/AccountLines.cpp index 982d2deef..ea867fe85 100644 --- a/src/rpc/handlers/AccountLines.cpp +++ b/src/rpc/handlers/AccountLines.cpp @@ -4,12 +4,10 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include #include -#include #include #include #include @@ -22,7 +20,6 @@ #include #include -#include #include #include #include @@ -122,11 +119,10 @@ AccountLinesHandler::process(AccountLinesHandler::Input const& input, Context co { auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "AccountLines' ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -134,19 +130,15 @@ AccountLinesHandler::process(AccountLinesHandler::Input const& input, Context co return Error{expectedLgrInfo.error()}; auto const& lgrInfo = *expectedLgrInfo; - auto const accountID = accountFromStringStrict(input.account); + auto const& accountID = input.account; auto const accountLedgerObject = sharedPtrBackend_->fetchLedgerObject( - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - xrpl::keylet::account(*accountID).key, - lgrInfo.seq, - ctx.yield + xrpl::keylet::account(accountID).key, lgrInfo.seq, ctx.yield ); if (not accountLedgerObject) return Error{Status{RippledError::RpcActNotFound}}; - auto const peerAccountID = - input.peer ? accountFromStringStrict(*(input.peer)) : std::optional{}; + auto const& peerAccountID = input.peer; Output response; response.lines.reserve(input.limit); @@ -163,13 +155,13 @@ AccountLinesHandler::process(AccountLinesHandler::Input const& input, Context co } if (not ignore) - addLine(response.lines, sle, *accountID, peerAccountID); + addLine(response.lines, sle, accountID, peerAccountID); } }; auto const expectedNext = traverseOwnedNodes( *sharedPtrBackend_, - *accountID, // NOLINT(bugprone-unchecked-optional-access) + accountID, lgrInfo.seq, input.limit, input.marker, @@ -182,7 +174,7 @@ AccountLinesHandler::process(AccountLinesHandler::Input const& input, Context co auto const nextMarker = *expectedNext; - response.account = input.account; + response.account = xrpl::to_string(accountID); response.limit = input.limit; // not documented, // https://github.com/XRPLF/xrpl-dev-portal/issues/1838 response.ledgerHash = xrpl::strHex(lgrInfo.hash); @@ -194,37 +186,6 @@ AccountLinesHandler::process(AccountLinesHandler::Input const& input, Context co return response; } -AccountLinesHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = AccountLinesHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - input.account = boost::json::value_to(jv.at(JS(account))); - if (jsonObject.contains(JS(limit))) - input.limit = util::integralValueAs(jv.at(JS(limit))); - - if (jsonObject.contains(JS(marker))) - input.marker = boost::json::value_to(jv.at(JS(marker))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(jv.at(JS(ledger_hash))); - - if (jsonObject.contains(JS(peer))) - input.peer = boost::json::value_to(jv.at(JS(peer))); - - if (jsonObject.contains(JS(ignore_default))) - input.ignoreDefault = jv.at(JS(ignore_default)).as_bool(); - - if (jsonObject.contains(JS(ledger_index))) { - auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index))); - if (expectedLedgerIndex.has_value()) - input.ledgerIndex = *expectedLedgerIndex; - } - - return input; -} - void tag_invoke( boost::json::value_from_tag, diff --git a/src/rpc/handlers/AccountLines.hpp b/src/rpc/handlers/AccountLines.hpp index 5bfd3cc12..28f244371 100644 --- a/src/rpc/handlers/AccountLines.hpp +++ b/src/rpc/handlers/AccountLines.hpp @@ -1,26 +1,20 @@ #pragma once #include "data/BackendInterface.hpp" -#include "rpc/Errors.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/Checkers.hpp" -#include "rpc/common/MetaProcessors.hpp" -#include "rpc/common/Modifiers.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include #include +#include +#include #include -#include #include -#include #include #include #include #include +#include #include namespace rpc { @@ -31,14 +25,15 @@ namespace rpc { * * For more details see: https://xrpl.org/account_lines.html */ -class AccountLinesHandler { +class AccountLinesHandler + : public rpc::spec::HandlerFor { // dependencies std::shared_ptr const sharedPtrBackend_; public: - static constexpr auto kLimitMin = 10; - static constexpr auto kLimitMax = 400; - static constexpr auto kLimitDefault = 200; + static constexpr auto kLimitMin = rpc::spec::handlers::account_lines::kLimitMin; + static constexpr auto kLimitMax = rpc::spec::handlers::account_lines::kLimitMax; + static constexpr auto kLimitDefault = rpc::spec::handlers::account_lines::kLimitDefault; /** * @brief A struct to hold data for one line response @@ -74,20 +69,6 @@ public: uint32_t limit{}; }; - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::string account; - std::optional ledgerHash; - std::optional ledgerIndex; - std::optional peer; - bool ignoreDefault = false; // TODO: document - // https://github.com/XRPLF/xrpl-dev-portal/issues/1839 - uint32_t limit = kLimitDefault; - std::optional marker; - }; - using Result = HandlerReturnType; /** @@ -100,42 +81,6 @@ public: { } - /** - * @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 kRpcSpec = RpcSpec{ - {JS(account), - validation::Required{}, - meta::WithCustomError{ - validation::CustomValidators::accountValidator, - Status(RippledError::RpcActMalformed) - }}, - {JS(peer), - meta::WithCustomError{ - validation::CustomValidators::accountValidator, - Status(RippledError::RpcActMalformed) - }}, - {JS(ignore_default), validation::Type{}}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(limit), - validation::Type{}, - validation::Min(1u), - modifiers::Clamp{kLimitMin, kLimitMax}}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(marker), validation::CustomValidators::accountMarkerValidator}, - {JS(ledger), check::Deprecated{}}, - {"peer_index", check::Deprecated{}}, - }; - - return kRpcSpec; - } - /** * @brief Process the AccountLines command * @@ -165,15 +110,6 @@ private: 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, boost::json::value const& jv); - /** * @brief Convert the LineResponse to a JSON object * diff --git a/src/rpc/handlers/AccountMPTokenIssuances.cpp b/src/rpc/handlers/AccountMPTokenIssuances.cpp index 22f0c261f..61ab0696c 100644 --- a/src/rpc/handlers/AccountMPTokenIssuances.cpp +++ b/src/rpc/handlers/AccountMPTokenIssuances.cpp @@ -4,12 +4,10 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include #include -#include #include #include #include @@ -124,11 +122,10 @@ AccountMPTokenIssuancesHandler::process( { auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "AccountMPTokenIssuances' ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -136,12 +133,9 @@ AccountMPTokenIssuancesHandler::process( return Error{expectedLgrInfo.error()}; auto const& lgrInfo = *expectedLgrInfo; - auto const accountID = accountFromStringStrict(input.account); + auto const& accountID = input.account; auto const accountLedgerObject = sharedPtrBackend_->fetchLedgerObject( - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - xrpl::keylet::account(*accountID).key, - lgrInfo.seq, - ctx.yield + xrpl::keylet::account(accountID).key, lgrInfo.seq, ctx.yield ); if (not accountLedgerObject.has_value()) @@ -152,13 +146,13 @@ AccountMPTokenIssuancesHandler::process( auto const addToResponse = [&](xrpl::SLE const& sle) { if (sle.getType() == xrpl::ltMPTOKEN_ISSUANCE) { - addMPTokenIssuance(response.issuances, sle, *accountID); + addMPTokenIssuance(response.issuances, sle, accountID); } }; auto const expectedNext = traverseOwnedNodes( *sharedPtrBackend_, - *accountID, // NOLINT(bugprone-unchecked-optional-access) + accountID, lgrInfo.seq, input.limit, input.marker, @@ -171,7 +165,7 @@ AccountMPTokenIssuancesHandler::process( auto const nextMarker = *expectedNext; - response.account = input.account; + response.account = xrpl::to_string(accountID); response.limit = input.limit; response.ledgerHash = xrpl::strHex(lgrInfo.hash); @@ -183,35 +177,6 @@ AccountMPTokenIssuancesHandler::process( return response; } -AccountMPTokenIssuancesHandler::Input -tag_invoke( - boost::json::value_to_tag, - boost::json::value const& jv -) -{ - auto input = AccountMPTokenIssuancesHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - input.account = boost::json::value_to(jv.at(JS(account))); - - if (jsonObject.contains(JS(limit))) - input.limit = util::integralValueAs(jv.at(JS(limit))); - - if (jsonObject.contains(JS(marker))) - input.marker = boost::json::value_to(jv.at(JS(marker))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(jv.at(JS(ledger_hash))); - - if (jsonObject.contains(JS(ledger_index))) { - auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index))); - if (expectedLedgerIndex.has_value()) - input.ledgerIndex = *expectedLedgerIndex; - } - - return input; -} - void tag_invoke( boost::json::value_from_tag, diff --git a/src/rpc/handlers/AccountMPTokenIssuances.hpp b/src/rpc/handlers/AccountMPTokenIssuances.hpp index 1cd679569..9222ab621 100644 --- a/src/rpc/handlers/AccountMPTokenIssuances.hpp +++ b/src/rpc/handlers/AccountMPTokenIssuances.hpp @@ -1,26 +1,20 @@ #pragma once #include "data/BackendInterface.hpp" -#include "rpc/Errors.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/Checkers.hpp" -#include "rpc/common/MetaProcessors.hpp" -#include "rpc/common/Modifiers.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include #include +#include +#include #include -#include #include -#include #include #include #include #include +#include #include namespace rpc { @@ -29,14 +23,16 @@ namespace rpc { * @brief The account_mptoken_issuances method returns information about all MPTokenIssuance objects * the account has created. */ -class AccountMPTokenIssuancesHandler { +class AccountMPTokenIssuancesHandler + : public rpc::spec::HandlerFor { // dependencies std::shared_ptr sharedPtrBackend_; public: - static constexpr auto kLimitMin = 10; - static constexpr auto kLimitMax = 400; - static constexpr auto kLimitDefault = 200; + static constexpr auto kLimitMin = rpc::spec::handlers::account_mptoken_issuances::kLimitMin; + static constexpr auto kLimitMax = rpc::spec::handlers::account_mptoken_issuances::kLimitMax; + static constexpr auto kLimitDefault = + rpc::spec::handlers::account_mptoken_issuances::kLimitDefault; /** * @brief A struct to hold data for one MPTokenIssuance response. @@ -92,17 +88,6 @@ public: uint32_t limit{}; }; - /** - * @brief A struct to hold the input data for the command. - */ - struct Input { - std::string account; - std::optional ledgerHash; - std::optional ledgerIndex; - uint32_t limit = kLimitDefault; - std::optional marker; - }; - using Result = HandlerReturnType; /** @@ -115,35 +100,6 @@ public: { } - /** - * @brief Returns the API specification for the command. - * - * @param apiVersion The API version to return the spec for. - * @return The spec for the given API version. - */ - static RpcSpecConstRef - spec([[maybe_unused]] uint32_t apiVersion) - { - static auto const kRpcSpec = RpcSpec{ - {JS(account), - validation::Required{}, - meta::WithCustomError{ - validation::CustomValidators::accountValidator, - Status(RippledError::RpcActMalformed) - }}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(limit), - validation::Type{}, - validation::Min(1u), - modifiers::Clamp{kLimitMin, kLimitMax}}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(marker), validation::CustomValidators::accountMarkerValidator}, - {JS(ledger), check::Deprecated{}}, - }; - - return kRpcSpec; - } - /** * @brief Process the AccountMPTokenIssuances command. * @@ -172,15 +128,6 @@ private: 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, boost::json::value const& jv); - /** * @brief Convert the MPTokenIssuanceResponse to a JSON object * diff --git a/src/rpc/handlers/AccountMPTokens.cpp b/src/rpc/handlers/AccountMPTokens.cpp index bede8e56c..6de41d7e1 100644 --- a/src/rpc/handlers/AccountMPTokens.cpp +++ b/src/rpc/handlers/AccountMPTokens.cpp @@ -4,13 +4,11 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include #include #include -#include #include #include #include @@ -88,11 +86,10 @@ AccountMPTokensHandler::process( { auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "AccountMPTokens' ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -100,12 +97,9 @@ AccountMPTokensHandler::process( return Error{expectedLgrInfo.error()}; auto const& lgrInfo = *expectedLgrInfo; - auto const accountID = accountFromStringStrict(input.account); + auto const& accountID = input.account; auto const accountLedgerObject = sharedPtrBackend_->fetchLedgerObject( - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - xrpl::keylet::account(*accountID).key, - lgrInfo.seq, - ctx.yield + xrpl::keylet::account(accountID).key, lgrInfo.seq, ctx.yield ); if (not accountLedgerObject.has_value()) @@ -122,7 +116,7 @@ AccountMPTokensHandler::process( auto const expectedNext = traverseOwnedNodes( *sharedPtrBackend_, - *accountID, // NOLINT(bugprone-unchecked-optional-access) + accountID, lgrInfo.seq, input.limit, input.marker, @@ -135,7 +129,7 @@ AccountMPTokensHandler::process( auto const& nextMarker = *expectedNext; - response.account = input.account; + response.account = xrpl::to_string(accountID); response.limit = input.limit; response.ledgerHash = xrpl::strHex(lgrInfo.hash); @@ -147,32 +141,6 @@ AccountMPTokensHandler::process( return response; } -AccountMPTokensHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - AccountMPTokensHandler::Input input{}; - auto const& jsonObject = jv.as_object(); - - input.account = boost::json::value_to(jv.at(JS(account))); - - if (jsonObject.contains(JS(limit))) - input.limit = util::integralValueAs(jv.at(JS(limit))); - - if (jsonObject.contains(JS(marker))) - input.marker = boost::json::value_to(jv.at(JS(marker))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(jv.at(JS(ledger_hash))); - - if (jsonObject.contains(JS(ledger_index))) { - auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index))); - if (expectedLedgerIndex.has_value()) - input.ledgerIndex = *expectedLedgerIndex; - } - - return input; -} - void tag_invoke( boost::json::value_from_tag, diff --git a/src/rpc/handlers/AccountMPTokens.hpp b/src/rpc/handlers/AccountMPTokens.hpp index c165ef2a7..2755c4df4 100644 --- a/src/rpc/handlers/AccountMPTokens.hpp +++ b/src/rpc/handlers/AccountMPTokens.hpp @@ -1,25 +1,19 @@ #pragma once #include "data/BackendInterface.hpp" -#include "rpc/Errors.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/Checkers.hpp" -#include "rpc/common/MetaProcessors.hpp" -#include "rpc/common/Modifiers.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include #include -#include -#include +#include +#include #include -#include #include +#include #include #include +#include #include namespace rpc { @@ -28,14 +22,15 @@ namespace rpc { * @brief The account_mptokens method returns information about the MPTokens the account currently * holds. */ -class AccountMPTokensHandler { +class AccountMPTokensHandler + : public rpc::spec::HandlerFor { // dependencies std::shared_ptr sharedPtrBackend_; public: - static constexpr auto kLimitMin = 10; - static constexpr auto kLimitMax = 400; - static constexpr auto kLimitDefault = 200; + static constexpr auto kLimitMin = rpc::spec::handlers::account_mptokens::kLimitMin; + static constexpr auto kLimitMax = rpc::spec::handlers::account_mptokens::kLimitMax; + static constexpr auto kLimitDefault = rpc::spec::handlers::account_mptokens::kLimitDefault; /** * @brief A struct to hold data for one MPToken response. @@ -71,17 +66,6 @@ public: uint32_t limit{}; }; - /** - * @brief A struct to hold the input data for the command. - */ - struct Input { - std::string account; - std::optional ledgerHash; - std::optional ledgerIndex; - uint32_t limit = kLimitDefault; - std::optional marker; - }; - using Result = HandlerReturnType; /** @@ -94,35 +78,6 @@ public: { } - /** - * @brief Returns the API specification for the command. - * - * @param apiVersion The API version to return the spec for. - * @return The spec for the given API version. - */ - static RpcSpecConstRef - spec([[maybe_unused]] uint32_t apiVersion) - { - static auto const kRpcSpec = RpcSpec{ - {JS(account), - validation::Required{}, - meta::WithCustomError{ - validation::CustomValidators::accountValidator, - Status(RippledError::RpcActMalformed) - }}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(limit), - validation::Type{}, - validation::Min(1u), - modifiers::Clamp{kLimitMin, kLimitMax}}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(marker), validation::CustomValidators::accountMarkerValidator}, - {JS(ledger), check::Deprecated{}}, - }; - - return kRpcSpec; - } - /** * @brief Process the AccountMPTokens command. * @@ -147,15 +102,6 @@ private: 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, boost::json::value const& jv); - /** * @brief Convert the MPTokenResponse to a JSON object * diff --git a/src/rpc/handlers/AccountNFTs.cpp b/src/rpc/handlers/AccountNFTs.cpp index 2e65fc6cb..bded73ccd 100644 --- a/src/rpc/handlers/AccountNFTs.cpp +++ b/src/rpc/handlers/AccountNFTs.cpp @@ -4,11 +4,9 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include -#include #include #include #include @@ -35,11 +33,10 @@ AccountNFTsHandler::process(AccountNFTsHandler::Input const& input, Context cons { auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "AccountNFT's ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -47,27 +44,22 @@ AccountNFTsHandler::process(AccountNFTsHandler::Input const& input, Context cons return Error{expectedLgrInfo.error()}; auto const& lgrInfo = *expectedLgrInfo; - auto const accountID = accountFromStringStrict(input.account); + auto const& accountID = input.account; auto const accountLedgerObject = sharedPtrBackend_->fetchLedgerObject( - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - xrpl::keylet::account(*accountID).key, - lgrInfo.seq, - ctx.yield + xrpl::keylet::account(accountID).key, lgrInfo.seq, ctx.yield ); if (!accountLedgerObject) return Error{Status{RippledError::RpcActNotFound}}; auto response = Output{}; - response.account = input.account; + response.account = xrpl::to_string(accountID); response.limit = input.limit; response.ledgerHash = xrpl::strHex(lgrInfo.hash); response.ledgerIndex = lgrInfo.seq; // if a marker was passed, start at the page specified in marker. Else, start at the max page - auto const pageKey = input.marker ? xrpl::uint256{input.marker->c_str()} - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - : xrpl::keylet::nftokenPageMax(*accountID).key; + auto const pageKey = input.marker ? *input.marker : xrpl::keylet::nftokenPageMax(accountID).key; auto const blob = sharedPtrBackend_->fetchLedgerObject(pageKey, lgrInfo.seq, ctx.yield); if (!blob) { @@ -152,30 +144,4 @@ tag_invoke( jv.as_object()[JS(marker)] = *output.marker; } -AccountNFTsHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = AccountNFTsHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - input.account = boost::json::value_to(jsonObject.at(JS(account))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(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(jsonObject.at(JS(limit))); - - if (jsonObject.contains(JS(marker))) - input.marker = boost::json::value_to(jsonObject.at(JS(marker))); - - return input; -} - } // namespace rpc diff --git a/src/rpc/handlers/AccountNFTs.hpp b/src/rpc/handlers/AccountNFTs.hpp index dcea64578..92d9e03a4 100644 --- a/src/rpc/handlers/AccountNFTs.hpp +++ b/src/rpc/handlers/AccountNFTs.hpp @@ -1,21 +1,19 @@ #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 #include #include -#include +#include +#include #include #include #include #include +#include namespace rpc { @@ -24,13 +22,13 @@ namespace rpc { * * For more details see: https://xrpl.org/account_nfts.html */ -class AccountNFTsHandler { +class AccountNFTsHandler : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; public: - static constexpr auto kLimitMin = 20; - static constexpr auto kLimitMax = 400; - static constexpr auto kLimitDefault = 100; + static constexpr auto kLimitMin = rpc::spec::handlers::account_nfts::kLimitMin; + static constexpr auto kLimitMax = rpc::spec::handlers::account_nfts::kLimitMax; + static constexpr auto kLimitDefault = rpc::spec::handlers::account_nfts::kLimitDefault; /** * @brief A struct to hold the output data of the command @@ -46,17 +44,6 @@ public: bool validated = true; }; - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::string account; - std::optional ledgerHash; - std::optional ledgerIndex; - uint32_t limit = kLimitDefault; // Limit the number of token pages to retrieve. [20,400] - std::optional marker; - }; - using Result = HandlerReturnType; /** @@ -69,29 +56,6 @@ public: { } - /** - * @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 kRpcSpec = RpcSpec{ - {JS(account), validation::Required{}, validation::CustomValidators::accountValidator}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(marker), validation::CustomValidators::uint256HexStringValidator}, - {JS(limit), - validation::Type{}, - validation::Min(1u), - modifiers::Clamp{kLimitMin, kLimitMax}}, - }; - - return kRpcSpec; - } - /** * @brief Process the AccountNFTs command * @@ -111,15 +75,6 @@ private: */ 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, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/AccountObjects.cpp b/src/rpc/handlers/AccountObjects.cpp index 5b1eacc7c..13a250e7b 100644 --- a/src/rpc/handlers/AccountObjects.cpp +++ b/src/rpc/handlers/AccountObjects.cpp @@ -4,24 +4,23 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include "util/LedgerUtils.hpp" #include #include #include -#include #include -#include #include +#include +#include #include #include #include +#include #include #include #include -#include #include #include #include @@ -37,11 +36,10 @@ AccountObjectsHandler::process(AccountObjectsHandler::Input const& input, Contex { auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "AccountObject's ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -49,12 +47,9 @@ AccountObjectsHandler::process(AccountObjectsHandler::Input const& input, Contex return Error{expectedLgrInfo.error()}; auto const& lgrInfo = *expectedLgrInfo; - auto const accountID = accountFromStringStrict(input.account); + auto const& accountID = input.account; auto const accountLedgerObject = sharedPtrBackend_->fetchLedgerObject( - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - xrpl::keylet::account(*accountID).key, - lgrInfo.seq, - ctx.yield + xrpl::keylet::account(accountID).key, lgrInfo.seq, ctx.yield ); if (!accountLedgerObject) @@ -78,8 +73,40 @@ AccountObjectsHandler::process(AccountObjectsHandler::Input const& input, Contex typeFilter = {*input.type}; } + auto const sponsorField = [](xrpl::SLE const& sle, + xrpl::SField const& field) -> std::optional { + if (sle.isFieldPresent(field)) + return sle.getAccountID(field); + return std::nullopt; + }; + + auto const sponsorOf = [&sponsorField](xrpl::SLE const& sle) { + // A trust line records a sponsor per side, so either one makes it sponsored. + if (sle.getType() == xrpl::ltRIPPLE_STATE) { + if (auto const high = sponsorField(sle, xrpl::sfHighSponsor); high.has_value()) + return high; + + return sponsorField(sle, xrpl::sfLowSponsor); + } + + // NFTokenPage is not in isLedgerEntrySupportedBySponsorship's allowlist, so that + // predicate alone would report every page as unsponsored; xrpld reads pages on a + // separate path for the same reason. No page can actually carry sfSponsor today -- + // SponsorshipTransfer::preclaim rejects unsupported types with tecNO_PERMISSION -- so + // this only starts to matter if NFTokenPage is added to that allowlist upstream. + if (sle.getType() == xrpl::ltNFTOKEN_PAGE or xrpl::isLedgerEntrySupportedBySponsorship(sle)) + return sponsorField(sle, xrpl::sfSponsor); + + return std::optional{}; + }; + Output response; auto const addToResponse = [&](xrpl::SLE&& sle) { + if (input.sponsored.has_value() and + sponsorOf(sle).has_value() != static_cast(*input.sponsored)) { + return true; + } + if (not typeFilter or std::find(std::begin(*typeFilter), std::end(*typeFilter), sle.getType()) != std::end(*typeFilter)) { @@ -90,7 +117,7 @@ AccountObjectsHandler::process(AccountObjectsHandler::Input const& input, Contex auto const expectedNext = traverseOwnedNodes( *sharedPtrBackend_, - *accountID, // NOLINT(bugprone-unchecked-optional-access) + accountID, lgrInfo.seq, input.limit, input.marker, @@ -105,7 +132,7 @@ AccountObjectsHandler::process(AccountObjectsHandler::Input const& input, Contex response.ledgerHash = xrpl::strHex(lgrInfo.hash); response.ledgerIndex = lgrInfo.seq; response.limit = input.limit; - response.account = input.account; + response.account = xrpl::to_string(accountID); auto const& nextMarker = *expectedNext; @@ -143,39 +170,4 @@ tag_invoke( jv.as_object()[JS(marker)] = *(output.marker); } -AccountObjectsHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = AccountObjectsHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - input.account = boost::json::value_to(jv.at(JS(account))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(jv.at(JS(ledger_hash))); - - if (jsonObject.contains(JS(ledger_index))) { - auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index))); - if (expectedLedgerIndex.has_value()) - input.ledgerIndex = *expectedLedgerIndex; - } - - if (jsonObject.contains(JS(type))) { - input.type = rpc::spec::accountOwnedLedgerTypeFromStr( - boost::json::value_to(jv.at(JS(type))) - ); - } - - if (jsonObject.contains(JS(limit))) - input.limit = util::integralValueAs(jv.at(JS(limit))); - - if (jsonObject.contains(JS(marker))) - input.marker = boost::json::value_to(jv.at(JS(marker))); - - if (jsonObject.contains(JS(deletion_blockers_only))) - input.deletionBlockersOnly = jsonObject.at(JS(deletion_blockers_only)).as_bool(); - - return input; -} - } // namespace rpc diff --git a/src/rpc/handlers/AccountObjects.hpp b/src/rpc/handlers/AccountObjects.hpp index da51a4391..ca7e17519 100644 --- a/src/rpc/handlers/AccountObjects.hpp +++ b/src/rpc/handlers/AccountObjects.hpp @@ -1,23 +1,19 @@ #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 #include +#include +#include #include -#include #include #include #include #include -#include -#include +#include #include namespace rpc { @@ -29,14 +25,15 @@ namespace rpc { * * For more details see: https://xrpl.org/account_objects.html */ -class AccountObjectsHandler { +class AccountObjectsHandler + : public rpc::spec::HandlerFor { // dependencies std::shared_ptr sharedPtrBackend_; public: - static constexpr auto kLimitMin = 10; - static constexpr auto kLimitMax = 400; - static constexpr auto kLimitDefault = 200; + static constexpr auto kLimitMin = rpc::spec::handlers::account_objects::kLimitMin; + static constexpr auto kLimitMax = rpc::spec::handlers::account_objects::kLimitMax; + static constexpr auto kLimitDefault = rpc::spec::handlers::account_objects::kLimitDefault; /** * @brief A struct to hold the output data of the command @@ -51,19 +48,6 @@ public: bool validated = true; }; - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::string account; - std::optional ledgerHash; - std::optional ledgerIndex; - uint32_t limit = kLimitDefault; // [10,400] - std::optional marker; - std::optional type; - bool deletionBlockersOnly = false; - }; - using Result = HandlerReturnType; /** @@ -76,31 +60,6 @@ public: { } - /** - * @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 kRpcSpec = RpcSpec{ - {JS(account), validation::Required{}, validation::CustomValidators::accountValidator}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(limit), - validation::Type{}, - validation::Min(1u), - modifiers::Clamp(kLimitMin, kLimitMax)}, - {JS(type), validation::CustomValidators::accountTypeValidator}, - {JS(marker), validation::CustomValidators::accountMarkerValidator}, - {JS(deletion_blockers_only), validation::Type{}}, - }; - - return kRpcSpec; - } - /** * @brief Process the AccountObjects command * @@ -120,15 +79,6 @@ private: */ 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, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/AccountOffers.cpp b/src/rpc/handlers/AccountOffers.cpp index c733ead9f..17787d50a 100644 --- a/src/rpc/handlers/AccountOffers.cpp +++ b/src/rpc/handlers/AccountOffers.cpp @@ -4,12 +4,10 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include #include -#include #include #include #include @@ -22,7 +20,6 @@ #include #include -#include #include #include @@ -52,11 +49,10 @@ AccountOffersHandler::process(AccountOffersHandler::Input const& input, Context { auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "AccountOffer's ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -64,19 +60,16 @@ AccountOffersHandler::process(AccountOffersHandler::Input const& input, Context return Error{expectedLgrInfo.error()}; auto const& lgrInfo = *expectedLgrInfo; - auto const accountID = accountFromStringStrict(input.account); + auto const& accountID = input.account; auto const accountLedgerObject = sharedPtrBackend_->fetchLedgerObject( - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - xrpl::keylet::account(*accountID).key, - lgrInfo.seq, - ctx.yield + xrpl::keylet::account(accountID).key, lgrInfo.seq, ctx.yield ); if (!accountLedgerObject) return Error{Status{RippledError::RpcActNotFound}}; Output response; - response.account = xrpl::to_string(*accountID); // NOLINT(bugprone-unchecked-optional-access) + response.account = xrpl::to_string(accountID); response.ledgerHash = xrpl::strHex(lgrInfo.hash); response.ledgerIndex = lgrInfo.seq; @@ -89,7 +82,7 @@ AccountOffersHandler::process(AccountOffersHandler::Input const& input, Context auto const expectedNext = traverseOwnedNodes( *sharedPtrBackend_, - *accountID, // NOLINT(bugprone-unchecked-optional-access) + accountID, lgrInfo.seq, input.limit, input.marker, @@ -161,30 +154,4 @@ tag_invoke( convertAmount(JS(taker_gets), offer.takerGets); } -AccountOffersHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = AccountOffersHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - input.account = boost::json::value_to(jsonObject.at(JS(account))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(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(jsonObject.at(JS(limit))); - - if (jsonObject.contains(JS(marker))) - input.marker = boost::json::value_to(jsonObject.at(JS(marker))); - - return input; -} - } // namespace rpc diff --git a/src/rpc/handlers/AccountOffers.hpp b/src/rpc/handlers/AccountOffers.hpp index 990f02b0f..1d79d97e9 100644 --- a/src/rpc/handlers/AccountOffers.hpp +++ b/src/rpc/handlers/AccountOffers.hpp @@ -1,23 +1,20 @@ #pragma once #include "data/BackendInterface.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/Checkers.hpp" -#include "rpc/common/Modifiers.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include #include +#include +#include #include #include -#include #include #include #include #include +#include #include namespace rpc { @@ -27,13 +24,14 @@ namespace rpc { * * For more details see: https://xrpl.org/account_offers.html */ -class AccountOffersHandler { +class AccountOffersHandler + : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; public: - static constexpr auto kLimitMin = 10; - static constexpr auto kLimitMax = 400; - static constexpr auto kLimitDefault = 200; + static constexpr auto kLimitMin = rpc::spec::handlers::account_offers::kLimitMin; + static constexpr auto kLimitMax = rpc::spec::handlers::account_offers::kLimitMax; + static constexpr auto kLimitDefault = rpc::spec::handlers::account_offers::kLimitDefault; /** * @brief A struct to hold data for one offer response @@ -60,17 +58,6 @@ public: bool validated = true; }; - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::string account; - std::optional ledgerHash; - std::optional ledgerIndex; - uint32_t limit = kLimitDefault; - std::optional marker; - }; - using Result = HandlerReturnType; /** @@ -83,31 +70,6 @@ public: { } - /** - * @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 kRpcSpec = RpcSpec{ - {JS(account), validation::Required{}, validation::CustomValidators::accountValidator}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(marker), validation::CustomValidators::accountMarkerValidator}, - {JS(limit), - validation::Type{}, - validation::Min(1u), - modifiers::Clamp{kLimitMin, kLimitMax}}, - {JS(ledger), check::Deprecated{}}, - {JS(strict), check::Deprecated{}}, - }; - - return kRpcSpec; - } - /** * @brief Process the AccountOffers command * @@ -131,15 +93,6 @@ private: 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, boost::json::value const& jv); - /** * @brief Convert the Offer to a JSON object * diff --git a/src/rpc/handlers/AccountTx.cpp b/src/rpc/handlers/AccountTx.cpp index afacae392..08b9d3f7f 100644 --- a/src/rpc/handlers/AccountTx.cpp +++ b/src/rpc/handlers/AccountTx.cpp @@ -3,7 +3,6 @@ #include "data/Types.hpp" #include "rpc/JS.hpp" #include "rpc/RPCHelpers.hpp" -#include "rpc/common/JsonBool.hpp" #include "rpc/common/Types.hpp" #include "rpc/filters/TransactionFilter.hpp" #include "rpc/filters/impl/DelegateTransactionsFilter.hpp" @@ -19,7 +18,7 @@ #include #include #include -#include +#include #include #include #include @@ -45,26 +44,26 @@ AccountTxHandler::process(AccountTxHandler::Input const& input, Context const& c auto [minIndex, maxIndex] = *range; // NOLINT(bugprone-unchecked-optional-access) if (input.ledgerIndexMin) { + // NOLINTBEGIN(bugprone-unchecked-optional-access) if (ctx.apiVersion > 1u && - (input.ledgerIndexMin > - range->maxSequence || // NOLINT(bugprone-unchecked-optional-access) - input.ledgerIndexMin < - range->minSequence)) { // NOLINT(bugprone-unchecked-optional-access) + (input.ledgerIndexMin > range->maxSequence || + input.ledgerIndexMin < range->minSequence)) { return Error{Status{RippledError::RpcLgrIdxMalformed, "ledgerSeqMinOutOfRange"}}; } + // NOLINTEND(bugprone-unchecked-optional-access) if (static_cast(*input.ledgerIndexMin) > minIndex) minIndex = *input.ledgerIndexMin; } if (input.ledgerIndexMax) { + // NOLINTBEGIN(bugprone-unchecked-optional-access) if (ctx.apiVersion > 1u && - (input.ledgerIndexMax > - range->maxSequence || // NOLINT(bugprone-unchecked-optional-access) - input.ledgerIndexMax < - range->minSequence)) { // NOLINT(bugprone-unchecked-optional-access) + (input.ledgerIndexMax > range->maxSequence || + input.ledgerIndexMax < range->minSequence)) { return Error{Status{RippledError::RpcLgrIdxMalformed, "ledgerSeqMaxOutOfRange"}}; } + // NOLINTEND(bugprone-unchecked-optional-access) if (static_cast(*input.ledgerIndexMax) < maxIndex) maxIndex = *input.ledgerIndexMax; @@ -77,7 +76,7 @@ AccountTxHandler::process(AccountTxHandler::Input const& input, Context const& c return Error{Status{RippledError::RpcInvalidLgrRange}}; } - if (input.ledgerHash || input.ledgerIndex || input.usingValidatedLedger) { + if (not input.ledger.isUnspecified()) { if (ctx.apiVersion > 1u && (input.ledgerIndexMax || input.ledgerIndexMin)) { return Error{Status{RippledError::RpcInvalidParams, "containsLedgerSpecifierAndRange"}}; } @@ -85,11 +84,10 @@ AccountTxHandler::process(AccountTxHandler::Input const& input, Context const& c if (!input.ledgerIndexMax && !input.ledgerIndexMin) { // mimic rippled, when both range and index specified, respect the range. // take ledger from ledgerHash or ledgerIndex only when range is not specified - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -115,20 +113,15 @@ AccountTxHandler::process(AccountTxHandler::Input const& input, Context const& c } } - auto const accountID = accountFromStringStrict(input.account); - std::optional txFilter; if (input.delegateFilter) { - txFilter.emplace( - *input.delegateFilter, - *accountID // NOLINT(bugprone-unchecked-optional-access) - ); + txFilter.emplace(*input.delegateFilter, input.account); } auto const limit = input.limit.value_or(kLimitDefault); auto const [txnsAndCursor, timeDiff] = util::timed([&]() { return sharedPtrBackend_->fetchAccountTransactions( - *accountID, limit, input.forward, cursor, ctx.yield + input.account, limit, input.forward, cursor, ctx.yield ); }); @@ -141,10 +134,6 @@ AccountTxHandler::process(AccountTxHandler::Input const& input, Context const& c if (retCursor) response.marker = {.ledger = retCursor->ledgerSequence, .seq = retCursor->transactionIndex}; - std::optional mptIssuanceFilter; - if (input.mptIssuanceId) - mptIssuanceFilter = xrpl::uint192{input.mptIssuanceId->c_str()}; - for (auto const& txnPlusMeta : blobs) { // over the range if ((txnPlusMeta.ledgerSequence < minIndex && !input.forward) || @@ -167,10 +156,10 @@ AccountTxHandler::process(AccountTxHandler::Input const& input, Context const& c boost::json::object obj; // Skip all Txns where the specified filter mpt_id doesn't match the query - if (mptIssuanceFilter) { + if (input.mptIssuanceId) { auto const [sttx, txMeta] = deserializeTxPlusMeta(txnPlusMeta, txnPlusMeta.ledgerSequence); - if (!util::referencesMptIssuance(*txMeta, *sttx, *mptIssuanceFilter)) + if (!util::referencesMptIssuance(*txMeta, *sttx, *input.mptIssuanceId)) continue; } @@ -244,7 +233,7 @@ AccountTxHandler::process(AccountTxHandler::Input const& input, Context const& c } response.limit = input.limit; - response.account = xrpl::to_string(*accountID); // NOLINT(bugprone-unchecked-optional-access) + response.account = xrpl::to_string(input.account); response.ledgerIndexMin = minIndex; response.ledgerIndexMax = maxIndex; @@ -273,12 +262,12 @@ tag_invoke( jv.as_object()[JS(limit)] = *(output.limit); } +} // namespace rpc + +namespace rpc::spec::handlers::account_tx { + void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - AccountTxHandler::Marker const& marker -) +tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Marker const& marker) { jv = { {JS(ledger), marker.ledger}, @@ -286,68 +275,4 @@ tag_invoke( }; } -AccountTxHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = AccountTxHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - input.account = boost::json::value_to(jsonObject.at(JS(account))); - - if (jsonObject.contains(JS(ledger_index_min)) && - util::integralValueAs(jsonObject.at(JS(ledger_index_min))) != -1) - input.ledgerIndexMin = util::integralValueAs(jsonObject.at(JS(ledger_index_min))); - - if (jsonObject.contains(JS(ledger_index_max)) && - util::integralValueAs(jsonObject.at(JS(ledger_index_max))) != -1) - input.ledgerIndexMax = util::integralValueAs(jsonObject.at(JS(ledger_index_max))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(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; - } else { - // could not get the latest validated ledger seq here, using this flag to indicate that - input.usingValidatedLedger = true; - } - } - - if (jsonObject.contains(JS(binary))) - input.binary = boost::json::value_to(jsonObject.at(JS(binary))); - - if (jsonObject.contains(JS(forward))) - input.forward = boost::json::value_to(jsonObject.at(JS(forward))); - - if (jsonObject.contains(JS(limit))) - input.limit = util::integralValueAs(jsonObject.at(JS(limit))); - - if (jsonObject.contains(JS(marker))) { - input.marker = AccountTxHandler::Marker{ - .ledger = util::integralValueAs( - jsonObject.at(JS(marker)).as_object().at(JS(ledger)) - ), - .seq = - util::integralValueAs(jsonObject.at(JS(marker)).as_object().at(JS(seq))) - }; - } - - if (jsonObject.contains("tx_type")) { - input.transactionTypeInLowercase = - boost::json::value_to(jsonObject.at("tx_type")); - } - - if (jsonObject.contains(JS(delegate))) - input.delegateFilter = parseDelegateFilter(jsonObject.at(JS(delegate)).as_object()); - - if (jsonObject.contains(JS(mpt_issuance_id))) { - input.mptIssuanceId = - boost::json::value_to(jsonObject.at(JS(mpt_issuance_id))); - } - - return input; -} - -} // namespace rpc +} // namespace rpc::spec::handlers::account_tx diff --git a/src/rpc/handlers/AccountTx.hpp b/src/rpc/handlers/AccountTx.hpp index 389f37dbf..60f6d5190 100644 --- a/src/rpc/handlers/AccountTx.hpp +++ b/src/rpc/handlers/AccountTx.hpp @@ -2,30 +2,22 @@ #include "data/BackendInterface.hpp" #include "etl/ETLServiceInterface.hpp" -#include "rpc/Errors.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/JsonBool.hpp" -#include "rpc/common/MetaProcessors.hpp" -#include "rpc/common/Modifiers.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include "util/log/Logger.hpp" #include #include #include #include -#include -#include -#include -#include +#include +#include #include +#include #include #include #include -#include +#include namespace rpc { @@ -35,23 +27,17 @@ namespace rpc { * * For more details see: https://xrpl.org/account_tx.html */ -class AccountTxHandler { +class AccountTxHandler : public rpc::spec::HandlerFor { util::Logger log_{"RPC"}; std::shared_ptr sharedPtrBackend_; std::shared_ptr etl_; public: - static constexpr auto kLimitMin = 1; - static constexpr auto kLimitMax = 1000; - static constexpr auto kLimitDefault = 200; + static constexpr auto kLimitMin = rpc::spec::handlers::account_tx::kLimitMin; + static constexpr auto kLimitMax = rpc::spec::handlers::account_tx::kLimitMax; + static constexpr auto kLimitDefault = rpc::spec::handlers::account_tx::kLimitDefault; - /** - * @brief A struct to hold the marker data - */ - struct Marker { - uint32_t ledger; - uint32_t seq; - }; + using Marker = rpc::spec::handlers::account_tx::Marker; /** * @brief A struct to hold the output data of the command @@ -68,27 +54,6 @@ public: bool validated = true; }; - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::string account; - // You must use at least one of the following fields in your request: - // ledger_index, ledger_hash, ledger_index_min, or ledger_index_max. - std::optional ledgerHash; - std::optional ledgerIndex; - std::optional ledgerIndexMin; - std::optional ledgerIndexMax; - bool usingValidatedLedger = false; - JsonBool binary{false}; - JsonBool forward{false}; - std::optional limit; - std::optional marker; - std::optional transactionTypeInLowercase; - std::optional delegateFilter; - std::optional mptIssuanceId; - }; - using Result = HandlerReturnType; /** @@ -105,61 +70,6 @@ public: { } - /** - * @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) - { - // TODO: goes away when account_tx moves to the shared spec, where the tx_type - // validator calls this internally. - auto const& typesKeysInLowercase = rpc::spec::txTypesInLowercase(); - static auto const kRpcSpecForV1 = RpcSpec{ - {JS(account), validation::Required{}, validation::CustomValidators::accountValidator}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(ledger_index_min), validation::Type{}}, - {JS(ledger_index_max), validation::Type{}}, - {JS(ctid), validation::Type{}}, - {JS(limit), - validation::Type{}, - validation::Min(1u), - modifiers::Clamp{kLimitMin, kLimitMax}}, - {JS(marker), - meta::WithCustomError{ - validation::Type{}, - Status{RippledError::RpcInvalidParams, "invalidMarker"}, - }, - meta::Section{ - {JS(ledger), validation::Required{}, validation::Type{}}, - {JS(seq), validation::Required{}, validation::Type{}}, - }}, - { - "tx_type", - validation::Type{}, - modifiers::ToLower{}, - validation::OneOf( - typesKeysInLowercase.cbegin(), typesKeysInLowercase.cend() - ), - }, - {JS(delegate), validation::CustomValidators::delegateValidator}, - {JS(mpt_issuance_id), validation::CustomValidators::uint192HexStringValidator}, - }; - - static auto const kRpcSpec = RpcSpec{ - kRpcSpecForV1, - { - {JS(binary), validation::Type{}}, - {JS(forward), validation::Type{}}, - } - }; - - return apiVersion == 1 ? kRpcSpecForV1 : kRpcSpec; - } - /** * @brief Process the AccountTx command * @@ -179,23 +89,13 @@ private: */ 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, boost::json::value const& jv); - - /** - * @brief Convert the Marker to a JSON object - * - * @param [out] jv The JSON object to convert to - * @param marker The marker to convert - */ - friend void - tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Marker const& marker); }; + } // namespace rpc + +namespace rpc::spec::handlers::account_tx { + +void +tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Marker const& marker); + +} // namespace rpc::spec::handlers::account_tx diff --git a/src/rpc/handlers/BookChanges.cpp b/src/rpc/handlers/BookChanges.cpp index 70d561543..3df3664a2 100644 --- a/src/rpc/handlers/BookChanges.cpp +++ b/src/rpc/handlers/BookChanges.cpp @@ -6,12 +6,10 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include #include -#include #include #include #include @@ -28,11 +26,10 @@ BookChangesHandler::process(BookChangesHandler::Input const& input, Context cons auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "BookChanges' ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -71,24 +68,6 @@ tag_invoke( }; } -BookChangesHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = BookChangesHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(jv.at(JS(ledger_hash))); - - if (jsonObject.contains(JS(ledger_index))) { - auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index))); - if (expectedLedgerIndex.has_value()) - input.ledgerIndex = *expectedLedgerIndex; - } - - return input; -} - [[nodiscard]] boost::json::object computeBookChanges( xrpl::LedgerHeader const& lgrInfo, diff --git a/src/rpc/handlers/BookChanges.hpp b/src/rpc/handlers/BookChanges.hpp index e82c5a0ce..fa09a51c7 100644 --- a/src/rpc/handlers/BookChanges.hpp +++ b/src/rpc/handlers/BookChanges.hpp @@ -2,19 +2,17 @@ #include "data/BackendInterface.hpp" #include "rpc/BookChangesHelper.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include #include -#include +#include +#include #include #include -#include #include +#include #include namespace rpc { @@ -24,7 +22,7 @@ namespace rpc { * * This API is not documented in the rippled API documentation. */ -class BookChangesHandler { +class BookChangesHandler : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; public: @@ -39,16 +37,6 @@ public: bool validated = true; }; - /** - * @brief A struct to hold the input data for the command - * - * @note Clio does not implement `deletion_blockers_only` - */ - struct Input { - std::optional ledgerHash; - std::optional ledgerIndex; - }; - using Result = HandlerReturnType; /** @@ -61,23 +49,6 @@ public: { } - /** - * @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 kRpcSpec = RpcSpec{ - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - }; - - return kRpcSpec; - } - /** * @brief Process the BookChanges command * @@ -97,15 +68,6 @@ private: */ 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, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/BookOffers.cpp b/src/rpc/handlers/BookOffers.cpp index e8038ff05..35ed2ef32 100644 --- a/src/rpc/handlers/BookOffers.cpp +++ b/src/rpc/handlers/BookOffers.cpp @@ -4,25 +4,17 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include #include -#include #include #include -#include -#include #include #include -#include #include -#include -#include #include -#include #include namespace rpc { @@ -30,7 +22,7 @@ namespace rpc { BookOffersHandler::Result BookOffersHandler::process(Input const& input, Context const& ctx) const { - auto bookMaybe = parseBook(input.paysAsset, input.getsAsset, input.domain); + auto bookMaybe = parseBook(input.takerPays, input.takerGets, input.domain); if (!bookMaybe.has_value()) return Error{bookMaybe.error()}; @@ -38,11 +30,10 @@ BookOffersHandler::process(Input const& input, Context const& ctx) const auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "BookOffer's ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -87,56 +78,4 @@ tag_invoke( }; } -BookOffersHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = BookOffersHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - auto const parseAsset = [](boost::json::object const& obj) -> xrpl::Asset { - if (obj.contains(JS(mpt_issuance_id))) { - xrpl::MPTID mptid; - auto const parsed = - mptid.parseHex(boost::json::value_to(obj.at(JS(mpt_issuance_id)))); - ASSERT(parsed, "mpt_issuance_id is guaranteed to be valid hex by the spec validator"); - return xrpl::MPTIssue{mptid}; - } - xrpl::Issue issue; - auto const currencyParsed = xrpl::toCurrency( - issue.currency, boost::json::value_to(obj.at(JS(currency))) - ); - ASSERT(currencyParsed, "currency is guaranteed to be valid by the spec validator"); - if (obj.contains(JS(issuer))) { - auto const issuerParsed = xrpl::toIssuer( - issue.account, boost::json::value_to(obj.at(JS(issuer))) - ); - ASSERT(issuerParsed, "issuer is guaranteed to be valid by the spec validator"); - } - return issue; - }; - - input.getsAsset = parseAsset(jv.at(JS(taker_gets)).as_object()); - input.paysAsset = parseAsset(jv.at(JS(taker_pays)).as_object()); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(jv.at(JS(ledger_hash))); - - if (jsonObject.contains(JS(ledger_index))) { - auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index))); - if (expectedLedgerIndex.has_value()) - input.ledgerIndex = *expectedLedgerIndex; - } - - if (jsonObject.contains(JS(taker))) - input.taker = accountFromStringStrict(boost::json::value_to(jv.at(JS(taker)))); - - if (jsonObject.contains(JS(domain))) - input.domain = boost::json::value_to(jv.at(JS(domain))); - - if (jsonObject.contains(JS(limit))) - input.limit = util::integralValueAs(jv.at(JS(limit))); - - return input; -} - } // namespace rpc diff --git a/src/rpc/handlers/BookOffers.hpp b/src/rpc/handlers/BookOffers.hpp index b95dd6088..dcbeb5685 100644 --- a/src/rpc/handlers/BookOffers.hpp +++ b/src/rpc/handlers/BookOffers.hpp @@ -2,28 +2,17 @@ #include "data/AmendmentCenterInterface.hpp" #include "data/BackendInterface.hpp" -#include "rpc/Errors.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/MetaProcessors.hpp" -#include "rpc/common/Modifiers.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include #include #include #include -#include -#include -#include -#include -#include -#include +#include +#include #include #include -#include #include #include @@ -35,14 +24,14 @@ namespace rpc { * * For more details see: https://xrpl.org/book_offers.html */ -class BookOffersHandler { +class BookOffersHandler : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; std::shared_ptr amendmentCenter_; public: - static constexpr auto kLimitMin = 1; - static constexpr auto kLimitMax = 100; - static constexpr auto kLimitDefault = 60; + static constexpr auto kLimitMin = rpc::spec::handlers::book_offers::kLimitMin; + static constexpr auto kLimitMax = rpc::spec::handlers::book_offers::kLimitMax; + static constexpr auto kLimitDefault = rpc::spec::handlers::book_offers::kLimitDefault; /** * @brief A struct to hold the output data of the command @@ -54,22 +43,6 @@ public: bool validated = true; }; - /** - * @brief A struct to hold the input data for the command - * - * @note The taker is not really used in both Clio and `rippled`, both of them return all the - * offers regardless of the funding status - */ - struct Input { - std::optional ledgerHash; - std::optional ledgerIndex; - uint32_t limit = kLimitDefault; - std::optional taker; - xrpl::Asset paysAsset = xrpl::xrpIssue(); - xrpl::Asset getsAsset = xrpl::xrpIssue(); - std::optional domain; - }; - using Result = HandlerReturnType; /** @@ -86,84 +59,6 @@ public: { } - /** - * @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 kRpcSpec = RpcSpec{ - {JS(taker_gets), - validation::Required{}, - validation::Type{}, - validation::CustomValidators::bookTakerValidator, - meta::Section{ - {JS(currency), - meta::WithCustomError{ - validation::CustomValidators::currencyValidator, - Status(RippledError::RpcDstAmtMalformed) - }}, - {JS(mpt_issuance_id), - meta::WithCustomError{ - validation::CustomValidators::uint192HexStringValidator, - Status(RippledError::RpcDstAmtMalformed) - }}, - {JS(issuer), - meta::WithCustomError{ - validation::CustomValidators::issuerValidator, - Status(RippledError::RpcDstIsrMalformed) - }} - }}, - {JS(taker_pays), - validation::Required{}, - validation::Type{}, - validation::CustomValidators::bookTakerValidator, - meta::Section{ - {JS(currency), - meta::WithCustomError{ - validation::CustomValidators::currencyValidator, - Status(RippledError::RpcSrcCurMalformed) - }}, - {JS(mpt_issuance_id), - meta::WithCustomError{ - validation::CustomValidators::uint192HexStringValidator, - Status(RippledError::RpcSrcCurMalformed) - }}, - {JS(issuer), - meta::WithCustomError{ - validation::CustomValidators::issuerValidator, - Status(RippledError::RpcSrcIsrMalformed) - }} - }}, - // return INVALID_PARAMS if account format is wrong for "taker" - {JS(taker), - meta::WithCustomError{ - validation::CustomValidators::accountValidator, - Status(RippledError::RpcInvalidParams, "Invalid field 'taker'.") - }}, - {JS(domain), - meta::WithCustomError{ - validation::Type{}, - Status(RippledError::RpcDomainMalformed, "Unable to parse domain.") - }, - meta::WithCustomError{ - validation::CustomValidators::uint256HexStringValidator, - Status(RippledError::RpcDomainMalformed, "Unable to parse domain.") - }}, - {JS(limit), - validation::Type{}, - validation::Min(1u), - modifiers::Clamp{kLimitMin, kLimitMax}}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - }; - - return kRpcSpec; - } - /** * @brief Process the BookOffers command * @@ -183,14 +78,5 @@ private: */ 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, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/DepositAuthorized.cpp b/src/rpc/handlers/DepositAuthorized.cpp index 5ba93019d..e23a9b7ff 100644 --- a/src/rpc/handlers/DepositAuthorized.cpp +++ b/src/rpc/handlers/DepositAuthorized.cpp @@ -5,16 +5,16 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include #include +#include #include -#include #include #include #include +#include #include #include #include @@ -24,6 +24,8 @@ #include #include +#include +#include #include #include @@ -38,11 +40,10 @@ DepositAuthorizedHandler::process( auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "DepositAuthorized ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -50,21 +51,17 @@ DepositAuthorizedHandler::process( return Error{expectedLgrInfo.error()}; auto const& lgrInfo = *expectedLgrInfo; - auto const sourceAccountID = accountFromStringStrict(input.sourceAccount); - auto const destinationAccountID = accountFromStringStrict(input.destinationAccount); + auto const& sourceAccountID = input.sourceAccount; + auto const& destinationAccountID = input.destinationAccount; auto const srcAccountLedgerObject = sharedPtrBackend_->fetchLedgerObject( - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - xrpl::keylet::account(*sourceAccountID).key, - lgrInfo.seq, - ctx.yield + xrpl::keylet::account(sourceAccountID).key, lgrInfo.seq, ctx.yield ); if (!srcAccountLedgerObject) return Error{Status{RippledError::RpcSrcActNotFound, "source_accountNotFound"}}; - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - auto const dstKeylet = xrpl::keylet::account(*destinationAccountID).key; + auto const dstKeylet = xrpl::keylet::account(destinationAccountID).key; auto const dstAccountLedgerObject = sharedPtrBackend_->fetchLedgerObject(dstKeylet, lgrInfo.seq, ctx.yield); @@ -91,11 +88,7 @@ DepositAuthorizedHandler::process( return Error{Status{RippledError::RpcInvalidParams, "credential array too long."}}; } auto const credArray = credentials::fetchCredentialArray( - input.credentials, - *sourceAccountID, // NOLINT(bugprone-unchecked-optional-access) - *sharedPtrBackend_, - lgrInfo, - ctx.yield + *creds, sourceAccountID, *sharedPtrBackend_, lgrInfo, ctx.yield ); if (!credArray.has_value()) return Error{std::move(credArray).error()}; @@ -115,52 +108,26 @@ DepositAuthorizedHandler::process( "should already be checked above that there is no duplicate" ); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - hashKey = xrpl::keylet::depositPreauth(*destinationAccountID, sortedAuthCreds).key; + hashKey = xrpl::keylet::depositPreauth(destinationAccountID, sortedAuthCreds).key; } else { - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - hashKey = xrpl::keylet::depositPreauth(*destinationAccountID, *sourceAccountID).key; + hashKey = xrpl::keylet::depositPreauth(destinationAccountID, sourceAccountID).key; } depositAuthorized = sharedPtrBackend_->fetchLedgerObject(hashKey, lgrInfo.seq, ctx.yield).has_value(); } - response.sourceAccount = input.sourceAccount; - response.destinationAccount = input.destinationAccount; + response.sourceAccount = xrpl::to_string(sourceAccountID); + response.destinationAccount = xrpl::to_string(destinationAccountID); response.ledgerHash = xrpl::strHex(lgrInfo.hash); response.ledgerIndex = lgrInfo.seq; response.depositAuthorized = depositAuthorized; if (credentialsPresent) - response.credentials = *input.credentials; + response.credentials = creds; return response; } -DepositAuthorizedHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = DepositAuthorizedHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - input.sourceAccount = boost::json::value_to(jv.at(JS(source_account))); - input.destinationAccount = boost::json::value_to(jv.at(JS(destination_account))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(jv.at(JS(ledger_hash))); - - if (jsonObject.contains(JS(ledger_index))) { - auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index))); - if (expectedLedgerIndex.has_value()) - input.ledgerIndex = *expectedLedgerIndex; - } - - if (jsonObject.contains(JS(credentials))) - input.credentials = boost::json::value_to(jv.at(JS(credentials))); - - return input; -} - void tag_invoke( boost::json::value_from_tag, @@ -176,8 +143,11 @@ tag_invoke( {JS(ledger_index), output.ledgerIndex}, {JS(validated), output.validated} }; - if (output.credentials) - jv.as_object()[JS(credentials)] = *output.credentials; + if (output.credentials.has_value()) { + jv.as_object()[JS(credentials)] = *output.credentials // + | std::views::transform([](auto const& cred) { return xrpl::strHex(cred); }) // + | std::ranges::to(); + } } } // namespace rpc diff --git a/src/rpc/handlers/DepositAuthorized.hpp b/src/rpc/handlers/DepositAuthorized.hpp index 20999738c..8907a49de 100644 --- a/src/rpc/handlers/DepositAuthorized.hpp +++ b/src/rpc/handlers/DepositAuthorized.hpp @@ -1,21 +1,23 @@ #pragma once #include "data/BackendInterface.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include #include #include +#include +#include +#include #include -#include #include +#include #include #include #include +#include +#include namespace rpc { @@ -28,7 +30,8 @@ namespace rpc { * * For more details see: https://xrpl.org/deposit_authorized.html */ -class DepositAuthorizedHandler { +class DepositAuthorizedHandler + : public rpc::spec::HandlerFor { // dependencies std::shared_ptr const sharedPtrBackend_; @@ -43,23 +46,12 @@ public: std::string destinationAccount; std::string ledgerHash; uint32_t ledgerIndex{}; - std::optional credentials; + std::optional> credentials; // validated should be sent via framework bool validated = true; }; - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::string sourceAccount; - std::string destinationAccount; - std::optional ledgerHash; - std::optional ledgerIndex; - std::optional credentials; - }; - using Result = HandlerReturnType; /** @@ -72,30 +64,6 @@ public: { } - /** - * @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 kRpcSpec = RpcSpec{ - {JS(source_account), - validation::Required{}, - validation::CustomValidators::accountValidator}, - {JS(destination_account), - validation::Required{}, - validation::CustomValidators::accountValidator}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(credentials), validation::Type{}, validation::Hex256ItemType()} - }; - - return kRpcSpec; - } - /** * @brief Process the DepositAuthorized command * @@ -115,14 +83,6 @@ private: */ 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, boost::json::value const& jv); }; + } // namespace rpc diff --git a/src/rpc/handlers/Feature.cpp b/src/rpc/handlers/Feature.cpp index a0b69c490..09d0f6c7a 100644 --- a/src/rpc/handlers/Feature.cpp +++ b/src/rpc/handlers/Feature.cpp @@ -3,16 +3,11 @@ #include "data/Types.hpp" #include "rpc/JS.hpp" #include "rpc/RPCHelpers.hpp" -#include "rpc/common/MetaProcessors.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include -#include #include #include #include @@ -21,7 +16,6 @@ #include #include -#include #include #include #include @@ -40,11 +34,10 @@ FeatureHandler::process(FeatureHandler::Input const& input, Context const& ctx) auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "Feature's ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -99,25 +92,6 @@ FeatureHandler::process(FeatureHandler::Input const& input, Context const& ctx) }; } -RpcSpecConstRef -FeatureHandler::spec([[maybe_unused]] uint32_t apiVersion) -{ - static RpcSpec const kRpcSpec = { - {JS(feature), validation::Type{}}, - {JS(vetoed), - meta::WithCustomError{ - validation::NotSupported{}, - Status( - RippledError::RpcNoPermission, - "The admin portion of feature API is not available through Clio." - ) - }}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - }; - return kRpcSpec; -} - void tag_invoke( boost::json::value_from_tag, @@ -157,25 +131,4 @@ tag_invoke( }; } -FeatureHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = FeatureHandler::Input{}; - auto const jsonObject = jv.as_object(); - - if (jsonObject.contains(JS(feature))) - input.feature = jv.at(JS(feature)).as_string(); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(jv.at(JS(ledger_hash))); - - if (jsonObject.contains(JS(ledger_index))) { - auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index))); - if (expectedLedgerIndex.has_value()) - input.ledgerIndex = *expectedLedgerIndex; - } - - return input; -} - } // namespace rpc diff --git a/src/rpc/handlers/Feature.hpp b/src/rpc/handlers/Feature.hpp index afda786df..02ea0c6ba 100644 --- a/src/rpc/handlers/Feature.hpp +++ b/src/rpc/handlers/Feature.hpp @@ -2,38 +2,29 @@ #include "data/AmendmentCenterInterface.hpp" #include "data/BackendInterface.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" #include #include -#include +#include +#include #include #include #include -#include #include +#include namespace rpc { /** * @brief Contains common functionality for handling the `server_info` command */ -class FeatureHandler { +class FeatureHandler : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; std::shared_ptr amendmentCenter_; public: - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::optional ledgerHash; - std::optional ledgerIndex; - std::optional feature; - }; - /** * @brief A struct to hold the output data of the command */ @@ -76,15 +67,6 @@ public: { } - /** - * @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); - /** * @brief Process the Feature command * @@ -116,15 +98,6 @@ private: */ friend void tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Output::Feature const& feature); - - /** - * @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, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/GatewayBalances.cpp b/src/rpc/handlers/GatewayBalances.cpp index 294bdd74c..1d93575d6 100644 --- a/src/rpc/handlers/GatewayBalances.cpp +++ b/src/rpc/handlers/GatewayBalances.cpp @@ -4,13 +4,11 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include #include #include -#include #include #include #include @@ -25,9 +23,7 @@ #include #include -#include #include -#include #include #include #include @@ -47,11 +43,10 @@ GatewayBalancesHandler::process( auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "GatewayBalances' ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -60,12 +55,9 @@ GatewayBalancesHandler::process( // check account auto const& lgrInfo = *expectedLgrInfo; - auto const accountID = accountFromStringStrict(input.account); + auto const& accountID = input.account; auto const accountLedgerObject = sharedPtrBackend_->fetchLedgerObject( - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - xrpl::keylet::account(*accountID).key, - lgrInfo.seq, - ctx.yield + xrpl::keylet::account(accountID).key, lgrInfo.seq, ctx.yield ); if (!accountLedgerObject) @@ -158,7 +150,7 @@ GatewayBalancesHandler::process( // traverse all owned nodes, limit->max, marker->empty auto const ret = traverseOwnedNodes( *sharedPtrBackend_, - *accountID, // NOLINT(bugprone-unchecked-optional-access) + accountID, lgrInfo.seq, std::numeric_limits::max(), {}, @@ -169,7 +161,7 @@ GatewayBalancesHandler::process( if (!ret.has_value()) return Error{ret.error()}; - output.accountID = input.account; + output.accountID = xrpl::to_string(accountID); output.ledgerHash = xrpl::strHex(lgrInfo.hash); output.ledgerIndex = lgrInfo.seq; @@ -241,43 +233,4 @@ tag_invoke( jv = std::move(obj); } -GatewayBalancesHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = GatewayBalancesHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - input.account = boost::json::value_to(jv.at(JS(account))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(jv.at(JS(ledger_hash))); - - if (jsonObject.contains(JS(ledger_index))) { - auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index))); - if (expectedLedgerIndex.has_value()) - input.ledgerIndex = *expectedLedgerIndex; - } - - if (jsonObject.contains(JS(hotwallet))) { - if (jsonObject.at(JS(hotwallet)).is_string()) { - input.hotWallets.insert( - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - *accountFromStringStrict(boost::json::value_to(jv.at(JS(hotwallet)))) - ); - } else { - auto const& hotWallets = jv.at(JS(hotwallet)).as_array(); - std::ranges::transform( - hotWallets, - - std::inserter(input.hotWallets, input.hotWallets.begin()), - [](auto const& hotWallet) { - return *accountFromStringStrict(boost::json::value_to(hotWallet)); - } - ); - } - } - - return input; -} - } // namespace rpc diff --git a/src/rpc/handlers/GatewayBalances.hpp b/src/rpc/handlers/GatewayBalances.hpp index 65b6e4405..4385d41f8 100644 --- a/src/rpc/handlers/GatewayBalances.hpp +++ b/src/rpc/handlers/GatewayBalances.hpp @@ -1,31 +1,22 @@ #pragma once #include "data/BackendInterface.hpp" -#include "rpc/Errors.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" -#include "util/AccountUtils.hpp" #include #include #include +#include +#include #include -#include -#include #include #include -#include -#include #include #include #include -#include -#include #include -#include +#include #include namespace rpc { @@ -38,7 +29,8 @@ namespace rpc { * * For more details see: https://xrpl.org/gateway_balances.html#gateway_balances */ -class GatewayBalancesHandler { +class GatewayBalancesHandler + : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; public: @@ -59,16 +51,6 @@ public: bool validated = true; }; - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::string account; - std::set hotWallets; - std::optional ledgerHash; - std::optional ledgerIndex; - }; - using Result = HandlerReturnType; /** @@ -81,67 +63,6 @@ public: { } - /** - * @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) - { - auto const getHotWalletValidator = [](RippledError errCode) { - return validation::CustomValidator{ - [errCode](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_string() && !value.is_array()) - return Error{Status{errCode, std::string(key) + "NotStringOrArray"}}; - - // wallet needs to be an valid accountID or public key - auto const wallets = - value.is_array() ? value.as_array() : boost::json::array{value}; - auto const getAccountID = [](auto const& j) -> std::optional { - if (j.is_string()) { - auto const pk = util::parseBase58Wrapper( - xrpl::TokenType::AccountPublic, - boost::json::value_to(j) - ); - - if (pk) - return xrpl::calcAccountID(*pk); - - return util::parseBase58Wrapper( - boost::json::value_to(j) - ); - } - - return {}; - }; - - for (auto const& wallet : wallets) { - if (!getAccountID(wallet)) - return Error{Status{errCode, std::string(key) + "Malformed"}}; - } - - return MaybeError{}; - } - }; - }; - - static auto const kSpecCommon = RpcSpec{ - {JS(account), validation::Required{}, validation::CustomValidators::accountValidator}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator} - }; - - static auto const kSpecV1 = RpcSpec{ - kSpecCommon, {{JS(hotwallet), getHotWalletValidator(xrpl::RpcInvalidHotwallet)}} - }; - static auto const kSpecV2 = - RpcSpec{kSpecCommon, {{JS(hotwallet), getHotWalletValidator(xrpl::RpcInvalidParams)}}}; - - return apiVersion == 1 ? kSpecV1 : kSpecV2; - } - /** * @brief Process the GatewayBalances command * @@ -161,15 +82,6 @@ private: */ 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, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/GetAggregatePrice.cpp b/src/rpc/handlers/GetAggregatePrice.cpp index 4d464a1f8..b3958e5f1 100644 --- a/src/rpc/handlers/GetAggregatePrice.cpp +++ b/src/rpc/handlers/GetAggregatePrice.cpp @@ -3,9 +3,7 @@ #include "rpc/JS.hpp" #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" -#include "util/AccountUtils.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include @@ -13,10 +11,9 @@ #include #include #include -#include +#include #include #include -#include #include #include #include @@ -47,11 +44,10 @@ GetAggregatePriceHandler::process( auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "GetAggregatePrice's ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -86,9 +82,8 @@ GetAggregatePriceHandler::process( series.begin(), series.end(), [&](xrpl::STObject const& o) -> bool { - return o.getFieldCurrency(xrpl::sfBaseAsset).getText() == - input.baseAsset and - o.getFieldCurrency(xrpl::sfQuoteAsset).getText() == input.quoteAsset and + return o.getFieldCurrency(xrpl::sfBaseAsset) == input.baseAsset and + o.getFieldCurrency(xrpl::sfQuoteAsset) == input.quoteAsset and o.isFieldPresent(xrpl::sfAssetPrice); } ); @@ -249,46 +244,6 @@ GetAggregatePriceHandler::tracebackOracleObject( } } -GetAggregatePriceHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = GetAggregatePriceHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(jv.at(JS(ledger_hash))); - - if (jsonObject.contains(JS(ledger_index))) { - auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index))); - if (expectedLedgerIndex.has_value()) - input.ledgerIndex = *expectedLedgerIndex; - } - - for (auto const& oracle : jsonObject.at(JS(oracles)).as_array()) { - input.oracles.push_back( - GetAggregatePriceHandler::Oracle{ - .documentId = boost::json::value_to( - oracle.as_object().at(JS(oracle_document_id)) - ), - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - .account = *util::parseBase58Wrapper( - boost::json::value_to(oracle.as_object().at(JS(account))) - ) - } - ); - } - input.baseAsset = boost::json::value_to(jv.at(JS(base_asset))); - input.quoteAsset = boost::json::value_to(jv.at(JS(quote_asset))); - - if (jsonObject.contains(JS(trim))) - input.trim = util::integralValueAs(jv.at(JS(trim))); - - if (jsonObject.contains(JS(time_threshold))) - input.timeThreshold = util::integralValueAs(jv.at(JS(time_threshold))); - - return input; -} - void tag_invoke( boost::json::value_from_tag, diff --git a/src/rpc/handlers/GetAggregatePrice.hpp b/src/rpc/handlers/GetAggregatePrice.hpp index fcadeb3e5..577e2247c 100644 --- a/src/rpc/handlers/GetAggregatePrice.hpp +++ b/src/rpc/handlers/GetAggregatePrice.hpp @@ -2,28 +2,27 @@ #include "data/BackendInterface.hpp" #include "rpc/Errors.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/MetaProcessors.hpp" -#include "rpc/common/Modifiers.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include #include #include +#include +#include +#include #include #include #include #include -#include +#include #include +#include #include #include #include #include -#include +#include #include namespace rpc { @@ -31,7 +30,8 @@ namespace rpc { /** *@brief The get_aggregate_price method. */ -class GetAggregatePriceHandler { +class GetAggregatePriceHandler + : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; public: @@ -58,27 +58,6 @@ public: bool validated = true; }; - /** - * @brief A struct to hold the input oracle data - */ - struct Oracle { - std::uint32_t documentId{0}; - xrpl::AccountID account; - }; - - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::optional ledgerHash; - std::optional ledgerIndex; - std::vector oracles; // valid range is 1-200 - std::string baseAsset; - std::string quoteAsset; - std::optional timeThreshold; - std::optional trim; // valid range is 1-25 - }; - using Result = HandlerReturnType; /** @@ -91,83 +70,6 @@ public: { } - /** - * @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 constexpr auto kOraclesMax = 200; - - static auto const kOraclesValidator = modifiers::CustomModifier{ - [](boost::json::value& value, std::string_view) -> MaybeError { - if (!value.is_array() or value.as_array().empty() or - value.as_array().size() > kOraclesMax) - return Error{Status{RippledError::RpcOracleMalformed}}; - - for (auto& oracle : value.as_array()) { - if (!oracle.is_object() or - !oracle.as_object().contains(JS(oracle_document_id)) or - !oracle.as_object().contains(JS(account))) - return Error{Status{RippledError::RpcOracleMalformed}}; - - auto maybeError = validation::Type{}.verify( - oracle, JS(oracle_document_id) - ); - if (!maybeError) - return maybeError; - - maybeError = modifiers::ToNumber::modify(oracle, JS(oracle_document_id)); - if (!maybeError) - return maybeError; - - maybeError = validation::CustomValidators::accountBase58Validator.verify( - oracle.as_object(), JS(account) - ); - if (!maybeError) - return Error{Status{RippledError::RpcInvalidParams}}; - }; - - return MaybeError{}; - } - }; - - static auto const kRpcSpec = RpcSpec{ - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - // validate quoteAsset and base_asset in accordance to the currency code found in XRPL - // doc: - // https://xrpl.org/docs/references/protocol/data-types/currency-formats#currency-codes - // usually Clio returns rpcMALFORMED_CURRENCY , return InvalidParam here just to mimic - // rippled - {JS(base_asset), - validation::Required{}, - meta::WithCustomError{ - validation::CustomValidators::currencyValidator, - Status(RippledError::RpcInvalidParams) - }}, - {JS(quote_asset), - validation::Required{}, - meta::WithCustomError{ - validation::CustomValidators::currencyValidator, - Status(RippledError::RpcInvalidParams) - }}, - {JS(oracles), validation::Required{}, kOraclesValidator}, - // note: Unlike `rippled`, Clio only supports UInt as input, no string, no `null`, etc. - {JS(time_threshold), validation::Type{}}, - { - JS(trim), - validation::Type{}, - validation::Between{1, 25}, - } - }; - - return kRpcSpec; - } - /** * @brief Process the GetAggregatePrice command * @@ -200,15 +102,6 @@ private: */ 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, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/Ledger.cpp b/src/rpc/handlers/Ledger.cpp index 9c3eee428..60e67e40d 100644 --- a/src/rpc/handlers/Ledger.cpp +++ b/src/rpc/handlers/Ledger.cpp @@ -5,7 +5,6 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include @@ -13,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -36,11 +34,10 @@ LedgerHandler::process(LedgerHandler::Input const& input, Context const& ctx) co auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "LedgerHandler's ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -61,7 +58,7 @@ LedgerHandler::process(LedgerHandler::Input const& input, Context const& ctx) co auto const expandTxJsonV1 = [&](data::TransactionAndMetadata const& tx) { if (!input.binary) { - auto [txn, meta] = toExpandedJson(tx, ctx.apiVersion); + auto [txn, meta] = toExpandedJson(tx, ctx.apiVersion, NFTokenjson::ENABLE); txn[JS(metaData)] = std::move(meta); return txn; } @@ -71,7 +68,7 @@ LedgerHandler::process(LedgerHandler::Input const& input, Context const& ctx) co auto const isoTimeStr = xrpl::toStringIso(lgrInfo.closeTime); auto const expandTxJsonV2 = [&](data::TransactionAndMetadata const& tx) { - auto [txn, meta] = toExpandedJson(tx, ctx.apiVersion); + auto [txn, meta] = toExpandedJson(tx, ctx.apiVersion, NFTokenjson::ENABLE); if (!input.binary) { boost::json::object entry; entry[JS(validated)] = true; @@ -189,37 +186,4 @@ tag_invoke(boost::json::value_from_tag, boost::json::value& jv, LedgerHandler::O }; } -LedgerHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = LedgerHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(jv.at(JS(ledger_hash))); - - if (jsonObject.contains(JS(ledger_index))) { - auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index))); - if (expectedLedgerIndex.has_value()) - input.ledgerIndex = *expectedLedgerIndex; - } - - if (jsonObject.contains(JS(transactions))) - input.transactions = jv.at(JS(transactions)).as_bool(); - - if (jsonObject.contains(JS(binary))) - input.binary = jv.at(JS(binary)).as_bool(); - - if (jsonObject.contains(JS(expand))) - input.expand = jv.at(JS(expand)).as_bool(); - - if (jsonObject.contains(JS(owner_funds))) - input.ownerFunds = jv.at(JS(owner_funds)).as_bool(); - - if (jsonObject.contains("diff")) - input.diff = jv.at("diff").as_bool(); - - return input; -} - } // namespace rpc diff --git a/src/rpc/handlers/Ledger.hpp b/src/rpc/handlers/Ledger.hpp index 7a06ef626..95227b275 100644 --- a/src/rpc/handlers/Ledger.hpp +++ b/src/rpc/handlers/Ledger.hpp @@ -2,20 +2,16 @@ #include "data/AmendmentCenterInterface.hpp" #include "data/BackendInterface.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/Checkers.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include #include #include -#include +#include +#include #include #include -#include #include #include @@ -26,7 +22,7 @@ namespace rpc { * * For more details see: https://xrpl.org/ledger.html */ -class LedgerHandler { +class LedgerHandler : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; std::shared_ptr amendmentCenter_; @@ -42,31 +38,6 @@ public: bool validated = true; }; - /** - * @brief A struct to hold the input data for the command - * - * Clio does not support: - * - queue - * - * And the following are deprecated altogether: - * - full - * - accounts - * - ledger - * - type - * - * Clio will throw an error when `queue`, `full` or `accounts` is set to `true`. - * @see https://github.com/XRPLF/clio/issues/603 and https://github.com/XRPLF/clio/issues/1537 - */ - struct Input { - std::optional ledgerHash; - std::optional ledgerIndex; - bool binary = false; - bool expand = false; - bool ownerFunds = false; - bool transactions = false; - bool diff = false; - }; - using Result = HandlerReturnType; /** @@ -84,35 +55,6 @@ public: { } - /** - * @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 kRpcSpec = RpcSpec{ - {JS(full), validation::Type{}, validation::NotSupported{true}}, - {JS(full), check::Deprecated{}}, - {JS(accounts), validation::Type{}, validation::NotSupported{true}}, - {JS(accounts), check::Deprecated{}}, - {JS(owner_funds), validation::Type{}}, - {JS(queue), validation::Type{}, validation::NotSupported{true}}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(transactions), validation::Type{}}, - {JS(expand), validation::Type{}}, - {JS(binary), validation::Type{}}, - {"diff", validation::Type{}}, - {JS(ledger), check::Deprecated{}}, - {JS(type), check::Deprecated{}}, - }; - - return kRpcSpec; - } - /** * @brief Process the Ledger command * @@ -132,14 +74,5 @@ private: */ 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, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/LedgerData.cpp b/src/rpc/handlers/LedgerData.cpp index 872dba02b..3a2435328 100644 --- a/src/rpc/handlers/LedgerData.cpp +++ b/src/rpc/handlers/LedgerData.cpp @@ -5,15 +5,13 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include "util/log/Logger.hpp" #include #include #include -#include #include -#include +#include #include #include #include @@ -27,8 +25,10 @@ #include #include #include +#include #include #include +#include #include namespace rpc { @@ -36,21 +36,24 @@ namespace rpc { LedgerDataHandler::Result LedgerDataHandler::process(Input const& input, Context const& ctx) const { - // marker must be int if outOfOrder is true - if (input.outOfOrder && input.marker) + using rpc::spec::handlers::ledger_data::MarkerValue; + + auto const* uint256Marker = input.marker ? std::get_if(&*input.marker) : nullptr; + auto const* diffMarker = input.marker ? std::get_if(&*input.marker) : nullptr; + + if (input.outOfOrder && uint256Marker != nullptr) return Error{Status{RippledError::RpcInvalidParams, "outOfOrderMarkerNotInt"}}; - if (!input.outOfOrder && input.diffMarker) + if (!input.outOfOrder && diffMarker != nullptr) return Error{Status{RippledError::RpcInvalidParams, "markerNotString"}}; auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "LedgerData's ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -62,11 +65,11 @@ LedgerDataHandler::process(Input const& input, Context const& ctx) const Output output; // no marker -> first call, return header information - if ((!input.marker) && (!input.diffMarker)) { + if (not input.marker.has_value()) { output.header = toJson(lgrInfo, input.binary, ctx.apiVersion); } else { - if (input.marker && - !sharedPtrBackend_->fetchLedgerObject(*(input.marker), lgrInfo.seq, ctx.yield)) + if (uint256Marker != nullptr && + !sharedPtrBackend_->fetchLedgerObject(*uint256Marker, lgrInfo.seq, ctx.yield)) return Error{Status{RippledError::RpcInvalidParams, "markerDoesNotExist"}}; } @@ -76,9 +79,9 @@ LedgerDataHandler::process(Input const& input, Context const& ctx) const auto const start = std::chrono::system_clock::now(); std::vector results; - if (input.diffMarker) { + if (diffMarker != nullptr) { // keep the same logic as previous implementation - auto diff = sharedPtrBackend_->fetchLedgerDiff(*(input.diffMarker), ctx.yield); + auto diff = sharedPtrBackend_->fetchLedgerDiff(*diffMarker, ctx.yield); std::vector keys; for (auto& [key, object] : diff) { @@ -94,17 +97,19 @@ LedgerDataHandler::process(Input const& input, Context const& ctx) const results.push_back({.key = keys[i], .blob = std::move(obj)}); } - if (*(input.diffMarker) > lgrInfo.seq) - output.diffMarker = *(input.diffMarker) - 1; + if (*diffMarker > lgrInfo.seq) + output.diffMarker = *diffMarker - 1; } else { - // limit's limitation is different based on binary or json - // framework can not handler the check right now, adjust the value here - auto const limit = std::min( - input.limit, - input.binary ? LedgerDataHandler::kLimitBinary : LedgerDataHandler::kLimitJson - ); + auto const maxLimit = + input.binary ? LedgerDataHandler::kLimitBinary : LedgerDataHandler::kLimitJson; + auto const limit = std::min(input.limit.value_or(maxLimit), maxLimit); + + std::optional pageMarker; + if (uint256Marker != nullptr) + pageMarker = *uint256Marker; + auto page = sharedPtrBackend_->fetchLedgerPage( - input.marker, lgrInfo.seq, limit, input.outOfOrder, ctx.yield + pageMarker, lgrInfo.seq, limit, input.outOfOrder, ctx.yield ); results = std::move(page.objects); @@ -178,48 +183,4 @@ tag_invoke( jv = std::move(obj); } -LedgerDataHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = LedgerDataHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - if (jsonObject.contains(JS(binary))) { - input.binary = jsonObject.at(JS(binary)).as_bool(); - input.limit = - input.binary ? LedgerDataHandler::kLimitBinary : LedgerDataHandler::kLimitJson; - } - - if (jsonObject.contains(JS(limit))) - input.limit = util::integralValueAs(jsonObject.at(JS(limit))); - - if (jsonObject.contains("out_of_order")) - input.outOfOrder = jsonObject.at("out_of_order").as_bool(); - - if (jsonObject.contains(JS(marker))) { - if (jsonObject.at(JS(marker)).is_string()) { - input.marker = - xrpl::uint256{boost::json::value_to(jsonObject.at(JS(marker))).data()}; - } else { - input.diffMarker = util::integralValueAs(jsonObject.at(JS(marker))); - } - } - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(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(type))) { - input.type = - rpc::spec::ledgerEntryTypeFromStr(boost::json::value_to(jv.at(JS(type)))); - } - - return input; -} - } // namespace rpc diff --git a/src/rpc/handlers/LedgerData.hpp b/src/rpc/handlers/LedgerData.hpp index f6f434b7f..85bb0c7fc 100644 --- a/src/rpc/handlers/LedgerData.hpp +++ b/src/rpc/handlers/LedgerData.hpp @@ -1,26 +1,25 @@ #pragma once #include "data/BackendInterface.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/Checkers.hpp" -#include "rpc/common/MetaProcessors.hpp" -#include "rpc/common/Specs.hpp" +#include "rpc/Errors.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include "util/log/Logger.hpp" #include #include #include #include +#include +#include #include -#include -#include +#include #include +#include #include #include #include +#include namespace rpc { @@ -30,15 +29,15 @@ namespace rpc { * * For more details see: https://xrpl.org/ledger_data.html */ -class LedgerDataHandler { +class LedgerDataHandler : public rpc::spec::HandlerFor { // dependencies std::shared_ptr sharedPtrBackend_; util::Logger log_{"RPC"}; public: // constants - static constexpr uint32_t kLimitBinary = 2048; - static constexpr uint32_t kLimitJson = 256; + static constexpr auto kLimitBinary = rpc::spec::handlers::ledger_data::kLimitBinary; + static constexpr auto kLimitJson = rpc::spec::handlers::ledger_data::kLimitJson; /** * @brief A struct to hold the output data of the command @@ -54,23 +53,6 @@ public: bool validated = true; }; - /** - * @brief A struct to hold the input data for the command - * - * @note `outOfOrder` is only for Clio, there is no document, traverse via seq diff (outOfOrder - * implementation is copied from old rpc handler) - */ - struct Input { - std::optional ledgerHash; - std::optional ledgerIndex; - bool binary = false; - uint32_t limit = LedgerDataHandler::kLimitJson; // max 256 for json ; 2048 for binary - std::optional marker; - std::optional diffMarker; - bool outOfOrder = false; - xrpl::LedgerEntryType type = xrpl::LedgerEntryType::ltANY; - }; - using Result = HandlerReturnType; /** @@ -83,30 +65,6 @@ public: { } - /** - * @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 kRpcSpec = RpcSpec{ - {JS(binary), validation::Type{}}, - {"out_of_order", validation::Type{}}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(limit), validation::Type{}, validation::Min(1u)}, - {JS(marker), - validation::Type{}, - meta::IfType{validation::CustomValidators::uint256HexStringValidator}}, - {JS(type), validation::CustomValidators::ledgerTypeValidator}, - {JS(ledger), check::Deprecated{}}, - }; - return kRpcSpec; - } - /** * @brief Process the LedgerData command * @@ -126,14 +84,6 @@ private: */ 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, boost::json::value const& jv); }; + } // namespace rpc diff --git a/src/rpc/handlers/LedgerEntry.cpp b/src/rpc/handlers/LedgerEntry.cpp index 60d939a61..934294913 100644 --- a/src/rpc/handlers/LedgerEntry.cpp +++ b/src/rpc/handlers/LedgerEntry.cpp @@ -1,266 +1,436 @@ #include "rpc/handlers/LedgerEntry.hpp" -#include "rpc/CredentialHelpers.hpp" #include "rpc/JS.hpp" #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" -#include "util/AccountUtils.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include #include -#include #include +#include #include #include #include #include #include -#include #include #include -#include #include #include -#include #include #include #include #include -#include #include -#include +#include #include +#include #include -#include -#include -#include +#include #include +#include +#include namespace rpc { +namespace { + +namespace le = rpc::spec::handlers::ledger_entry; +using Input = LedgerEntryHandler::Input; + +/** + * @brief The ledger key a request resolves to, plus the entry type it must have. + * + * @c expectedType is @c ltANY when the key was computed from a precise keylet, since the + * type is then implied by construction and needs no verification after the read. + */ +struct Locator { + xrpl::uint256 key; + xrpl::LedgerEntryType expectedType = xrpl::ltANY; +}; + +/** + * @brief A resolved Locator, or the Status explaining why the request could not name one. + */ +using LocatorOrStatus = std::expected; + +xrpl::STXChainBridge +makeBridge(le::BridgeSpec const& spec) +{ + return xrpl::STXChainBridge{ + spec.lockingChainDoor, spec.lockingChainIssue, spec.issuingChainDoor, spec.issuingChainIssue + }; +} + +/** + * @brief Resolve a locator field that accepts either a hex key or an entry object. + * + * A raw hex key only names the entry, so @p type is recorded and verified after the read. + * The object form computes a precise keylet via @p fromEntry and needs no verification. + * + * @param field The variant field to resolve + * @param type The entry type implied by the hex form + * @param fromEntry Computes the locator from the object form + * @return The resolved locator, or a Status describing why it could not be + */ +template +LocatorOrStatus +locatorFrom( + std::variant const& field, + xrpl::LedgerEntryType type, + FromEntry&& fromEntry +) +{ + if (auto const* hash = std::get_if(&field)) + return Locator{.key = *hash, .expectedType = type}; + + return std::forward(fromEntry)(std::get(field)); +} + +/** + * @brief One kHexLocators entry: the Input field carrying the key, and the type it implies. + */ +struct HexLocator { + std::optional Input::* field; + xrpl::LedgerEntryType type; +}; + +/** + * @brief Locator fields that carry a ledger key directly, each implying its entry type. + * + * These are pure data: the field is the key, and naming the field names the type. Kept in + * request-field order so the search order matches the rest of resolveLocator(). + */ +constexpr auto kHexLocators = std::to_array({ + {.field = &Input::check, .type = xrpl::ltCHECK}, + {.field = &Input::paymentChannel, .type = xrpl::ltPAYCHAN}, + {.field = &Input::nftPage, .type = xrpl::ltNFTOKEN_PAGE}, + {.field = &Input::nftOffer, .type = xrpl::ltNFTOKEN_OFFER}, + {.field = &Input::signerList, .type = xrpl::ltSIGNER_LIST}, + {.field = &Input::amendments, .type = xrpl::ltAMENDMENTS}, + {.field = &Input::fee, .type = xrpl::ltFEE_SETTINGS}, + {.field = &Input::hashes, .type = xrpl::ltLEDGER_HASHES}, + {.field = &Input::nunl, .type = xrpl::ltNEGATIVE_UNL}, +}); + +LocatorOrStatus +directoryLocator(le::DirectoryEntry const& entry) +{ + // xrpld folds these into a single "exactly one of" check; Clio reports them separately. + // This should be unified after xrpld is migrated to rpc-spec. + if (entry.dirRoot.has_value() and entry.owner.has_value()) { + return std::unexpected{ + Status{RippledError::RpcInvalidParams, "mayNotSpecifyBothDirRootAndOwner"} + }; + } + + if (not entry.dirRoot.has_value() and not entry.owner.has_value()) + return std::unexpected{Status{RippledError::RpcInvalidParams, "missingOwnerOrDirRoot"}}; + + auto const subIndex = entry.subIndex.value_or(0); + if (entry.dirRoot.has_value()) + return Locator{.key = xrpl::keylet::page(*entry.dirRoot, subIndex).key}; + + return Locator{.key = xrpl::keylet::page(xrpl::keylet::ownerDir(*entry.owner), subIndex).key}; +} + +LocatorOrStatus +depositPreauthLocator(le::DepositPreauthEntry const& entry) +{ + // Exactly one of authorized or authorized_credentials MUST exist. + if (entry.authorized.has_value() == entry.authorizedCredentials.has_value()) { + return std::unexpected{Status{ + ClioError::RpcMalformedRequest, "Must have one of authorized or authorized_credentials." + }}; + } + + if (entry.authorized.has_value()) + return Locator{.key = xrpl::keylet::depositPreauth(entry.owner, *entry.authorized).key}; + + std::set> authCreds; + + // Keep the decoded credential-type bytes alive while the Slices that reference them + // are used to build the keylet. + std::vector buffers; + buffers.reserve(entry.authorizedCredentials->size()); + + for (auto const& cred : *entry.authorizedCredentials) { + auto const decoded = xrpl::strUnHex(cred.credentialType); + ASSERT(decoded.has_value(), "credential_type is hex-validated by the spec"); + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + buffers.push_back(*decoded); + authCreds.emplace(cred.issuer, xrpl::Slice(buffers.back().data(), buffers.back().size())); + } + + if (authCreds.size() != entry.authorizedCredentials->size()) { + return std::unexpected{ + Status{ClioError::RpcMalformedAuthorizedCredentials, "duplicates in credentials."} + }; + } + + return Locator{.key = xrpl::keylet::depositPreauth(entry.owner, authCreds).key}; +} + +LocatorOrStatus +credentialLocator(le::CredentialEntry const& entry) +{ + auto const credType = xrpl::strUnHex(entry.credentialType); + ASSERT(credType.has_value(), "credential_type is hex-validated by the spec"); + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + auto const credSlice = xrpl::Slice(credType->data(), credType->size()); + + return Locator{.key = xrpl::keylet::credential(entry.subject, entry.issuer, credSlice).key}; +} + +/** + * @brief Resolve the `bridge` locator, the only one needing a second request field. + * + * @param bridge The `bridge` field + * @param bridgeAccount The `bridge_account` field, which is required alongside it + * @return The resolved locator, or a Status describing why it could not be + */ +LocatorOrStatus +bridgeLocator(le::BridgeSpec const& bridge, std::optional const& bridgeAccount) +{ + if (not bridgeAccount.has_value()) + return std::unexpected{Status{ClioError::RpcMalformedRequest}}; + + auto const stBridge = makeBridge(bridge); + auto const chainType = + xrpl::STXChainBridge::srcChain(*bridgeAccount == bridge.lockingChainDoor); + + if (*bridgeAccount != stBridge.door(chainType)) + return std::unexpected{Status{ClioError::RpcMalformedRequest}}; + + return Locator{.key = xrpl::keylet::bridge(stBridge, chainType).key}; +} + +/** + * @brief Work out which ledger key the request asks for. + * + * The spec admits one locator field per request; fields are searched in request-field order + * so that a malformed multi-locator request resolves deterministically. + * + * @param input The request input + * @param apiVersion The API version, which selects the error for a request with no locator + * @return The resolved locator, or a Status describing why it could not be + */ +LocatorOrStatus +resolveLocator(Input const& input, uint32_t apiVersion) +{ + if (input.index.has_value()) { + // A raw key names no entry type, so nothing is verified after the read. + if (input.index->isZero()) + return std::unexpected{Status{RippledError::RpcEntryNotFound}}; + + return Locator{.key = *input.index}; + } + + if (input.accountRoot.has_value()) + return Locator{.key = xrpl::keylet::account(*input.accountRoot).key}; + + if (input.did.has_value()) + return Locator{.key = xrpl::keylet::did(*input.did).key}; + + for (auto const& [field, type] : kHexLocators) { + if (auto const& hexKey = input.*field; hexKey.has_value()) + return Locator{.key = *hexKey, .expectedType = type}; + } + + if (input.mptIssuance.has_value()) + return Locator{.key = xrpl::keylet::mptokenIssuance(*input.mptIssuance).key}; + + if (input.directory.has_value()) + return locatorFrom(*input.directory, xrpl::ltDIR_NODE, directoryLocator); + + if (input.offer.has_value()) { + return locatorFrom(*input.offer, xrpl::ltOFFER, [](le::OfferEntry const& entry) { + return LocatorOrStatus{Locator{ + .key = + xrpl::keylet::offer(entry.account, xrpl::SeqProxy::rawSequence(entry.seq)).key + }}; + }); + } + + if (input.rippleStateAccount.has_value()) { + auto const& state = *input.rippleStateAccount; + return Locator{ + .key = xrpl::keylet::trustLine(state.accounts[0], state.accounts[1], state.currency).key + }; + } + + if (input.escrow.has_value()) { + return locatorFrom(*input.escrow, xrpl::ltESCROW, [](le::EscrowEntry const& entry) { + return LocatorOrStatus{Locator{ + .key = xrpl::keylet::escrow(entry.owner, xrpl::SeqProxy::rawSequence(entry.seq)).key + }}; + }); + } + + if (input.depositPreauth.has_value()) + return locatorFrom(*input.depositPreauth, xrpl::ltDEPOSIT_PREAUTH, depositPreauthLocator); + + if (input.ticket.has_value()) { + return locatorFrom(*input.ticket, xrpl::ltTICKET, [](le::TicketEntry const& entry) { + return LocatorOrStatus{Locator{ + .key = + xrpl::keylet::ticket(entry.account, xrpl::SeqProxy::rawTicket(entry.ticketSeq)) + .key + }}; + }); + } + + if (input.amm.has_value()) { + return locatorFrom(*input.amm, xrpl::ltAMM, [](le::AmmEntry const& entry) { + return LocatorOrStatus{ + Locator{.key = xrpl::keylet::amm(entry.asset, entry.asset2).key} + }; + }); + } + + if (input.bridge.has_value()) + return bridgeLocator(*input.bridge, input.bridgeAccount); + + if (input.xchainOwnedClaimId.has_value()) { + return locatorFrom( + *input.xchainOwnedClaimId, + xrpl::ltXCHAIN_OWNED_CLAIM_ID, + [](le::XChainClaimIdEntry const& entry) { + return LocatorOrStatus{Locator{ + .key = xrpl::keylet::xChainClaimID(makeBridge(entry.bridge), entry.claimId).key + }}; + } + ); + } + + if (input.xchainOwnedCreateAccountClaimId.has_value()) { + return locatorFrom( + *input.xchainOwnedCreateAccountClaimId, + xrpl::ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID, + [](le::XChainClaimIdEntry const& entry) { + return LocatorOrStatus{Locator{ + .key = xrpl::keylet::xChainCreateAccountClaimID( + makeBridge(entry.bridge), entry.claimId + ) + .key + }}; + } + ); + } + + if (input.oracle.has_value()) { + return locatorFrom(*input.oracle, xrpl::ltORACLE, [](le::OracleEntry const& entry) { + return LocatorOrStatus{ + Locator{.key = xrpl::keylet::oracle(entry.account, entry.oracleDocumentId).key} + }; + }); + } + + if (input.credential.has_value()) + return locatorFrom(*input.credential, xrpl::ltCREDENTIAL, credentialLocator); + + if (input.mptoken.has_value()) { + return locatorFrom(*input.mptoken, xrpl::ltMPTOKEN, [](le::MptokenEntry const& entry) { + return LocatorOrStatus{ + Locator{.key = xrpl::keylet::mptoken(entry.mptIssuanceId, entry.account).key} + }; + }); + } + + if (input.permissionedDomain.has_value()) { + return locatorFrom( + *input.permissionedDomain, + xrpl::ltPERMISSIONED_DOMAIN, + [](le::PermissionedDomainEntry const& entry) { + return LocatorOrStatus{Locator{ + .key = xrpl::keylet::permissionedDomain( + entry.account, xrpl::SeqProxy::rawSequence(entry.seq) + ) + .key + }}; + } + ); + } + + if (input.vault.has_value()) { + return locatorFrom(*input.vault, xrpl::ltVAULT, [](le::VaultEntry const& entry) { + return LocatorOrStatus{Locator{ + .key = xrpl::keylet::vault(entry.owner, xrpl::SeqProxy::rawSequence(entry.seq)).key + }}; + }); + } + + if (input.loanBroker.has_value()) { + return locatorFrom( + *input.loanBroker, xrpl::ltLOAN_BROKER, [](le::LoanBrokerEntry const& entry) { + return LocatorOrStatus{Locator{ + .key = xrpl::keylet::loanBroker( + entry.owner, xrpl::SeqProxy::rawSequence(entry.seq) + ) + .key + }}; + } + ); + } + + if (input.loan.has_value()) { + return locatorFrom(*input.loan, xrpl::ltLOAN, [](le::LoanEntry const& entry) { + return LocatorOrStatus{Locator{ + .key = xrpl::keylet::loan( + entry.loanBrokerId, xrpl::SeqProxy::rawSequence(entry.loanSeq) + ) + .key + }}; + }); + } + + if (input.delegate.has_value()) { + return locatorFrom(*input.delegate, xrpl::ltDELEGATE, [](le::DelegateEntry const& entry) { + return LocatorOrStatus{ + Locator{.key = xrpl::keylet::delegate(entry.account, entry.authorize).key} + }; + }); + } + + if (input.sponsorship.has_value()) { + return locatorFrom( + *input.sponsorship, xrpl::ltSPONSORSHIP, [](le::SponsorshipEntry const& entry) { + return LocatorOrStatus{ + Locator{.key = xrpl::keylet::sponsorship(entry.sponsor, entry.sponsee).key} + }; + } + ); + } + + if (apiVersion == 1u) + return std::unexpected{Status{ClioError::RpcUnknownOption}}; + + return std::unexpected{ + Status{RippledError::RpcInvalidParams, "No ledger_entry params provided."} + }; +} + +} // namespace + LedgerEntryHandler::Result LedgerEntryHandler::process(LedgerEntryHandler::Input const& input, Context const& ctx) const { - xrpl::uint256 key; + auto const locator = resolveLocator(input, ctx.apiVersion); + if (not locator.has_value()) + return Error{locator.error()}; - if (input.index) { - key = xrpl::uint256{std::string_view(*(input.index))}; - if (key.isZero()) - return Error{Status{RippledError::RpcEntryNotFound}}; - } else if (input.accountRoot) { - key = xrpl::keylet::account( - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - *util::parseBase58Wrapper(*(input.accountRoot)) - ) - .key; - } else if (input.did) { - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - key = xrpl::keylet::did(*util::parseBase58Wrapper(*(input.did))).key; - } else if (input.directory) { - auto const expectedkey = composeKeyFromDirectory(*input.directory); - if (!expectedkey.has_value()) - return Error{expectedkey.error()}; + auto const key = locator->key; + auto const expectedType = locator->expectedType; - key = expectedkey.value(); // std::expected, not optional - } else if (input.offer) { - auto const id = util::parseBase58Wrapper( - boost::json::value_to(input.offer->at(JS(account))) - ); - - // NOLINTBEGIN(bugprone-unchecked-optional-access) - key = xrpl::keylet::offer( - *id, - xrpl::SeqProxy::rawSequence( - boost::json::value_to(input.offer->at(JS(seq))) - ) - ) - .key; - // NOLINTEND(bugprone-unchecked-optional-access) - } else if (input.rippleStateAccount) { - auto const id1 = - util::parseBase58Wrapper(boost::json::value_to( - input.rippleStateAccount->at(JS(accounts)).as_array().at(0) - )); - auto const id2 = - util::parseBase58Wrapper(boost::json::value_to( - input.rippleStateAccount->at(JS(accounts)).as_array().at(1) - )); - auto const currency = xrpl::toCurrency( - boost::json::value_to(input.rippleStateAccount->at(JS(currency))) - ); - - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - key = xrpl::keylet::trustLine(*id1, *id2, currency).key; - } else if (input.escrow) { - auto const id = util::parseBase58Wrapper( - boost::json::value_to(input.escrow->at(JS(owner))) - ); - key = xrpl::keylet::escrow( - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - *id, - xrpl::SeqProxy::rawSequence( - util::integralValueAs(input.escrow->at(JS(seq))) - ) - ) - .key; - } else if (input.depositPreauth) { - auto const owner = util::parseBase58Wrapper( - boost::json::value_to(input.depositPreauth->at(JS(owner))) - ); - // Only one of authorize or authorized_credentials MUST exist; - if (input.depositPreauth->contains(JS(authorized)) == - input.depositPreauth->contains(JS(authorized_credentials))) { - return Error{Status{ - ClioError::RpcMalformedRequest, - "Must have one of authorized or authorized_credentials." - }}; - } - - if (input.depositPreauth->contains(JS(authorized))) { - auto const authorized = util::parseBase58Wrapper( - boost::json::value_to(input.depositPreauth->at(JS(authorized))) - ); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - key = xrpl::keylet::depositPreauth(*owner, *authorized).key; - } else { - auto const authorizedCredentials = rpc::credentials::parseAuthorizeCredentials( - input.depositPreauth->at(JS(authorized_credentials)).as_array() - ); - - auto const authCreds = credentials::createAuthCredentials(authorizedCredentials); - if (authCreds.size() != authorizedCredentials.size()) { - return Error{Status{ - ClioError::RpcMalformedAuthorizedCredentials, "duplicates in credentials." - }}; - } - - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - key = xrpl::keylet::depositPreauth(*owner, authCreds).key; - } - } else if (input.ticket) { - auto const id = util::parseBase58Wrapper( - boost::json::value_to(input.ticket->at(JS(account))) - ); - - key = xrpl::keylet::ticket( - *id, // NOLINT(bugprone-unchecked-optional-access) - xrpl::SeqProxy::rawTicket( - util::integralValueAs(input.ticket->at(JS(ticket_seq))) - ) - ) - .key; - } else if (input.amm) { - auto const getIssuerFromJson = [](auto const& assetJson) { - // the field check has been done in validator - auto const currency = - xrpl::toCurrency(boost::json::value_to(assetJson.at(JS(currency)))); - if (xrpl::isXRP(currency)) { - return xrpl::xrpIssue(); - } - auto const issuer = util::parseBase58Wrapper( - boost::json::value_to(assetJson.at(JS(issuer))) - ); - return xrpl::Issue{currency, *issuer}; - }; - - key = xrpl::keylet::amm( - getIssuerFromJson(input.amm->at(JS(asset))), - getIssuerFromJson(input.amm->at(JS(asset2))) - ) - .key; - } else if (input.bridge) { - if (!input.bridgeAccount && !input.chainClaimId && !input.createAccountClaimId) - return Error{Status{ClioError::RpcMalformedRequest}}; - - if (input.bridgeAccount) { - auto const bridgeAccount = - util::parseBase58Wrapper(*(input.bridgeAccount)); - auto const chainType = - xrpl::STXChainBridge::srcChain(bridgeAccount == input.bridge->lockingChainDoor()); - - if (bridgeAccount != input.bridge->door(chainType)) - return Error{Status{ClioError::RpcMalformedRequest}}; - - key = xrpl::keylet::bridge(input.bridge->value(), chainType).key; - } else if (input.chainClaimId) { - key = xrpl::keylet::xChainClaimID(input.bridge->value(), *input.chainClaimId).key; - } else { - key = xrpl::keylet::xChainCreateAccountClaimID( - input.bridge->value(), *input.createAccountClaimId - ) - .key; - } - } else if (input.oracleNode) { - key = *input.oracleNode; - } else if (input.credential) { - key = *input.credential; - } else if (input.mptIssuance) { - auto const mptIssuanceID = xrpl::uint192{std::string_view(*(input.mptIssuance))}; - key = xrpl::keylet::mptokenIssuance(mptIssuanceID).key; - } else if (input.mptoken) { - auto const holder = xrpl::parseBase58( - boost::json::value_to(input.mptoken->at(JS(account))) - ); - auto const mptIssuanceID = xrpl::uint192{std::string_view( - boost::json::value_to(input.mptoken->at(JS(mpt_issuance_id))) - )}; - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - key = xrpl::keylet::mptoken(mptIssuanceID, *holder).key; - } else if (input.permissionedDomain) { - auto const account = xrpl::parseBase58( - boost::json::value_to(input.permissionedDomain->at(JS(account))) - ); - auto const seq = util::integralValueAs(input.permissionedDomain->at(JS(seq))); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - key = xrpl::keylet::permissionedDomain(*account, xrpl::SeqProxy::rawSequence(seq)).key; - } else if (input.vault) { - auto const account = xrpl::parseBase58( - boost::json::value_to(input.vault->at(JS(owner))) - ); - auto const seq = util::integralValueAs(input.vault->at(JS(seq))); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - key = xrpl::keylet::vault(*account, xrpl::SeqProxy::rawSequence(seq)).key; - } else if (input.loanBroker) { - auto const account = xrpl::parseBase58( - boost::json::value_to(input.loanBroker->at(JS(owner))) - ); - auto const seq = util::integralValueAs(input.loanBroker->at(JS(seq))); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - key = xrpl::keylet::loanBroker(*account, xrpl::SeqProxy::rawSequence(seq)).key; - } else if (input.loan) { - auto const id = xrpl::uint256{ - boost::json::value_to(input.loan->at(JS(loan_broker_id))).data() - }; - auto const seq = util::integralValueAs(input.loan->at(JS(loan_seq))); - key = xrpl::keylet::loan(id, xrpl::SeqProxy::rawSequence(seq)).key; - } else if (input.delegate) { - auto const account = xrpl::parseBase58( - boost::json::value_to(input.delegate->at(JS(account))) - ); - auto const authorize = xrpl::parseBase58( - boost::json::value_to(input.delegate->at(JS(authorize))) - ); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - key = xrpl::keylet::delegate(*account, *authorize).key; - } else { - // Must specify 1 of the following fields to indicate what type - if (ctx.apiVersion == 1) - return Error{Status{ClioError::RpcUnknownOption}}; - return Error{Status{RippledError::RpcInvalidParams, "No ledger_entry params provided."}}; - } - - // check ledger exists auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "LedgerEntry's ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -271,15 +441,15 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input const& input, Context cons auto output = LedgerEntryHandler::Output{}; auto ledgerObject = sharedPtrBackend_->fetchLedgerObject(key, lgrInfo.seq, ctx.yield); - if (!ledgerObject || ledgerObject->empty()) { + if (not ledgerObject.has_value() or ledgerObject->empty()) { if (not input.includeDeleted) return Error{Status{RippledError::RpcEntryNotFound}}; auto const deletedSeq = sharedPtrBackend_->fetchLedgerObjectSeq(key, lgrInfo.seq, ctx.yield); - if (!deletedSeq) + if (not deletedSeq.has_value()) return Error{Status{RippledError::RpcEntryNotFound}}; ledgerObject = sharedPtrBackend_->fetchLedgerObject(key, *deletedSeq - 1, ctx.yield); - if (!ledgerObject || ledgerObject->empty()) + if (not ledgerObject.has_value() or ledgerObject->empty()) return Error{Status{RippledError::RpcEntryNotFound}}; output.deletedLedgerIndex = deletedSeq; } @@ -288,7 +458,7 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input const& input, Context cons xrpl::SerialIter{ledgerObject->data(), ledgerObject->size()}, key }; - if (input.expectedType != xrpl::ltANY && sle.getType() != input.expectedType) + if (expectedType != xrpl::ltANY && sle.getType() != expectedType) return Error{Status{RippledError::RpcUnexpectedLedgerType}}; output.index = xrpl::strHex(key); @@ -304,38 +474,6 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input const& input, Context cons return output; } -std::expected -LedgerEntryHandler::composeKeyFromDirectory(boost::json::object const& directory) noexcept -{ - // can not specify both dir_root and owner. - if (directory.contains(JS(dir_root)) && directory.contains(JS(owner))) { - return std::unexpected{ - Status{RippledError::RpcInvalidParams, "mayNotSpecifyBothDirRootAndOwner"} - }; - } - - // at least one should available - if (!(directory.contains(JS(dir_root)) || directory.contains(JS(owner)))) - return std::unexpected{Status{RippledError::RpcInvalidParams, "missingOwnerOrDirRoot"}}; - - uint64_t const subIndex = directory.contains(JS(sub_index)) - ? boost::json::value_to(directory.at(JS(sub_index))) - : 0; - - if (directory.contains(JS(dir_root))) { - xrpl::uint256 const uDirRoot{ - boost::json::value_to(directory.at(JS(dir_root))).data() - }; - return xrpl::keylet::page(uDirRoot, subIndex).key; - } - - auto const ownerID = util::parseBase58Wrapper( - boost::json::value_to(directory.at(JS(owner))) - ); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - return xrpl::keylet::page(xrpl::keylet::ownerDir(*ownerID), subIndex).key; -} - void tag_invoke( boost::json::value_from_tag, @@ -350,10 +488,10 @@ tag_invoke( {JS(index), output.index}, }; - if (output.deletedLedgerIndex) + if (output.deletedLedgerIndex.has_value()) object["deleted_ledger_index"] = *(output.deletedLedgerIndex); - if (output.nodeBinary) { + if (output.nodeBinary.has_value()) { object[JS(node_binary)] = *(output.nodeBinary); } else { object[JS(node)] = *(output.node); // NOLINT(bugprone-unchecked-optional-access) @@ -362,168 +500,4 @@ tag_invoke( jv = std::move(object); } -LedgerEntryHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = LedgerEntryHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(jv.at(JS(ledger_hash))); - - if (jsonObject.contains(JS(ledger_index))) { - auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index))); - if (expectedLedgerIndex.has_value()) - input.ledgerIndex = *expectedLedgerIndex; - } - - if (jsonObject.contains(JS(binary))) - input.binary = jv.at(JS(binary)).as_bool(); - - // check all the potential index - static auto const kIndexFieldTypeMap = std::unordered_map{ - {JS(index), xrpl::ltANY}, - {JS(directory), xrpl::ltDIR_NODE}, - {JS(offer), xrpl::ltOFFER}, - {JS(check), xrpl::ltCHECK}, - {JS(escrow), xrpl::ltESCROW}, - {JS(payment_channel), xrpl::ltPAYCHAN}, - {JS(deposit_preauth), xrpl::ltDEPOSIT_PREAUTH}, - {JS(ticket), xrpl::ltTICKET}, - {JS(nft_page), xrpl::ltNFTOKEN_PAGE}, - {JS(amm), xrpl::ltAMM}, - {JS(xchain_owned_create_account_claim_id), xrpl::ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}, - {JS(xchain_owned_claim_id), xrpl::ltXCHAIN_OWNED_CLAIM_ID}, - {JS(oracle), xrpl::ltORACLE}, - {JS(credential), xrpl::ltCREDENTIAL}, - {JS(mptoken), xrpl::ltMPTOKEN}, - {JS(permissioned_domain), xrpl::ltPERMISSIONED_DOMAIN}, - {JS(vault), xrpl::ltVAULT}, - {JS(loan_broker), xrpl::ltLOAN_BROKER}, - {JS(loan), xrpl::ltLOAN}, - {JS(delegate), xrpl::ltDELEGATE}, - {JS(amendments), xrpl::ltAMENDMENTS}, - {JS(fee), xrpl::ltFEE_SETTINGS}, - {JS(hashes), xrpl::ltLEDGER_HASHES}, - {JS(nft_offer), xrpl::ltNFTOKEN_OFFER}, - {JS(nunl), xrpl::ltNEGATIVE_UNL}, - {JS(signer_list), xrpl::ltSIGNER_LIST}, - }; - - auto const parseBridgeFromJson = [](boost::json::value const& bridgeJson) { - auto const lockingDoor = - *util::parseBase58Wrapper(boost::json::value_to( - bridgeJson.at(xrpl::sfLockingChainDoor.getJsonName().cStr()) - )); - auto const issuingDoor = - *util::parseBase58Wrapper(boost::json::value_to( - bridgeJson.at(xrpl::sfIssuingChainDoor.getJsonName().cStr()) - )); - auto const lockingIssue = - parseIssue(bridgeJson.at(xrpl::sfLockingChainIssue.getJsonName().cStr()).as_object()); - auto const issuingIssue = - parseIssue(bridgeJson.at(xrpl::sfIssuingChainIssue.getJsonName().cStr()).as_object()); - - return xrpl::STXChainBridge{lockingDoor, lockingIssue, issuingDoor, issuingIssue}; - }; - - auto const parseOracleFromJson = [](boost::json::value const& json) { - auto const account = util::parseBase58Wrapper( - boost::json::value_to(json.at(JS(account))) - ); - auto const documentId = boost::json::value_to(json.at(JS(oracle_document_id))); - - return xrpl::keylet::oracle(*account, documentId).key; - }; - - auto const parseCredentialFromJson = - [](boost::json::value const& json) -> std::optional { - auto const subject = util::parseBase58Wrapper( - boost::json::value_to(json.at(JS(subject))) - ); - auto const issuer = util::parseBase58Wrapper( - boost::json::value_to(json.at(JS(issuer))) - ); - auto const credTypeOpt = - xrpl::strUnHex(boost::json::value_to(json.at(JS(credential_type)))); - - return credTypeOpt.transform([&](xrpl::Blob const& credType) -> xrpl::uint256 { - return xrpl::keylet::credential( - *subject, *issuer, xrpl::Slice(credType.data(), credType.size()) - ) - .key; - }); - }; - - auto const indexFieldType = - std::ranges::find_if(kIndexFieldTypeMap, [&jsonObject](auto const& pair) { - auto const& [field, _] = pair; - return jsonObject.contains(field) && jsonObject.at(field).is_string(); - }); - - if (indexFieldType != kIndexFieldTypeMap.end()) { - input.index = boost::json::value_to(jv.at(indexFieldType->first)); - input.expectedType = indexFieldType->second; - } - // check if request for account root - else if (jsonObject.contains(JS(account_root))) { - input.accountRoot = boost::json::value_to(jv.at(JS(account_root))); - } else if (jsonObject.contains(JS(did))) { - input.did = boost::json::value_to(jv.at(JS(did))); - } else if (jsonObject.contains(JS(mpt_issuance))) { - input.mptIssuance = boost::json::value_to(jv.at(JS(mpt_issuance))); - } - // no need to check if_object again, validator only allows string or object - else if (jsonObject.contains(JS(directory))) { - input.directory = jv.at(JS(directory)).as_object(); - } else if (jsonObject.contains(JS(offer))) { - input.offer = jv.at(JS(offer)).as_object(); - } else if (jsonObject.contains(JS(ripple_state))) { - input.rippleStateAccount = jv.at(JS(ripple_state)).as_object(); - } else if (jsonObject.contains(JS(escrow))) { - input.escrow = jv.at(JS(escrow)).as_object(); - } else if (jsonObject.contains(JS(deposit_preauth))) { - input.depositPreauth = jv.at(JS(deposit_preauth)).as_object(); - } else if (jsonObject.contains(JS(ticket))) { - input.ticket = jv.at(JS(ticket)).as_object(); - } else if (jsonObject.contains(JS(amm))) { - input.amm = jv.at(JS(amm)).as_object(); - } else if (jsonObject.contains(JS(bridge))) { - input.bridge.emplace(parseBridgeFromJson(jv.at(JS(bridge)))); - if (jsonObject.contains(JS(bridge_account))) - input.bridgeAccount = boost::json::value_to(jv.at(JS(bridge_account))); - } else if (jsonObject.contains(JS(xchain_owned_claim_id))) { - input.bridge.emplace(parseBridgeFromJson(jv.at(JS(xchain_owned_claim_id)))); - input.chainClaimId = boost::json::value_to( - jv.at(JS(xchain_owned_claim_id)).at(JS(xchain_owned_claim_id)) - ); - } else if (jsonObject.contains(JS(xchain_owned_create_account_claim_id))) { - input.bridge.emplace(parseBridgeFromJson(jv.at(JS(xchain_owned_create_account_claim_id)))); - input.createAccountClaimId = - boost::json::value_to(jv.at(JS(xchain_owned_create_account_claim_id)) - .at(JS(xchain_owned_create_account_claim_id))); - } else if (jsonObject.contains(JS(oracle))) { - input.oracleNode = parseOracleFromJson(jv.at(JS(oracle))); - } else if (jsonObject.contains(JS(credential))) { - input.credential = parseCredentialFromJson(jv.at(JS(credential))); - } else if (jsonObject.contains(JS(mptoken))) { - 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(loan_broker))) { - input.loanBroker = jv.at(JS(loan_broker)).as_object(); - } else if (jsonObject.contains(JS(loan))) { - input.loan = jv.at(JS(loan)).as_object(); - } else if (jsonObject.contains(JS(delegate))) { - input.delegate = jv.at(JS(delegate)).as_object(); - } - - if (jsonObject.contains("include_deleted")) - input.includeDeleted = jv.at("include_deleted").as_bool(); - - return input; -} - } // namespace rpc diff --git a/src/rpc/handlers/LedgerEntry.hpp b/src/rpc/handlers/LedgerEntry.hpp index ea32d9450..9ed608e84 100644 --- a/src/rpc/handlers/LedgerEntry.hpp +++ b/src/rpc/handlers/LedgerEntry.hpp @@ -1,38 +1,18 @@ #pragma once #include "data/BackendInterface.hpp" -#include "rpc/Errors.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/Checkers.hpp" -#include "rpc/common/MetaProcessors.hpp" -#include "rpc/common/Modifiers.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" -#include "util/AccountUtils.hpp" -#include -#include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include #include #include #include #include -#include -#include +#include namespace rpc { @@ -42,7 +22,7 @@ namespace rpc { * * For more details see: https://xrpl.org/ledger_entry.html */ -class LedgerEntryHandler { +class LedgerEntryHandler : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; public: @@ -59,47 +39,6 @@ public: bool validated = true; }; - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::optional ledgerHash; - std::optional ledgerIndex; - bool binary = false; - // id of this ledger entry: 256 bits hex string - std::optional index; - // index can be extracted from payment_channel, check, escrow, offer - // etc, expectedType is used to save the type of index - xrpl::LedgerEntryType expectedType = xrpl::ltANY; - // account id to address account root object - std::optional accountRoot; - // account id to address did object - std::optional did; - // mpt issuance id to address mptIssuance object - std::optional mptIssuance; - // TODO: extract into custom objects, remove json from Input - std::optional directory; - std::optional offer; - std::optional rippleStateAccount; - std::optional escrow; - std::optional depositPreauth; - std::optional ticket; - std::optional amm; - std::optional mptoken; - std::optional permissionedDomain; - std::optional vault; - std::optional loanBroker; - std::optional loan; - std::optional bridge; - std::optional bridgeAccount; - std::optional chainClaimId; - std::optional createAccountClaimId; - std::optional oracleNode; - std::optional credential; - std::optional delegate; - bool includeDeleted = false; - }; - using Result = HandlerReturnType; /** @@ -112,449 +51,6 @@ public: { } - /** - * @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) - { - // Validator only works in this handler - // The accounts array must have two different elements - // Each element must be a valid address - static auto const kRippleStateAccountsCheck = validation::CustomValidator{ - [](boost::json::value const& value, std::string_view /* key */) -> MaybeError { - if (!value.is_array() || value.as_array().size() != 2 || - !value.as_array()[0].is_string() || !value.as_array()[1].is_string() || - value.as_array()[0].as_string() == value.as_array()[1].as_string()) { - return Error{Status{RippledError::RpcInvalidParams, "malformedAccounts"}}; - } - - auto const id1 = util::parseBase58Wrapper( - boost::json::value_to(value.as_array()[0]) - ); - auto const id2 = util::parseBase58Wrapper( - boost::json::value_to(value.as_array()[1]) - ); - - if (!id1 || !id2) - return Error{Status{ClioError::RpcMalformedAddress, "malformedAddresses"}}; - - return MaybeError{}; - } - }; - - static auto const kMalformedRequestHexStringValidator = meta::WithCustomError{ - validation::CustomValidators::uint256HexStringValidator, - Status(ClioError::RpcMalformedRequest) - }; - - static auto const kMalformedRequestIntValidator = meta::WithCustomError{ - validation::Type{}, Status(ClioError::RpcMalformedRequest) - }; - - static auto const kBridgeJsonValidator = meta::WithCustomError{ - meta::IfType{meta::Section{ - {xrpl::sfLockingChainDoor.getJsonName().cStr(), - validation::Required{}, - validation::CustomValidators::accountBase58Validator}, - {xrpl::sfIssuingChainDoor.getJsonName().cStr(), - validation::Required{}, - validation::CustomValidators::accountBase58Validator}, - {xrpl::sfLockingChainIssue.getJsonName().cStr(), - validation::Required{}, - validation::CustomValidators::currencyIssueValidator}, - {xrpl::sfIssuingChainIssue.getJsonName().cStr(), - validation::Required{}, - validation::CustomValidators::currencyIssueValidator}, - }}, - Status(ClioError::RpcMalformedRequest) - }; - - static auto const kRpcSpec = RpcSpec{ - {JS(binary), validation::Type{}}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(index), kMalformedRequestHexStringValidator}, - {JS(account_root), validation::CustomValidators::accountBase58Validator}, - {JS(did), validation::CustomValidators::accountBase58Validator}, - {JS(check), kMalformedRequestHexStringValidator}, - {JS(deposit_preauth), - validation::Type{}, - meta::IfType{kMalformedRequestHexStringValidator}, - meta::IfType{ - meta::Section{ - {JS(owner), - validation::Required{}, - meta::WithCustomError{ - validation::CustomValidators::accountBase58Validator, - Status(ClioError::RpcMalformedOwner) - }}, - {JS(authorized), validation::CustomValidators::accountBase58Validator}, - {JS(authorized_credentials), - validation::CustomValidators::authorizeCredentialValidator} - }, - }}, - {JS(directory), - validation::Type{}, - meta::IfType{kMalformedRequestHexStringValidator}, - meta::IfType{meta::Section{ - {JS(owner), validation::CustomValidators::accountBase58Validator}, - {JS(dir_root), validation::CustomValidators::uint256HexStringValidator}, - {JS(sub_index), kMalformedRequestIntValidator} - }}}, - {JS(escrow), - validation::Type{}, - meta::IfType{kMalformedRequestHexStringValidator}, - meta::IfType{ - meta::Section{ - {JS(owner), - validation::Required{}, - meta::WithCustomError{ - validation::CustomValidators::accountBase58Validator, - Status(ClioError::RpcMalformedOwner) - }}, - {JS(seq), validation::Required{}, kMalformedRequestIntValidator}, - }, - }}, - {JS(offer), - validation::Type{}, - meta::IfType{kMalformedRequestHexStringValidator}, - meta::IfType{ - meta::Section{ - {JS(account), - validation::Required{}, - validation::CustomValidators::accountBase58Validator}, - {JS(seq), validation::Required{}, kMalformedRequestIntValidator}, - }, - }}, - {JS(payment_channel), kMalformedRequestHexStringValidator}, - {JS(ripple_state), - validation::Type{}, - meta::Section{ - {JS(accounts), validation::Required{}, kRippleStateAccountsCheck}, - {JS(currency), - validation::Required{}, - validation::CustomValidators::currencyValidator}, - }}, - {JS(ticket), - validation::Type{}, - meta::IfType{kMalformedRequestHexStringValidator}, - meta::IfType{ - meta::Section{ - {JS(account), - validation::Required{}, - validation::CustomValidators::accountBase58Validator}, - {JS(ticket_seq), validation::Required{}, kMalformedRequestIntValidator}, - }, - }}, - {JS(nft_page), kMalformedRequestHexStringValidator}, - {JS(amm), - validation::Type{}, - meta::IfType{kMalformedRequestHexStringValidator}, - meta::IfType{ - meta::Section{ - {JS(asset), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcMalformedRequest) - }, - meta::WithCustomError{ - validation::Type{}, - Status(ClioError::RpcMalformedRequest) - }, - validation::CustomValidators::currencyIssueValidator}, - {JS(asset2), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcMalformedRequest) - }, - meta::WithCustomError{ - validation::Type{}, - Status(ClioError::RpcMalformedRequest) - }, - validation::CustomValidators::currencyIssueValidator}, - }, - }}, - {JS(bridge), - meta::WithCustomError{ - validation::Type{}, Status(ClioError::RpcMalformedRequest) - }, - kBridgeJsonValidator}, - {JS(bridge_account), - meta::WithCustomError{ - validation::CustomValidators::accountBase58Validator, - Status(ClioError::RpcMalformedRequest) - }}, - {JS(xchain_owned_claim_id), - meta::WithCustomError{ - validation::Type{}, - Status(ClioError::RpcMalformedRequest) - }, - meta::IfType{kMalformedRequestHexStringValidator}, - kBridgeJsonValidator, - meta::WithCustomError{ - meta::IfType{meta::Section{ - {JS(xchain_owned_claim_id), - validation::Required{}, - validation::Type{}} - }}, - Status(ClioError::RpcMalformedRequest) - }}, - {JS(xchain_owned_create_account_claim_id), - meta::WithCustomError{ - validation::Type{}, - Status(ClioError::RpcMalformedRequest) - }, - meta::IfType{kMalformedRequestHexStringValidator}, - kBridgeJsonValidator, - meta::WithCustomError{ - meta::IfType{meta::Section{ - {JS(xchain_owned_create_account_claim_id), - validation::Required{}, - validation::Type{}} - }}, - Status(ClioError::RpcMalformedRequest) - }}, - {JS(oracle), - meta::WithCustomError{ - validation::Type{}, - Status(ClioError::RpcMalformedRequest) - }, - meta::IfType{meta::WithCustomError{ - kMalformedRequestHexStringValidator, Status(ClioError::RpcMalformedAddress) - }}, - meta::IfType{meta::Section{ - {JS(account), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcMalformedRequest) - }, - meta::WithCustomError{ - validation::CustomValidators::accountBase58Validator, - Status(ClioError::RpcMalformedAddress) - }}, - // note: Unlike `rippled`, Clio only supports UInt as input, no string, no `null`, - // etc.: - {JS(oracle_document_id), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcMalformedRequest) - }, - meta::WithCustomError{ - validation::Type{}, - Status(ClioError::RpcMalformedOracleDocumentId) - }, - meta::WithCustomError{ - modifiers::ToNumber{}, Status(ClioError::RpcMalformedOracleDocumentId) - }}, - }}}, - {JS(credential), - meta::WithCustomError{ - validation::Type{}, - Status(ClioError::RpcMalformedRequest) - }, - meta::IfType{meta::WithCustomError{ - kMalformedRequestHexStringValidator, Status(ClioError::RpcMalformedAddress) - }}, - meta::IfType{meta::Section{ - {JS(subject), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcMalformedRequest) - }, - meta::WithCustomError{ - validation::CustomValidators::accountBase58Validator, - Status(ClioError::RpcMalformedAddress) - }}, - {JS(issuer), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcMalformedRequest) - }, - meta::WithCustomError{ - validation::CustomValidators::accountBase58Validator, - Status(ClioError::RpcMalformedAddress) - }}, - { - JS(credential_type), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcMalformedRequest) - }, - meta::WithCustomError{ - validation::Type{}, Status(ClioError::RpcMalformedRequest) - }, - validation::CustomValidators::credentialTypeValidator, - }, - }}}, - {JS(mpt_issuance), - meta::WithCustomError{ - validation::CustomValidators::uint192HexStringValidator, - Status(ClioError::RpcMalformedRequest) - }}, - {JS(mptoken), - meta::WithCustomError{ - validation::Type{}, - Status(ClioError::RpcMalformedRequest) - }, - meta::IfType{kMalformedRequestHexStringValidator}, - meta::IfType{ - meta::Section{ - { - JS(account), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcMalformedRequest) - }, - meta::WithCustomError{ - validation::CustomValidators::accountBase58Validator, - Status(ClioError::RpcMalformedAddress) - }, - }, - { - JS(mpt_issuance_id), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcMalformedRequest) - }, - meta::WithCustomError{ - validation::CustomValidators::uint192HexStringValidator, - Status(ClioError::RpcMalformedRequest) - }, - }, - }, - }}, - {JS(permissioned_domain), - meta::WithCustomError{ - validation::Type{}, - Status(ClioError::RpcMalformedRequest) - }, - meta::IfType{kMalformedRequestHexStringValidator}, - meta::IfType{meta::Section{ - {JS(seq), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcMalformedRequest) - }, - meta::WithCustomError{ - validation::Type{}, Status(ClioError::RpcMalformedRequest) - }}, - { - JS(account), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcMalformedRequest) - }, - meta::WithCustomError{ - validation::CustomValidators::accountBase58Validator, - Status(ClioError::RpcMalformedAddress) - }, - }, - }}}, - {JS(vault), - meta::WithCustomError{ - validation::Type{}, - Status(ClioError::RpcMalformedRequest) - }, - meta::IfType{kMalformedRequestHexStringValidator}, - meta::IfType{meta::Section{ - {JS(seq), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcMalformedRequest) - }, - meta::WithCustomError{ - validation::Type{}, Status(ClioError::RpcMalformedRequest) - }}, - { - JS(owner), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcMalformedRequest) - }, - meta::WithCustomError{ - validation::CustomValidators::accountBase58Validator, - Status(ClioError::RpcMalformedOwner) - }, - }, - }}}, - {JS(loan_broker), - meta::WithCustomError{ - validation::Type{}, - Status(ClioError::RpcMalformedRequest) - }, - meta::IfType{kMalformedRequestHexStringValidator}, - meta::IfType{meta::Section{ - {JS(seq), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcMalformedRequest) - }, - meta::WithCustomError{ - validation::Type{}, Status(ClioError::RpcMalformedRequest) - }}, - { - JS(owner), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcMalformedRequest) - }, - meta::WithCustomError{ - validation::CustomValidators::accountBase58Validator, - Status(ClioError::RpcMalformedOwner) - }, - }, - }}}, - {JS(loan), - meta::WithCustomError{ - validation::Type{}, - Status(ClioError::RpcMalformedRequest) - }, - meta::IfType{kMalformedRequestHexStringValidator}, - meta::IfType{meta::Section{ - {JS(loan_seq), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcMalformedRequest) - }, - meta::WithCustomError{ - validation::Type{}, Status(ClioError::RpcMalformedRequest) - }}, - { - JS(loan_broker_id), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcMalformedRequest) - }, - meta::WithCustomError{ - validation::CustomValidators::uint256HexStringValidator, - Status(ClioError::RpcMalformedRequest) - }, - }, - }}}, - {JS(delegate), - meta::WithCustomError{ - validation::Type{}, - Status(ClioError::RpcMalformedRequest) - }, - meta::IfType{kMalformedRequestHexStringValidator}, - meta::IfType{meta::Section{ - {JS(account), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcMalformedRequest) - }, - meta::WithCustomError{ - validation::CustomValidators::accountBase58Validator, - Status(ClioError::RpcMalformedAddress) - }}, - {JS(authorize), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcMalformedRequest) - }, - meta::WithCustomError{ - validation::CustomValidators::accountBase58Validator, - Status(ClioError::RpcMalformedAddress) - }} - }}}, - {JS(amendments), kMalformedRequestHexStringValidator}, - {JS(fee), kMalformedRequestHexStringValidator}, - {JS(hashes), kMalformedRequestHexStringValidator}, - {JS(nft_offer), kMalformedRequestHexStringValidator}, - {JS(nunl), kMalformedRequestHexStringValidator}, - {JS(signer_list), kMalformedRequestHexStringValidator}, - {JS(ledger), check::Deprecated{}}, - {"include_deleted", validation::Type{}}, - }; - - return kRpcSpec; - } - /** * @brief Process the LedgerEntry command * @@ -566,11 +62,6 @@ public: process(Input const& input, Context const& ctx) const; private: - // dir_root and owner can not be both empty or filled at the same time - // This function will return an error if this is the case - static std::expected - composeKeyFromDirectory(boost::json::object const& directory) noexcept; - /** * @brief Convert the Output to a JSON object * @@ -579,15 +70,6 @@ private: */ 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, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/LedgerIndex.cpp b/src/rpc/handlers/LedgerIndex.cpp index 350f96b09..917cde0f7 100644 --- a/src/rpc/handlers/LedgerIndex.cpp +++ b/src/rpc/handlers/LedgerIndex.cpp @@ -41,13 +41,7 @@ LedgerIndexHandler::process(LedgerIndexHandler::Input const& input, Context cons if (!input.date) return fillOutputByIndex(maxIndex); - auto const convertISOTimeStrToTicks = [](std::string const& isoTimeStr) { - auto const systemTime = util::systemTpFromUtcStr(isoTimeStr, kDateFormat); - // systemTime must be valid after validation passed - return systemTime->time_since_epoch().count(); - }; - - auto const ticks = convertISOTimeStrToTicks(*input.date); + auto const ticks = input.date->time_since_epoch().count(); auto const earlierThan = [&](std::uint32_t ledgerIndex) { auto const header = sharedPtrBackend_->fetchLedgerBySequence(ledgerIndex, ctx.yield); @@ -75,17 +69,6 @@ LedgerIndexHandler::process(LedgerIndexHandler::Input const& input, Context cons return fillOutputByIndex(maxIndex); } -LedgerIndexHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = LedgerIndexHandler::Input{}; - - if (jv.as_object().contains(JS(date))) - input.date = jv.at(JS(date)).as_string(); - - return input; -} - void tag_invoke( boost::json::value_from_tag, diff --git a/src/rpc/handlers/LedgerIndex.hpp b/src/rpc/handlers/LedgerIndex.hpp index 682fadc0e..bde2ec687 100644 --- a/src/rpc/handlers/LedgerIndex.hpp +++ b/src/rpc/handlers/LedgerIndex.hpp @@ -1,19 +1,17 @@ #pragma once #include "data/BackendInterface.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include #include -#include +#include +#include #include #include -#include #include +#include namespace rpc { @@ -21,9 +19,8 @@ namespace rpc { * @brief The ledger_index method fetches the latest closed ledger before the given date. * */ -class LedgerIndexHandler { +class LedgerIndexHandler : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; - static constexpr auto kDateFormat = "%Y-%m-%dT%TZ"; public: /** @@ -35,13 +32,6 @@ public: std::string closeTimeIso; }; - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::optional date; - }; - using Result = HandlerReturnType; /** @@ -54,23 +44,6 @@ public: { } - /** - * @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 kRpcSpec = RpcSpec{ - {JS(date), - validation::Type{}, - validation::TimeFormatValidator{kDateFormat}}, - }; - return kRpcSpec; - } - /** * @brief Process the LedgerIndex command * @@ -90,15 +63,6 @@ private: */ 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, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/MPTHolders.cpp b/src/rpc/handlers/MPTHolders.cpp index e84181ccd..90d49f1ff 100644 --- a/src/rpc/handlers/MPTHolders.cpp +++ b/src/rpc/handlers/MPTHolders.cpp @@ -1,10 +1,10 @@ #include "rpc/handlers/MPTHolders.hpp" +#include "data/Types.hpp" #include "rpc/JS.hpp" #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include @@ -22,25 +22,96 @@ #include #include -#include -#include -#include +#include +#include using namespace xrpl; namespace rpc { +namespace { + +/** + * @brief Serialize a single MPToken ledger object blob into the mpt_holders JSON. + * + * @param mptID The MPTokenIssuance ID the holder belongs to. + * @param mpt The serialized MPToken ledger object. + * @return The holder entry as a JSON object. + */ +boost::json::object +mpTokenToJson(xrpl::uint192 const& mptID, data::Blob const& mpt) +{ + xrpl::STLedgerEntry const sle{xrpl::SerialIter{mpt.data(), mpt.size()}, xrpl::uint256{}}; + auto const mptokenKey = keylet::mptoken(mptID, sle[xrpl::sfAccount]).key; + boost::json::object mptJson; + + mptJson[JS(account)] = toBase58(sle[xrpl::sfAccount]); + mptJson[JS(flags)] = sle.getFlags(); + mptJson[JS(mpt_amount)] = toBoostJson( + xrpl::STUInt64{xrpl::sfMPTAmount, sle[xrpl::sfMPTAmount]}.getJson(JsonOptions::Values::None) + ); + mptJson[JS(mptoken_index)] = xrpl::to_string(mptokenKey); + + if (sle.isFieldPresent(xrpl::sfLockedAmount)) { + mptJson["locked_amount"] = toBoostJson( + xrpl::STUInt64{xrpl::sfLockedAmount, sle[xrpl::sfLockedAmount]}.getJson( + JsonOptions::Values::None + ) + ); + } + + if (sle.isFieldPresent(xrpl::sfConfidentialBalanceInbox)) { + mptJson[JS(confidential_balance_inbox)] = + xrpl::strHex(sle.getFieldVL(xrpl::sfConfidentialBalanceInbox)); + } + + if (sle.isFieldPresent(xrpl::sfConfidentialBalanceSpending)) { + mptJson[JS(confidential_balance_spending)] = + xrpl::strHex(sle.getFieldVL(xrpl::sfConfidentialBalanceSpending)); + } + + if (sle.isFieldPresent(xrpl::sfConfidentialBalanceVersion)) + mptJson[JS(confidential_balance_version)] = sle[xrpl::sfConfidentialBalanceVersion]; + + if (sle.isFieldPresent(xrpl::sfIssuerEncryptedBalance)) { + mptJson[JS(issuer_encrypted_balance)] = + xrpl::strHex(sle.getFieldVL(xrpl::sfIssuerEncryptedBalance)); + } + + if (sle.isFieldPresent(xrpl::sfAuditorEncryptedBalance)) { + mptJson[JS(auditor_encrypted_balance)] = + xrpl::strHex(sle.getFieldVL(xrpl::sfAuditorEncryptedBalance)); + } + + if (sle.isFieldPresent(xrpl::sfHolderEncryptionKey)) { + mptJson[JS(holder_encryption_key)] = + xrpl::strHex(sle.getFieldVL(xrpl::sfHolderEncryptionKey)); + } + + return mptJson; +} + +} // namespace + MPTHoldersHandler::Result MPTHoldersHandler::process(MPTHoldersHandler::Input const& input, Context const& ctx) const { + // Account-list filter is a bounded, unpaginated lookup by key: marker/limit does not make sense + // alongside it. + if (input.accounts) { + if (input.marker.has_value()) + return Error{Status{RippledError::RpcInvalidParams, "accountsWithMarker"}}; + if (input.limit.has_value()) + return Error{Status{RippledError::RpcInvalidParams, "accountsWithLimit"}}; + } + auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "MPTHolder's ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); if (not expectedLgrInfo.has_value()) @@ -48,7 +119,7 @@ MPTHoldersHandler::process(MPTHoldersHandler::Input const& input, Context const& auto const& lgrInfo = *expectedLgrInfo; auto const limit = input.limit.value_or(MPTHoldersHandler::kLimitDefault); - auto const mptID = xrpl::uint192{input.mptID.c_str()}; + auto const& mptID = input.mptID; auto const issuanceLedgerObject = sharedPtrBackend_->fetchLedgerObject( xrpl::keylet::mptokenIssuance(mptID).key, lgrInfo.seq, ctx.yield @@ -56,73 +127,38 @@ MPTHoldersHandler::process(MPTHoldersHandler::Input const& input, Context const& if (!issuanceLedgerObject) return Error{Status{RippledError::RpcObjectNotFound, "objectNotFound"}}; - std::optional cursor; - if (input.marker) - cursor = xrpl::AccountID{input.marker->c_str()}; - - auto const dbResponse = - sharedPtrBackend_->fetchMPTHolders(mptID, limit, cursor, lgrInfo.seq, ctx.yield); auto output = MPTHoldersHandler::Output{}; output.mptID = to_string(mptID); - output.limit = limit; output.ledgerIndex = lgrInfo.seq; - boost::json::array const mpts; - for (auto const& mpt : dbResponse.mptokens) { - xrpl::STLedgerEntry const sle{ - xrpl::SerialIter{mpt.data(), mpt.size()}, keylet::mptokenIssuance(mptID).key - }; - boost::json::object mptJson; - - mptJson[JS(account)] = toBase58(sle[xrpl::sfAccount]); - mptJson[JS(flags)] = sle.getFlags(); - mptJson[JS(mpt_amount)] = toBoostJson( - xrpl::STUInt64{xrpl::sfMPTAmount, sle[xrpl::sfMPTAmount]}.getJson( - JsonOptions::Values::None - ) - ); - mptJson[JS(mptoken_index)] = - xrpl::to_string(xrpl::keylet::mptoken(mptID, sle[xrpl::sfAccount]).key); - - if (sle.isFieldPresent(xrpl::sfLockedAmount)) { - mptJson["locked_amount"] = toBoostJson( - xrpl::STUInt64{xrpl::sfLockedAmount, sle[xrpl::sfLockedAmount]}.getJson( - JsonOptions::Values::None - ) - ); + // Account-list filter: bounded lookup by key. Duplicates are dropped in first-seen + // order, non-holders are omitted, and no paging marker is produced. + if (input.accounts) { + std::vector keys; + keys.reserve(input.accounts->size()); + for (auto const& accountID : *input.accounts) { + auto const key = xrpl::keylet::mptoken(mptID, accountID).key; + if (not std::ranges::contains(keys, key)) + keys.push_back(key); } - if (sle.isFieldPresent(xrpl::sfConfidentialBalanceInbox)) { - mptJson[JS(confidential_balance_inbox)] = - xrpl::strHex(sle.getFieldVL(xrpl::sfConfidentialBalanceInbox)); + auto const mptObjects = sharedPtrBackend_->fetchLedgerObjects(keys, lgrInfo.seq, ctx.yield); + for (auto const& mpt : mptObjects) { + if (not mpt.empty()) + output.mpts.push_back(mpTokenToJson(mptID, mpt)); } - if (sle.isFieldPresent(xrpl::sfConfidentialBalanceSpending)) { - mptJson[JS(confidential_balance_spending)] = - xrpl::strHex(sle.getFieldVL(xrpl::sfConfidentialBalanceSpending)); - } - - if (sle.isFieldPresent(xrpl::sfConfidentialBalanceVersion)) - mptJson[JS(confidential_balance_version)] = sle[xrpl::sfConfidentialBalanceVersion]; - - if (sle.isFieldPresent(xrpl::sfIssuerEncryptedBalance)) { - mptJson[JS(issuer_encrypted_balance)] = - xrpl::strHex(sle.getFieldVL(xrpl::sfIssuerEncryptedBalance)); - } - - if (sle.isFieldPresent(xrpl::sfAuditorEncryptedBalance)) { - mptJson[JS(auditor_encrypted_balance)] = - xrpl::strHex(sle.getFieldVL(xrpl::sfAuditorEncryptedBalance)); - } - - if (sle.isFieldPresent(xrpl::sfHolderEncryptionKey)) { - mptJson[JS(holder_encryption_key)] = - xrpl::strHex(sle.getFieldVL(xrpl::sfHolderEncryptionKey)); - } - - output.mpts.push_back(mptJson); + return output; } + output.limit = limit; + + auto const dbResponse = + sharedPtrBackend_->fetchMPTHolders(mptID, limit, input.marker, lgrInfo.seq, ctx.yield); + + for (auto const& mpt : dbResponse.mptokens) + output.mpts.push_back(mpTokenToJson(mptID, mpt)); + if (dbResponse.cursor.has_value()) output.marker = strHex(*dbResponse.cursor); @@ -138,39 +174,16 @@ tag_invoke( { jv = { {JS(mpt_issuance_id), output.mptID}, - {JS(limit), output.limit}, {JS(ledger_index), output.ledgerIndex}, {"mptokens", output.mpts}, {JS(validated), output.validated}, }; + if (output.limit.has_value()) + jv.as_object()[JS(limit)] = *(output.limit); + if (output.marker.has_value()) jv.as_object()[JS(marker)] = *(output.marker); } -MPTHoldersHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto const& jsonObject = jv.as_object(); - MPTHoldersHandler::Input input; - - input.mptID = jsonObject.at(JS(mpt_issuance_id)).as_string().c_str(); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = jsonObject.at(JS(ledger_hash)).as_string().c_str(); - - 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(jsonObject.at(JS(limit))); - - if (jsonObject.contains(JS(marker))) - input.marker = jsonObject.at(JS(marker)).as_string().c_str(); - - return input; -} } // namespace rpc diff --git a/src/rpc/handlers/MPTHolders.hpp b/src/rpc/handlers/MPTHolders.hpp index daed106ed..a899f7110 100644 --- a/src/rpc/handlers/MPTHolders.hpp +++ b/src/rpc/handlers/MPTHolders.hpp @@ -1,35 +1,39 @@ #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 #include #include -#include +#include +#include #include #include #include #include +#include namespace rpc { /** - * @brief The mpt_holders command asks the Clio server for all holders of a particular + * @brief The mpt_holders command asks the Clio server for holders of a particular * MPTokenIssuance. + * + * When `accounts` is provided, those accounts are looked up directly by + * `keylet::mptoken` instead of scanning the holder index. Duplicate accounts are + * collapsed in first-seen order and non-holders are omitted. This filtered mode is + * not paginated, so `marker` and `limit` are rejected. */ -class MPTHoldersHandler { +class MPTHoldersHandler : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; public: - static constexpr auto kLimitMin = 1; - static constexpr auto kLimitMax = 100; - static constexpr auto kLimitDefault = 50; + static constexpr auto kLimitMin = rpc::spec::handlers::mpt_holders::kLimitMin; + static constexpr auto kLimitMax = rpc::spec::handlers::mpt_holders::kLimitMax; + static constexpr auto kLimitDefault = rpc::spec::handlers::mpt_holders::kLimitDefault; + static constexpr auto kMaxAccounts = rpc::spec::handlers::mpt_holders::kMaxAccounts; /** * @brief A struct to hold the output data of the command @@ -39,19 +43,8 @@ public: uint32_t ledgerIndex; std::string mptID; bool validated = true; - uint32_t limit; - std::optional marker; - }; - - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::string mptID; - std::optional ledgerHash; - std::optional ledgerIndex; - std::optional marker; std::optional limit; + std::optional marker; }; using Result = HandlerReturnType; @@ -66,31 +59,6 @@ public: { } - /** - * @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 kRpcSpec = RpcSpec{ - {JS(mpt_issuance_id), - validation::Required{}, - validation::CustomValidators::uint192HexStringValidator}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(limit), - validation::Type{}, - validation::Min(1u), - modifiers::Clamp{kLimitMin, kLimitMax}}, - {JS(marker), validation::CustomValidators::uint160HexStringValidator}, - }; - - return kRpcSpec; - } - /** * @brief Process the MPTHolders command * @@ -110,14 +78,5 @@ private: */ 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, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/MPTokenIssuanceHistory.cpp b/src/rpc/handlers/MPTokenIssuanceHistory.cpp index b03d6b78c..5b0d4196a 100644 --- a/src/rpc/handlers/MPTokenIssuanceHistory.cpp +++ b/src/rpc/handlers/MPTokenIssuanceHistory.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -56,7 +57,7 @@ MPTokenIssuanceHistoryHandler::process( if (not range.has_value()) return Error{range.error()}; - auto const mptIssuanceID = xrpl::uint192{input.mptIssuanceID.c_str()}; + auto const& mptIssuanceID = input.mptIssuanceId; auto const [page, timeDiff] = util::timed([&] { return fetchTransactions(input, ctx, mptIssuanceID, *range); }); @@ -120,13 +121,13 @@ MPTokenIssuanceHistoryHandler::resolveSequenceRange(Input const& input, Context if (resolved.min > resolved.max) return Error{Status{RippledError::RpcLgrIdxsInvalid}}; - if (input.ledgerHash.has_value() || input.ledgerIndex.has_value()) { + if (not input.ledger.isUnspecified()) { // rippled does not have this check if (input.ledgerIndexMax.has_value() || input.ledgerIndexMin.has_value()) return Error{Status{RippledError::RpcInvalidParams, "containsLedgerSpecifierAndRange"}}; - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( - *sharedPtrBackend_, ctx.yield, input.ledgerHash, input.ledgerIndex, dbMaxSeq + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( + *sharedPtrBackend_, ctx.yield, input.ledger, dbMaxSeq ); if (not expectedLgrInfo.has_value()) @@ -163,13 +164,8 @@ MPTokenIssuanceHistoryHandler::fetchTransactions( // tx_type is applied post-fetch, as account_tx does. if (input.account.has_value()) { - auto const account = accountFromStringStrict(*input.account); - if (not account.has_value()) { - ASSERT(false, "Account must be decodable after spec validation"); - std::unreachable(); - } return sharedPtrBackend_->fetchAccountMPTokenIssuanceTransactions( - mptIssuanceID, *account, limit, input.forward, startCursor, ctx.yield + mptIssuanceID, *input.account, limit, input.forward, startCursor, ctx.yield ); } @@ -199,7 +195,7 @@ MPTokenIssuanceHistoryHandler::processTransactionsPage( if (txnPlusMeta.transaction.empty() || txnPlusMeta.metadata.empty()) { LOG(log_.warn()) << "Skipping index entry with no matching transaction record; " "mpt_issuance_id = " - << input.mptIssuanceID; + << xrpl::to_string(input.mptIssuanceId); continue; } @@ -304,12 +300,12 @@ tag_invoke( jv.as_object()[JS(limit)] = *(output.limit); } +} // namespace rpc + +namespace rpc::spec::handlers::mptoken_issuance_history { + void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - MPTokenIssuanceHistoryHandler::Marker const& marker -) +tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Marker const& marker) { jv = { {JS(ledger), marker.ledger}, @@ -317,62 +313,4 @@ tag_invoke( }; } -MPTokenIssuanceHistoryHandler::Input -tag_invoke( - boost::json::value_to_tag, - boost::json::value const& jv -) -{ - auto const& jsonObject = jv.as_object(); - auto input = MPTokenIssuanceHistoryHandler::Input{}; - - input.mptIssuanceID = boost::json::value_to(jsonObject.at(JS(mpt_issuance_id))); - - if (jsonObject.contains(JS(account))) - input.account = boost::json::value_to(jsonObject.at(JS(account))); - - if (jsonObject.contains("tx_type")) { - input.transactionTypeInLowercase = - boost::json::value_to(jsonObject.at("tx_type")); - } - - if (jsonObject.contains(JS(ledger_index_min)) && - util::integralValueAs(jsonObject.at(JS(ledger_index_min))) != -1) - input.ledgerIndexMin = util::integralValueAs(jsonObject.at(JS(ledger_index_min))); - - if (jsonObject.contains(JS(ledger_index_max)) && - util::integralValueAs(jsonObject.at(JS(ledger_index_max))) != -1) - input.ledgerIndexMax = util::integralValueAs(jsonObject.at(JS(ledger_index_max))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(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(binary))) - input.binary = jsonObject.at(JS(binary)).as_bool(); - - if (jsonObject.contains(JS(forward))) - input.forward = jsonObject.at(JS(forward)).as_bool(); - - if (jsonObject.contains(JS(limit))) - input.limit = util::integralValueAs(jsonObject.at(JS(limit))); - - if (jsonObject.contains(JS(marker))) { - input.marker = MPTokenIssuanceHistoryHandler::Marker{ - .ledger = util::integralValueAs( - jsonObject.at(JS(marker)).as_object().at(JS(ledger)) - ), - .seq = - util::integralValueAs(jsonObject.at(JS(marker)).as_object().at(JS(seq))) - }; - } - - return input; -} - -} // namespace rpc +} // namespace rpc::spec::handlers::mptoken_issuance_history diff --git a/src/rpc/handlers/MPTokenIssuanceHistory.hpp b/src/rpc/handlers/MPTokenIssuanceHistory.hpp index 3d0732bed..ae33144f8 100644 --- a/src/rpc/handlers/MPTokenIssuanceHistory.hpp +++ b/src/rpc/handlers/MPTokenIssuanceHistory.hpp @@ -2,23 +2,16 @@ #include "data/BackendInterface.hpp" #include "data/Types.hpp" -#include "rpc/Errors.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/MetaProcessors.hpp" -#include "rpc/common/Modifiers.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include "util/log/Logger.hpp" #include #include #include #include -#include +#include +#include #include -#include -#include #include #include @@ -37,7 +30,8 @@ namespace rpc { * @note This is a Clio-only method. Requests fail with `notReady` until the issuance-history * backfill reports `Migrated`, so partial history is never served. */ -class MPTokenIssuanceHistoryHandler { +class MPTokenIssuanceHistoryHandler + : public rpc::spec::HandlerFor { util::Logger log_{"RPC"}; std::shared_ptr sharedPtrBackend_; @@ -49,9 +43,10 @@ class MPTokenIssuanceHistoryHandler { std::shared_ptr migrated_ = std::make_shared(false); public: - static constexpr auto kLimitMin = 1; - static constexpr auto kLimitMax = 100; - static constexpr auto kLimitDefault = 50; + static constexpr auto kLimitMin = rpc::spec::handlers::mptoken_issuance_history::kLimitMin; + static constexpr auto kLimitMax = rpc::spec::handlers::mptoken_issuance_history::kLimitMax; + static constexpr auto kLimitDefault = + rpc::spec::handlers::mptoken_issuance_history::kLimitDefault; /** * @brief The name used to query the issuance-history migrator's status. @@ -63,10 +58,7 @@ public: /** * @brief A struct to hold the marker data. */ - struct Marker { - uint32_t ledger; - uint32_t seq; - }; + using Marker = rpc::spec::handlers::mptoken_issuance_history::Marker; /** * @brief A struct to hold the output data of the command. @@ -83,26 +75,6 @@ public: bool validated = true; }; - /** - * @brief A struct to hold the input data for the command. - * - * @note When no ledger selector is provided, the request uses the backend's full available - * ledger range. - */ - struct Input { - std::string mptIssuanceID; - std::optional account; - std::optional transactionTypeInLowercase; - std::optional ledgerHash; - std::optional ledgerIndex; - std::optional ledgerIndexMin; - std::optional ledgerIndexMax; - bool binary = false; - bool forward = false; - std::optional limit; - std::optional marker; - }; - using Result = HandlerReturnType; /** @@ -115,55 +87,6 @@ public: { } - /** - * @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) - { - // TODO: goes away when mptoken_issuance_history moves to the shared spec, where the - // validator calls this internally. - auto const& typesKeysInLowercase = rpc::spec::txTypesInLowercase(); - static auto const kRpcSpec = RpcSpec{ - {JS(mpt_issuance_id), - validation::Required{}, - validation::CustomValidators::uint192HexStringValidator}, - {JS(account), validation::CustomValidators::accountValidator}, - { - "tx_type", - validation::Type{}, - modifiers::ToLower{}, - validation::OneOf( - typesKeysInLowercase.cbegin(), typesKeysInLowercase.cend() - ), - }, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(ledger_index_min), validation::Type{}}, - {JS(ledger_index_max), validation::Type{}}, - {JS(binary), validation::Type{}}, - {JS(forward), validation::Type{}}, - {JS(limit), - validation::Type{}, - validation::Min(1u), - modifiers::Clamp{kLimitMin, kLimitMax}}, - {JS(marker), - meta::WithCustomError{ - validation::Type{}, - Status{RippledError::RpcInvalidParams, "invalidMarker"} - }, - meta::Section{ - {JS(ledger), validation::Required{}, validation::Type{}}, - {JS(seq), validation::Required{}, validation::Type{}}, - }}, - }; - - return kRpcSpec; - } - /** * @brief Process the MPTokenIssuanceHistory command. * @@ -295,24 +218,13 @@ private: */ 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, boost::json::value const& jv); - - /** - * @brief Convert the Marker to a JSON object. - * - * @param [out] jv The JSON object to convert to. - * @param marker The marker to convert. - */ - friend void - tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Marker const& marker); }; } // namespace rpc + +namespace rpc::spec::handlers::mptoken_issuance_history { + +void +tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Marker const& marker); + +} // namespace rpc::spec::handlers::mptoken_issuance_history diff --git a/src/rpc/handlers/NFTBuyOffers.cpp b/src/rpc/handlers/NFTBuyOffers.cpp index 481c26da0..2918d6cf1 100644 --- a/src/rpc/handlers/NFTBuyOffers.cpp +++ b/src/rpc/handlers/NFTBuyOffers.cpp @@ -2,7 +2,6 @@ #include "rpc/common/Types.hpp" -#include #include using namespace xrpl; @@ -12,7 +11,7 @@ namespace rpc { NFTBuyOffersHandler::Result NFTBuyOffersHandler::process(NFTBuyOffersHandler::Input const& input, Context const& ctx) const { - auto const tokenID = uint256{input.nftID.c_str()}; + auto const& tokenID = input.nftID; auto const directory = keylet::nftBuys(tokenID); return iterateOfferDirectory(input, tokenID, directory, ctx.yield); diff --git a/src/rpc/handlers/NFTHistory.cpp b/src/rpc/handlers/NFTHistory.cpp index f2229f721..22edb5c4d 100644 --- a/src/rpc/handlers/NFTHistory.cpp +++ b/src/rpc/handlers/NFTHistory.cpp @@ -5,7 +5,6 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include "util/Profiler.hpp" #include "util/log/Logger.hpp" @@ -13,8 +12,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -45,7 +44,7 @@ NFTHistoryHandler::process(NFTHistoryHandler::Input const& input, Context const& return Error{Status{RippledError::RpcLgrIdxMalformed, "ledgerSeqMinOutOfRange"}}; // NOLINTEND(bugprone-unchecked-optional-access) - minIndex = *input.ledgerIndexMin; + minIndex = static_cast(*input.ledgerIndexMin); } if (input.ledgerIndexMax) { @@ -54,23 +53,22 @@ NFTHistoryHandler::process(NFTHistoryHandler::Input const& input, Context const& return Error{Status{RippledError::RpcLgrIdxMalformed, "ledgerSeqMaxOutOfRange"}}; // NOLINTEND(bugprone-unchecked-optional-access) - maxIndex = *input.ledgerIndexMax; + maxIndex = static_cast(*input.ledgerIndexMax); } if (minIndex > maxIndex) return Error{Status{RippledError::RpcLgrIdxsInvalid}}; - if (input.ledgerHash || input.ledgerIndex) { + if (not input.ledger.isUnspecified()) { // rippled does not have this check if (input.ledgerIndexMax || input.ledgerIndexMin) { return Error{Status{RippledError::RpcInvalidParams, "containsLedgerSpecifierAndRange"}}; } - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -94,7 +92,7 @@ NFTHistoryHandler::process(NFTHistoryHandler::Input const& input, Context const& } auto const limit = input.limit.value_or(kLimitDefault); - auto const tokenID = xrpl::uint256{input.nftID.c_str()}; + auto const& tokenID = input.nftID; auto const [txnsAndCursor, timeDiff] = util::timed([&]() { return sharedPtrBackend_->fetchNFTTransactions( @@ -185,12 +183,12 @@ tag_invoke( jv.as_object()[JS(limit)] = *(output.limit); } +} // namespace rpc + +namespace rpc::spec::handlers::nft_history { + void -tag_invoke( - boost::json::value_from_tag, - boost::json::value& jv, - NFTHistoryHandler::Marker const& marker -) +tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Marker const& marker) { jv = { {JS(ledger), marker.ledger}, @@ -198,51 +196,4 @@ tag_invoke( }; } -NFTHistoryHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto const& jsonObject = jv.as_object(); - auto input = NFTHistoryHandler::Input{}; - - input.nftID = boost::json::value_to(jsonObject.at(JS(nft_id))); - - if (jsonObject.contains(JS(ledger_index_min)) && - util::integralValueAs(jsonObject.at(JS(ledger_index_min))) != -1) - input.ledgerIndexMin = util::integralValueAs(jsonObject.at(JS(ledger_index_min))); - - if (jsonObject.contains(JS(ledger_index_max)) && - util::integralValueAs(jsonObject.at(JS(ledger_index_max))) != -1) - input.ledgerIndexMax = util::integralValueAs(jsonObject.at(JS(ledger_index_max))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(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(binary))) - input.binary = jsonObject.at(JS(binary)).as_bool(); - - if (jsonObject.contains(JS(forward))) - input.forward = jsonObject.at(JS(forward)).as_bool(); - - if (jsonObject.contains(JS(limit))) - input.limit = util::integralValueAs(jsonObject.at(JS(limit))); - - if (jsonObject.contains(JS(marker))) { - input.marker = NFTHistoryHandler::Marker{ - .ledger = util::integralValueAs( - jsonObject.at(JS(marker)).as_object().at(JS(ledger)) - ), - .seq = - util::integralValueAs(jsonObject.at(JS(marker)).as_object().at(JS(seq))) - }; - } - - return input; -} - -} // namespace rpc +} // namespace rpc::spec::handlers::nft_history diff --git a/src/rpc/handlers/NFTHistory.hpp b/src/rpc/handlers/NFTHistory.hpp index a4eba808d..c0cd5a106 100644 --- a/src/rpc/handlers/NFTHistory.hpp +++ b/src/rpc/handlers/NFTHistory.hpp @@ -2,25 +2,22 @@ #include "data/BackendInterface.hpp" #include "rpc/Errors.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/MetaProcessors.hpp" -#include "rpc/common/Modifiers.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include "util/log/Logger.hpp" #include #include #include #include -#include -#include +#include +#include #include +#include #include #include #include +#include namespace rpc { @@ -30,23 +27,19 @@ namespace rpc { * * For more details see: https://xrpl.org/nft_history.html#nft_history */ -class NFTHistoryHandler { +class NFTHistoryHandler : public rpc::spec::HandlerFor { util::Logger log_{"RPC"}; std::shared_ptr sharedPtrBackend_; public: - static constexpr auto kLimitMin = 1; - static constexpr auto kLimitMax = 100; - static constexpr auto kLimitDefault = 50; + static constexpr auto kLimitMin = rpc::spec::handlers::nft_history::kLimitMin; + static constexpr auto kLimitMax = rpc::spec::handlers::nft_history::kLimitMax; + static constexpr auto kLimitDefault = rpc::spec::handlers::nft_history::kLimitDefault; /** * @brief A struct to hold the marker data */ - // TODO: this marker is same as account_tx, reuse in future - struct Marker { - uint32_t ledger; - uint32_t seq; - }; + using Marker = rpc::spec::handlers::nft_history::Marker; /** * @brief A struct to hold the output data of the command @@ -63,23 +56,6 @@ public: bool validated = true; }; - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::string nftID; - // You must use at least one of the following fields in your request: - // ledger_index, ledger_hash, ledger_index_min, or ledger_index_max. - std::optional ledgerHash; - std::optional ledgerIndex; - std::optional ledgerIndexMin; - std::optional ledgerIndexMax; - bool binary = false; - bool forward = false; - std::optional limit; - std::optional marker; - }; - using Result = HandlerReturnType; /** @@ -92,43 +68,6 @@ public: { } - /** - * @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 kRpcSpec = RpcSpec{ - {JS(nft_id), - validation::Required{}, - validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(ledger_index_min), validation::Type{}}, - {JS(ledger_index_max), validation::Type{}}, - {JS(binary), validation::Type{}}, - {JS(forward), validation::Type{}}, - {JS(limit), - validation::Type{}, - validation::Min(1u), - modifiers::Clamp{kLimitMin, kLimitMax}}, - {JS(marker), - meta::WithCustomError{ - validation::Type{}, - Status{RippledError::RpcInvalidParams, "invalidMarker"} - }, - meta::Section{ - {JS(ledger), validation::Required{}, validation::Type{}}, - {JS(seq), validation::Required{}, validation::Type{}}, - }}, - }; - - return kRpcSpec; - } - /** * @brief Process the NFTHistory command * @@ -148,24 +87,13 @@ private: */ 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, boost::json::value const& jv); - - /** - * @brief Convert the Marker to a JSON object - * - * @param [out] jv The JSON object to convert to - * @param marker The marker to convert - */ - friend void - tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Marker const& marker); }; } // namespace rpc + +namespace rpc::spec::handlers::nft_history { + +void +tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Marker const& marker); + +} // namespace rpc::spec::handlers::nft_history diff --git a/src/rpc/handlers/NFTInfo.cpp b/src/rpc/handlers/NFTInfo.cpp index 6506d78ce..cc416cdea 100644 --- a/src/rpc/handlers/NFTInfo.cpp +++ b/src/rpc/handlers/NFTInfo.cpp @@ -4,14 +4,11 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include #include -#include #include -#include #include #include #include @@ -27,15 +24,14 @@ namespace rpc { NFTInfoHandler::Result NFTInfoHandler::process(NFTInfoHandler::Input const& input, Context const& ctx) const { - auto const tokenID = xrpl::uint256{input.nftID.c_str()}; + auto const& tokenID = input.nftID; auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "NFTInfo's ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -93,24 +89,4 @@ tag_invoke( }; } -NFTInfoHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto const& jsonObject = jv.as_object(); - auto input = NFTInfoHandler::Input{}; - - input.nftID = boost::json::value_to(jsonObject.at(JS(nft_id))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(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; - } - - return input; -} - } // namespace rpc diff --git a/src/rpc/handlers/NFTInfo.hpp b/src/rpc/handlers/NFTInfo.hpp index 56ddd95bf..02fffc86c 100644 --- a/src/rpc/handlers/NFTInfo.hpp +++ b/src/rpc/handlers/NFTInfo.hpp @@ -1,19 +1,17 @@ #pragma once #include "data/BackendInterface.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include #include -#include +#include +#include #include #include -#include #include +#include namespace rpc { @@ -22,7 +20,7 @@ namespace rpc { * * For more details see: https://xrpl.org/nft_info.html */ -class NFTInfoHandler { +class NFTInfoHandler : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; public: @@ -46,15 +44,6 @@ public: bool validated = true; }; - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::string nftID; - std::optional ledgerHash; - std::optional ledgerIndex; - }; - using Result = HandlerReturnType; /** @@ -67,26 +56,6 @@ public: { } - /** - * @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 kRpcSpec = RpcSpec{ - {JS(nft_id), - validation::Required{}, - validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - }; - - return kRpcSpec; - } - /** * @brief Process the NFTInfo command * @@ -106,15 +75,6 @@ private: */ 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, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/NFTOffersCommon.cpp b/src/rpc/handlers/NFTOffersCommon.cpp index b3797b14e..40a8f4788 100644 --- a/src/rpc/handlers/NFTOffersCommon.cpp +++ b/src/rpc/handlers/NFTOffersCommon.cpp @@ -4,15 +4,14 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include #include #include -#include #include #include +#include #include #include #include @@ -74,11 +73,10 @@ NFTOffersHandlerBase::iterateOfferDirectory( auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "NFTOffersCommon's ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -91,14 +89,15 @@ NFTOffersHandlerBase::iterateOfferDirectory( if (not sharedPtrBackend_->fetchLedgerObject(directory.key, lgrInfo.seq, yield)) return Error{Status{RippledError::RpcObjectNotFound, "notFound"}}; - auto output = Output{.nftID = input.nftID, .offers = {}, .limit = {}, .marker = {}}; + auto output = + Output{.nftID = xrpl::strHex(input.nftID), .offers = {}, .limit = {}, .marker = {}}; auto offers = std::vector{}; auto reserve = input.limit; auto cursor = uint256{}; auto startHint = uint64_t{0ul}; if (input.marker) { - cursor = uint256(input.marker->c_str()); + cursor = *input.marker; // We have a start point. Use limit - 1 from the result and use the very last one for the // resume. @@ -181,30 +180,4 @@ tag_invoke( jv = std::move(object); } -NFTOffersHandlerBase::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = NFTOffersHandlerBase::Input{}; - auto const& jsonObject = jv.as_object(); - - input.nftID = boost::json::value_to(jsonObject.at(JS(nft_id))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(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(marker))) - input.marker = boost::json::value_to(jsonObject.at(JS(marker))); - - if (jsonObject.contains(JS(limit))) - input.limit = util::integralValueAs(jsonObject.at(JS(limit))); - - return input; -} - } // namespace rpc diff --git a/src/rpc/handlers/NFTOffersCommon.hpp b/src/rpc/handlers/NFTOffersCommon.hpp index d933febbc..69f749e11 100644 --- a/src/rpc/handlers/NFTOffersCommon.hpp +++ b/src/rpc/handlers/NFTOffersCommon.hpp @@ -1,24 +1,22 @@ #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 #include #include +#include +#include #include #include #include -#include #include #include #include #include +#include #include namespace rpc { @@ -26,13 +24,14 @@ namespace rpc { /** * @brief Contains common functionality for handling the `nft_offers` command */ -class NFTOffersHandlerBase { +class NFTOffersHandlerBase + : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; public: - static constexpr auto kLimitMin = 50; - static constexpr auto kLimitMax = 500; - static constexpr auto kLimitDefault = 250; + static constexpr auto kLimitMin = rpc::spec::handlers::nft_offers_common::kLimitMin; + static constexpr auto kLimitMax = rpc::spec::handlers::nft_offers_common::kLimitMax; + static constexpr auto kLimitDefault = rpc::spec::handlers::nft_offers_common::kLimitDefault; /** * @brief A struct to hold the output data of the command @@ -47,17 +46,6 @@ public: std::optional marker; }; - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::string nftID; - std::optional ledgerHash; - std::optional ledgerIndex; - uint32_t limit = kLimitDefault; - std::optional marker; - }; - using Result = HandlerReturnType; /** @@ -70,31 +58,6 @@ public: { } - /** - * @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 kRpcSpec = RpcSpec{ - {JS(nft_id), - validation::Required{}, - validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(limit), - validation::Type{}, - validation::Min(1u), - modifiers::Clamp{kLimitMin, kLimitMax}}, - {JS(marker), validation::CustomValidators::uint256HexStringValidator}, - }; - - return kRpcSpec; - } - protected: /** * @brief Iterate the NFT offer directory @@ -122,15 +85,6 @@ private: */ 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, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/NFTSellOffers.cpp b/src/rpc/handlers/NFTSellOffers.cpp index 0d46330e8..d51dc64ed 100644 --- a/src/rpc/handlers/NFTSellOffers.cpp +++ b/src/rpc/handlers/NFTSellOffers.cpp @@ -2,7 +2,6 @@ #include "rpc/common/Types.hpp" -#include #include using namespace xrpl; @@ -12,7 +11,7 @@ namespace rpc { NFTSellOffersHandler::Result NFTSellOffersHandler::process(NFTSellOffersHandler::Input const& input, Context const& ctx) const { - auto const tokenID = uint256{input.nftID.c_str()}; + auto const& tokenID = input.nftID; auto const directory = keylet::nftSells(tokenID); return iterateOfferDirectory(input, tokenID, directory, ctx.yield); diff --git a/src/rpc/handlers/NFTsByIssuer.cpp b/src/rpc/handlers/NFTsByIssuer.cpp index ca5a2eed8..43f620e19 100644 --- a/src/rpc/handlers/NFTsByIssuer.cpp +++ b/src/rpc/handlers/NFTsByIssuer.cpp @@ -4,14 +4,11 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include #include -#include #include -#include #include #include #include @@ -19,7 +16,6 @@ #include #include -#include #include #include @@ -33,11 +29,10 @@ NFTsByIssuerHandler::process(NFTsByIssuerHandler::Input const& input, Context co auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "NFTsByIssuer's ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); if (not expectedLgrInfo.has_value()) @@ -45,36 +40,25 @@ NFTsByIssuerHandler::process(NFTsByIssuerHandler::Input const& input, Context co auto const& lgrInfo = *expectedLgrInfo; - auto const limit = input.limit.value_or(NFTsByIssuerHandler::kLimitDefault); + auto const limit = input.limit; - auto const issuer = accountFromStringStrict(input.issuer); + auto const& issuer = input.issuer; auto const accountLedgerObject = sharedPtrBackend_->fetchLedgerObject( - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - xrpl::keylet::account(*issuer).key, - lgrInfo.seq, - ctx.yield + xrpl::keylet::account(issuer).key, lgrInfo.seq, ctx.yield ); if (!accountLedgerObject) return Error{Status{RippledError::RpcActNotFound}}; - std::optional cursor; - if (input.marker) - cursor = uint256{input.marker->c_str()}; + auto const cursor = input.marker; auto const dbResponse = sharedPtrBackend_->fetchNFTsByIssuer( - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - *issuer, - input.nftTaxon, - lgrInfo.seq, - limit, - cursor, - ctx.yield + issuer, input.nftTaxon, lgrInfo.seq, limit, cursor, ctx.yield ); auto output = NFTsByIssuerHandler::Output{}; - output.issuer = toBase58(*issuer); // NOLINT(bugprone-unchecked-optional-access) + output.issuer = toBase58(issuer); output.limit = limit; output.ledgerIndex = lgrInfo.seq; output.nftTaxon = input.nftTaxon; @@ -125,32 +109,4 @@ tag_invoke( jv.as_object()[JS(nft_taxon)] = *(output.nftTaxon); } -NFTsByIssuerHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto const& jsonObject = jv.as_object(); - NFTsByIssuerHandler::Input input; - - input.issuer = boost::json::value_to(jsonObject.at(JS(issuer))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(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(jsonObject.at(JS(limit))); - - if (jsonObject.contains(JS(nft_taxon))) - input.nftTaxon = util::integralValueAs(jsonObject.at(JS(nft_taxon))); - - if (jsonObject.contains(JS(marker))) - input.marker = boost::json::value_to(jsonObject.at(JS(marker))); - - return input; -} } // namespace rpc diff --git a/src/rpc/handlers/NFTsByIssuer.hpp b/src/rpc/handlers/NFTsByIssuer.hpp index 6e79a6a3a..59209d72a 100644 --- a/src/rpc/handlers/NFTsByIssuer.hpp +++ b/src/rpc/handlers/NFTsByIssuer.hpp @@ -1,34 +1,33 @@ #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 #include #include -#include +#include +#include #include #include #include #include +#include namespace rpc { /** * @brief Handler for the `nfts_by_issuer` command */ -class NFTsByIssuerHandler { +class NFTsByIssuerHandler + : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; public: - static constexpr auto kLimitMin = 1; - static constexpr auto kLimitMax = 100; - static constexpr auto kLimitDefault = 50; + static constexpr auto kLimitMin = rpc::spec::handlers::nfts_by_issuer::kLimitMin; + static constexpr auto kLimitMax = rpc::spec::handlers::nfts_by_issuer::kLimitMax; + static constexpr auto kLimitDefault = rpc::spec::handlers::nfts_by_issuer::kLimitDefault; /** * @brief A struct to hold the output data of the command @@ -43,18 +42,6 @@ public: std::optional marker; }; - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::string issuer; - std::optional nftTaxon; - std::optional ledgerHash; - std::optional ledgerIndex; - std::optional marker; - std::optional limit; - }; - using Result = HandlerReturnType; /** @@ -67,30 +54,6 @@ public: { } - /** - * @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 kRpcSpec = RpcSpec{ - {JS(issuer), validation::Required{}, validation::CustomValidators::accountValidator}, - {JS(nft_taxon), validation::Type{}}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(limit), - validation::Type{}, - validation::Min(1u), - modifiers::Clamp{kLimitMin, kLimitMax}}, - {JS(marker), validation::CustomValidators::uint256HexStringValidator}, - }; - - return kRpcSpec; - } - /** * @brief Process the NFTsByIssuer command * @@ -110,14 +73,5 @@ private: */ 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, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/NoRippleCheck.cpp b/src/rpc/handlers/NoRippleCheck.cpp index 2a5ff1601..7602153d5 100644 --- a/src/rpc/handlers/NoRippleCheck.cpp +++ b/src/rpc/handlers/NoRippleCheck.cpp @@ -2,16 +2,13 @@ #include "rpc/JS.hpp" #include "rpc/RPCHelpers.hpp" -#include "rpc/common/JsonBool.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include #include #include -#include #include #include #include @@ -43,11 +40,10 @@ NoRippleCheckHandler::process(NoRippleCheckHandler::Input const& input, Context auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "NoRippleCheck's ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -55,9 +51,8 @@ NoRippleCheckHandler::process(NoRippleCheckHandler::Input const& input, Context return Error{expectedLgrInfo.error()}; auto const& lgrInfo = *expectedLgrInfo; - auto const accountID = accountFromStringStrict(input.account); - auto const keylet = - xrpl::keylet::account(*accountID).key; // NOLINT(bugprone-unchecked-optional-access) + auto const& accountID = input.account; + auto const keylet = xrpl::keylet::account(accountID).key; auto const accountObj = sharedPtrBackend_->fetchLedgerObject(keylet, lgrInfo.seq, ctx.yield); if (!accountObj) @@ -94,8 +89,7 @@ NoRippleCheckHandler::process(NoRippleCheckHandler::Input const& input, Context output.problems.emplace_back("You should immediately set your default ripple flag"); if (input.transactions) { - auto tx = - getBaseTx(*accountID, accountSeq++); // NOLINT(bugprone-unchecked-optional-access) + auto tx = getBaseTx(accountID, accountSeq++); tx[JS(TransactionType)] = "AccountSet"; tx[JS(SetFlag)] = xrpl::asfDefaultRipple; output.transactions->push_back(tx); @@ -106,7 +100,7 @@ NoRippleCheckHandler::process(NoRippleCheckHandler::Input const& input, Context traverseOwnedNodes( *sharedPtrBackend_, - *accountID, // NOLINT(bugprone-unchecked-optional-access) + accountID, lgrInfo.seq, std::numeric_limits::max(), {}, @@ -154,7 +148,7 @@ NoRippleCheckHandler::process(NoRippleCheckHandler::Input const& input, Context xrpl::Issue{limitAmount.get().currency, peer} ); - auto tx = getBaseTx(*accountID, accountSeq++); + auto tx = getBaseTx(accountID, accountSeq++); tx[JS(TransactionType)] = "TrustSet"; tx[JS(LimitAmount)] = @@ -176,33 +170,6 @@ NoRippleCheckHandler::process(NoRippleCheckHandler::Input const& input, Context return output; } -NoRippleCheckHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = NoRippleCheckHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - input.account = boost::json::value_to(jsonObject.at(JS(account))); - input.roleGateway = jsonObject.at(JS(role)).as_string() == "gateway"; - - if (jsonObject.contains(JS(limit))) - input.limit = util::integralValueAs(jsonObject.at(JS(limit))); - - if (jsonObject.contains(JS(transactions))) - input.transactions = boost::json::value_to(jsonObject.at(JS(transactions))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(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; - } - - return input; -} - void tag_invoke( boost::json::value_from_tag, diff --git a/src/rpc/handlers/NoRippleCheck.hpp b/src/rpc/handlers/NoRippleCheck.hpp index 53e5bed92..f32088271 100644 --- a/src/rpc/handlers/NoRippleCheck.hpp +++ b/src/rpc/handlers/NoRippleCheck.hpp @@ -1,25 +1,19 @@ #pragma once #include "data/BackendInterface.hpp" -#include "rpc/Errors.hpp" -#include "rpc/JS.hpp" -#include "rpc/common/JsonBool.hpp" -#include "rpc/common/MetaProcessors.hpp" -#include "rpc/common/Modifiers.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include #include #include -#include -#include +#include +#include #include #include #include #include +#include #include namespace rpc { @@ -32,7 +26,8 @@ namespace rpc { * * For more details see: https://xrpl.org/noripple_check.html */ -class NoRippleCheckHandler { +class NoRippleCheckHandler + : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; public: @@ -52,18 +47,6 @@ public: bool validated = true; }; - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::string account; - bool roleGateway = false; - std::optional ledgerHash; - std::optional ledgerIndex; - uint32_t limit = kLimitDefault; - JsonBool transactions{false}; - }; - using Result = HandlerReturnType; /** @@ -76,41 +59,6 @@ public: { } - /** - * @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 kRpcSpecV1 = RpcSpec{ - {JS(account), validation::Required{}, validation::CustomValidators::accountValidator}, - {JS(role), - validation::Required{}, - meta::WithCustomError{ - validation::OneOf{"gateway", "user"}, - Status{RippledError::RpcInvalidParams, "role field is invalid"} - }}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - {JS(limit), - validation::Type(), - validation::Min(1u), - modifiers::Clamp{kLimitMin, kLimitMax}} - }; - - static auto const kRpcSpec = RpcSpec{ - kRpcSpecV1, - { - {JS(transactions), validation::Type()}, - } - }; - - return apiVersion == 1 ? kRpcSpecV1 : kRpcSpec; - } - /** * @brief Process the NoRippleCheck command * @@ -130,14 +78,5 @@ private: */ 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, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/ServerInfo.hpp b/src/rpc/handlers/ServerInfo.hpp index 5afd10fc1..6b019af2b 100644 --- a/src/rpc/handlers/ServerInfo.hpp +++ b/src/rpc/handlers/ServerInfo.hpp @@ -5,9 +5,7 @@ #include "etl/ETLServiceInterface.hpp" #include "etl/LoadBalancerInterface.hpp" #include "feed/SubscriptionManagerInterface.hpp" -#include "rpc/Errors.hpp" #include "rpc/JS.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" #include "util/build/Build.hpp" @@ -16,10 +14,12 @@ #include #include #include +#include +#include +#include #include #include #include -#include #include #include #include @@ -31,6 +31,7 @@ #include #include #include +#include namespace rpc { class Counters; @@ -44,7 +45,8 @@ namespace rpc { * @tparam CountersType The type of the counters */ template -class BaseServerInfoHandler { +class BaseServerInfoHandler + : public rpc::spec::HandlerFor { static constexpr auto kBackendCountersKey = "backend_counters"; std::shared_ptr backend_; @@ -54,13 +56,6 @@ class BaseServerInfoHandler { std::reference_wrapper counters_; public: - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - bool backendCounters = false; - }; - /** * @brief A struct to hold the admin section of the output */ @@ -147,19 +142,6 @@ public: { } - /** - * @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 RpcSpec const kRpcSpec = {}; - return kRpcSpec; - } - /** * @brief Process the ServerInfo command * @@ -254,12 +236,10 @@ private: tag_invoke(boost::json::value_from_tag, boost::json::value& jv, InfoSection const& info) { using boost::json::value_from; - using xrpl::to_string; - jv = { {JS(complete_ledgers), info.completeLedgers}, {JS(load_factor), info.loadFactor}, - {JS(time), to_string(std::chrono::floor(info.time))}, + {JS(time), xrpl::to_string(std::chrono::floor(info.time))}, {JS(uptime), info.uptime.count()}, {"clio_version", info.clioVersion}, {"libxrpl_version", info.xrplVersion}, @@ -328,17 +308,6 @@ private: {"successor_hit_rate", cache.successorHitRate}, }; } - - friend Input - tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) - { - auto input = BaseServerInfoHandler::Input{}; - auto const jsonObject = jv.as_object(); - if (jsonObject.contains(kBackendCountersKey) && - jsonObject.at(kBackendCountersKey).is_bool()) - input.backendCounters = jv.at(kBackendCountersKey).as_bool(); - return input; - } }; /** diff --git a/src/rpc/handlers/Subscribe.cpp b/src/rpc/handlers/Subscribe.cpp index d0f2a4074..a2097e2a8 100644 --- a/src/rpc/handlers/Subscribe.cpp +++ b/src/rpc/handlers/Subscribe.cpp @@ -7,11 +7,7 @@ #include "feed/Types.hpp" #include "rpc/JS.hpp" #include "rpc/RPCHelpers.hpp" -#include "rpc/common/Checkers.hpp" -#include "rpc/common/MetaProcessors.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include "util/Assert.hpp" #include @@ -19,17 +15,15 @@ #include #include #include -#include -#include +#include #include +#include #include #include -#include +#include #include #include -#include -#include #include #include @@ -46,62 +40,6 @@ SubscribeHandler::SubscribeHandler( { } -RpcSpecConstRef -SubscribeHandler::spec([[maybe_unused]] uint32_t apiVersion) -{ - static auto const kBooksValidator = validation::CustomValidator{ - [](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_array()) { - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotArray"}}; - } - - for (auto const& book : value.as_array()) { - if (!book.is_object()) { - return Error{ - Status{RippledError::RpcInvalidParams, std::string(key) + "ItemNotObject"} - }; - } - - if (book.as_object().contains("both") && !book.as_object().at("both").is_bool()) - return Error{Status{RippledError::RpcInvalidParams, "bothNotBool"}}; - - if (book.as_object().contains("snapshot") && - !book.as_object().at("snapshot").is_bool()) - return Error{Status{RippledError::RpcInvalidParams, "snapshotNotBool"}}; - - if (book.as_object().contains("taker")) { - if (auto err = - meta::WithCustomError( - validation::CustomValidators::accountValidator, - Status{RippledError::RpcBadIssuer, "Issuer account malformed."} - ) - .verify(book.as_object(), "taker"); - !err) - return err; - } - - auto const parsedBook = parseBook(book.as_object()); - if (!parsedBook) - return Error(parsedBook.error()); - } - - return MaybeError{}; - } - }; - - static auto const kRpcSpec = RpcSpec{ - {JS(streams), validation::CustomValidators::subscribeStreamValidator}, - {JS(accounts), validation::CustomValidators::subscribeAccountsValidator}, - {JS(accounts_proposed), validation::CustomValidators::subscribeAccountsValidator}, - {JS(books), kBooksValidator}, - {"user", check::Deprecated{}}, - {JS(password), check::Deprecated{}}, - {JS(rt_accounts), check::Deprecated{}} - }; - - return kRpcSpec; -} - SubscribeHandler::Result SubscribeHandler::process(Input const& input, Context const& ctx) const { @@ -131,25 +69,37 @@ SubscribeHandler::process(Input const& input, Context const& ctx) const boost::json::object SubscribeHandler::subscribeToStreams( boost::asio::yield_context yield, - std::vector const& streams, + std::vector const& streams, feed::SubscriberSharedPtr const& session ) const { auto response = boost::json::object{}; for (auto const& stream : streams) { - if (stream == "ledger") { - response = subscriptions_->subLedger(yield, session); - } else if (stream == "transactions") { - subscriptions_->subTransactions(session); - } else if (stream == "transactions_proposed") { - subscriptions_->subProposedTransactions(session); - } else if (stream == "validations") { - subscriptions_->subValidation(session); - } else if (stream == "manifests") { - subscriptions_->subManifest(session); - } else if (stream == "book_changes") { - subscriptions_->subBookChanges(session); + switch (stream) { + case StreamType::Ledger: + response = subscriptions_->subLedger(yield, session); + break; + case StreamType::Transactions: + subscriptions_->subTransactions(session); + break; + case StreamType::TransactionsProposed: + subscriptions_->subProposedTransactions(session); + break; + case StreamType::Validations: + subscriptions_->subValidation(session); + break; + case StreamType::Manifests: + subscriptions_->subManifest(session); + break; + case StreamType::BookChanges: + subscriptions_->subBookChanges(session); + break; + case StreamType::Server: + case StreamType::PeerStatus: + case StreamType::Consensus: + // Not served by Clio. + break; } } @@ -158,28 +108,22 @@ SubscribeHandler::subscribeToStreams( void SubscribeHandler::subscribeToAccountsProposed( - std::vector const& accounts, + std::vector const& accounts, feed::SubscriberSharedPtr const& session ) const { - for (auto const& account : accounts) { - auto const accountID = accountFromStringStrict(account); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - subscriptions_->subProposedAccount(*accountID, session); - } + for (auto const& account : accounts) + subscriptions_->subProposedAccount(account, session); } void SubscribeHandler::subscribeToAccounts( - std::vector const& accounts, + std::vector const& accounts, feed::SubscriberSharedPtr const& session ) const { - for (auto const& account : accounts) { - auto const accountID = accountFromStringStrict(account); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - subscriptions_->subAccount(*accountID, session); - } + for (auto const& account : accounts) + subscriptions_->subAccount(account, session); } void @@ -263,54 +207,4 @@ tag_invoke( jv.as_object().emplace(JS(bids), *(output.bids)); } -SubscribeHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = SubscribeHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - if (auto const& streams = jsonObject.find(JS(streams)); streams != jsonObject.end()) { - input.streams = std::vector(); - for (auto const& stream : streams->value().as_array()) - input.streams->push_back(boost::json::value_to(stream)); - } - - if (auto const& accounts = jsonObject.find(JS(accounts)); accounts != jsonObject.end()) { - input.accounts = std::vector(); - for (auto const& account : accounts->value().as_array()) - input.accounts->push_back(boost::json::value_to(account)); - } - - if (auto const& accountsProposed = jsonObject.find(JS(accounts_proposed)); - accountsProposed != jsonObject.end()) { - input.accountsProposed = std::vector(); - for (auto const& account : accountsProposed->value().as_array()) - input.accountsProposed->push_back(boost::json::value_to(account)); - } - - if (auto const& books = jsonObject.find(JS(books)); books != jsonObject.end()) { - input.books = std::vector(); - for (auto const& book : books->value().as_array()) { - auto internalBook = SubscribeHandler::OrderBook{}; - auto const& bookObject = book.as_object(); - - if (auto const taker = bookObject.find(JS(taker)); taker != bookObject.end()) - internalBook.taker = boost::json::value_to(taker->value()); - - if (auto const both = bookObject.find(JS(both)); both != bookObject.end()) - internalBook.both = both->value().as_bool(); - - if (auto const snapshot = bookObject.find(JS(snapshot)); snapshot != bookObject.end()) - internalBook.snapshot = snapshot->value().as_bool(); - - auto const parsedBookMaybe = parseBook(book.as_object()); - ASSERT(parsedBookMaybe.has_value(), "Book parsing failed"); - internalBook.book = *parsedBookMaybe; - input.books->push_back(internalBook); - } - } - - return input; -} - } // namespace rpc diff --git a/src/rpc/handlers/Subscribe.hpp b/src/rpc/handlers/Subscribe.hpp index dc002e121..9d9e4c61f 100644 --- a/src/rpc/handlers/Subscribe.hpp +++ b/src/rpc/handlers/Subscribe.hpp @@ -4,7 +4,6 @@ #include "data/BackendInterface.hpp" #include "feed/SubscriptionManagerInterface.hpp" #include "feed/Types.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" #include @@ -12,16 +11,13 @@ #include #include #include -#include -#include -#include -#include -#include +#include +#include +#include -#include +#include #include #include -#include #include namespace rpc { @@ -33,7 +29,7 @@ namespace rpc { * For more details see: https://xrpl.org/subscribe.html */ -class SubscribeHandler { +class SubscribeHandler : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; std::shared_ptr amendmentCenter_; std::shared_ptr subscriptions_; @@ -59,22 +55,12 @@ public: /** * @brief A struct to hold the data for one order book */ - struct OrderBook { - xrpl::Book book; - std::optional taker; - bool snapshot = false; - bool both = false; - }; + using OrderBook = rpc::spec::handlers::subscribe::OrderBook; /** - * @brief A struct to hold the input data for the command + * @brief A subscribable stream type. */ - struct Input { - std::optional> accounts; - std::optional> streams; - std::optional> accountsProposed; - std::optional> books; - }; + using StreamType = rpc::spec::handlers::subscribe::StreamType; using Result = HandlerReturnType; @@ -91,15 +77,6 @@ public: std::shared_ptr const& subscriptions ); - /** - * @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); - /** * @brief Process the Subscribe command * @@ -114,19 +91,19 @@ private: [[nodiscard]] boost::json::object subscribeToStreams( boost::asio::yield_context yield, - std::vector const& streams, + std::vector const& streams, feed::SubscriberSharedPtr const& session ) const; void subscribeToAccounts( - std::vector const& accounts, + std::vector const& accounts, feed::SubscriberSharedPtr const& session ) const; void subscribeToAccountsProposed( - std::vector const& accounts, + std::vector const& accounts, feed::SubscriberSharedPtr const& session ) const; @@ -146,15 +123,6 @@ private: */ friend void tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Output const& output); - - /** - * @brief Convert json value to input - * - * @param jv The json value to convert from - * @return The input to convert to - */ - friend Input - tag_invoke(boost::json::value_to_tag, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/TransactionEntry.cpp b/src/rpc/handlers/TransactionEntry.cpp index 083023bfb..6e0f09de0 100644 --- a/src/rpc/handlers/TransactionEntry.cpp +++ b/src/rpc/handlers/TransactionEntry.cpp @@ -4,13 +4,10 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include -#include #include -#include #include #include #include @@ -29,11 +26,10 @@ TransactionEntryHandler::process( auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "TransactionEntry's ledger range must be available"); - auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - input.ledgerHash, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -44,8 +40,7 @@ TransactionEntryHandler::process( output.apiVersion = ctx.apiVersion; output.ledgerHeader = *expectedLgrInfo; - auto const dbRet = - sharedPtrBackend_->fetchTransaction(xrpl::uint256{input.txHash.c_str()}, ctx.yield); + auto const dbRet = sharedPtrBackend_->fetchTransaction(input.txHash, ctx.yield); // Note: transaction_entry is meant to only search a specified ledger for // the specified transaction. tx searches the entire range of history. For // rippled, having two separate commands made sense, as tx would use SQLite @@ -98,24 +93,4 @@ tag_invoke( } } -TransactionEntryHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = TransactionEntryHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - input.txHash = boost::json::value_to(jv.at(JS(tx_hash))); - - if (jsonObject.contains(JS(ledger_hash))) - input.ledgerHash = boost::json::value_to(jv.at(JS(ledger_hash))); - - if (jsonObject.contains(JS(ledger_index))) { - auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index))); - if (expectedLedgerIndex.has_value()) - input.ledgerIndex = *expectedLedgerIndex; - } - - return input; -} - } // namespace rpc diff --git a/src/rpc/handlers/TransactionEntry.hpp b/src/rpc/handlers/TransactionEntry.hpp index 12d94e890..4d48453a2 100644 --- a/src/rpc/handlers/TransactionEntry.hpp +++ b/src/rpc/handlers/TransactionEntry.hpp @@ -1,23 +1,19 @@ #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 #include #include +#include +#include #include -#include #include #include #include -#include +#include namespace rpc { @@ -27,7 +23,8 @@ namespace rpc { * * For more details see: https://xrpl.org/transaction_entry.html */ -class TransactionEntryHandler { +class TransactionEntryHandler + : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; public: @@ -44,15 +41,6 @@ public: uint32_t apiVersion; }; - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::string txHash; - std::optional ledgerHash; - std::optional ledgerIndex; - }; - using Result = HandlerReturnType; /** @@ -65,28 +53,6 @@ public: { } - /** - * @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 kRpcSpec = RpcSpec{ - {JS(tx_hash), - meta::WithCustomError{ - validation::Required{}, Status(ClioError::RpcFieldNotFoundTransaction) - }, - validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - }; - - return kRpcSpec; - } - /** * @brief Process the transaction_entry command * @@ -106,15 +72,6 @@ private: */ 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, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/Tx.hpp b/src/rpc/handlers/Tx.hpp index b4bdf2cf2..5c45c1f90 100644 --- a/src/rpc/handlers/Tx.hpp +++ b/src/rpc/handlers/Tx.hpp @@ -3,15 +3,10 @@ #include "data/BackendInterface.hpp" #include "data/Types.hpp" #include "etl/ETLServiceInterface.hpp" -#include "rpc/Errors.hpp" #include "rpc/JS.hpp" #include "rpc/RPCHelpers.hpp" -#include "rpc/common/JsonBool.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include @@ -19,10 +14,11 @@ #include #include #include -#include +#include +#include +#include #include #include -#include #include #include @@ -39,7 +35,7 @@ namespace rpc { * * For more details see: https://xrpl.org/tx.html */ -class TxHandler { +class TxHandler : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; std::shared_ptr etl_; @@ -68,17 +64,6 @@ public: bool validated = true; }; - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::optional transaction; - std::optional ctid; - bool binary = false; - std::optional minLedger; - std::optional maxLedger; - }; - using Result = HandlerReturnType; /** @@ -95,28 +80,6 @@ public: { } - /** - * @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(uint32_t apiVersion) - { - static RpcSpec const kRpcSpecForV1 = { - {JS(transaction), validation::CustomValidators::uint256HexStringValidator}, - {JS(min_ledger), validation::Type{}}, - {JS(max_ledger), validation::Type{}}, - {JS(ctid), validation::Type{}}, - }; - - static auto const kRpcSpec = - RpcSpec{kRpcSpecForV1, {{JS(binary), validation::Type{}}}}; - - return apiVersion == 1 ? kRpcSpecForV1 : kRpcSpec; - } - /** * @brief Process the Tx command * @@ -170,9 +133,7 @@ public: dbResponse = fetchTxViaCtid(lgrSeq, txnIdx, ctx.yield); } else { - dbResponse = sharedPtrBackend_->fetchTransaction( - xrpl::uint256{input.transaction->c_str()}, ctx.yield - ); + dbResponse = sharedPtrBackend_->fetchTransaction(*input.transaction, ctx.yield); } auto output = TxHandler::Output{.apiVersion = ctx.apiVersion}; @@ -312,32 +273,6 @@ private: jv = std::move(obj); } - - friend Input - tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) - { - auto input = TxHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - if (jsonObject.contains(JS(transaction))) - input.transaction = boost::json::value_to(jv.at(JS(transaction))); - - if (jsonObject.contains(JS(ctid))) { - input.ctid = boost::json::value_to(jv.at(JS(ctid))); - input.ctid = util::toUpper(*input.ctid); - } - - if (jsonObject.contains(JS(binary))) - input.binary = boost::json::value_to(jsonObject.at(JS(binary))); - - if (jsonObject.contains(JS(min_ledger))) - input.minLedger = util::integralValueAs(jv.at(JS(min_ledger))); - - if (jsonObject.contains(JS(max_ledger))) - input.maxLedger = util::integralValueAs(jv.at(JS(max_ledger))); - - return input; - } }; } // namespace rpc diff --git a/src/rpc/handlers/Unsubscribe.cpp b/src/rpc/handlers/Unsubscribe.cpp index f7c521adf..b56704f48 100644 --- a/src/rpc/handlers/Unsubscribe.cpp +++ b/src/rpc/handlers/Unsubscribe.cpp @@ -2,25 +2,14 @@ #include "feed/SubscriptionManagerInterface.hpp" #include "feed/Types.hpp" -#include "rpc/JS.hpp" -#include "rpc/RPCHelpers.hpp" -#include "rpc/common/Checkers.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" -#include "util/Assert.hpp" -#include -#include -#include -#include +#include +#include #include -#include -#include +#include #include -#include -#include #include namespace rpc { @@ -32,47 +21,6 @@ UnsubscribeHandler::UnsubscribeHandler( { } -RpcSpecConstRef -UnsubscribeHandler::spec([[maybe_unused]] uint32_t apiVersion) -{ - static auto const kBooksValidator = validation::CustomValidator{ - [](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_array()) { - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotArray"}}; - } - - for (auto const& book : value.as_array()) { - if (!book.is_object()) { - return Error{ - Status{RippledError::RpcInvalidParams, std::string(key) + "ItemNotObject"} - }; - } - - if (book.as_object().contains("both") && !book.as_object().at("both").is_bool()) - return Error{Status{RippledError::RpcInvalidParams, "bothNotBool"}}; - - auto const parsedBook = parseBook(book.as_object()); - if (!parsedBook.has_value()) - return Error(parsedBook.error()); - } - - return MaybeError{}; - } - }; - - static auto const kRpcSpec = RpcSpec{ - {JS(streams), validation::CustomValidators::subscribeStreamValidator}, - {JS(accounts), validation::CustomValidators::subscribeAccountsValidator}, - {JS(accounts_proposed), validation::CustomValidators::subscribeAccountsValidator}, - {JS(books), kBooksValidator}, - {JS(url), check::Deprecated{}}, - {JS(rt_accounts), check::Deprecated{}}, - {"rt_transactions", check::Deprecated{}}, - }; - - return kRpcSpec; -} - UnsubscribeHandler::Result UnsubscribeHandler::process(Input const& input, Context const& ctx) const { @@ -93,54 +41,59 @@ UnsubscribeHandler::process(Input const& input, Context const& ctx) const void UnsubscribeHandler::unsubscribeFromStreams( - std::vector const& streams, + std::vector const& streams, feed::SubscriberSharedPtr const& session ) const { for (auto const& stream : streams) { - if (stream == "ledger") { - subscriptions_->unsubLedger(session); - } else if (stream == "transactions") { - subscriptions_->unsubTransactions(session); - } else if (stream == "transactions_proposed") { - subscriptions_->unsubProposedTransactions(session); - } else if (stream == "validations") { - subscriptions_->unsubValidation(session); - } else if (stream == "manifests") { - subscriptions_->unsubManifest(session); - } else if (stream == "book_changes") { - subscriptions_->unsubBookChanges(session); - } else { - ASSERT(false, "Unknown stream: {}", stream); + switch (stream) { + case StreamType::Ledger: + subscriptions_->unsubLedger(session); + break; + case StreamType::Transactions: + subscriptions_->unsubTransactions(session); + break; + case StreamType::TransactionsProposed: + subscriptions_->unsubProposedTransactions(session); + break; + case StreamType::Validations: + subscriptions_->unsubValidation(session); + break; + case StreamType::Manifests: + subscriptions_->unsubManifest(session); + break; + case StreamType::BookChanges: + subscriptions_->unsubBookChanges(session); + break; + case StreamType::Server: + case StreamType::PeerStatus: + case StreamType::Consensus: + // Not served by Clio. + break; } } } void UnsubscribeHandler::unsubscribeFromAccounts( - std::vector accounts, + std::vector const& accounts, feed::SubscriberSharedPtr const& session ) const { - for (auto const& account : accounts) { - auto const accountID = accountFromStringStrict(account); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - subscriptions_->unsubAccount(*accountID, session); - } + for (auto const& account : accounts) + subscriptions_->unsubAccount(account, session); } void UnsubscribeHandler::unsubscribeFromProposedAccounts( - std::vector accountsProposed, + std::vector const& accountsProposed, feed::SubscriberSharedPtr const& session ) const { - for (auto const& account : accountsProposed) { - auto const accountID = accountFromStringStrict(account); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - subscriptions_->unsubProposedAccount(*accountID, session); - } + for (auto const& account : accountsProposed) + subscriptions_->unsubProposedAccount(account, session); } + void UnsubscribeHandler::unsubscribeFromBooks( std::vector const& books, @@ -155,44 +108,4 @@ UnsubscribeHandler::unsubscribeFromBooks( } } -UnsubscribeHandler::Input -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - auto input = UnsubscribeHandler::Input{}; - auto const& jsonObject = jv.as_object(); - - if (auto const& streams = jsonObject.find(JS(streams)); streams != jsonObject.end()) { - input.streams = std::vector(); - for (auto const& stream : streams->value().as_array()) - input.streams->push_back(boost::json::value_to(stream)); - } - if (auto const& accounts = jsonObject.find(JS(accounts)); accounts != jsonObject.end()) { - input.accounts = std::vector(); - for (auto const& account : accounts->value().as_array()) - input.accounts->push_back(boost::json::value_to(account)); - } - if (auto const& accountsProposed = jsonObject.find(JS(accounts_proposed)); - accountsProposed != jsonObject.end()) { - input.accountsProposed = std::vector(); - for (auto const& account : accountsProposed->value().as_array()) - input.accountsProposed->push_back(boost::json::value_to(account)); - } - if (auto const& books = jsonObject.find(JS(books)); books != jsonObject.end()) { - input.books = std::vector(); - for (auto const& book : books->value().as_array()) { - auto internalBook = UnsubscribeHandler::OrderBook{}; - auto const& bookObject = book.as_object(); - - if (auto const& both = bookObject.find(JS(both)); both != bookObject.end()) - internalBook.both = both->value().as_bool(); - - auto const parsedBookMaybe = parseBook(book.as_object()); - ASSERT(parsedBookMaybe.has_value(), "Invalid book format"); - internalBook.book = *parsedBookMaybe; - input.books->push_back(internalBook); - } - } - - return input; -} } // namespace rpc diff --git a/src/rpc/handlers/Unsubscribe.hpp b/src/rpc/handlers/Unsubscribe.hpp index 42962a712..4ab9c43be 100644 --- a/src/rpc/handlers/Unsubscribe.hpp +++ b/src/rpc/handlers/Unsubscribe.hpp @@ -2,20 +2,14 @@ #include "feed/SubscriptionManagerInterface.hpp" #include "feed/Types.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include -#include -#include -#include -#include -#include +#include +#include +#include -#include +#include #include -#include -#include #include namespace rpc { @@ -28,27 +22,19 @@ namespace rpc { * For more details see: https://xrpl.org/unsubscribe.html */ -class UnsubscribeHandler { +class UnsubscribeHandler : public rpc::spec::HandlerFor { std::shared_ptr subscriptions_; public: /** * @brief A struct to hold one order book */ - struct OrderBook { - xrpl::Book book; - bool both = false; - }; + using OrderBook = rpc::spec::handlers::unsubscribe::OrderBook; /** - * @brief A struct to hold the input data for the command + * @brief A subscribable stream type. */ - struct Input { - std::optional> accounts; - std::optional> streams; - std::optional> accountsProposed; - std::optional> books; - }; + using StreamType = rpc::spec::handlers::unsubscribe::StreamType; using Output = VoidOutput; using Result = HandlerReturnType; @@ -60,15 +46,6 @@ public: */ UnsubscribeHandler(std::shared_ptr const& subscriptions); - /** - * @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); - /** * @brief Process the Unsubscribe command * @@ -82,19 +59,19 @@ public: private: void unsubscribeFromStreams( - std::vector const& streams, + std::vector const& streams, feed::SubscriberSharedPtr const& session ) const; void unsubscribeFromAccounts( - std::vector accounts, + std::vector const& accounts, feed::SubscriberSharedPtr const& session ) const; void unsubscribeFromProposedAccounts( - std::vector accountsProposed, + std::vector const& accountsProposed, feed::SubscriberSharedPtr const& session ) const; @@ -103,15 +80,6 @@ private: std::vector const& books, feed::SubscriberSharedPtr const& session ) const; - - /** - * @brief Convert a JSON object to an Input - * - * @param jv The JSON object to convert - * @return The Input object - */ - friend Input - tag_invoke(boost::json::value_to_tag, boost::json::value const& jv); }; } // namespace rpc diff --git a/src/rpc/handlers/VaultInfo.cpp b/src/rpc/handlers/VaultInfo.cpp index 480e8b153..9449ee880 100644 --- a/src/rpc/handlers/VaultInfo.cpp +++ b/src/rpc/handlers/VaultInfo.cpp @@ -5,13 +5,11 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/Types.hpp" #include "util/Assert.hpp" -#include "util/JsonUtils.hpp" #include #include #include #include -#include #include #include #include @@ -22,10 +20,9 @@ #include #include -#include +#include #include #include -#include #include namespace rpc { @@ -46,7 +43,7 @@ validate(VaultInfoHandler::Input const& input) bool const hasOwner = input.owner.has_value(); bool const hasSeq = input.tnxSequence.has_value(); - // Only valid combinations: (vaultID) or (owner + ledgerIndex) + // Only valid combinations: (vaultID) or (owner + seq) // NOLINTNEXTLINE(readability-simplify-boolean-expr) return (hasVaultId && !hasOwner && !hasSeq) || (!hasVaultId && hasOwner && hasSeq); } @@ -61,18 +58,16 @@ VaultInfoHandler::VaultInfoHandler(std::shared_ptr sharedPtrBa VaultInfoHandler::Result VaultInfoHandler::process(VaultInfoHandler::Input const& 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( + auto const expectedLgrInfo = getLedgerHeaderFromLedgerSpecifier( *sharedPtrBackend_, ctx.yield, - std::nullopt, - input.ledgerIndex, + input.ledger, range->maxSequence // NOLINT(bugprone-unchecked-optional-access) ); @@ -84,12 +79,11 @@ VaultInfoHandler::process(VaultInfoHandler::Input const& input, Context const& c // Extract the vault keylet based on input auto const vaultKeylet = [&]() -> std::expected { if (input.owner && input.tnxSequence) { - auto const accountStr = *input.owner; - auto const accountID = accountFromStringStrict(accountStr); + auto const& accountID = *input.owner; // checks that account exists { - auto const accountKeylet = xrpl::keylet::account(*accountID); + auto const accountKeylet = xrpl::keylet::account(accountID); auto const accountLedgerObject = sharedPtrBackend_->fetchLedgerObject(accountKeylet.key, lgrInfo.seq, ctx.yield); @@ -97,13 +91,10 @@ VaultInfoHandler::process(VaultInfoHandler::Input const& input, Context const& c return std::unexpected{Status{RippledError::RpcEntryNotFound}}; } - return xrpl::keylet::vault(*accountID, xrpl::SeqProxy::rawSequence(*input.tnxSequence)); + return xrpl::keylet::vault(accountID, xrpl::SeqProxy::rawSequence(*input.tnxSequence)); } - xrpl::uint256 nodeIndex; - if (nodeIndex.parseHex(*input.vaultID)) - return xrpl::keylet::vault(nodeIndex); - return std::unexpected{Status{RippledError::RpcEntryNotFound}}; + return xrpl::keylet::vault(*input.vaultID); }(); if (not vaultKeylet.has_value()) @@ -157,28 +148,4 @@ tag_invoke( }; } -VaultInfoHandler::Input -tag_invoke(boost::json::value_to_tag, 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 = util::integralValueAs(jsonObject.at(JS(seq))); - - if (jsonObject.contains(JS(vault_id))) - input.vaultID = jsonObject.at(JS(vault_id)).as_string(); - - if (jsonObject.contains(JS(ledger_index))) { - auto const expectedLedgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index))); - if (expectedLedgerIndex.has_value()) - input.ledgerIndex = *expectedLedgerIndex; - } - - return input; -} - } // namespace rpc diff --git a/src/rpc/handlers/VaultInfo.hpp b/src/rpc/handlers/VaultInfo.hpp index c2a04818c..9a7a3a2b4 100644 --- a/src/rpc/handlers/VaultInfo.hpp +++ b/src/rpc/handlers/VaultInfo.hpp @@ -1,30 +1,22 @@ #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 -#include #include -#include -#include +#include +#include #include #include -#include -#include namespace rpc { /** * @brief The vault_info command retrieves information about a vault, currency, shares etc. */ -class VaultInfoHandler { +class VaultInfoHandler : public rpc::spec::HandlerFor { std::shared_ptr sharedPtrBackend_; public: @@ -35,16 +27,6 @@ public: */ VaultInfoHandler(std::shared_ptr sharedPtrBackend); - /** - * @brief A struct to hold the input data for the command - */ - struct Input { - std::optional vaultID; - std::optional owner; - std::optional tnxSequence; - std::optional ledgerIndex; - }; - /** * @brief A struct to hold the output data for the command */ @@ -56,36 +38,6 @@ public: using Result = HandlerReturnType; - /** - * @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 kRpcSpec = 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{}, Status(ClioError::RpcMalformedRequest) - }}, - {JS(ledger_index), validation::CustomValidators::ledgerIndexValidator}, - }; - - return kRpcSpec; - } - /** * @brief Process the VaultInfo command * @@ -105,15 +57,6 @@ private: */ 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, boost::json::value const& jv); }; } // namespace rpc diff --git a/tests/common/rpc/FakesAndMocks.hpp b/tests/common/rpc/FakesAndMocks.hpp index 62a0765b2..5505502c8 100644 --- a/tests/common/rpc/FakesAndMocks.hpp +++ b/tests/common/rpc/FakesAndMocks.hpp @@ -1,9 +1,7 @@ #pragma once #include "rpc/Errors.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include #include @@ -24,30 +22,11 @@ namespace tests::common { -// input data for the test handlers below -struct TestInput { - std::string hello; - std::optional limit; -}; - // output data produced by the test handlers below struct TestOutput { std::string computed; }; -// must be implemented as per rpc/common/Concepts.h -inline TestInput -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - std::optional optLimit; - if (jv.as_object().contains("limit")) - optLimit = jv.at("limit").as_int64(); - - return { - .hello = boost::json::value_to(jv.as_object().at("hello")), .limit = optLimit - }; -} - // must be implemented as per rpc/common/Concepts.h inline void tag_invoke(boost::json::value_from_tag, boost::json::value& jv, TestOutput const& output) @@ -55,33 +34,6 @@ tag_invoke(boost::json::value_from_tag, boost::json::value& jv, TestOutput const jv = {{"computed", output.computed}}; } -// example handler -class HandlerFake { -public: - using Input = TestInput; - using Output = TestOutput; - using Result = rpc::HandlerReturnType; - - static rpc::RpcSpecConstRef - spec([[maybe_unused]] uint32_t apiVersion) - { - using namespace rpc::validation; - - static auto const kRpcSpec = rpc::RpcSpec{ - {"hello", Required{}, Type{}, EqualTo{"world"}}, - {"limit", Type{}, Between{0, 100}}, // optional field - }; - - return kRpcSpec; - } - - static Result - process(Input input, [[maybe_unused]] rpc::Context const& ctx) - { - return Output{input.hello + '_' + std::to_string(input.limit.value_or(0))}; - } -}; - class NoInputHandlerFake { public: using Output = TestOutput; @@ -94,34 +46,6 @@ public: } }; -// example handler that returns custom error -class FailingHandlerFake { -public: - using Input = TestInput; - using Output = TestOutput; - using Result = rpc::HandlerReturnType; - - static rpc::RpcSpecConstRef - spec([[maybe_unused]] uint32_t apiVersion) - { - using namespace rpc::validation; - - static auto const kRpcSpec = rpc::RpcSpec{ - {"hello", Required{}, Type{}, EqualTo{"world"}}, - {"limit", Type{}, Between{0u, 100u}}, // optional field - }; - - return kRpcSpec; - } - - static Result - process([[maybe_unused]] Input input, [[maybe_unused]] rpc::Context const& ctx) - { - // always fail - return rpc::Error{rpc::Status{"Very custom error"}}; - } -}; - struct InOutFake { std::string something; @@ -144,15 +68,6 @@ tag_invoke(boost::json::value_from_tag, boost::json::value& jv, InOutFake const& jv = {{"something", output.something}}; } -struct HandlerMock { - using Input = InOutFake; - using Output = InOutFake; - using Result = rpc::HandlerReturnType; - - MOCK_METHOD(rpc::RpcSpecConstRef, spec, (uint32_t), (const)); - MOCK_METHOD(Result, process, (Input, rpc::Context const&), (const)); -}; - struct HandlerWithoutInputMock { using Output = InOutFake; using Result = rpc::HandlerReturnType; @@ -164,15 +79,26 @@ struct HandlerWithoutInputMock { // `specFor` hook, so the fake Input below needs its own namespace to host that hook. namespace typed_fake { -// input data for TypedHandlerFake; mirrors TestInput so the two paths stay comparable +// input data for TypedHandlerFake struct TypedInput { std::string hello; std::optional limit; }; inline constexpr auto kInputSpec = rpc::spec::spec( - rpc::spec::field("hello", &TypedInput::hello, rpc::spec::required, rpc::spec::asString), - rpc::spec::field("limit", &TypedInput::limit, rpc::spec::asUint32), + rpc::spec::field( + "hello", + &TypedInput::hello, + rpc::spec::required, + rpc::spec::oneOf("world"), + rpc::spec::asString + ), + rpc::spec::field( + "limit", + &TypedInput::limit, + rpc::spec::between(uint32_t{0}, uint32_t{100}), + rpc::spec::asUint32 + ), rpc::spec::field("old_field", rpc::spec::deprecated) ); diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index c87a454ee..629a7ba87 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -94,12 +94,9 @@ target_sources( migration/SpecTests.cpp # RPC rpc/APIVersionTests.cpp - rpc/BaseTests.cpp rpc/CountersTests.cpp rpc/ErrorTests.cpp rpc/ForwardingProxyTests.cpp - rpc/common/CheckersTests.cpp - rpc/common/SpecsTests.cpp rpc/common/TypesTests.cpp rpc/common/impl/HandlerProviderTests.cpp rpc/filters/impl/DelegateTransactionsFilterTests.cpp @@ -146,7 +143,6 @@ target_sources( rpc/handlers/UnsubscribeTests.cpp rpc/handlers/VersionHandlerTests.cpp rpc/handlers/VaultInfoTests.cpp - rpc/JsonBoolTests.cpp rpc/RPCEngineTests.cpp rpc/RPCHelpersTests.cpp rpc/WorkQueueTests.cpp diff --git a/tests/unit/rpc/BaseTests.cpp b/tests/unit/rpc/BaseTests.cpp deleted file mode 100644 index 70f49b011..000000000 --- a/tests/unit/rpc/BaseTests.cpp +++ /dev/null @@ -1,941 +0,0 @@ -#include "rpc/common/MetaProcessors.hpp" -#include "rpc/common/Modifiers.hpp" -#include "rpc/common/Specs.hpp" -#include "rpc/common/Types.hpp" -#include "rpc/common/ValidationHelpers.hpp" -#include "rpc/common/Validators.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -using namespace std; - -using namespace rpc; -using namespace rpc::validation; -using namespace rpc::meta; -using namespace rpc::modifiers; - -class RPCBaseTest : public virtual ::testing::Test {}; - -TEST_F(RPCBaseTest, CheckTypeString) -{ - auto const jString = boost::json::value("a string"); - ASSERT_TRUE(checkType(jString)); - ASSERT_FALSE(checkType(jString)); -} - -TEST_F(RPCBaseTest, CheckTypeUint) -{ - auto const jUint = boost::json::value(123u); - ASSERT_TRUE(checkType(jUint)); - ASSERT_TRUE(checkType(jUint)); - ASSERT_FALSE(checkType(jUint)); -} - -TEST_F(RPCBaseTest, CheckTypeInt) -{ - auto jInt = boost::json::value(123); - ASSERT_TRUE(checkType(jInt)); - ASSERT_TRUE(checkType(jInt)); - ASSERT_FALSE(checkType(jInt)); - - jInt = boost::json::value(-123); - ASSERT_TRUE(checkType(jInt)); - ASSERT_FALSE(checkType(jInt)); // Unsigned can't be negative - ASSERT_FALSE(checkType(jInt)); -} - -TEST_F(RPCBaseTest, CheckTypeBool) -{ - auto const jBool = boost::json::value(true); - ASSERT_TRUE(checkType(jBool)); - ASSERT_FALSE(checkType(jBool)); -} - -TEST_F(RPCBaseTest, CheckTypeDouble) -{ - auto const jDouble = boost::json::value(0.123); - ASSERT_TRUE(checkType(jDouble)); - ASSERT_TRUE(checkType(jDouble)); - ASSERT_FALSE(checkType(jDouble)); -} - -TEST_F(RPCBaseTest, CheckTypeArray) -{ - auto const jArr = boost::json::value({1, 2, 3}); - ASSERT_TRUE(checkType(jArr)); - ASSERT_FALSE(checkType(jArr)); -} - -TEST_F(RPCBaseTest, CheckTypeAndClampValueUnchanged) -{ - auto jUint = boost::json::value(123u); - ASSERT_TRUE(checkTypeAndClamp(jUint)); - ASSERT_EQ(jUint.as_uint64(), 123u); - ASSERT_TRUE(checkTypeAndClamp(jUint)); - ASSERT_EQ(jUint.as_uint64(), 123u); - - auto jInt = boost::json::value(123); - ASSERT_TRUE(checkTypeAndClamp(jInt)); - ASSERT_EQ(jInt.as_int64(), 123); - ASSERT_TRUE(checkTypeAndClamp(jInt)); - ASSERT_EQ(jInt.as_int64(), 123); - - jInt = boost::json::value(-123); - ASSERT_TRUE(checkTypeAndClamp(jInt)); - ASSERT_EQ(jInt.as_int64(), -123); -} - -TEST_F(RPCBaseTest, CheckTypeAndClampInvalidValues) -{ - auto jInt = boost::json::value(-123); - ASSERT_FALSE(checkTypeAndClamp(jInt)); // Unsigned can't be negative -} - -TEST_F(RPCBaseTest, CheckTypeAndClampOverflow) -{ - auto jBigUint = boost::json::value(std::numeric_limits::max()); - ASSERT_TRUE(checkTypeAndClamp(jBigUint)); - ASSERT_EQ(jBigUint.as_uint64(), std::numeric_limits::max()); - - auto jBigInt = boost::json::value(std::numeric_limits::max()); - ASSERT_TRUE(checkTypeAndClamp(jBigInt)); - ASSERT_EQ(jBigInt.as_int64(), std::numeric_limits::max()); -} - -TEST_F(RPCBaseTest, CheckTypeAndClampUnderflow) -{ - auto jLowInt = boost::json::value(std::numeric_limits::min()); - ASSERT_TRUE(checkTypeAndClamp(jLowInt)); - ASSERT_EQ(jLowInt.as_int64(), std::numeric_limits::min()); -} - -TEST_F(RPCBaseTest, TypeValidator) -{ - auto spec = RpcSpec{ - {"uint", Type{}}, - {"int", Type{}}, - {"str", Type{}}, - {"double", Type{}}, - {"bool", Type{}}, - {"arr", Type{}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ - "uint": 123, - "int": 321, - "str": "a string", - "double": 1.0, - "bool": true, - "arr": [] - })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - { - auto failingInput = boost::json::parse(R"JSON({ "uint": "a string" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - } - { - auto failingInput = boost::json::parse(R"JSON({ "int": "a string" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - } - { - auto failingInput = boost::json::parse(R"JSON({ "str": 1234 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - } - { - auto failingInput = boost::json::parse(R"JSON({ "double": "a string" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - } - { - auto failingInput = boost::json::parse(R"JSON({ "bool": "a string" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - } - { - auto failingInput = boost::json::parse(R"JSON({ "arr": "a string" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - } -} - -TEST_F(RPCBaseTest, TypeValidatorMultipleTypes) -{ - auto spec = RpcSpec{ - // either int or string - {"test", Type{}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "test": "1234" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto passingInput2 = boost::json::parse(R"JSON({ "test": 1234 })JSON"); - ASSERT_TRUE(spec.process(passingInput2)); - - auto failingInput = boost::json::parse(R"JSON({ "test": true })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, RequiredValidator) -{ - auto spec = RpcSpec{ - {"required", Required{}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "required": "present" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto passingInput2 = boost::json::parse(R"JSON({ "required": true })JSON"); - ASSERT_TRUE(spec.process(passingInput2)); - - auto failingInput = boost::json::parse(R"JSON({})JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, BetweenValidator) -{ - auto spec = RpcSpec{ - {"amount", Between{10u, 20u}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "amount": 15 })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto passingInput2 = boost::json::parse(R"JSON({ "amount": 10 })JSON"); - ASSERT_TRUE(spec.process(passingInput2)); - - auto passingInput3 = boost::json::parse(R"JSON({ "amount": 20 })JSON"); - ASSERT_TRUE(spec.process(passingInput3)); - - auto failingInput = boost::json::parse(R"JSON({ "amount": 9 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - auto failingInput2 = boost::json::parse(R"JSON({ "amount": 21 })JSON"); - ASSERT_FALSE(spec.process(failingInput2)); -} - -TEST_F(RPCBaseTest, MinValidator) -{ - auto spec = RpcSpec{ - {"amount", Min{6}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "amount": 7 })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto passingInput2 = boost::json::parse(R"JSON({ "amount": 6 })JSON"); - ASSERT_TRUE(spec.process(passingInput2)); - - auto failingInput = boost::json::parse(R"JSON({ "amount": 5 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, MinValidatorAfterType) -{ - auto spec = RpcSpec{ - {"amount", Type{}, Min{std::numeric_limits::max()}}, - {"amount2", Type{}, Min{std::numeric_limits::max()}}, - {"amount3", Type{}, Min{std::numeric_limits::min()}}, - }; - - auto bigInput = boost::json::parse( - R"JSON({ "amount": 9999999999, "amount2": 9999999999, "amount3": -9999999999 })JSON" - ); - ASSERT_TRUE(spec.process(bigInput)); // type check clamps to type's max/min value -} - -TEST_F(RPCBaseTest, MaxValidator) -{ - auto spec = RpcSpec{ - {"amount", Max{6}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "amount": 5 })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto passingInput2 = boost::json::parse(R"JSON({ "amount": 6 })JSON"); - ASSERT_TRUE(spec.process(passingInput2)); - - auto failingInput = boost::json::parse(R"JSON({ "amount": 7 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, MaxValidatorAfterType) -{ - auto spec = RpcSpec{ - {"amount", Type{}, Max{std::numeric_limits::max()}}, - {"amount2", Type{}, Max{std::numeric_limits::max()}}, - {"amount3", Type{}, Max{std::numeric_limits::min()}}, - }; - - auto bigInput = boost::json::parse( - R"JSON({ "amount": 9999999999, "amount2": 9999999999, "amount3": -9999999999 })JSON" - ); - ASSERT_TRUE(spec.process(bigInput)); // type check clamps to type's min/max value -} - -TEST_F(RPCBaseTest, OneOfValidator) -{ - auto spec = RpcSpec{ - {"currency", OneOf{"XRP", "USD"}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "currency": "XRP" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto passingInput2 = boost::json::parse(R"JSON({ "currency": "USD" })JSON"); - ASSERT_TRUE(spec.process(passingInput2)); - - auto failingInput = boost::json::parse(R"JSON({ "currency": "PRX" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, EqualToValidator) -{ - auto spec = RpcSpec{ - {"exact", EqualTo{"CaseSensitive"}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "exact": "CaseSensitive" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "exact": "Different" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, ArrayAtValidator) -{ - auto spec = RpcSpec{ - {"arr", - Required{}, - Type{}, - ValidateArrayAt{ - 0, - { - {"limit", Required{}, Type{}, Between{0, 100}}, - } - }}, - {"arr2", - ValidateArrayAt{ - 0, - { - {"limit", Required{}, Type{}, Between{0, 100}}, - } - }}, - }; - // clang-format on - - auto passingInput = boost::json::parse(R"JSON({ "arr": [{"limit": 42}] })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "arr": [{"limit": "not int"}] })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = - boost::json::parse(R"JSON({ "arr": [{"limit": 42}], "arr2": "not array type" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = boost::json::parse(R"JSON({ "arr": [] })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, IfTypeValidator) -{ - auto spec = RpcSpec{ - {"mix", - Required{}, - Type{}, - IfType{ - Section{{"limit", Required{}, Type{}, Between{0, 100}}}, - Section{{"limit2", Required{}, Type{}, Between{0, 100}}} - }, - IfType{CustomValidators::uint256HexStringValidator}}, - {"mix2", - Section{{"limit", Required{}, Type{}, Between{0, 100}}}, - Type{}}, - }; - - // if json object pass - auto passingInput = boost::json::parse(R"JSON({ "mix": {"limit": 42, "limit2": 22} })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - // if string pass - passingInput = boost::json::parse( - R"JSON({ "mix": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC" })JSON" - ); - ASSERT_TRUE(spec.process(passingInput)); - - // if json object fail at first requirement - auto failingInput = boost::json::parse(R"JSON({ "mix": {"limit": "not int"} })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - // if json object fail at second requirement - failingInput = boost::json::parse(R"JSON({ "mix": {"limit": 22, "limit2": "y"} })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - // if string fail - failingInput = boost::json::parse(R"JSON({ "mix": "not hash" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - // type check fail - failingInput = boost::json::parse(R"JSON({ "mix": 1213 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = - boost::json::parse(R"JSON({ "mix": {"limit": 42, "limit2": 22}, "mix2": 1213 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, WithCustomError) -{ - auto const spec = RpcSpec{ - {"transaction", - WithCustomError{ - CustomValidators::uint256HexStringValidator, - rpc::Status{xrpl::RpcBadFeature, "MyCustomError"} - }}, - {"other", - WithCustomError{ - Type{}, rpc::Status{xrpl::RpcAlreadyMultisig, "MyCustomError2"} - }} - }; - - auto passingInput = boost::json::parse( - R"JSON({ "transaction": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", "other": "1"})JSON" - ); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse( - R"JSON({ "transaction": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515B"})JSON" - ); - auto err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "MyCustomError"); - ASSERT_EQ(err.error(), xrpl::RpcBadFeature); - - failingInput = boost::json::parse(R"JSON({ "other": 1})JSON"); - err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "MyCustomError2"); - ASSERT_EQ(err.error(), xrpl::RpcAlreadyMultisig); -} - -TEST_F(RPCBaseTest, TimeFormatValidator) -{ - auto const spec = RpcSpec{ - {"date", TimeFormatValidator{"%Y-%m-%dT%H:%M:%SZ"}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "date": "2023-01-01T00:00:00Z" })JSON"); - EXPECT_TRUE(spec.process(passingInput)); - - passingInput = boost::json::parse("123"); - EXPECT_TRUE(spec.process(passingInput)); - - // key not exists - passingInput = boost::json::parse(R"JSON({ "date1": "2023-01-01T00:00:00Z" })JSON"); - EXPECT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "date": "2023-01-01-00:00:00" })JSON"); - auto err = spec.process(failingInput); - EXPECT_FALSE(err); - EXPECT_EQ(err.error(), xrpl::RpcInvalidParams); - - failingInput = boost::json::parse(R"JSON({ "date": "01-01-2024T00:00:00" })JSON"); - EXPECT_FALSE(spec.process(failingInput)); - - failingInput = boost::json::parse(R"JSON({ "date": "2024-01-01T29:00:00" })JSON"); - EXPECT_FALSE(spec.process(failingInput)); - - failingInput = boost::json::parse(R"JSON({ "date": "" })JSON"); - EXPECT_FALSE(spec.process(failingInput)); - - failingInput = boost::json::parse(R"JSON({ "date": 1 })JSON"); - err = spec.process(failingInput); - EXPECT_FALSE(err); - EXPECT_EQ(err.error(), xrpl::RpcInvalidParams); -} - -TEST_F(RPCBaseTest, CustomValidator) -{ - auto customFormatCheck = CustomValidator{ - [](boost::json::value const& value, std::string_view /* key */) -> MaybeError { - return value.as_string().size() == 34 ? MaybeError{} : Error{rpc::Status{"Uh oh"}}; - } - }; - - auto spec = RpcSpec{ - {"taker", customFormatCheck}, - }; - - auto passingInput = - boost::json::parse(R"JSON({ "taker": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "taker": "wrongformat" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, NotSupported) -{ - auto spec = RpcSpec{ - {"taker", Type{}, NotSupported{123}}, - {"getter", NotSupported{}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "taker": 2 })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "taker": 123 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = boost::json::parse(R"JSON({ "taker": 2, "getter": 2 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, LedgerIndexValidator) -{ - auto spec = RpcSpec{ - {"ledgerIndex", CustomValidators::ledgerIndexValidator}, - }; - auto passingInput = boost::json::parse(R"JSON({ "ledgerIndex": "validated" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - passingInput = boost::json::parse(R"JSON({ "ledgerIndex": "256" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - passingInput = boost::json::parse(R"JSON({ "ledgerIndex": 256 })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "ledgerIndex": "wrongformat" })JSON"); - auto err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "ledgerIndexMalformed"); - - failingInput = boost::json::parse(R"JSON({ "ledgerIndex": true })JSON"); - err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "ledgerIndexMalformed"); -} - -TEST_F(RPCBaseTest, AccountValidator) -{ - auto spec = RpcSpec{ - {"account", CustomValidators::accountValidator}, - }; - auto failingInput = boost::json::parse(R"JSON({ "account": 256 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = - boost::json::parse(R"JSON({ "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jp" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = boost::json::parse( - R"JSON({ "account": "02000000000000000000000000000000000000000000000000000000000000000" })JSON" - ); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = - boost::json::parse(R"JSON({ "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jp?" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - auto passingInput = - boost::json::parse(R"JSON({ "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - passingInput = boost::json::parse( - R"JSON({ "account": "020000000000000000000000000000000000000000000000000000000000000000" })JSON" - ); - ASSERT_TRUE(spec.process(passingInput)); -} - -TEST_F(RPCBaseTest, AccountBase58Validator) -{ - auto spec = RpcSpec{ - {"account", CustomValidators::accountBase58Validator}, - }; - auto failingInput = boost::json::parse(R"JSON({ "account": 256 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = - boost::json::parse(R"JSON({ "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jp" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = boost::json::parse( - R"JSON({ "account": "020000000000000000000000000000000000000000000000000000000000000000" })JSON" - ); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = - boost::json::parse(R"JSON({ "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jp?" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - auto passingInput = - boost::json::parse(R"JSON({ "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); -} - -TEST_F(RPCBaseTest, AccountMarkerValidator) -{ - auto spec = RpcSpec{ - {"marker", CustomValidators::accountMarkerValidator}, - }; - auto failingInput = boost::json::parse(R"JSON({ "marker": 256 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = boost::json::parse(R"JSON({ "marker": "testtest" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = boost::json::parse(R"JSON({ "marker": "ABAB1234:1H" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - auto passingInput = boost::json::parse(R"JSON({ "account": "ABAB1234:123" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); -} - -TEST_F(RPCBaseTest, Uint160HexStringValidator) -{ - auto const spec = RpcSpec{{"marker", CustomValidators::uint160HexStringValidator}}; - auto passingInput = - boost::json::parse(R"JSON({ "marker": "F609A18102218C75767209946A77523CBD97E225"})JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "marker": 160})JSON"); - auto err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "markerNotString"); - - failingInput = boost::json::parse( - R"JSON({ "marker": "F609A18102218C75767209946A77523CBD97E2253515BC"})JSON" - ); - err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "markerMalformed"); -} - -TEST_F(RPCBaseTest, Uint192HexStringValidator) -{ - auto const spec = RpcSpec{{"mpt_issuance_id", CustomValidators::uint192HexStringValidator}}; - auto passingInput = boost::json::parse( - R"JSON({ "mpt_issuance_id": "0000012F27A9DE73EAA1E8831FA253E19030A17E2D038198"})JSON" - ); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "mpt_issuance_id": 192})JSON"); - auto err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "mpt_issuance_idNotString"); - - failingInput = boost::json::parse( - R"JSON({ "mpt_issuance_id": "0000012F27A9DE73EAA1E8831FA253E19030A17E2D038198983515BC"})JSON" - ); - err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "mpt_issuance_idMalformed"); -} - -TEST_F(RPCBaseTest, Uint256HexStringValidator) -{ - auto const spec = RpcSpec{{"transaction", CustomValidators::uint256HexStringValidator}}; - auto passingInput = boost::json::parse( - R"JSON({ "transaction": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"})JSON" - ); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "transaction": 256})JSON"); - auto err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "transactionNotString"); - - failingInput = boost::json::parse( - R"JSON({ "transaction": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC"})JSON" - ); - err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "transactionMalformed"); -} - -TEST_F(RPCBaseTest, CurrencyValidator) -{ - auto const spec = RpcSpec{{"currency", CustomValidators::currencyValidator}}; - auto passingInput = boost::json::parse(R"JSON({ "currency": "GBP"})JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - passingInput = - boost::json::parse(R"JSON({ "currency": "0158415500000000C1F76FF6ECB0BAC600000000"})JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - passingInput = - boost::json::parse(R"JSON({ "currency": "0158415500000000c1f76ff6ecb0bac600000000"})JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - for (auto const& currency : {"[]<", ">()", "{}|", "?!@", "#$%", "^&*"}) { - passingInput = - boost::json::parse(fmt::format(R"JSON({{ "currency": "{}" }})JSON", currency)); - ASSERT_TRUE(spec.process(passingInput)); - } - - auto failingInput = boost::json::parse(R"JSON({ "currency": 256})JSON"); - auto err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "currencyNotString"); - - failingInput = boost::json::parse(R"JSON({ "currency": "12314"})JSON"); - err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "malformedCurrency"); -} - -TEST_F(RPCBaseTest, IssuerValidator) -{ - auto const spec = RpcSpec{{"issuer", CustomValidators::issuerValidator}}; - auto passingInput = - boost::json::parse(R"JSON({ "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"})JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "issuer": 256})JSON"); - auto err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "issuerNotString"); - - failingInput = boost::json::parse( - fmt::format(R"JSON({{ "issuer": "{}"}})JSON", toBase58(xrpl::noAccount())) - ); - err = spec.process(failingInput); - ASSERT_FALSE(err); -} - -TEST_F(RPCBaseTest, BookTakerValidator) -{ - auto const spec = RpcSpec{{"taker_gets", CustomValidators::bookTakerValidator}}; - - // An asset specified by currency, optionally with an issuer, is valid. - auto passingInput = boost::json::parse(R"JSON({ "taker_gets": { "currency": "XRP" }})JSON"); - EXPECT_TRUE(spec.process(passingInput)); - - passingInput = boost::json::parse( - R"JSON({ "taker_gets": { "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" }})JSON" - ); - EXPECT_TRUE(spec.process(passingInput)); - - // An asset specified by mpt_issuance_id alone is valid. - passingInput = boost::json::parse( - R"JSON({ "taker_gets": { "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405DBADD" }})JSON" - ); - EXPECT_TRUE(spec.process(passingInput)); - - // Non-object values are deferred to the type validator; the field being absent is deferred to - // the required validator. Both pass here. - passingInput = boost::json::parse(R"JSON({ "taker_gets": "not_an_object" })JSON"); - EXPECT_TRUE(spec.process(passingInput)); - - passingInput = boost::json::parse(R"JSON({ })JSON"); - EXPECT_TRUE(spec.process(passingInput)); - - // Neither currency nor mpt_issuance_id -> invalidParams, reporting the currency field. - auto failingInput = boost::json::parse(R"JSON({ "taker_gets": { }})JSON"); - auto err = spec.process(failingInput); - ASSERT_FALSE(err); - EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams}); - EXPECT_EQ(err.error().message, "Missing field 'taker_gets.currency'."); - - // currency and mpt_issuance_id are mutually exclusive. - failingInput = boost::json::parse( - R"JSON({ "taker_gets": { "currency": "USD", "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405DBADD" }})JSON" - ); - err = spec.process(failingInput); - ASSERT_FALSE(err); - EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams}); - EXPECT_EQ(err.error().message, "Invalid field 'taker_gets'."); - - // mpt_issuance_id must not be combined with an issuer. - failingInput = boost::json::parse( - R"JSON({ "taker_gets": { "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405DBADD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" }})JSON" - ); - err = spec.process(failingInput); - ASSERT_FALSE(err); - EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams}); - EXPECT_EQ(err.error().message, "Invalid field 'taker_gets'."); - - // A present-but-non-string currency -> expectedFieldError against '.currency'. - failingInput = boost::json::parse(R"JSON({ "taker_gets": { "currency": 123 }})JSON"); - err = spec.process(failingInput); - ASSERT_FALSE(err); - EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams}); - EXPECT_EQ(err.error().message, "Invalid field 'taker_gets.currency', not string."); - - // A present-but-non-string mpt_issuance_id is reported the same way (against '.currency', - // matching xrpld's validateTakerJSON). - failingInput = boost::json::parse(R"JSON({ "taker_gets": { "mpt_issuance_id": 123 }})JSON"); - err = spec.process(failingInput); - ASSERT_FALSE(err); - EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams}); - EXPECT_EQ(err.error().message, "Invalid field 'taker_gets.currency', not string."); -} - -TEST_F(RPCBaseTest, BookTakerValidatorUsesFieldKey) -{ - // The error messages must reflect the validated field's key (taker_pays vs taker_gets). - auto const spec = RpcSpec{{"taker_pays", CustomValidators::bookTakerValidator}}; - - auto failingInput = boost::json::parse(R"JSON({ "taker_pays": { }})JSON"); - auto err = spec.process(failingInput); - ASSERT_FALSE(err); - EXPECT_EQ(err.error().message, "Missing field 'taker_pays.currency'."); - - failingInput = boost::json::parse( - R"JSON({ "taker_pays": { "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405DBADD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" }})JSON" - ); - err = spec.process(failingInput); - ASSERT_FALSE(err); - EXPECT_EQ(err.error().message, "Invalid field 'taker_pays'."); -} - -TEST_F(RPCBaseTest, SubscribeStreamValidator) -{ - auto const spec = RpcSpec{{"streams", CustomValidators::subscribeStreamValidator}}; - auto passingInput = boost::json::parse( - R"JSON({ - "streams": [ - "ledger", - "transactions_proposed", - "validations", - "transactions", - "manifests", - "transactions", - "book_changes" - ] - })JSON" - ); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "streams": 256})JSON"); - auto err = spec.process(failingInput); - ASSERT_FALSE(err); - - failingInput = boost::json::parse(R"JSON({ "streams": ["test"]})JSON"); - err = spec.process(failingInput); - ASSERT_FALSE(err); - - failingInput = boost::json::parse(R"JSON({ "streams": [123]})JSON"); - err = spec.process(failingInput); - ASSERT_FALSE(err); -} - -TEST_F(RPCBaseTest, SubscribeAccountsValidator) -{ - auto const spec = RpcSpec{{"accounts", CustomValidators::subscribeAccountsValidator}}; - auto passingInput = boost::json::parse( - R"JSON({ "accounts": ["rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"]})JSON" - ); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "accounts": 256})JSON"); - auto err = spec.process(failingInput); - ASSERT_FALSE(err); - - failingInput = boost::json::parse(R"JSON({ "accounts": ["test"]})JSON"); - err = spec.process(failingInput); - ASSERT_FALSE(err); - - failingInput = boost::json::parse(R"JSON({ "accounts": [123]})JSON"); - err = spec.process(failingInput); - ASSERT_FALSE(err); -} - -TEST_F(RPCBaseTest, ClampingModifier) -{ - auto spec = RpcSpec{ - {"amount", Clamp{10u, 20u}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "amount": 15 })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto passingInput2 = boost::json::parse(R"JSON({ "amount": 5 })JSON"); - ASSERT_TRUE(spec.process(passingInput2)); - ASSERT_EQ(passingInput2.at("amount").as_uint64(), 10u); // clamped - - auto passingInput3 = boost::json::parse(R"JSON({ "amount": 25 })JSON"); - ASSERT_TRUE(spec.process(passingInput3)); - ASSERT_EQ(passingInput3.at("amount").as_uint64(), 20u); // clamped -} - -TEST_F(RPCBaseTest, ToLowerModifier) -{ - auto spec = RpcSpec{ - {"str", ToLower{}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "str": "TesT" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - ASSERT_EQ(passingInput.at("str").as_string(), "test"); - - auto passingInput2 = boost::json::parse(R"JSON({ "str2": "TesT" })JSON"); - ASSERT_TRUE(spec.process(passingInput2)); // no str no problem - - auto passingInput3 = boost::json::parse(R"JSON({ "str": "already lower case" })JSON"); - ASSERT_TRUE(spec.process(passingInput3)); - ASSERT_EQ(passingInput3.at("str").as_string(), "already lower case"); - - auto passingInput4 = boost::json::parse(R"JSON({ "str": "" })JSON"); - ASSERT_TRUE(spec.process(passingInput4)); // empty str no problem - ASSERT_EQ(passingInput4.at("str").as_string(), ""); -} - -TEST_F(RPCBaseTest, ToNumberModifier) -{ - auto const spec = RpcSpec{ - {"str", ToNumber{}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "str": [] })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - passingInput = boost::json::parse(R"JSON({ "str2": "TesT" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - passingInput = boost::json::parse(R"JSON([])JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - passingInput = boost::json::parse(R"JSON({ "str": "123" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - ASSERT_EQ(passingInput.at("str").as_int64(), 123); - - auto failingInput = boost::json::parse(R"JSON({ "str": "ok" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = boost::json::parse(R"JSON({ "str": "123.123" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, CustomModifier) -{ - testing::StrictMock< - testing::MockFunction> - mockModifier; - auto const customModifier = CustomModifier{mockModifier.AsStdFunction()}; - auto const spec = RpcSpec{ - {"str", customModifier}, - }; - - EXPECT_CALL(mockModifier, Call).WillOnce(testing::Return(MaybeError{})); - auto passingInput = boost::json::parse(R"JSON({ "str": "sss" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - passingInput = boost::json::parse(R"JSON({ "strNotExist": 123 })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - // not a json object - passingInput = boost::json::parse(R"JSON([])JSON"); - ASSERT_TRUE(spec.process(passingInput)); -} diff --git a/tests/unit/rpc/ForwardingProxyTests.cpp b/tests/unit/rpc/ForwardingProxyTests.cpp index a8cac87ea..87927d50d 100644 --- a/tests/unit/rpc/ForwardingProxyTests.cpp +++ b/tests/unit/rpc/ForwardingProxyTests.cpp @@ -119,6 +119,17 @@ generateTestValuesForParametersTest() .called = 1, .isAdmin = isAdmin, .expected = shouldForward}, + // The isClioOnly check short-circuits before the current/closed check, so a Clio-only + // method is NOT forwarded even when it names a ledger Clio does not hold. Its handler has + // to reject the shortcut itself - see getLedgerHeaderFromLedgerSpecifier. + {.testName = "ShouldForwardReturnsFalseIfClioOnlyEvenWithCurrentLedger", + .apiVersion = 2u, + .method = "nft_info", + .testJson = R"JSON({"ledger_index": "current"})JSON", + .mockedIsClioOnly = isClioOnly, + .called = 1, + .isAdmin = !isAdmin, + .expected = !shouldForward}, {.testName = "ShouldForwardReturnsTrueIfCurrentLedgerSpecified", .apiVersion = 2u, .method = "anymethod", diff --git a/tests/unit/rpc/JsonBoolTests.cpp b/tests/unit/rpc/JsonBoolTests.cpp deleted file mode 100644 index 95a546cb3..000000000 --- a/tests/unit/rpc/JsonBoolTests.cpp +++ /dev/null @@ -1,82 +0,0 @@ -#include "rpc/common/JsonBool.hpp" -#include "util/NameGenerator.hpp" - -#include -#include -#include - -#include -#include - -using namespace rpc; -using namespace testing; - -struct JsonBoolTestsCaseBundle { - std::string testName; - std::string json; - bool expectedBool; -}; - -class JsonBoolTests : public TestWithParam { -public: - static auto - generateTestValuesForParametersTest() - { - return std::vector{ - {.testName = "NullValue", - .json = R"JSON({ "test_bool": null })JSON", - .expectedBool = false}, - {.testName = "BoolTrueValue", - .json = R"JSON({ "test_bool": true })JSON", - .expectedBool = true}, - {.testName = "BoolFalseValue", - .json = R"JSON({ "test_bool": false })JSON", - .expectedBool = false}, - {.testName = "IntTrueValue", - .json = R"JSON({ "test_bool": 1 })JSON", - .expectedBool = true}, - {.testName = "IntFalseValue", - .json = R"JSON({ "test_bool": 0 })JSON", - .expectedBool = false}, - {.testName = "DoubleTrueValue", - .json = R"JSON({ "test_bool": 0.1 })JSON", - .expectedBool = true}, - {.testName = "DoubleFalseValue", - .json = R"JSON({ "test_bool": 0.0 })JSON", - .expectedBool = false}, - {.testName = "StringTrueValue", - .json = R"JSON({ "test_bool": "true" })JSON", - .expectedBool = true}, - {.testName = "StringFalseValue", - .json = R"JSON({ "test_bool": "false" })JSON", - .expectedBool = true}, - {.testName = "ArrayTrueValue", - .json = R"JSON({ "test_bool": [0] })JSON", - .expectedBool = true}, - {.testName = "ArrayFalseValue", - .json = R"JSON({ "test_bool": [] })JSON", - .expectedBool = false}, - {.testName = "ObjectTrueValue", - .json = R"JSON({ "test_bool": { "key": null } })JSON", - .expectedBool = true}, - {.testName = "ObjectFalseValue", - .json = R"JSON({ "test_bool": {} })JSON", - .expectedBool = false} - }; - } -}; - -INSTANTIATE_TEST_CASE_P( - JsonBoolCheckGroup, - JsonBoolTests, - ValuesIn(JsonBoolTests::generateTestValuesForParametersTest()), - tests::util::kNameGenerator -); - -TEST_P(JsonBoolTests, Parse) -{ - auto const testBundle = GetParam(); - auto const jv = boost::json::parse(testBundle.json).as_object(); - ASSERT_TRUE(jv.contains("test_bool")); - EXPECT_EQ(testBundle.expectedBool, value_to(jv.at("test_bool")).value); -} diff --git a/tests/unit/rpc/RPCEngineTests.cpp b/tests/unit/rpc/RPCEngineTests.cpp index 840e05c5c..0bfae0b62 100644 --- a/tests/unit/rpc/RPCEngineTests.cpp +++ b/tests/unit/rpc/RPCEngineTests.cpp @@ -220,12 +220,12 @@ TEST_P(RPCEngineFlowParameterTest, Test) } else { if (testBundle.handlerReturnError) { EXPECT_CALL(*handlerProvider, getHandler) - .WillOnce(Return(AnyHandler{tests::common::FailingHandlerFake{}})); + .WillOnce(Return(AnyHandler{tests::common::FailingTypedHandlerFake{}})); EXPECT_CALL(*mockCountersPtr_, rpcErrored(testBundle.method)); EXPECT_CALL(*handlerProvider, contains(testBundle.method)).WillOnce(Return(true)); } else { EXPECT_CALL(*handlerProvider, getHandler(testBundle.method)) - .WillOnce(Return(AnyHandler{tests::common::HandlerFake{}})); + .WillOnce(Return(AnyHandler{tests::common::TypedHandlerFake{}})); } } } @@ -261,7 +261,7 @@ TEST_F(RPCEngineTest, ThrowDatabaseError) ); EXPECT_CALL(*backend_, isTooBusy).WillOnce(Return(false)); EXPECT_CALL(*handlerProvider, getHandler(method)) - .WillOnce(Return(AnyHandler{tests::common::FailingHandlerFake{}})); + .WillOnce(Return(AnyHandler{tests::common::FailingTypedHandlerFake{}})); EXPECT_CALL(*mockCountersPtr_, rpcErrored(method)).WillOnce(Throw(data::DatabaseError{})); EXPECT_CALL(*handlerProvider, contains(method)).WillOnce(Return(true)); EXPECT_CALL(*mockCountersPtr_, onTooBusy()); @@ -293,7 +293,7 @@ TEST_F(RPCEngineTest, ThrowException) ); EXPECT_CALL(*backend_, isTooBusy).WillOnce(Return(false)); EXPECT_CALL(*handlerProvider, getHandler(method)) - .WillOnce(Return(AnyHandler{tests::common::FailingHandlerFake{}})); + .WillOnce(Return(AnyHandler{tests::common::FailingTypedHandlerFake{}})); EXPECT_CALL(*mockCountersPtr_, rpcErrored(method)).WillOnce(Throw(std::exception{})); EXPECT_CALL(*handlerProvider, contains(method)).WillOnce(Return(true)); EXPECT_CALL(*mockCountersPtr_, onInternalError()); @@ -486,7 +486,7 @@ TEST_F(RPCEngineTest, NonBareRequestBypassesCache) EXPECT_CALL(*backend_, isTooBusy).Times(callTime).WillRepeatedly(Return(false)); EXPECT_CALL(*handlerProvider, getHandler) .Times(callTime) - .WillRepeatedly(Return(AnyHandler{tests::common::HandlerFake{}})); + .WillRepeatedly(Return(AnyHandler{tests::common::TypedHandlerFake{}})); while (callTime-- != 0) { runSpawn([&](auto yield) { @@ -538,7 +538,7 @@ TEST_F(RPCEngineTest, NotCacheIfErrorHappen) EXPECT_CALL(*backend_, isTooBusy).Times(callTime).WillRepeatedly(Return(false)); EXPECT_CALL(*handlerProvider, getHandler) .Times(callTime) - .WillRepeatedly(Return(AnyHandler{tests::common::FailingHandlerFake{}})); + .WillRepeatedly(Return(AnyHandler{tests::common::FailingTypedHandlerFake{}})); EXPECT_CALL(*mockCountersPtr_, rpcErrored(method)).Times(callTime); EXPECT_CALL(*handlerProvider, isClioOnly).Times(callTime).WillRepeatedly(Return(false)); EXPECT_CALL(*handlerProvider, contains).Times(callTime).WillRepeatedly(Return(true)); diff --git a/tests/unit/rpc/RPCHelpersTests.cpp b/tests/unit/rpc/RPCHelpersTests.cpp index 990e00286..f1ace39a2 100644 --- a/tests/unit/rpc/RPCHelpersTests.cpp +++ b/tests/unit/rpc/RPCHelpersTests.cpp @@ -7,7 +7,6 @@ #include "util/AsioContextTestFixture.hpp" #include "util/LoggerFixtures.hpp" #include "util/MockAmendmentCenter.hpp" -#include "util/MockAssert.hpp" #include "util/MockBackendTestFixture.hpp" #include "util/MockPrometheus.hpp" #include "util/NameGenerator.hpp" @@ -707,8 +706,6 @@ TEST_F(RPCHelpersTest, ParseBookBadMarket) auto const book = rpc::parseBook(usd, usd, std::nullopt); ASSERT_FALSE(book.has_value()); EXPECT_TRUE(book.error().code == CombinedError{RippledError::RpcBadMarket}); - // badMarket carries no explicit message; it renders from the code's default, matching - // rippled's "No such market.". EXPECT_EQ(rpc::makeError(book.error()).at("error_message").as_string(), "No such market."); } @@ -720,8 +717,6 @@ TEST_F(RPCHelpersTest, ParseBookBadMarket) auto const book = rpc::parseBook(mpt, mpt, std::nullopt); ASSERT_FALSE(book.has_value()); EXPECT_TRUE(book.error().code == CombinedError{RippledError::RpcBadMarket}); - // badMarket carries no explicit message; it renders from the code's default, matching - // rippled's "No such market.". EXPECT_EQ(rpc::makeError(book.error()).at("error_message").as_string(), "No such market."); } } @@ -774,8 +769,6 @@ TEST_F(RPCHelpersTest, ParseBookCurrencyOverloadDelegates) ); ASSERT_FALSE(book.has_value()); EXPECT_TRUE(book.error().code == CombinedError{RippledError::RpcBadMarket}); - // badMarket carries no explicit message; it renders from the code's default, matching - // rippled's "No such market.". EXPECT_EQ(rpc::makeError(book.error()).at("error_message").as_string(), "No such market."); } } @@ -837,110 +830,6 @@ TEST_F(RPCHelpersTest, FetchAndCheckAnyFlagExists_TrustLineIsFrozenAndCheckFreez }); } -TEST_F(RPCHelpersTest, ParseDelegateType) -{ - auto result = parseDelegateType(boost::json::value("authorizer")); - ASSERT_TRUE(result.has_value()); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(*result, DelegateFilter::Role::Authorizer); - - result = parseDelegateType(boost::json::value("actor")); - ASSERT_TRUE(result.has_value()); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(*result, DelegateFilter::Role::Actor); - - // invalid types - result = parseDelegateType(boost::json::value("invalid_type")); - EXPECT_FALSE(result.has_value()); - - result = parseDelegateType(boost::json::value(123)); - EXPECT_FALSE(result.has_value()); - - result = parseDelegateType(boost::json::value(true)); - EXPECT_FALSE(result.has_value()); -} - -TEST_F(RPCHelpersTest, ParseDelegateFilter_Success) -{ - // only delegate agent is valid - { - auto const jsonStr = R"JSON({ - "delegate_filter": "authorizer" - })JSON"; - auto const json = boost::json::parse(jsonStr).as_object(); - - auto const result = parseDelegateFilter(json); - ASSERT_TRUE(result.has_value()); - // NOLINTBEGIN(bugprone-unchecked-optional-access) - EXPECT_EQ(result->delegateType, DelegateFilter::Role::Authorizer); - EXPECT_FALSE(result->counterParty.has_value()); - // NOLINTEND(bugprone-unchecked-optional-access) - } - - // delegate agent + counter_party is valid - { - auto const jsonStr = fmt::format( - R"JSON({{ - "delegate_filter": "actor", - "counter_party": "{}" - }})JSON", - kAccount2 - ); - auto const json = boost::json::parse(jsonStr).as_object(); - - auto const result = parseDelegateFilter(json); - ASSERT_TRUE(result.has_value()); - // NOLINTBEGIN(bugprone-unchecked-optional-access) - EXPECT_EQ(result->delegateType, DelegateFilter::Role::Actor); - ASSERT_TRUE(result->counterParty.has_value()); - EXPECT_EQ(*result->counterParty, kAccount2); - // NOLINTEND(bugprone-unchecked-optional-access) - } -} - -TEST_F(RPCHelpersTest, ParseDelegateFilter_Failures) -{ - // Missing required "delegate_filter" key - { - auto const jsonStr = fmt::format( - R"JSON({{ - "counter_party": "{}" - }})JSON", - kAccount2 - ); - auto const json = boost::json::parse(jsonStr).as_object(); - EXPECT_FALSE(parseDelegateFilter(json).has_value()); - } - - // "delegate_filter" is not a string (it's an integer) - { - auto const jsonStr = R"JSON({ - "delegate_filter": 123 - })JSON"; - auto const json = boost::json::parse(jsonStr).as_object(); - EXPECT_FALSE(parseDelegateFilter(json).has_value()); - } - - // "delegate_filter" is a string but invalid value - { - auto const jsonStr = R"JSON({ - "delegate_filter": "random_string" - })JSON"; - auto const json = boost::json::parse(jsonStr).as_object(); - EXPECT_FALSE(parseDelegateFilter(json).has_value()); - } - - // "counter_party" exists but is not a string (it's a number) - { - auto const jsonStr = R"JSON({ - "delegate_filter": "authorizer", - "counter_party": 9999 - })JSON"; - auto const json = boost::json::parse(jsonStr).as_object(); - EXPECT_FALSE(parseDelegateFilter(json).has_value()); - } -} - namespace { xrpl::MPTIssue @@ -2155,43 +2044,40 @@ TEST_F(RPCHelpersTest, LedgerHeaderFromSpecifierValidatedUsesMaxSeq) }); } -struct RPCHelpersAssertTest : RPCHelpersTest, common::util::WithMockAssert {}; - -TEST_F(RPCHelpersAssertTest, LedgerHeaderFromSpecifierCurrentAsserts) +// `current` and `closed` name ledgers Clio does not hold. Most methods never reach here because +// ForwardingProxy diverts them to xrpld, but it skips that check for Clio-only methods, so the +// resolver has to reject the shortcut rather than treat it as unreachable. +TEST_F(RPCHelpersTest, LedgerHeaderFromSpecifierCurrentRejected) { EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(0); runSpawn([&, this](auto yield) { - EXPECT_CLIO_ASSERT_FAIL_WITH_MESSAGE( - { - [[maybe_unused]] auto const res = getLedgerHeaderFromLedgerSpecifier( - *backend_, - yield, - rpc::spec::LedgerSpecifier{rpc::spec::LedgerShortcut::Current}, - kSpecifierRangeMax - ); - }, - "must be forwarded before dispatch" + auto const res = getLedgerHeaderFromLedgerSpecifier( + *backend_, + yield, + rpc::spec::LedgerSpecifier{rpc::spec::LedgerShortcut::Current}, + kSpecifierRangeMax ); + ASSERT_FALSE(res.has_value()); + EXPECT_EQ(res.error().code, rpc::CombinedError{rpc::RippledError::RpcInvalidParams}); + EXPECT_EQ(res.error().message, "ledgerIndexMalformed"); }); } -TEST_F(RPCHelpersAssertTest, LedgerHeaderFromSpecifierClosedAsserts) +TEST_F(RPCHelpersTest, LedgerHeaderFromSpecifierClosedRejected) { EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(0); runSpawn([&, this](auto yield) { - EXPECT_CLIO_ASSERT_FAIL_WITH_MESSAGE( - { - [[maybe_unused]] auto const res = getLedgerHeaderFromLedgerSpecifier( - *backend_, - yield, - rpc::spec::LedgerSpecifier{rpc::spec::LedgerShortcut::Closed}, - kSpecifierRangeMax - ); - }, - "must be forwarded before dispatch" + auto const res = getLedgerHeaderFromLedgerSpecifier( + *backend_, + yield, + rpc::spec::LedgerSpecifier{rpc::spec::LedgerShortcut::Closed}, + kSpecifierRangeMax ); + ASSERT_FALSE(res.has_value()); + EXPECT_EQ(res.error().code, rpc::CombinedError{rpc::RippledError::RpcInvalidParams}); + EXPECT_EQ(res.error().message, "ledgerIndexMalformed"); }); } diff --git a/tests/unit/rpc/common/CheckersTests.cpp b/tests/unit/rpc/common/CheckersTests.cpp deleted file mode 100644 index a9c99d899..000000000 --- a/tests/unit/rpc/common/CheckersTests.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "rpc/common/Checkers.hpp" - -#include -#include -#include - -#include - -using namespace rpc; -using namespace rpc::check; - -struct DeprecatedTests : ::testing::Test { - boost::json::value const json{ - {"some_string", "some_value"}, - {"some_number", 42}, - {"some_bool", false}, - {"some_float", 3.14} - }; -}; - -TEST_F(DeprecatedTests, Field) -{ - auto warning = Deprecated<>::check(json, "some_string"); - ASSERT_TRUE(warning.has_value()); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(warning->warningCode, WarningCode::WarnRpcDeprecated); - - warning = Deprecated<>::check(json, "other"); - EXPECT_FALSE(warning.has_value()); -} - -TEST_F(DeprecatedTests, FieldWithStringValue) -{ - Deprecated const checker{"some_value"}; - auto warning = checker.check(json, "some_string"); - ASSERT_TRUE(warning.has_value()); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(warning->warningCode, WarningCode::WarnRpcDeprecated); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(warning->extraMessage, "Value 'some_value' for field 'some_string' is deprecated"); - EXPECT_FALSE(Deprecated{"other"}.check(json, "some_string")); -} - -TEST_F(DeprecatedTests, FieldWithIntValue) -{ - Deprecated const checker{42}; - auto warning = checker.check(json, "some_number"); - ASSERT_TRUE(warning.has_value()); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(warning->warningCode, WarningCode::WarnRpcDeprecated); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(warning->extraMessage, "Value '42' for field 'some_number' is deprecated"); - EXPECT_FALSE(Deprecated{43}.check(json, "some_number")); -} - -TEST_F(DeprecatedTests, FieldWithBoolValue) -{ - Deprecated const checker{false}; - auto warning = checker.check(json, "some_bool"); - ASSERT_TRUE(warning.has_value()); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(warning->warningCode, WarningCode::WarnRpcDeprecated); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(warning->extraMessage, "Value 'false' for field 'some_bool' is deprecated"); - EXPECT_FALSE(Deprecated{true}.check(json, "some_bool")); -} - -TEST_F(DeprecatedTests, FieldWithFloatValue) -{ - Deprecated const checker{3.14}; - auto warning = checker.check(json, "some_float"); - ASSERT_TRUE(warning.has_value()); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(warning->warningCode, WarningCode::WarnRpcDeprecated); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(warning->extraMessage, "Value '3.14' for field 'some_float' is deprecated"); - EXPECT_FALSE(Deprecated{3.15}.check(json, "some_float")); -} diff --git a/tests/unit/rpc/common/SpecsTests.cpp b/tests/unit/rpc/common/SpecsTests.cpp deleted file mode 100644 index 620a22b7c..000000000 --- a/tests/unit/rpc/common/SpecsTests.cpp +++ /dev/null @@ -1,284 +0,0 @@ -#include "rpc/common/Checkers.hpp" -#include "rpc/common/Specs.hpp" -#include "rpc/common/Types.hpp" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -using namespace rpc; - -using testing::StrictMock; - -struct SpecsTests : testing::Test { - struct RequirementMock { - MOCK_METHOD(MaybeError, verify, (boost::json::value const&, std::string)); - }; - - struct RequirementMockRef { - RequirementMockRef(StrictMock& ref) : ref(ref) - { - } - MaybeError - verify(boost::json::value& value, std::string key) const - { - return ref.verify(value, key); - } - - StrictMock& ref; - }; - - struct CheckMock { - MOCK_METHOD(std::optional, check, (boost::json::value const&, std::string)); - }; - - struct CheckMockRef { - CheckMockRef(StrictMock& ref) : ref(ref) - { - } - - [[nodiscard]] std::optional - check(boost::json::value const& value, std::string key) const - { - return ref.check(value, key); - } - - StrictMock& ref; - }; - - StrictMock requirementMock; - StrictMock anotherRequirementMock; - StrictMock checkMock; - StrictMock anotherCheckMock; -}; - -struct ProcessorTestBundle { - std::string name; - MaybeError requirementResult; - std::optional otherRequirementResult; - MaybeError expectedResult; -}; - -struct FieldProcessorTests : SpecsTests, testing::WithParamInterface { -protected: - FieldSpec spec_{ - "key", - RequirementMockRef(requirementMock), - RequirementMockRef(anotherRequirementMock) - }; - boost::json::value json_; -}; - -INSTANTIATE_TEST_SUITE_P( - FieldSpecTestGroup, - FieldProcessorTests, - testing::Values( - ProcessorTestBundle{"NoErrors", MaybeError{}, MaybeError{}, MaybeError{}}, - ProcessorTestBundle{ - "FirstError", - Error{Status{"error1"}}, - std::nullopt, - Error{Status{"error1"}}, - }, - ProcessorTestBundle{ - "SecondError", - MaybeError{}, - Error{Status{"error2"}}, - Error{Status{"error2"}}, - } - ), - [](testing::TestParamInfo const& info) { return info.param.name; } -); - -TEST_P(FieldProcessorTests, FieldSpecWithRequirementProcess) -{ - EXPECT_CALL(requirementMock, verify).WillOnce(testing::Return(GetParam().requirementResult)); - if (GetParam().otherRequirementResult.has_value()) { - EXPECT_CALL(anotherRequirementMock, verify) - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - .WillOnce(testing::Return(GetParam().otherRequirementResult.value())); - } - - auto const result = spec_.process(json_); - EXPECT_EQ(result, GetParam().expectedResult); -} - -TEST_F(FieldProcessorTests, FieldSpecWithRequirementCheck) -{ - auto const result = spec_.check(json_); - EXPECT_EQ(result, check::Warnings{}); -} - -struct FieldCheckerTestBundle { - std::string name; - std::optional checkResult; - std::optional otherCheckResult; - check::Warnings expectedWarnings; -}; - -struct FieldCheckerTests : SpecsTests, testing::WithParamInterface { -protected: - FieldSpec spec_{"key", CheckMockRef(checkMock), CheckMockRef(anotherCheckMock)}; - boost::json::value json_; -}; - -INSTANTIATE_TEST_SUITE_P( - FieldSpecTestGroup, - FieldCheckerTests, - testing::Values( - FieldCheckerTestBundle{"NoWarnings", std::nullopt, std::nullopt, check::Warnings{}}, - FieldCheckerTestBundle{ - "FirstWarning", - check::Warning{WarningCode::WarnUnknown, "error1"}, - std::nullopt, - check::Warnings{check::Warning{WarningCode::WarnUnknown, "error1"}} - }, - FieldCheckerTestBundle{ - "SecondWarning", - std::nullopt, - check::Warning{WarningCode::WarnUnknown, "error2"}, - check::Warnings{check::Warning{WarningCode::WarnUnknown, "error2"}} - }, - FieldCheckerTestBundle{ - "BothWarnings", - check::Warning{WarningCode::WarnUnknown, "error1"}, - check::Warning{WarningCode::WarnUnknown, "error2"}, - check::Warnings{ - check::Warning{WarningCode::WarnUnknown, "error1"}, - check::Warning{WarningCode::WarnUnknown, "error2"} - } - } - ), - [](testing::TestParamInfo const& info) { return info.param.name; } -); - -TEST_F(FieldCheckerTests, FieldSpecWithCheckProcess) -{ - auto const result = spec_.process(json_); - EXPECT_EQ(result, MaybeError{}); -} - -TEST_P(FieldCheckerTests, FieldSpecWithCheck) -{ - EXPECT_CALL(checkMock, check).WillOnce(testing::Return(GetParam().checkResult)); - EXPECT_CALL(anotherCheckMock, check).WillOnce(testing::Return(GetParam().otherCheckResult)); - auto const result = spec_.check(json_); - EXPECT_EQ(result, GetParam().expectedWarnings); -} - -struct RpcSpecProcessTests : SpecsTests, testing::WithParamInterface { - RpcSpec spec{ - {"key1", RequirementMockRef(requirementMock)}, - {"key2", RequirementMockRef(anotherRequirementMock)} - }; - boost::json::value json; -}; - -INSTANTIATE_TEST_SUITE_P( - RpcSpecProcessTestGroup, - RpcSpecProcessTests, - testing::Values( - ProcessorTestBundle{"NoErrors", MaybeError{}, MaybeError{}, MaybeError{}}, - ProcessorTestBundle{ - "FirstError", - Error{Status{"error1"}}, - std::nullopt, - Error{Status{"error1"}}, - }, - ProcessorTestBundle{ - "SecondError", - MaybeError{}, - Error{Status{"error2"}}, - Error{Status{"error2"}}, - } - ), - [](testing::TestParamInfo const& info) { return info.param.name; } -); - -TEST_P(RpcSpecProcessTests, Process) -{ - EXPECT_CALL(requirementMock, verify).WillOnce(testing::Return(GetParam().requirementResult)); - if (GetParam().otherRequirementResult.has_value()) { - EXPECT_CALL(anotherRequirementMock, verify) - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - .WillOnce(testing::Return(GetParam().otherRequirementResult.value())); - } - auto const result = spec.process(json); - EXPECT_EQ(result, GetParam().expectedResult); -} - -struct RpcSpecCheckTestBundle { - std::string name; - std::optional checkResult; - std::optional otherCheckResult; - std::unordered_map> expectedWarnings; -}; - -struct RpcSpecCheckTests : SpecsTests, testing::WithParamInterface { -protected: - RpcSpec spec_{{"key1", CheckMockRef(checkMock)}, {"key2", CheckMockRef(anotherCheckMock)}}; - boost::json::value json_; -}; - -INSTANTIATE_TEST_SUITE_P( - RpcSpecCheckTestGroup, - RpcSpecCheckTests, - testing::Values( - RpcSpecCheckTestBundle{"NoWarnings", std::nullopt, std::nullopt, {}}, - RpcSpecCheckTestBundle{ - "FirstWarning", - check::Warning{WarningCode::WarnUnknown, "error1"}, - std::nullopt, - {{WarningCode::WarnUnknown, {"error1"}}} - }, - RpcSpecCheckTestBundle{ - "SecondWarning", - std::nullopt, - check::Warning{WarningCode::WarnUnknown, "error2"}, - {{WarningCode::WarnUnknown, {"error2"}}} - }, - RpcSpecCheckTestBundle{ - "BothWarnings", - check::Warning{WarningCode::WarnUnknown, "error1"}, - check::Warning{WarningCode::WarnUnknown, "error2"}, - {{WarningCode::WarnUnknown, {"error1", "error2"}}} - }, - RpcSpecCheckTestBundle{ - "DifferentWarningCodes", - check::Warning{WarningCode::WarnUnknown, "error1"}, - check::Warning{WarningCode::WarnRpcClio, "error2"}, - {{WarningCode::WarnUnknown, {"error1"}}, {WarningCode::WarnRpcClio, {"error2"}}} - } - - ), - [](testing::TestParamInfo const& info) { return info.param.name; } -); - -TEST_P(RpcSpecCheckTests, Check) -{ - EXPECT_CALL(checkMock, check).WillOnce(testing::Return(GetParam().checkResult)); - EXPECT_CALL(anotherCheckMock, check).WillOnce(testing::Return(GetParam().otherCheckResult)); - - auto const result = spec_.check(json_); - ASSERT_EQ(result.size(), GetParam().expectedWarnings.size()); - for (auto const& entry : result) { - ASSERT_TRUE(entry.is_object()); - auto const& object = entry.as_object(); - ASSERT_TRUE(object.contains("id")); - ASSERT_TRUE(object.at("id").is_int64()); - ASSERT_TRUE(object.contains("message")); - ASSERT_TRUE(object.at("message").is_string()); - auto it = GetParam().expectedWarnings.find(object.at("id").as_int64()); - ASSERT_NE(it, GetParam().expectedWarnings.end()); - for (auto const& message : it->second) { - EXPECT_NE(object.at("message").as_string().find(message), std::string::npos); - } - } -} diff --git a/tests/unit/rpc/filters/impl/DelegateTransactionsFilterTests.cpp b/tests/unit/rpc/filters/impl/DelegateTransactionsFilterTests.cpp index 157388e48..86c297167 100644 --- a/tests/unit/rpc/filters/impl/DelegateTransactionsFilterTests.cpp +++ b/tests/unit/rpc/filters/impl/DelegateTransactionsFilterTests.cpp @@ -1,5 +1,5 @@ #include "data/Types.hpp" -#include "rpc/common/Types.hpp" +#include "rpc/filters/TransactionFilter.hpp" #include "rpc/filters/impl/DelegateTransactionsFilter.hpp" #include "util/TestObject.hpp" @@ -36,7 +36,9 @@ protected: TEST_F(DelegateTransactionFilterTest, ReturnsFalseIfNoDelegateField) { - DelegateFilter const filterParams{DelegateFilter::Role::Authorizer}; + DelegateFilter const filterParams{ + .delegateType = DelegateFilter::Role::Authorizer, .counterParty = std::nullopt + }; DelegateTransactionFilter const filter(filterParams, kAccountOwner); // Create standard tx (no delegate field) using standard TestObject helper @@ -57,7 +59,9 @@ TEST_F(DelegateTransactionFilterTest, ReturnsFalseIfNoDelegateField) TEST_F(DelegateTransactionFilterTest, RoleAuthorizer_MatchesWhenUserIsSigner) { - DelegateFilter const filterParams{DelegateFilter::Role::Authorizer}; + DelegateFilter const filterParams{ + .delegateType = DelegateFilter::Role::Authorizer, .counterParty = std::nullopt + }; DelegateTransactionFilter const filter(filterParams, kAccountDelegator); auto blob = createBlob(to_string(kAccountOwner), to_string(kAccountDelegator)); @@ -70,7 +74,9 @@ TEST_F(DelegateTransactionFilterTest, RoleAuthorizer_MatchesWhenUserIsSigner) TEST_F(DelegateTransactionFilterTest, RoleAuthorizer_FailsWhenUserIsNotSigner) { - DelegateFilter const filterParams{DelegateFilter::Role::Authorizer}; + DelegateFilter const filterParams{ + .delegateType = DelegateFilter::Role::Authorizer, .counterParty = std::nullopt + }; DelegateTransactionFilter const filter(filterParams, kAccountDestination); auto blob = createBlob(to_string(kAccountOwner), to_string(kAccountDelegator)); @@ -81,7 +87,9 @@ TEST_F(DelegateTransactionFilterTest, RoleAuthorizer_FailsWhenUserIsNotSigner) TEST_F(DelegateTransactionFilterTest, RoleAuthorizer_WithCounterparty_Match) { - DelegateFilter const filterParams{DelegateFilter::Role::Authorizer, to_string(kAccountOwner)}; + DelegateFilter const filterParams{ + .delegateType = DelegateFilter::Role::Authorizer, .counterParty = to_string(kAccountOwner) + }; DelegateTransactionFilter const filter(filterParams, kAccountDelegator); auto blob = createBlob(to_string(kAccountOwner), to_string(kAccountDelegator)); @@ -95,7 +103,8 @@ TEST_F(DelegateTransactionFilterTest, RoleAuthorizer_WithCounterparty_Match) TEST_F(DelegateTransactionFilterTest, RoleAuthorizer_WithCounterparty_Mismatch) { DelegateFilter const filterParams{ - DelegateFilter::Role::Authorizer, to_string(kAccountDestination) + .delegateType = DelegateFilter::Role::Authorizer, + .counterParty = to_string(kAccountDestination) }; DelegateTransactionFilter const filter(filterParams, kAccountDelegator); @@ -107,7 +116,9 @@ TEST_F(DelegateTransactionFilterTest, RoleAuthorizer_WithCounterparty_Mismatch) TEST_F(DelegateTransactionFilterTest, RoleActor_MatchesWhenUserIsOwner) { - DelegateFilter const filterParams{DelegateFilter::Role::Actor}; + DelegateFilter const filterParams{ + .delegateType = DelegateFilter::Role::Actor, .counterParty = std::nullopt + }; DelegateTransactionFilter const filter(filterParams, kAccountOwner); auto blob = createBlob(to_string(kAccountOwner), to_string(kAccountDelegator)); @@ -120,7 +131,9 @@ TEST_F(DelegateTransactionFilterTest, RoleActor_MatchesWhenUserIsOwner) TEST_F(DelegateTransactionFilterTest, RoleActor_FailsWhenUserIsNotOwner) { - DelegateFilter const filterParams{DelegateFilter::Role::Actor}; + DelegateFilter const filterParams{ + .delegateType = DelegateFilter::Role::Actor, .counterParty = std::nullopt + }; DelegateTransactionFilter const filter(filterParams, kAccountDestination); auto blob = createBlob(to_string(kAccountOwner), to_string(kAccountDelegator)); @@ -131,7 +144,9 @@ TEST_F(DelegateTransactionFilterTest, RoleActor_FailsWhenUserIsNotOwner) TEST_F(DelegateTransactionFilterTest, RoleActor_WithCounterparty_Match) { - DelegateFilter const filterParams{DelegateFilter::Role::Actor, to_string(kAccountDelegator)}; + DelegateFilter const filterParams{ + .delegateType = DelegateFilter::Role::Actor, .counterParty = to_string(kAccountDelegator) + }; DelegateTransactionFilter const filter(filterParams, kAccountOwner); auto blob = createBlob(to_string(kAccountOwner), to_string(kAccountDelegator)); @@ -144,7 +159,9 @@ TEST_F(DelegateTransactionFilterTest, RoleActor_WithCounterparty_Match) TEST_F(DelegateTransactionFilterTest, RoleActor_WithCounterparty_Mismatch) { - DelegateFilter const filterParams{DelegateFilter::Role::Actor, to_string(kAccountDestination)}; + DelegateFilter const filterParams{ + .delegateType = DelegateFilter::Role::Actor, .counterParty = to_string(kAccountDestination) + }; DelegateTransactionFilter const filter(filterParams, kAccountOwner); auto blob = createBlob(to_string(kAccountOwner), to_string(kAccountDelegator)); diff --git a/tests/unit/rpc/handlers/AccountCurrenciesTests.cpp b/tests/unit/rpc/handlers/AccountCurrenciesTests.cpp index 909a75279..f48a8c5f3 100644 --- a/tests/unit/rpc/handlers/AccountCurrenciesTests.cpp +++ b/tests/unit/rpc/handlers/AccountCurrenciesTests.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -152,6 +153,82 @@ TEST_F(RPCAccountCurrenciesHandlerTest, LedgerNonExistViaHash) }); } +TEST_F(RPCAccountCurrenciesHandlerTest, LedgerHashMalformed) +{ + static auto const kInput = boost::json::parse( + fmt::format(R"JSON({{ "account": "{}", "ledger_hash": "1" }})JSON", kAccount) + ); + auto const handler = AnyHandler{AccountCurrenciesHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kInput, Context{yield}); + ASSERT_FALSE(output); + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "ledger_hashMalformed"); + }); +} + +TEST_F(RPCAccountCurrenciesHandlerTest, LedgerIndexMalformed) +{ + static auto const kInput = boost::json::parse( + fmt::format(R"JSON({{ "account": "{}", "ledger_index": "a" }})JSON", kAccount) + ); + auto const handler = AnyHandler{AccountCurrenciesHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kInput, Context{yield}); + ASSERT_FALSE(output); + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerIndexMalformed"); + }); +} + +TEST_F(RPCAccountCurrenciesHandlerTest, LedgerIndexEmptyStringMalformed) +{ + static auto const kInput = boost::json::parse( + fmt::format(R"JSON({{ "account": "{}", "ledger_index": "" }})JSON", kAccount) + ); + auto const handler = AnyHandler{AccountCurrenciesHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kInput, Context{yield}); + ASSERT_FALSE(output); + EXPECT_EQ(rpc::makeError(output.result.error()).at("error").as_string(), "invalidParams"); + }); +} + +TEST_F(RPCAccountCurrenciesHandlerTest, LedgerIndexOutOfRangeMalformed) +{ + static auto const kInput = boost::json::parse( + fmt::format(R"JSON({{ "account": "{}", "ledger_index": 4294967296 }})JSON", kAccount) + ); + auto const handler = AnyHandler{AccountCurrenciesHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kInput, Context{yield}); + ASSERT_FALSE(output); + EXPECT_EQ(rpc::makeError(output.result.error()).at("error").as_string(), "invalidParams"); + }); +} + +TEST_F(RPCAccountCurrenciesHandlerTest, ValidLedgerHashWithMalformedLedgerIndex) +{ + static auto const kInput = boost::json::parse( + fmt::format( + R"JSON({{ "account": "{}", "ledger_hash": "{}", "ledger_index": "a" }})JSON", + kAccount, + kLedgerHash + ) + ); + auto const handler = AnyHandler{AccountCurrenciesHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kInput, Context{yield}); + ASSERT_FALSE(output); + EXPECT_EQ( + rpc::makeError(output.result.error()).at("error_message").as_string(), + "ledgerIndexMalformed" + ); + }); +} + TEST_F(RPCAccountCurrenciesHandlerTest, DefaultParameter) { static constexpr auto kOutput = R"JSON({ @@ -318,7 +395,7 @@ TEST(RPCAccountCurrenciesHandlerSpecTest, DeprecatedFields) {"strict", true} }; auto const spec = AccountCurrenciesHandler::spec(2); - auto const warnings = spec.check(json); + auto const warnings = rpc::spec::toJsonArray(spec.check(json)); ASSERT_EQ(warnings.size(), 1); ASSERT_TRUE(warnings[0].is_object()); auto const& warning = warnings[0].as_object(); diff --git a/tests/unit/rpc/handlers/AccountInfoTests.cpp b/tests/unit/rpc/handlers/AccountInfoTests.cpp index e28bceb96..ef08161f0 100644 --- a/tests/unit/rpc/handlers/AccountInfoTests.cpp +++ b/tests/unit/rpc/handlers/AccountInfoTests.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -952,7 +953,7 @@ TEST(RPCAccountInfoHandlerSpecTest, DeprecatedFields) {"strict", true} }; auto const spec = AccountInfoHandler::spec(2); - auto const warnings = spec.check(json); + auto const warnings = rpc::spec::toJsonArray(spec.check(json)); ASSERT_EQ(warnings.size(), 1); auto const& warning = warnings[0]; ASSERT_TRUE(warning.is_object()); diff --git a/tests/unit/rpc/handlers/AccountLinesTests.cpp b/tests/unit/rpc/handlers/AccountLinesTests.cpp index e9f42ba93..5a63192a0 100644 --- a/tests/unit/rpc/handlers/AccountLinesTests.cpp +++ b/tests/unit/rpc/handlers/AccountLinesTests.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -1254,7 +1255,7 @@ TEST(RPCAccountLinesHandlerSpecTest, DeprecatedFields) {"peer_index", 456} }; auto const spec = AccountLinesHandler::spec(2); - auto const warnings = spec.check(json); + auto const warnings = rpc::spec::toJsonArray(spec.check(json)); ASSERT_EQ(warnings.size(), 1); ASSERT_TRUE(warnings[0].is_object()); auto const& warning = warnings[0].as_object(); diff --git a/tests/unit/rpc/handlers/AccountMPTokenIssuancesTests.cpp b/tests/unit/rpc/handlers/AccountMPTokenIssuancesTests.cpp index 002c137c9..6b5cf9417 100644 --- a/tests/unit/rpc/handlers/AccountMPTokenIssuancesTests.cpp +++ b/tests/unit/rpc/handlers/AccountMPTokenIssuancesTests.cpp @@ -2,7 +2,6 @@ #include "rpc/Errors.hpp" #include "rpc/common/AnyHandler.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include "rpc/handlers/AccountMPTokenIssuances.hpp" #include "util/HandlerBaseTestFixture.hpp" #include "util/NameGenerator.hpp" @@ -1213,11 +1212,8 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, MPTokenIssuanceIdIsDerivedIdNotLed EXPECT_EQ(mptIssuanceId, expectedMptIssuanceId(kSequence)); EXPECT_NE(mptIssuanceId, kIssuanceIndex1); - auto const asOuterObject = boost::json::value{{"mpt_issuance_id", mptIssuanceId}}; - auto const validated = validation::CustomValidators::uint192HexStringValidator.verify( - asOuterObject, "mpt_issuance_id" - ); - EXPECT_TRUE(validated.has_value()); + xrpl::uint192 parsedMptIssuanceId; + EXPECT_TRUE(parsedMptIssuanceId.parseHex(mptIssuanceId)); }); } diff --git a/tests/unit/rpc/handlers/AccountObjectsTests.cpp b/tests/unit/rpc/handlers/AccountObjectsTests.cpp index 9de0dc2ac..c011efe67 100644 --- a/tests/unit/rpc/handlers/AccountObjectsTests.cpp +++ b/tests/unit/rpc/handlers/AccountObjectsTests.cpp @@ -962,6 +962,190 @@ TEST_F( }); } +// `sponsored` mirrors xrpld: a trust line is sponsored via sfHighSponsor OR sfLowSponsor, +// every other supported type via sfSponsor. sfSponsor is a common field on all ledger entries +// (LedgerFormats::getCommonFields), so any entry can carry one. +namespace { + +// Owner-dir fixture shared by the sponsored filter tests: two sponsored trust lines (high and +// low side), a sponsored pay channel, and an unsponsored offer. +std::vector +makeSponsoredMixObjects() +{ + auto lineHigh = createRippleStateLedgerObject( + "USD", kIssuer, 100, kAccount, 10, kAccount2, 20, kTxnId, 123, 0 + ); + lineHigh.setAccountID(xrpl::sfHighSponsor, getAccountIdWithString(kAccount2)); + + auto lineLow = createRippleStateLedgerObject( + "USD", kIssuer, 100, kAccount, 10, kAccount2, 20, kTxnId, 123, 0 + ); + lineLow.setAccountID(xrpl::sfLowSponsor, getAccountIdWithString(kAccount2)); + + auto channel = createPaymentChannelLedgerObject(kAccount, kAccount2, 100, 10, 32, kTxnId, 28); + channel.setAccountID(xrpl::sfSponsor, getAccountIdWithString(kAccount2)); + + auto const offer = createOfferLedgerObject( + kAccount, + 10, + 20, + xrpl::to_string(xrpl::toCurrency("USD")), + xrpl::to_string(xrpl::xrpCurrency()), + kAccount2, + toBase58(xrpl::xrpAccount()), + kIndex1 + ); + + return { + lineHigh.getSerializer().peekData(), + lineLow.getSerializer().peekData(), + channel.getSerializer().peekData(), + offer.getSerializer().peekData() + }; +} + +} // namespace + +TEST_F(RPCAccountObjectsHandlerTest, SponsoredFilterTrueReturnsOnlySponsored) +{ + auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); + + auto const account = getAccountIdWithString(kAccount); + EXPECT_CALL(*backend_, doFetchLedgerObject(xrpl::keylet::account(account).key, kMaxSeq, _)) + .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const ownerDir = createOwnerDirLedgerObject( + {xrpl::uint256{kIndex1}, + xrpl::uint256{kIndex1}, + xrpl::uint256{kIndex1}, + xrpl::uint256{kIndex1}}, + kIndex1 + ); + EXPECT_CALL(*backend_, doFetchLedgerObject(xrpl::keylet::ownerDir(account).key, kMaxSeq, _)) + .WillOnce(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL( + *backend_, doFetchLedgerObject(xrpl::keylet::nftokenPageMax(account).key, kMaxSeq, _) + ) + .WillOnce(Return(std::nullopt)); + EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(makeSponsoredMixObjects())); + + static auto const kInput = boost::json::parse( + fmt::format(R"JSON({{"account": "{}", "sponsored": true}})JSON", kAccount) + ); + + auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kInput, Context{yield}); + ASSERT_TRUE(output); + // two trust lines (high/low sponsor) + the pay channel; the offer is unsponsored + EXPECT_EQ(output.result->as_object().at("account_objects").as_array().size(), 3); + }); +} + +TEST_F(RPCAccountObjectsHandlerTest, SponsoredFilterFalseReturnsOnlyUnsponsored) +{ + auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); + + auto const account = getAccountIdWithString(kAccount); + EXPECT_CALL(*backend_, doFetchLedgerObject(xrpl::keylet::account(account).key, kMaxSeq, _)) + .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const ownerDir = createOwnerDirLedgerObject( + {xrpl::uint256{kIndex1}, + xrpl::uint256{kIndex1}, + xrpl::uint256{kIndex1}, + xrpl::uint256{kIndex1}}, + kIndex1 + ); + EXPECT_CALL(*backend_, doFetchLedgerObject(xrpl::keylet::ownerDir(account).key, kMaxSeq, _)) + .WillOnce(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL( + *backend_, doFetchLedgerObject(xrpl::keylet::nftokenPageMax(account).key, kMaxSeq, _) + ) + .WillOnce(Return(std::nullopt)); + EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(makeSponsoredMixObjects())); + + static auto const kInput = boost::json::parse( + fmt::format(R"JSON({{"account": "{}", "sponsored": false}})JSON", kAccount) + ); + + auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kInput, Context{yield}); + ASSERT_TRUE(output); + auto const& objects = output.result->as_object().at("account_objects").as_array(); + ASSERT_EQ(objects.size(), 1); + EXPECT_EQ(objects.at(0).as_object().at("LedgerEntryType").as_string(), "Offer"); + }); +} + +// Pins the NFTokenPage branch of sponsorOf. A page cannot carry sfSponsor in libxrpl 3.4.0 +// (SponsorshipTransfer::preclaim rejects types outside isLedgerEntrySupportedBySponsorship), so +// this constructs state that the protocol cannot currently produce. It is here to lock in the +// intended classification for when NFTokenPage joins that allowlist, matching xrpld's own +// separate read of the page sponsor. +TEST_F(RPCAccountObjectsHandlerTest, SponsoredFilterTrueMatchesSponsoredNFTokenPage) +{ + auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); + + auto const account = getAccountIdWithString(kAccount); + EXPECT_CALL(*backend_, doFetchLedgerObject(xrpl::keylet::account(account).key, kMaxSeq, _)) + .WillOnce(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const ownerDir = createOwnerDirLedgerObject({xrpl::uint256{kIndex1}}, kIndex1); + EXPECT_CALL(*backend_, doFetchLedgerObject(xrpl::keylet::ownerDir(account).key, kMaxSeq, _)) + .WillOnce(Return(ownerDir.getSerializer().peekData())); + + auto const nftPage2KK = + xrpl::keylet::nftokenPage(xrpl::keylet::nftokenPageMin(account), xrpl::uint256{kIndex1}) + .key; + + auto sponsoredPage = createNftTokenPage( + std::vector{std::make_pair(kTokenId, "www.ok.com")}, nftPage2KK + ); + sponsoredPage.setAccountID(xrpl::sfSponsor, getAccountIdWithString(kAccount2)); + EXPECT_CALL( + *backend_, doFetchLedgerObject(xrpl::keylet::nftokenPageMax(account).key, kMaxSeq, _) + ) + .WillOnce(Return(sponsoredPage.getSerializer().peekData())); + + auto const plainPage = createNftTokenPage( + std::vector{std::make_pair(kTokenId, "www.ok.com")}, std::nullopt + ); + EXPECT_CALL(*backend_, doFetchLedgerObject(nftPage2KK, kMaxSeq, _)) + .WillOnce(Return(plainPage.getSerializer().peekData())); + + auto const offer = createOfferLedgerObject( + kAccount, + 10, + 20, + xrpl::to_string(xrpl::toCurrency("USD")), + xrpl::to_string(xrpl::xrpCurrency()), + kAccount2, + toBase58(xrpl::xrpAccount()), + kIndex1 + ); + EXPECT_CALL(*backend_, doFetchLedgerObjects) + .WillOnce(Return(std::vector{offer.getSerializer().peekData()})); + + static auto const kInput = boost::json::parse( + fmt::format(R"JSON({{"account": "{}", "sponsored": true}})JSON", kAccount) + ); + + auto const handler = AnyHandler{AccountObjectsHandler{backend_}}; + runSpawn([&](auto yield) { + auto const output = handler.process(kInput, Context{yield}); + ASSERT_TRUE(output); + auto const& objects = output.result->as_object().at("account_objects").as_array(); + ASSERT_EQ(objects.size(), 1); + EXPECT_EQ(objects.at(0).as_object().at("LedgerEntryType").as_string(), "NFTokenPage"); + EXPECT_TRUE(objects.at(0).as_object().contains("Sponsor")); + }); +} + TEST_F(RPCAccountObjectsHandlerTest, NFTMixOtherObjects) { static constexpr auto kExpectedOut = R"JSON({ diff --git a/tests/unit/rpc/handlers/AccountOffersTests.cpp b/tests/unit/rpc/handlers/AccountOffersTests.cpp index e9d70b111..ce3a63af1 100644 --- a/tests/unit/rpc/handlers/AccountOffersTests.cpp +++ b/tests/unit/rpc/handlers/AccountOffersTests.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -532,7 +533,7 @@ TEST_F(RPCAccountOffersHandlerTest, LimitLessThanMin) offer.setFieldU32(xrpl::sfExpiration, 123); bbs.reserve(AccountOffersHandler::kLimitMin + 1); - for (auto i = 0; i < AccountOffersHandler::kLimitMin + 1; i++) + for (auto i = 0u; i < AccountOffersHandler::kLimitMin + 1; i++) bbs.push_back(offer.getSerializer().peekData()); ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(bbs)); @@ -590,7 +591,7 @@ TEST_F(RPCAccountOffersHandlerTest, LimitMoreThanMax) ); offer.setFieldU32(xrpl::sfExpiration, 123); bbs.reserve(AccountOffersHandler::kLimitMax + 1); - for (auto i = 0; i < AccountOffersHandler::kLimitMax + 1; i++) + for (auto i = 0u; i < AccountOffersHandler::kLimitMax + 1; i++) bbs.push_back(offer.getSerializer().peekData()); ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(bbs)); @@ -626,7 +627,7 @@ TEST(RPCAccountOffersHandlerSpecTest, DeprecatedFields) {"strict", true}, }; auto const spec = AccountOffersHandler::spec(2); - auto const warnings = spec.check(json); + auto const warnings = rpc::spec::toJsonArray(spec.check(json)); ASSERT_EQ(warnings.size(), 1); ASSERT_TRUE(warnings[0].is_object()); auto const& warning = warnings[0].as_object(); diff --git a/tests/unit/rpc/handlers/AllHandlerTests.cpp b/tests/unit/rpc/handlers/AllHandlerTests.cpp index 8f739c596..092502d12 100644 --- a/tests/unit/rpc/handlers/AllHandlerTests.cpp +++ b/tests/unit/rpc/handlers/AllHandlerTests.cpp @@ -45,6 +45,7 @@ #include #include +#include #include #include #include @@ -168,8 +169,7 @@ AccountInfoHandler::Input createInput() { AccountInfoHandler::Input input{}; - input.account = kAccount; - input.ident = "asdf"; + input.account = getAccountIdWithString(kAccount); return input; } @@ -178,7 +178,7 @@ AccountTxHandler::Input createInput() { AccountTxHandler::Input input{}; - input.account = kAccount; + input.account = getAccountIdWithString(kAccount); return input; } @@ -196,8 +196,8 @@ BookOffersHandler::Input createInput() { BookOffersHandler::Input input{}; - input.paysAsset = xrpl::xrpIssue(); - input.getsAsset = xrpl::Issue{xrpl::Currency(kCurrency), getAccountIdWithString(kAccount)}; + input.takerPays = xrpl::xrpIssue(); + input.takerGets = xrpl::Issue{xrpl::Currency(kCurrency), getAccountIdWithString(kAccount)}; return input; } @@ -207,7 +207,7 @@ LedgerEntryHandler::Input createInput() { LedgerEntryHandler::Input input{}; - input.index = kIndex1; + input.index = xrpl::uint256{kIndex1}; return input; } @@ -216,7 +216,7 @@ NFTBuyOffersHandler::Input createInput() { NFTBuyOffersHandler::Input input{}; - input.nftID = kNftId; + input.nftID = xrpl::uint256{kNftId}; return input; } @@ -225,7 +225,7 @@ NFTInfoHandler::Input createInput() { NFTInfoHandler::Input input{}; - input.nftID = kNftId; + input.nftID = xrpl::uint256{kNftId}; return input; } @@ -234,7 +234,7 @@ NFTSellOffersHandler::Input createInput() { NFTSellOffersHandler::Input input{}; - input.nftID = kNftId; + input.nftID = xrpl::uint256{kNftId}; return input; } @@ -255,7 +255,7 @@ VaultInfoHandler::Input createInput() { VaultInfoHandler::Input input{}; - input.vaultID = kVaultId; + input.vaultID = xrpl::uint256{kVaultId}; return input; } diff --git a/tests/unit/rpc/handlers/BookOffersTests.cpp b/tests/unit/rpc/handlers/BookOffersTests.cpp index 97e5dfc79..9b19b30c8 100644 --- a/tests/unit/rpc/handlers/BookOffersTests.cpp +++ b/tests/unit/rpc/handlers/BookOffersTests.cpp @@ -194,8 +194,10 @@ generateParameterBookOffersTestBundles() .expectedErrorMessage = "Source currency is malformed." }, ParameterTestBundle{ - // A present-but-non-string currency is reported by validateTakerJSON as an - // expectedFieldError ('.currency', not string) before the per-field validators. + // Clio deliberately skips the early "is currency a string" check (see kTakerValidator) + // and lets it fall through to the section's own currency validator, which is wrapped + // in withCustomError(currency, RpcDstAmtMalformed) with no message override, so the + // default rippled message/token for that code is what surfaces. .testName = "TakerGetsCurrencyNotString", .testJson = R"JSON({ "taker_gets": { @@ -490,6 +492,32 @@ generateParameterBookOffersTestBundles() .expectedError = "badMarket", .expectedErrorMessage = "No such market." }, + ParameterTestBundle{ + .testName = "TakerGetsMptIdNotString", + .testJson = R"JSON({ + "taker_gets": { + "mpt_issuance_id": 123 + }, + "taker_pays": { + "currency": "XRP" + } + })JSON", + .expectedError = "invalidParams", + .expectedErrorMessage = "Invalid field 'taker_gets.mpt_issuance_id', not string." + }, + ParameterTestBundle{ + .testName = "TakerPaysMptIdNotString", + .testJson = R"JSON({ + "taker_gets": { + "currency": "XRP" + }, + "taker_pays": { + "mpt_issuance_id": true + } + })JSON", + .expectedError = "invalidParams", + .expectedErrorMessage = "Invalid field 'taker_pays.mpt_issuance_id', not string." + }, ParameterTestBundle{ .testName = "TakerGetsMptIdAndCurrency", .testJson = R"JSON({ diff --git a/tests/unit/rpc/handlers/CredentialHelpersTests.cpp b/tests/unit/rpc/handlers/CredentialHelpersTests.cpp index b11935bb5..e10482746 100644 --- a/tests/unit/rpc/handlers/CredentialHelpersTests.cpp +++ b/tests/unit/rpc/handlers/CredentialHelpersTests.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -25,6 +26,7 @@ #include #include #include +#include using namespace rpc; using namespace testing; @@ -101,7 +103,7 @@ class CredentialHelperTest : public util::prometheus::WithPrometheus, TEST_F(CredentialHelperTest, GetInvalidCredentialArray) { - boost::json::array credentialsArray = {kCredentialId}; + std::vector const credentialsArray{xrpl::uint256{kCredentialId}}; auto const info = createLedgerHeader(kIndex1, 30); util::spawn(ctx_, [&](boost::asio::yield_context yield) { @@ -128,7 +130,7 @@ TEST_F(CredentialHelperTest, GetValidCredentialArray) .WillByDefault(Return(credLedgerObject.getSerializer().peekData())); EXPECT_CALL(*backend_, doFetchLedgerObject).Times(1); - boost::json::array credentialsArray = {kCredentialId}; + std::vector const credentialsArray{xrpl::uint256{kCredentialId}}; xrpl::STArray expectedAuthCreds; xrpl::STObject credential(xrpl::sfCredential); diff --git a/tests/unit/rpc/handlers/DefaultProcessorTests.cpp b/tests/unit/rpc/handlers/DefaultProcessorTests.cpp index 5c0db6296..6295435d1 100644 --- a/tests/unit/rpc/handlers/DefaultProcessorTests.cpp +++ b/tests/unit/rpc/handlers/DefaultProcessorTests.cpp @@ -1,9 +1,7 @@ #include "rpc/Errors.hpp" #include "rpc/FakesAndMocks.hpp" #include "rpc/common/Concepts.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include "rpc/common/impl/Processors.hpp" #include "util/HandlerBaseTestFixture.hpp" @@ -15,30 +13,15 @@ using namespace testing; using namespace std; using namespace rpc; -using namespace rpc::validation; using namespace tests::common; class RPCDefaultProcessorTest : public HandlerBaseTest {}; -TEST_F(RPCDefaultProcessorTest, ValidInput) -{ - runSpawn([](auto yield) { - HandlerMock const handler; - rpc::impl::DefaultProcessor const processor; +static_assert(SomeHandlerWithTypedInput); +static_assert(SomeHandlerWithTypedInput); +static_assert(SomeHandlerWithoutInput); - auto const input = boost::json::parse(R"JSON({ "something": "works" })JSON"); - auto const spec = RpcSpec{{"something", Required{}}}; - auto const data = InOutFake{"works"}; - EXPECT_CALL(handler, spec(_)).WillOnce(ReturnRef(spec)); - EXPECT_CALL(handler, process(Eq(data), _)).WillOnce(Return(data)); - - auto const ret = processor(handler, input, Context{yield}); - ASSERT_TRUE(ret); // no error - EXPECT_TRUE(ret.warnings.empty()); - }); -} - -TEST_F(RPCDefaultProcessorTest, NoInputValidCall) +TEST_F(RPCDefaultProcessorTest, NoInputHandler_ValidCall) { runSpawn([](auto yield) { HandlerWithoutInputMock const handler; @@ -54,34 +37,10 @@ TEST_F(RPCDefaultProcessorTest, NoInputValidCall) }); } -TEST_F(RPCDefaultProcessorTest, InvalidInput) -{ - runSpawn([](auto yield) { - HandlerMock const handler; - rpc::impl::DefaultProcessor const processor; +// These exercise the input path of a handler whose spec, validation and +// deserialization all come from the shared consteval spec via HandlerFor. - auto const input = boost::json::parse(R"JSON({ "other": "nope" })JSON"); - auto const spec = RpcSpec{{"something", Required{}}}; - EXPECT_CALL(handler, spec(_)).WillOnce(ReturnRef(spec)); - - auto const ret = processor(handler, input, Context{yield}); - ASSERT_FALSE(ret); // returns error - EXPECT_TRUE(ret.warnings.empty()); - }); -} - -static_assert(SomeHandlerWithTypedInput); -static_assert(not SomeHandlerWithInput); -static_assert(SomeHandlerWithTypedInput); -static_assert(SomeHandlerWithInput); -static_assert(not SomeHandlerWithTypedInput); - -// The four tests below exercise the typed path — a handler whose spec, validation and -// deserialization all come from the shared consteval spec via HandlerFor. They run -// against the same DefaultProcessor as the legacy tests above, which is the point: the -// dual path is a dispatch detail, not a second processor. - -TEST_F(RPCDefaultProcessorTest, NewSpecHandler_HappyPath) +TEST_F(RPCDefaultProcessorTest, SpecHandler_HappyPath) { runSpawn([](auto yield) { TypedHandlerFake const handler; @@ -96,7 +55,7 @@ TEST_F(RPCDefaultProcessorTest, NewSpecHandler_HappyPath) }); } -TEST_F(RPCDefaultProcessorTest, NewSpecHandler_MissingRequiredField_ReturnsError) +TEST_F(RPCDefaultProcessorTest, SpecHandler_MissingRequiredField_ReturnsError) { runSpawn([](auto yield) { TypedHandlerFake const handler; @@ -110,7 +69,7 @@ TEST_F(RPCDefaultProcessorTest, NewSpecHandler_MissingRequiredField_ReturnsError }); } -TEST_F(RPCDefaultProcessorTest, NewSpecHandler_DeprecatedField_WarningsForwarded) +TEST_F(RPCDefaultProcessorTest, SpecHandler_DeprecatedField_WarningsForwarded) { runSpawn([](auto yield) { TypedHandlerFake const handler; @@ -124,7 +83,7 @@ TEST_F(RPCDefaultProcessorTest, NewSpecHandler_DeprecatedField_WarningsForwarded }); } -TEST_F(RPCDefaultProcessorTest, NewSpecHandler_HandlerReturnsError_ForwardsError) +TEST_F(RPCDefaultProcessorTest, SpecHandler_HandlerReturnsError_ForwardsError) { runSpawn([](auto yield) { FailingTypedHandlerFake const handler; @@ -139,7 +98,7 @@ TEST_F(RPCDefaultProcessorTest, NewSpecHandler_HandlerReturnsError_ForwardsError }); } -TEST_F(RPCDefaultProcessorTest, NewSpecHandler_HandlerReturnsError_StillForwardsWarnings) +TEST_F(RPCDefaultProcessorTest, SpecHandler_HandlerReturnsError_StillForwardsWarnings) { runSpawn([](auto yield) { FailingTypedHandlerFake const handler; @@ -154,7 +113,7 @@ TEST_F(RPCDefaultProcessorTest, NewSpecHandler_HandlerReturnsError_StillForwards }); } -TEST_F(RPCDefaultProcessorTest, NewSpecHandler_DeprecatedFieldAbsent_NoWarnings) +TEST_F(RPCDefaultProcessorTest, SpecHandler_DeprecatedFieldAbsent_NoWarnings) { runSpawn([](auto yield) { TypedHandlerFake const handler; diff --git a/tests/unit/rpc/handlers/LedgerDataTests.cpp b/tests/unit/rpc/handlers/LedgerDataTests.cpp index 3bf7ed676..c3d44bc03 100644 --- a/tests/unit/rpc/handlers/LedgerDataTests.cpp +++ b/tests/unit/rpc/handlers/LedgerDataTests.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -703,6 +704,43 @@ TEST_F(RPCLedgerDataHandlerTest, Binary) }); } +// The default page size depends on binary vs json, so an omitted `limit` must fall back to +// kLimitBinary here rather than the json default. Mirrors xrpld's pageLength(isBinary). +TEST_F(RPCLedgerDataHandlerTest, BinaryNoLimitUsesBinaryDefault) +{ + EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); + ON_CALL(*backend_, fetchLedgerBySequence(kRangeMax, _)) + .WillByDefault(Return(createLedgerHeader(kLedgerHash, kRangeMax))); + + auto limit = LedgerDataHandler::kLimitBinary + 1; + std::vector bbs; + + EXPECT_CALL(*backend_, doFetchSuccessorKey).Times(LedgerDataHandler::kLimitBinary); + ON_CALL(*backend_, doFetchSuccessorKey(_, kRangeMax, _)) + .WillByDefault(Return(xrpl::uint256{kIndex2})); + + while ((limit--) != 0u) { + auto const line = createRippleStateLedgerObject( + "USD", kAccount2, 10, kAccount, 100, kAccount2, 200, kTxnId, 123 + ); + bbs.push_back(line.getSerializer().peekData()); + } + + ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{LedgerDataHandler{backend_}}; + auto const req = boost::json::parse(R"JSON({"binary": true})JSON"); + auto const output = handler.process(req, Context{yield}); + ASSERT_TRUE(output); + EXPECT_EQ( + output.result->as_object().at("state").as_array().size(), + LedgerDataHandler::kLimitBinary + ); + }); +} + TEST_F(RPCLedgerDataHandlerTest, BinaryLimitMoreThanMax) { EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); @@ -891,7 +929,7 @@ TEST(RPCLedgerDataHandlerSpecTest, DeprecatedFields) {"ledger", "some"} }; auto const spec = LedgerDataHandler::spec(2); - auto const warnings = spec.check(json); + auto const warnings = rpc::spec::toJsonArray(spec.check(json)); ASSERT_EQ(warnings.size(), 1); ASSERT_TRUE(warnings[0].is_object()); auto const& warning = warnings[0].as_object(); diff --git a/tests/unit/rpc/handlers/LedgerEntryTests.cpp b/tests/unit/rpc/handlers/LedgerEntryTests.cpp index b3a7b7046..b75ba417b 100644 --- a/tests/unit/rpc/handlers/LedgerEntryTests.cpp +++ b/tests/unit/rpc/handlers/LedgerEntryTests.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -3443,6 +3444,37 @@ generateTestValuesForNormalPathTest() .mockedEntity = createDelegateObject(kAccount, kAccount2, kIndex1, 0, xrpl::uint256{0}, 0) }, + NormalPathTestBundle{ + .testName = "SponsorshipViaStringIndex", + .testJson = fmt::format( + R"JSON({{ + "binary": true, + "sponsorship": "{}" + }})JSON", + kIndex1 + ), + .expectedIndex = xrpl::uint256{kIndex1}, + .mockedEntity = xrpl::STLedgerEntry{xrpl::ltSPONSORSHIP, xrpl::uint256{kIndex1}} + }, + NormalPathTestBundle{ + .testName = "SponsorshipViaObject", + .testJson = fmt::format( + R"JSON({{ + "binary": true, + "sponsorship": {{ + "sponsor": "{}", + "sponsee": "{}" + }} + }})JSON", + kAccount, + kAccount2 + ), + .expectedIndex = xrpl::keylet::sponsorship( + getAccountIdWithString(kAccount), getAccountIdWithString(kAccount2) + ) + .key, + .mockedEntity = xrpl::STLedgerEntry{xrpl::ltSPONSORSHIP, xrpl::uint256{kIndex1}} + }, }; } @@ -3730,6 +3762,196 @@ TEST_F(RPCLedgerEntryTest, UnexpectedLedgerType) }); } +// Each variant field's hex-key arm sets its own expectedType, so a wrong constant there would +// silently accept the wrong object type. +struct LedgerEntryHexKeyTypeTestBundle { + std::string testName; + std::string field; + xrpl::LedgerEntryType expectedType; +}; + +struct LedgerEntryHexKeyTypeTest : public RPCLedgerEntryTest, + public WithParamInterface {}; + +TEST_P(LedgerEntryHexKeyTypeTest, WrongLedgerTypeForHexKey) +{ + auto const ledgerHeader = createLedgerHeader(kLedgerHash, kRangeMax); + EXPECT_CALL(*backend_, fetchLedgerBySequence(kRangeMax, _)) + .WillRepeatedly(Return(ledgerHeader)); + + // a valid, deserializable object of a type that none of these fields expect + auto const ledgerEntry = + createPaymentChannelLedgerObject(kAccount, kAccount2, 100, 200, 300, kIndex1, 400); + EXPECT_CALL(*backend_, doFetchLedgerObject(xrpl::uint256{kIndex1}, kRangeMax, _)) + .WillRepeatedly(Return(ledgerEntry.getSerializer().peekData())); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{LedgerEntryHandler{backend_}}; + auto const req = + boost::json::parse(fmt::format(R"JSON({{"{}": "{}"}})JSON", GetParam().field, kIndex1)); + auto const output = handler.process(req, Context{yield}); + ASSERT_FALSE(output); + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "unexpectedLedgerType"); + }); +} + +TEST_P(LedgerEntryHexKeyTypeTest, MatchingLedgerTypeForHexKeyAccepted) +{ + auto const ledgerHeader = createLedgerHeader(kLedgerHash, kRangeMax); + EXPECT_CALL(*backend_, fetchLedgerBySequence(kRangeMax, _)) + .WillRepeatedly(Return(ledgerHeader)); + + // a bare object of exactly the type this field's hex key implies + xrpl::STLedgerEntry const ledgerEntry{GetParam().expectedType, xrpl::uint256{kIndex1}}; + EXPECT_CALL(*backend_, doFetchLedgerObject(xrpl::uint256{kIndex1}, kRangeMax, _)) + .WillRepeatedly(Return(ledgerEntry.getSerializer().peekData())); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{LedgerEntryHandler{backend_}}; + auto const req = + boost::json::parse(fmt::format(R"JSON({{"{}": "{}"}})JSON", GetParam().field, kIndex1)); + auto const output = handler.process(req, Context{yield}); + ASSERT_TRUE(output); + EXPECT_EQ(output.result->as_object().at("index").as_string(), kIndex1); + }); +} + +INSTANTIATE_TEST_CASE_P( + RPCLedgerEntryHexKeyTypeGroup, + LedgerEntryHexKeyTypeTest, + ValuesIn({ + // kHexLocators table entries + LedgerEntryHexKeyTypeTestBundle{ + .testName = "NftOffer", + .field = "nft_offer", + .expectedType = xrpl::ltNFTOKEN_OFFER + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "SignerList", + .field = "signer_list", + .expectedType = xrpl::ltSIGNER_LIST + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "Amendments", + .field = "amendments", + .expectedType = xrpl::ltAMENDMENTS + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "Fee", + .field = "fee", + .expectedType = xrpl::ltFEE_SETTINGS + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "Hashes", + .field = "hashes", + .expectedType = xrpl::ltLEDGER_HASHES + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "Nunl", + .field = "nunl", + .expectedType = xrpl::ltNEGATIVE_UNL + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "Check", + .field = "check", + .expectedType = xrpl::ltCHECK + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "NftPage", + .field = "nft_page", + .expectedType = xrpl::ltNFTOKEN_PAGE + }, + // variant fields' hex arms + LedgerEntryHexKeyTypeTestBundle{ + .testName = "Directory", + .field = "directory", + .expectedType = xrpl::ltDIR_NODE + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "Offer", + .field = "offer", + .expectedType = xrpl::ltOFFER + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "Escrow", + .field = "escrow", + .expectedType = xrpl::ltESCROW + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "DepositPreauth", + .field = "deposit_preauth", + .expectedType = xrpl::ltDEPOSIT_PREAUTH + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "Ticket", + .field = "ticket", + .expectedType = xrpl::ltTICKET + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "Amm", + .field = "amm", + .expectedType = xrpl::ltAMM + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "Oracle", + .field = "oracle", + .expectedType = xrpl::ltORACLE + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "Credential", + .field = "credential", + .expectedType = xrpl::ltCREDENTIAL + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "Mptoken", + .field = "mptoken", + .expectedType = xrpl::ltMPTOKEN + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "PermissionedDomain", + .field = "permissioned_domain", + .expectedType = xrpl::ltPERMISSIONED_DOMAIN + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "Vault", + .field = "vault", + .expectedType = xrpl::ltVAULT + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "LoanBroker", + .field = "loan_broker", + .expectedType = xrpl::ltLOAN_BROKER + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "Loan", + .field = "loan", + .expectedType = xrpl::ltLOAN + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "Delegate", + .field = "delegate", + .expectedType = xrpl::ltDELEGATE + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "Sponsorship", + .field = "sponsorship", + .expectedType = xrpl::ltSPONSORSHIP + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "XChainOwnedClaimId", + .field = "xchain_owned_claim_id", + .expectedType = xrpl::ltXCHAIN_OWNED_CLAIM_ID + }, + LedgerEntryHexKeyTypeTestBundle{ + .testName = "XChainOwnedCreateAccountClaimId", + .field = "xchain_owned_create_account_claim_id", + .expectedType = xrpl::ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID + }, + }), + tests::util::kNameGenerator +); + TEST_F(RPCLedgerEntryTest, LedgerNotExistViaIntSequence) { EXPECT_CALL(*backend_, fetchLedgerBySequence(kRangeMax, _)) @@ -3835,7 +4057,7 @@ TEST(RPCLedgerEntrySpecTest, DeprecatedFields) { boost::json::value const json{{"ledger", 2}}; auto const spec = LedgerEntryHandler::spec(2); - auto const warnings = spec.check(json); + auto const warnings = rpc::spec::toJsonArray(spec.check(json)); ASSERT_EQ(warnings.size(), 1); ASSERT_TRUE(warnings[0].is_object()); auto const& warning = warnings[0].as_object(); diff --git a/tests/unit/rpc/handlers/LedgerIndexTests.cpp b/tests/unit/rpc/handlers/LedgerIndexTests.cpp index 4224dba07..0cdbb37e0 100644 --- a/tests/unit/rpc/handlers/LedgerIndexTests.cpp +++ b/tests/unit/rpc/handlers/LedgerIndexTests.cpp @@ -47,6 +47,39 @@ TEST_F(RPCLedgerIndexTest, DateStrNotValid) }); } +TEST_F(RPCLedgerIndexTest, DateStrSpaceInsteadOfT) +{ + auto const handler = AnyHandler{LedgerIndexHandler{backend_}}; + auto const req = boost::json::parse(R"JSON({"date": "2024-06-25 12:23:05Z"})JSON"); + runSpawn([&](auto yield) { + auto const output = handler.process(req, Context{yield}); + ASSERT_FALSE(output); + EXPECT_EQ(rpc::makeError(output.result.error()).at("error").as_string(), "invalidParams"); + }); +} + +TEST_F(RPCLedgerIndexTest, DateStrMissingZ) +{ + auto const handler = AnyHandler{LedgerIndexHandler{backend_}}; + auto const req = boost::json::parse(R"JSON({"date": "2024-06-25T12:23:05"})JSON"); + runSpawn([&](auto yield) { + auto const output = handler.process(req, Context{yield}); + ASSERT_FALSE(output); + EXPECT_EQ(rpc::makeError(output.result.error()).at("error").as_string(), "invalidParams"); + }); +} + +TEST_F(RPCLedgerIndexTest, DateStrDateOnly) +{ + auto const handler = AnyHandler{LedgerIndexHandler{backend_}}; + auto const req = boost::json::parse(R"JSON({"date": "2024-06-25"})JSON"); + runSpawn([&](auto yield) { + auto const output = handler.process(req, Context{yield}); + ASSERT_FALSE(output); + EXPECT_EQ(rpc::makeError(output.result.error()).at("error").as_string(), "invalidParams"); + }); +} + TEST_F(RPCLedgerIndexTest, NoDateGiven) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, kRangeMax, 5); diff --git a/tests/unit/rpc/handlers/LedgerTests.cpp b/tests/unit/rpc/handlers/LedgerTests.cpp index 6ce57bb94..36fb9b1a8 100644 --- a/tests/unit/rpc/handlers/LedgerTests.cpp +++ b/tests/unit/rpc/handlers/LedgerTests.cpp @@ -2,7 +2,6 @@ #include "rpc/Errors.hpp" #include "rpc/JS.hpp" #include "rpc/common/AnyHandler.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" #include "rpc/handlers/Ledger.hpp" #include "util/HandlerBaseTestFixture.hpp" @@ -16,6 +15,8 @@ #include #include #include +#include +#include #include #include #include @@ -36,6 +37,7 @@ constexpr auto kLedgerHash = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25 constexpr auto kIndex1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"; constexpr auto kIndex2 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515B1"; constexpr auto kCurrency = "0158415500000000C1F76FF6ECB0BAC600000000"; +constexpr auto kNftId = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DF"; constexpr auto kRangeMin = 10; constexpr auto kRangeMax = 30; @@ -577,6 +579,42 @@ TEST_F(RPCLedgerHandlerTest, TransactionsExpandNotBinary) }); } +// xrpld injects the NFT synthetic fields into `ledger` transaction metadata since 3.4.0 +// (insertAllSyntheticInJson), matching what `tx` and `account_tx` already returned. +TEST_F(RPCLedgerHandlerTest, TransactionsExpandNotBinaryIncludesNFTokenID) +{ + auto const ledgerHeader = createLedgerHeader(kLedgerHash, kRangeMax); + EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); + ON_CALL(*backend_, fetchLedgerBySequence(kRangeMax, _)).WillByDefault(Return(ledgerHeader)); + + auto tx = createAcceptNftBuyerOfferTxWithMetadata(kAccount, 1, 50, kNftId, kIndex1); + tx.ledgerSequence = kRangeMax; + + EXPECT_CALL(*backend_, fetchAllTransactionsInLedger).Times(1); + ON_CALL(*backend_, fetchAllTransactionsInLedger(kRangeMax, _)) + .WillByDefault(Return(std::vector{tx})); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{LedgerHandler{backend_, mockAmendmentCenterPtr_}}; + auto const req = boost::json::parse( + R"JSON({ + "binary": false, + "expand": true, + "transactions": true + })JSON" + ); + auto const output = handler.process(req, Context{yield}); + ASSERT_TRUE(output); + + auto const& txs = + output.result->as_object().at("ledger").as_object().at("transactions").as_array(); + ASSERT_EQ(txs.size(), 1); + EXPECT_EQ( + txs.at(0).as_object().at("metaData").as_object().at("nftoken_id").as_string(), kNftId + ); + }); +} + TEST_F(RPCLedgerHandlerTest, TransactionsExpandNotBinaryV2) { static constexpr auto kExpectedOut = @@ -1393,7 +1431,7 @@ struct RPCLedgerHandlerSpecCheckTestBundle { struct RPCLedgerHandlerSpecCheckTest : ::testing::TestWithParam { - RpcSpec spec = LedgerHandler::spec(2); + rpc::spec::RpcSpecView spec = LedgerHandler::spec(2); }; INSTANTIATE_TEST_SUITE_P( @@ -1439,7 +1477,7 @@ INSTANTIATE_TEST_SUITE_P( TEST_P(RPCLedgerHandlerSpecCheckTest, CheckSpec) { - auto const warnings = spec.check(GetParam().json); + auto const warnings = rpc::spec::toJsonArray(spec.check(GetParam().json)); ASSERT_EQ(warnings.size(), GetParam().expectedWarning.size()); for (auto const& warn : warnings) { ASSERT_TRUE(warn.is_object()); diff --git a/tests/unit/rpc/handlers/MPTHoldersTests.cpp b/tests/unit/rpc/handlers/MPTHoldersTests.cpp index 9d4321a59..1d5a7f985 100644 --- a/tests/unit/rpc/handlers/MPTHoldersTests.cpp +++ b/tests/unit/rpc/handlers/MPTHoldersTests.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -28,12 +29,12 @@ using namespace testing; namespace { -constexpr auto kHoldeR1Account = "rrnAZCqMahreZrKMcZU3t2DZ6yUndT4ubN"; -constexpr auto kHoldeR2Account = "rEiNkzogdHEzUxPfsri5XSMqtXUixf2Yx"; +constexpr auto kHolder1Account = "rrnAZCqMahreZrKMcZU3t2DZ6yUndT4ubN"; +constexpr auto kHolder2Account = "rEiNkzogdHEzUxPfsri5XSMqtXUixf2Yx"; constexpr auto kLedgerHash = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; constexpr auto kMptId = "000004C463C52827307480341125DA0577DEFC38405B0E3E"; -std::string const kMptOuT1 = +std::string const kMptOut1 = R"JSON({ "account": "rrnAZCqMahreZrKMcZU3t2DZ6yUndT4ubN", "flags": 0, @@ -41,7 +42,7 @@ std::string const kMptOuT1 = "mptoken_index": "D137F2E5A5767A06CB7A8F060ADE442A30CFF95028E1AF4B8767E3A56877205A" })JSON"; -std::string const kMptOuT2 = +std::string const kMptOut2 = R"JSON({ "account": "rEiNkzogdHEzUxPfsri5XSMqtXUixf2Yx", "flags": 0, @@ -389,7 +390,7 @@ TEST_F(RPCMPTHoldersHandlerTest, DefaultParameters) "validated": true }})JSON", kMptId, - kMptOuT1 + kMptOut1 ); auto ledgerInfo = createLedgerHeader(kLedgerHash, 30); @@ -398,7 +399,7 @@ TEST_F(RPCMPTHoldersHandlerTest, DefaultParameters) ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _)) .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); - auto const mptoken = createMpTokenObject(kHoldeR1Account, xrpl::uint192(kMptId)); + auto const mptoken = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId)); std::vector const mpts = {mptoken.getSerializer().peekData()}; ON_CALL(*backend_, fetchMPTHolders) .WillByDefault(Return(MPTHoldersAndCursor{.mptokens = mpts, .cursor = {}})); @@ -452,7 +453,7 @@ TEST_F(RPCMPTHoldersHandlerTest, CustomAmounts) ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _)) .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); - auto const mptoken = createMpTokenObject(kHoldeR1Account, xrpl::uint192(kMptId), 0); + auto const mptoken = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId), 0); std::vector const mpts = {mptoken.getSerializer().peekData()}; ON_CALL(*backend_, fetchMPTHolders) .WillByDefault(Return(MPTHoldersAndCursor{.mptokens = mpts, .cursor = {}})); @@ -511,7 +512,7 @@ TEST_F(RPCMPTHoldersHandlerTest, ConfidentialFields) "validated": true }})JSON", kMptId, - kHoldeR1Account, + kHolder1Account, kLockedAmount, xrpl::strHex(std::string_view{kConfidentialBalanceInbox}), xrpl::strHex(std::string_view{kConfidentialBalanceSpending}), @@ -528,7 +529,7 @@ TEST_F(RPCMPTHoldersHandlerTest, ConfidentialFields) .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); auto const mptoken = createMpTokenObject( - kHoldeR1Account, + kHolder1Account, xrpl::uint192(kMptId), 1, 0, @@ -580,7 +581,7 @@ TEST_F(RPCMPTHoldersHandlerTest, SpecificLedgerIndex) }})JSON", kMptId, specificLedger, - kMptOuT1 + kMptOut1 ); auto ledgerInfo = createLedgerHeader(kLedgerHash, specificLedger); @@ -590,7 +591,7 @@ TEST_F(RPCMPTHoldersHandlerTest, SpecificLedgerIndex) ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, specificLedger, _)) .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); - auto const mptoken = createMpTokenObject(kHoldeR1Account, xrpl::uint192(kMptId)); + auto const mptoken = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId)); std::vector const mpts = {mptoken.getSerializer().peekData()}; ON_CALL(*backend_, fetchMPTHolders) .WillByDefault(Return(MPTHoldersAndCursor{.mptokens = mpts, .cursor = {}})); @@ -636,8 +637,8 @@ TEST_F(RPCMPTHoldersHandlerTest, MarkerParameter) "marker": "{}" }})JSON", kMptId, - kMptOuT2, - xrpl::strHex(getAccountIdWithString(kHoldeR1Account)) + kMptOut2, + xrpl::strHex(getAccountIdWithString(kHolder1Account)) ); auto ledgerInfo = createLedgerHeader(kLedgerHash, 30); @@ -646,9 +647,9 @@ TEST_F(RPCMPTHoldersHandlerTest, MarkerParameter) ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _)) .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); - auto const mptoken = createMpTokenObject(kHoldeR2Account, xrpl::uint192(kMptId)); + auto const mptoken = createMpTokenObject(kHolder2Account, xrpl::uint192(kMptId)); std::vector const mpts = {mptoken.getSerializer().peekData()}; - auto const marker = getAccountIdWithString(kHoldeR1Account); + auto const marker = getAccountIdWithString(kHolder1Account); ON_CALL(*backend_, fetchMPTHolders) .WillByDefault(Return(MPTHoldersAndCursor{.mptokens = mpts, .cursor = marker})); EXPECT_CALL( @@ -659,7 +660,7 @@ TEST_F(RPCMPTHoldersHandlerTest, MarkerParameter) ) .Times(1); - auto const holder1AccountId = xrpl::strHex(getAccountIdWithString(kHoldeR1Account)); + auto const holder1AccountId = xrpl::strHex(getAccountIdWithString(kHolder1Account)); auto const input = boost::json::parse( fmt::format( R"JSON({{ @@ -689,8 +690,8 @@ TEST_F(RPCMPTHoldersHandlerTest, MultipleMPTs) "validated": true }})JSON", kMptId, - kMptOuT1, - kMptOuT2 + kMptOut1, + kMptOut2 ); auto ledgerInfo = createLedgerHeader(kLedgerHash, 30); @@ -699,8 +700,8 @@ TEST_F(RPCMPTHoldersHandlerTest, MultipleMPTs) ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _)) .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); - auto const mptoken1 = createMpTokenObject(kHoldeR1Account, xrpl::uint192(kMptId)); - auto const mptoken2 = createMpTokenObject(kHoldeR2Account, xrpl::uint192(kMptId)); + auto const mptoken1 = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId)); + auto const mptoken2 = createMpTokenObject(kHolder2Account, xrpl::uint192(kMptId)); std::vector const mpts = { mptoken1.getSerializer().peekData(), mptoken2.getSerializer().peekData() }; @@ -730,6 +731,577 @@ TEST_F(RPCMPTHoldersHandlerTest, MultipleMPTs) }); } +TEST_F(RPCMPTHoldersHandlerTest, AccountsNotArray) +{ + runSpawn([this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{MPTHoldersHandler{backend_}}; + auto const input = boost::json::parse( + fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "accounts": "{}" + }})JSON", + kMptId, + kHolder1Account + ) + ); + auto const output = handler.process(input, Context{.yield = std::ref(yield)}); + ASSERT_FALSE(output); + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "Invalid field 'accounts', not array."); + }); +} + +TEST_F(RPCMPTHoldersHandlerTest, AccountsEmpty) +{ + runSpawn([this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{MPTHoldersHandler{backend_}}; + auto const input = boost::json::parse( + fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "accounts": [] + }})JSON", + kMptId + ) + ); + auto const output = handler.process(input, Context{.yield = std::ref(yield)}); + ASSERT_FALSE(output); + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ( + err.at("error_message").as_string(), + "Invalid field 'accounts', not an array of 1 to 100 account IDs." + ); + }); +} + +TEST_F(RPCMPTHoldersHandlerTest, AccountsMalformedEntry) +{ + runSpawn([this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{MPTHoldersHandler{backend_}}; + auto const input = boost::json::parse( + fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "accounts": ["{}", "not-an-account"] + }})JSON", + kMptId, + kHolder1Account + ) + ); + auto const output = handler.process(input, Context{.yield = std::ref(yield)}); + ASSERT_FALSE(output); + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ( + err.at("error_message").as_string(), + "Invalid field 'accounts', not an array of account IDs." + ); + }); +} + +TEST_F(RPCMPTHoldersHandlerTest, AccountsTooMany) +{ + std::string accountsList; + for (std::size_t i = 0; i <= MPTHoldersHandler::kMaxAccounts; ++i) + accountsList += fmt::format(R"("{}",)", kHolder1Account); + accountsList.pop_back(); // drop trailing comma + + runSpawn([&, this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{MPTHoldersHandler{backend_}}; + auto const input = boost::json::parse( + fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "accounts": [{}] + }})JSON", + kMptId, + accountsList + ) + ); + auto const output = handler.process(input, Context{.yield = std::ref(yield)}); + ASSERT_FALSE(output); + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ( + err.at("error_message").as_string(), + "Invalid field 'accounts', not an array of 1 to 100 account IDs." + ); + }); +} + +TEST_F(RPCMPTHoldersHandlerTest, AccountsWithMarker) +{ + auto const marker = xrpl::strHex(getAccountIdWithString(kHolder1Account)); + runSpawn([&, this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{MPTHoldersHandler{backend_}}; + auto const input = boost::json::parse( + fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "accounts": ["{}"], + "marker": "{}" + }})JSON", + kMptId, + kHolder1Account, + marker + ) + ); + auto const output = handler.process(input, Context{.yield = std::ref(yield)}); + ASSERT_FALSE(output); + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "accountsWithMarker"); + }); +} + +TEST_F(RPCMPTHoldersHandlerTest, AccountsWithLimit) +{ + runSpawn([this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{MPTHoldersHandler{backend_}}; + auto const input = boost::json::parse( + fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "accounts": ["{}"], + "limit": 1 + }})JSON", + kMptId, + kHolder1Account + ) + ); + auto const output = handler.process(input, Context{.yield = std::ref(yield)}); + ASSERT_FALSE(output); + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "accountsWithLimit"); + }); +} + +TEST_F(RPCMPTHoldersHandlerTest, AccountsFilter) +{ + auto const currentOutput = fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "ledger_index": 30, + "mptokens": [{}, {}], + "validated": true + }})JSON", + kMptId, + kMptOut1, + kMptOut2 + ); + + auto ledgerInfo = createLedgerHeader(kLedgerHash, 30); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerInfo)); + auto const issuanceKk = xrpl::keylet::mptokenIssuance(xrpl::uint192(kMptId)).key; + ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _)) + .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const mptoken1 = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId)); + auto const mptoken2 = createMpTokenObject(kHolder2Account, xrpl::uint192(kMptId)); + std::vector const objs = { + mptoken1.getSerializer().peekData(), mptoken2.getSerializer().peekData() + }; + // The handler must query exactly the MPToken keys for the requested accounts, in order. + std::vector const expectedKeys = { + xrpl::keylet::mptoken(xrpl::uint192(kMptId), getAccountIdWithString(kHolder1Account)).key, + xrpl::keylet::mptoken(xrpl::uint192(kMptId), getAccountIdWithString(kHolder2Account)).key + }; + EXPECT_CALL(*backend_, doFetchLedgerObjects(expectedKeys, 30, _)).WillOnce(Return(objs)); + // The accounts filter must not fall back to the holder-index scan. + EXPECT_CALL(*backend_, fetchMPTHolders).Times(0); + + auto const input = boost::json::parse( + fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "accounts": ["{}", "{}"] + }})JSON", + kMptId, + kHolder1Account, + kHolder2Account + ) + ); + runSpawn([&, this](auto& yield) { + auto handler = AnyHandler{MPTHoldersHandler{this->backend_}}; + auto const output = handler.process(input, Context{yield}); + ASSERT_TRUE(output); + EXPECT_EQ(boost::json::parse(currentOutput), *output.result); + }); +} + +TEST_F(RPCMPTHoldersHandlerTest, AccountsFilterMissingHolderOmitted) +{ + auto const currentOutput = fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "ledger_index": 30, + "mptokens": [{}], + "validated": true + }})JSON", + kMptId, + kMptOut1 + ); + + auto ledgerInfo = createLedgerHeader(kLedgerHash, 30); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerInfo)); + auto const issuanceKk = xrpl::keylet::mptokenIssuance(xrpl::uint192(kMptId)).key; + ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _)) + .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const mptoken1 = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId)); + // holder2 is not a holder: its object comes back empty and must be dropped. + std::vector const objs = {mptoken1.getSerializer().peekData(), Blob{}}; + ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(objs)); + EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1); + EXPECT_CALL(*backend_, fetchMPTHolders).Times(0); + + auto const input = boost::json::parse( + fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "accounts": ["{}", "{}"] + }})JSON", + kMptId, + kHolder1Account, + kHolder2Account + ) + ); + runSpawn([&, this](auto& yield) { + auto handler = AnyHandler{MPTHoldersHandler{this->backend_}}; + auto const output = handler.process(input, Context{yield}); + ASSERT_TRUE(output); + EXPECT_EQ(boost::json::parse(currentOutput), *output.result); + }); +} + +TEST_F(RPCMPTHoldersHandlerTest, AccountsNonStringEntry) +{ + runSpawn([this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{MPTHoldersHandler{backend_}}; + auto const input = boost::json::parse( + fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "accounts": ["{}", 123] + }})JSON", + kMptId, + kHolder1Account + ) + ); + auto const output = handler.process(input, Context{.yield = std::ref(yield)}); + ASSERT_FALSE(output); + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ( + err.at("error_message").as_string(), + "Invalid field 'accounts', not an array of account IDs." + ); + }); +} + +TEST_F(RPCMPTHoldersHandlerTest, AccountsMaxAllowed) +{ + std::string accountsList; + for (std::size_t i = 0; i < MPTHoldersHandler::kMaxAccounts; ++i) + accountsList += fmt::format(R"("{}",)", kHolder1Account); + accountsList.pop_back(); // drop trailing comma + + auto ledgerInfo = createLedgerHeader(kLedgerHash, 30); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerInfo)); + auto const issuanceKk = xrpl::keylet::mptokenIssuance(xrpl::uint192(kMptId)).key; + ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _)) + .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const mptoken = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId)); + std::vector const objs = {mptoken.getSerializer().peekData()}; + auto const key = + xrpl::keylet::mptoken(xrpl::uint192(kMptId), getAccountIdWithString(kHolder1Account)).key; + std::vector const expectedKeys = {key}; + EXPECT_CALL(*backend_, doFetchLedgerObjects(expectedKeys, 30, _)).WillOnce(Return(objs)); + EXPECT_CALL(*backend_, fetchMPTHolders).Times(0); + + auto const input = boost::json::parse( + fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "accounts": [{}] + }})JSON", + kMptId, + accountsList + ) + ); + runSpawn([&, this](auto& yield) { + auto handler = AnyHandler{MPTHoldersHandler{this->backend_}}; + auto const output = handler.process(input, Context{yield}); + ASSERT_TRUE(output); + EXPECT_EQ(output.result->as_object().at("mptokens").as_array().size(), 1u); + }); +} + +TEST_F(RPCMPTHoldersHandlerTest, AccountsFilterAllAbsent) +{ + auto const currentOutput = fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "ledger_index": 30, + "mptokens": [], + "validated": true + }})JSON", + kMptId + ); + + auto ledgerInfo = createLedgerHeader(kLedgerHash, 30); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerInfo)); + auto const issuanceKk = xrpl::keylet::mptokenIssuance(xrpl::uint192(kMptId)).key; + ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _)) + .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + std::vector const objs = {Blob{}, Blob{}}; + ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(objs)); + EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1); + EXPECT_CALL(*backend_, fetchMPTHolders).Times(0); + + auto const input = boost::json::parse( + fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "accounts": ["{}", "{}"] + }})JSON", + kMptId, + kHolder1Account, + kHolder2Account + ) + ); + runSpawn([&, this](auto& yield) { + auto handler = AnyHandler{MPTHoldersHandler{this->backend_}}; + auto const output = handler.process(input, Context{yield}); + ASSERT_TRUE(output); + EXPECT_EQ(boost::json::parse(currentOutput), *output.result); + }); +} + +TEST_F(RPCMPTHoldersHandlerTest, AccountsFilterConfidentialFields) +{ + constexpr auto kConfidentialBalanceInbox = "inbox-ciphertext"; + constexpr auto kConfidentialBalanceSpending = "spending-ciphertext"; + constexpr auto kConfidentialBalanceVersion = 3u; + constexpr auto kIssuerEncryptedBalance = "issuer-balance-ciphertext"; + constexpr auto kAuditorEncryptedBalance = "auditor-balance-ciphertext"; + constexpr auto kHolderEncryptionKey = "holder-pubkey"; + constexpr auto kLockedAmount = 5; + + auto const currentOutput = fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "ledger_index": 30, + "mptokens": [{{ + "account": "{}", + "flags": 0, + "mpt_amount": "1", + "mptoken_index": "D137F2E5A5767A06CB7A8F060ADE442A30CFF95028E1AF4B8767E3A56877205A", + "locked_amount": "{}", + "confidential_balance_inbox": "{}", + "confidential_balance_spending": "{}", + "confidential_balance_version": {}, + "issuer_encrypted_balance": "{}", + "auditor_encrypted_balance": "{}", + "holder_encryption_key": "{}" + }}], + "validated": true + }})JSON", + kMptId, + kHolder1Account, + kLockedAmount, + xrpl::strHex(std::string_view{kConfidentialBalanceInbox}), + xrpl::strHex(std::string_view{kConfidentialBalanceSpending}), + kConfidentialBalanceVersion, + xrpl::strHex(std::string_view{kIssuerEncryptedBalance}), + xrpl::strHex(std::string_view{kAuditorEncryptedBalance}), + xrpl::strHex(std::string_view{kHolderEncryptionKey}) + ); + + auto ledgerInfo = createLedgerHeader(kLedgerHash, 30); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerInfo)); + auto const issuanceKk = xrpl::keylet::mptokenIssuance(xrpl::uint192(kMptId)).key; + ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _)) + .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const mptoken = createMpTokenObject( + kHolder1Account, + xrpl::uint192(kMptId), + 1, + 0, + kLockedAmount, + kConfidentialBalanceInbox, + kConfidentialBalanceSpending, + kConfidentialBalanceVersion, + kIssuerEncryptedBalance, + kAuditorEncryptedBalance, + kHolderEncryptionKey + ); + std::vector const objs = {mptoken.getSerializer().peekData()}; + ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(objs)); + EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1); + EXPECT_CALL(*backend_, fetchMPTHolders).Times(0); + + auto const input = boost::json::parse( + fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "accounts": ["{}"] + }})JSON", + kMptId, + kHolder1Account + ) + ); + runSpawn([&, this](auto& yield) { + auto handler = AnyHandler{MPTHoldersHandler{this->backend_}}; + auto const output = handler.process(input, Context{yield}); + ASSERT_TRUE(output); + EXPECT_EQ(boost::json::parse(currentOutput), *output.result); + }); +} + +TEST_F(RPCMPTHoldersHandlerTest, AccountsFilterMPTNotFound) +{ + auto ledgerinfo = createLedgerHeader(kLedgerHash, 30); + ON_CALL(*backend_, fetchLedgerByHash(xrpl::uint256{kLedgerHash}, _)) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*backend_, fetchLedgerByHash).Times(1); + ON_CALL(*backend_, doFetchLedgerObject).WillByDefault(Return(std::optional{})); + EXPECT_CALL(*backend_, doFetchLedgerObject).Times(1); + // Must not attempt to fetch holder objects when the issuance does not exist. + EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(0); + + auto const input = boost::json::parse( + fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "ledger_hash": "{}", + "accounts": ["{}"] + }})JSON", + kMptId, + kLedgerHash, + kHolder1Account + ) + ); + runSpawn([&, this](boost::asio::yield_context yield) { + auto handler = AnyHandler{MPTHoldersHandler{this->backend_}}; + auto const output = handler.process(input, Context{.yield = yield}); + ASSERT_FALSE(output); + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "objectNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "objectNotFound"); + }); +} + +TEST_F(RPCMPTHoldersHandlerTest, AccountsFilterSpecificLedger) +{ + auto const specificLedger = 20; + auto const currentOutput = fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "ledger_index": {}, + "mptokens": [{}], + "validated": true + }})JSON", + kMptId, + specificLedger, + kMptOut1 + ); + + auto ledgerInfo = createLedgerHeader(kLedgerHash, specificLedger); + ON_CALL(*backend_, fetchLedgerBySequence(specificLedger, _)).WillByDefault(Return(ledgerInfo)); + EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); + auto const issuanceKk = xrpl::keylet::mptokenIssuance(xrpl::uint192(kMptId)).key; + ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, specificLedger, _)) + .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const mptoken1 = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId)); + std::vector const objs = {mptoken1.getSerializer().peekData()}; + std::vector const expectedKeys = { + xrpl::keylet::mptoken(xrpl::uint192(kMptId), getAccountIdWithString(kHolder1Account)).key + }; + EXPECT_CALL(*backend_, doFetchLedgerObjects(expectedKeys, specificLedger, _)) + .WillOnce(Return(objs)); + EXPECT_CALL(*backend_, fetchMPTHolders).Times(0); + + auto const input = boost::json::parse( + fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "ledger_index": {}, + "accounts": ["{}"] + }})JSON", + kMptId, + specificLedger, + kHolder1Account + ) + ); + runSpawn([&, this](auto& yield) { + auto handler = AnyHandler{MPTHoldersHandler{this->backend_}}; + auto const output = handler.process(input, Context{yield}); + ASSERT_TRUE(output); + EXPECT_EQ(boost::json::parse(currentOutput), *output.result); + }); +} + +TEST_F(RPCMPTHoldersHandlerTest, AccountsFilterDuplicates) +{ + auto const currentOutput = fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "ledger_index": 30, + "mptokens": [{}, {}], + "validated": true + }})JSON", + kMptId, + kMptOut1, + kMptOut2 + ); + + auto ledgerInfo = createLedgerHeader(kLedgerHash, 30); + EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerInfo)); + auto const issuanceKk = xrpl::keylet::mptokenIssuance(xrpl::uint192(kMptId)).key; + ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _)) + .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const mptoken1 = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId)); + auto const mptoken2 = createMpTokenObject(kHolder2Account, xrpl::uint192(kMptId)); + std::vector const objs = { + mptoken1.getSerializer().peekData(), mptoken2.getSerializer().peekData() + }; + std::vector const expectedKeys = { + xrpl::keylet::mptoken(xrpl::uint192(kMptId), getAccountIdWithString(kHolder1Account)).key, + xrpl::keylet::mptoken(xrpl::uint192(kMptId), getAccountIdWithString(kHolder2Account)).key + }; + EXPECT_CALL(*backend_, doFetchLedgerObjects(expectedKeys, 30, _)).WillOnce(Return(objs)); + EXPECT_CALL(*backend_, fetchMPTHolders).Times(0); + + auto const input = boost::json::parse( + fmt::format( + R"JSON({{ + "mpt_issuance_id": "{}", + "accounts": ["{}", "{}", "{}"] + }})JSON", + kMptId, + kHolder1Account, + kHolder2Account, + kHolder1Account + ) + ); + runSpawn([&, this](auto& yield) { + auto handler = AnyHandler{MPTHoldersHandler{this->backend_}}; + auto const output = handler.process(input, Context{yield}); + ASSERT_TRUE(output); + EXPECT_EQ(boost::json::parse(currentOutput), *output.result); + }); +} + TEST_F(RPCMPTHoldersHandlerTest, LimitMoreThanMAx) { auto const currentOutput = fmt::format( @@ -741,7 +1313,7 @@ TEST_F(RPCMPTHoldersHandlerTest, LimitMoreThanMAx) "validated": true }})JSON", kMptId, - kMptOuT1 + kMptOut1 ); auto ledgerInfo = createLedgerHeader(kLedgerHash, 30); @@ -750,7 +1322,7 @@ TEST_F(RPCMPTHoldersHandlerTest, LimitMoreThanMAx) ON_CALL(*backend_, doFetchLedgerObject(issuanceKk, 30, _)) .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); - auto const mptoken = createMpTokenObject(kHoldeR1Account, xrpl::uint192(kMptId)); + auto const mptoken = createMpTokenObject(kHolder1Account, xrpl::uint192(kMptId)); std::vector const mpts = {mptoken.getSerializer().peekData()}; ON_CALL(*backend_, fetchMPTHolders) .WillByDefault(Return(MPTHoldersAndCursor{.mptokens = mpts, .cursor = {}})); diff --git a/tests/unit/rpc/handlers/MPTokenIssuanceHistoryTests.cpp b/tests/unit/rpc/handlers/MPTokenIssuanceHistoryTests.cpp index f2ceed962..6ef543f5c 100644 --- a/tests/unit/rpc/handlers/MPTokenIssuanceHistoryTests.cpp +++ b/tests/unit/rpc/handlers/MPTokenIssuanceHistoryTests.cpp @@ -1855,14 +1855,18 @@ TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, LedgerHashWithOnlyLedgerIndexMax) }); } -TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, LedgerIndexValidatedStringIsNotASpecifier) +// `ledger_index` selects a single ledger, and "validated" is just a shortcut naming one, so +// the search range collapses to it. Matches account_tx here and in xrpld (AccountTx.cpp +// getLedgerRange() sets uLedgerMin = uLedgerMax = seq for any non-range specifier). +TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, LedgerIndexValidatedStringSelectsThatLedger) { - // "validated" resolves to no concrete index, so the request keeps the full ledger range. - auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); + auto const transactions = genTransactions(kMaxSeq, kMaxSeq - 1); auto const transCursor = TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; ON_CALL(*backend_, fetchMPTokenIssuanceTransactions).WillByDefault(Return(transCursor)); - EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(0); + + auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); + EXPECT_CALL(*backend_, fetchLedgerBySequence(kMaxSeq, _)).WillOnce(Return(ledgerHeader)); EXPECT_CALL(*backend_, fetchLedgerByHash).Times(0); runSpawn([&, this](auto yield) { @@ -1878,9 +1882,8 @@ TEST_F(RPCMPTokenIssuanceHistoryHandlerTest, LedgerIndexValidatedStringIsNotASpe ); auto const output = handler.process(req, Context{yield}); ASSERT_TRUE(output); - EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMinSeq); + EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMaxSeq); EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq); - EXPECT_EQ(output.result->at("transactions").as_array().size(), 2); }); } diff --git a/tests/unit/rpc/handlers/NFTBuyOffersTests.cpp b/tests/unit/rpc/handlers/NFTBuyOffersTests.cpp index 6a1f600b1..25f6959ff 100644 --- a/tests/unit/rpc/handlers/NFTBuyOffersTests.cpp +++ b/tests/unit/rpc/handlers/NFTBuyOffersTests.cpp @@ -688,7 +688,7 @@ TEST_F(RPCNFTBuyOffersHandlerTest, LimitLessThanMin) std::vector bbs; auto const offer = createNftBuyOffer(kNftId, kAccount); bbs.reserve(NFTBuyOffersHandler::kLimitMin + 1); - for (auto i = 0; i < NFTBuyOffersHandler::kLimitMin + 1; i++) + for (auto i = 0u; i < NFTBuyOffersHandler::kLimitMin + 1; i++) bbs.push_back(offer.getSerializer().peekData()); ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(bbs)); EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1); @@ -733,7 +733,7 @@ TEST_F(RPCNFTBuyOffersHandlerTest, LimitMoreThanMax) std::vector bbs; auto const offer = createNftBuyOffer(kNftId, kAccount); bbs.reserve(NFTBuyOffersHandler::kLimitMax + 1); - for (auto i = 0; i < NFTBuyOffersHandler::kLimitMax + 1; i++) + for (auto i = 0u; i < NFTBuyOffersHandler::kLimitMax + 1; i++) bbs.push_back(offer.getSerializer().peekData()); ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(bbs)); EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1); diff --git a/tests/unit/rpc/handlers/NFTHistoryTests.cpp b/tests/unit/rpc/handlers/NFTHistoryTests.cpp index e0d9f271f..ed1d5f7ad 100644 --- a/tests/unit/rpc/handlers/NFTHistoryTests.cpp +++ b/tests/unit/rpc/handlers/NFTHistoryTests.cpp @@ -908,6 +908,38 @@ TEST_F(RPCNFTHistoryHandlerTest, SpecificLedgerIndex) }); } +// "validated" is a shortcut naming a single ledger, so the search range collapses to it +// rather than staying at the full available range. Matches account_tx and xrpld's +// AccountTx.cpp getLedgerRange(), which sets uLedgerMin = uLedgerMax for any non-range +// specifier (the shortcuts included). +TEST_F(RPCNFTHistoryHandlerTest, SpecificLedgerIndexValidated) +{ + auto const transactions = genTransactions(kMaxSeq, kMaxSeq - 1); + auto const transCursor = + TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; + ON_CALL(*backend_, fetchNFTTransactions).WillByDefault(Return(transCursor)); + + auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); + EXPECT_CALL(*backend_, fetchLedgerBySequence(kMaxSeq, _)).WillOnce(Return(ledgerHeader)); + + runSpawn([&, this](auto yield) { + auto const handler = AnyHandler{NFTHistoryHandler{backend_}}; + static auto const kInput = boost::json::parse( + fmt::format( + R"JSON({{ + "nft_id": "{}", + "ledger_index": "validated" + }})JSON", + kNftId + ) + ); + auto const output = handler.process(kInput, Context{yield}); + ASSERT_TRUE(output); + EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMaxSeq); + EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq); + }); +} + TEST_F(RPCNFTHistoryHandlerTest, SpecificNonexistLedgerIntIndex) { EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); diff --git a/tests/unit/rpc/handlers/NFTInfoTests.cpp b/tests/unit/rpc/handlers/NFTInfoTests.cpp index 443199e03..c9402979f 100644 --- a/tests/unit/rpc/handlers/NFTInfoTests.cpp +++ b/tests/unit/rpc/handlers/NFTInfoTests.cpp @@ -59,6 +59,53 @@ TEST_F(RPCNFTInfoHandlerTest, NonHexLedgerHash) }); } +// nft_info is Clio-only, so ForwardingProxy::shouldForward returns false before it gets to the +// current/closed check and the request is dispatched here rather than sent to xrpld. Clio holds +// neither ledger, so the resolver has to reject them itself. +TEST_F(RPCNFTInfoHandlerTest, CurrentLedgerIndexRejected) +{ + runSpawn([this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{NFTInfoHandler{backend_}}; + auto const input = boost::json::parse( + fmt::format( + R"JSON({{ + "nft_id": "{}", + "ledger_index": "current" + }})JSON", + kNftId + ) + ); + auto const output = handler.process(input, Context{.yield = yield}); + ASSERT_FALSE(output); + + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerIndexMalformed"); + }); +} + +TEST_F(RPCNFTInfoHandlerTest, ClosedLedgerIndexRejected) +{ + runSpawn([this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{NFTInfoHandler{backend_}}; + auto const input = boost::json::parse( + fmt::format( + R"JSON({{ + "nft_id": "{}", + "ledger_index": "closed" + }})JSON", + kNftId + ) + ); + auto const output = handler.process(input, Context{.yield = yield}); + ASSERT_FALSE(output); + + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerIndexMalformed"); + }); +} + TEST_F(RPCNFTInfoHandlerTest, NonStringLedgerHash) { runSpawn([this](boost::asio::yield_context yield) { diff --git a/tests/unit/rpc/handlers/NFTSellOffersTests.cpp b/tests/unit/rpc/handlers/NFTSellOffersTests.cpp index e793316be..437919587 100644 --- a/tests/unit/rpc/handlers/NFTSellOffersTests.cpp +++ b/tests/unit/rpc/handlers/NFTSellOffersTests.cpp @@ -688,7 +688,7 @@ TEST_F(RPCNFTSellOffersHandlerTest, LimitLessThanMin) std::vector bbs; auto const offer = createNftSellOffer(kNftId, kAccount); bbs.reserve(NFTSellOffersHandler::kLimitMin + 1); - for (auto i = 0; i < NFTSellOffersHandler::kLimitMin + 1; i++) + for (auto i = 0u; i < NFTSellOffersHandler::kLimitMin + 1; i++) bbs.push_back(offer.getSerializer().peekData()); ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(bbs)); EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1); @@ -733,7 +733,7 @@ TEST_F(RPCNFTSellOffersHandlerTest, LimitMoreThanMax) std::vector bbs; auto const offer = createNftSellOffer(kNftId, kAccount); bbs.reserve(NFTSellOffersHandler::kLimitMax + 1); - for (auto i = 0; i < NFTSellOffersHandler::kLimitMax + 1; i++) + for (auto i = 0u; i < NFTSellOffersHandler::kLimitMax + 1; i++) bbs.push_back(offer.getSerializer().peekData()); ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(bbs)); EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1); diff --git a/tests/unit/rpc/handlers/SubscribeTests.cpp b/tests/unit/rpc/handlers/SubscribeTests.cpp index 83d8af706..0f83a3b0d 100644 --- a/tests/unit/rpc/handlers/SubscribeTests.cpp +++ b/tests/unit/rpc/handlers/SubscribeTests.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -475,7 +476,7 @@ generateTestValuesForParametersTest() ] })JSON", .expectedError = "badMarket", - .expectedErrorMessage = "badMarket" + .expectedErrorMessage = "No such market." }, SubscribeParamTestCaseBundle{ .testName = "BooksItemInvalidSnapshot", @@ -1175,7 +1176,7 @@ TEST(RPCSubscribeHandlerSpecTest, DeprecatedFields) {"rt_accounts", true} }; auto const spec = SubscribeHandler::spec(2); - auto const warnings = spec.check(json); + auto const warnings = rpc::spec::toJsonArray(spec.check(json)); ASSERT_EQ(warnings.size(), 1); auto const& warning = warnings[0]; ASSERT_TRUE(warning.is_object()); diff --git a/tests/unit/rpc/handlers/TestHandlerTests.cpp b/tests/unit/rpc/handlers/TestHandlerTests.cpp index 84a07769f..3d3c9bd9c 100644 --- a/tests/unit/rpc/handlers/TestHandlerTests.cpp +++ b/tests/unit/rpc/handlers/TestHandlerTests.cpp @@ -10,7 +10,6 @@ using namespace std; using namespace rpc; -using namespace rpc::validation; using namespace tests::common; class RPCTestHandlerTest : public HandlerBaseTest {}; @@ -19,7 +18,7 @@ class RPCTestHandlerTest : public HandlerBaseTest {}; TEST_F(RPCTestHandlerTest, HandlerSuccess) { runSpawn([](auto yield) { - auto const handler = AnyHandler{HandlerFake{}}; + auto const handler = AnyHandler{TypedHandlerFake{}}; auto const input = boost::json::parse(R"JSON({ "hello": "world", "limit": 10 @@ -48,7 +47,7 @@ TEST_F(RPCTestHandlerTest, NoInputHandlerSuccess) TEST_F(RPCTestHandlerTest, HandlerErrorHandling) { runSpawn([](auto yield) { - auto const handler = AnyHandler{HandlerFake{}}; + auto const handler = AnyHandler{TypedHandlerFake{}}; auto const input = boost::json::parse(R"JSON({ "hello": "not world", "limit": 10 @@ -67,7 +66,7 @@ TEST_F(RPCTestHandlerTest, HandlerErrorHandling) TEST_F(RPCTestHandlerTest, HandlerInnerErrorHandling) { runSpawn([](auto yield) { - auto const handler = AnyHandler{FailingHandlerFake{}}; + auto const handler = AnyHandler{FailingTypedHandlerFake{}}; auto const input = boost::json::parse(R"JSON({ "hello": "world", "limit": 10 diff --git a/tests/unit/rpc/handlers/TxTests.cpp b/tests/unit/rpc/handlers/TxTests.cpp index e7e4f63fc..06611e3af 100644 --- a/tests/unit/rpc/handlers/TxTests.cpp +++ b/tests/unit/rpc/handlers/TxTests.cpp @@ -151,6 +151,37 @@ TEST_F(RPCTxTest, ExcessiveLgrRange) }); } +// `transaction` is validated by rpc-spec's uint256 converter, which reports a distinct +// message for a non-string value versus an unparsable (but string) hash. +TEST_F(RPCTxTest, TransactionNotString) +{ + runSpawn([this](auto yield) { + auto const handler = AnyHandler{TestTxHandler{backend_, mockETLServicePtr_}}; + auto const req = boost::json::parse(R"JSON({"command": "tx", "transaction": 123})JSON"); + auto const output = handler.process(req, Context{yield}); + ASSERT_FALSE(output); + + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "transactionNotString"); + }); +} + +TEST_F(RPCTxTest, TransactionMalformed) +{ + runSpawn([this](auto yield) { + auto const handler = AnyHandler{TestTxHandler{backend_, mockETLServicePtr_}}; + auto const req = + boost::json::parse(R"JSON({"command": "tx", "transaction": "nothex"})JSON"); + auto const output = handler.process(req, Context{yield}); + ASSERT_FALSE(output); + + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "transactionMalformed"); + }); +} + TEST_F(RPCTxTest, InvalidBinaryV1) { TransactionAndMetadata tx; diff --git a/tests/unit/rpc/handlers/UnsubscribeTests.cpp b/tests/unit/rpc/handlers/UnsubscribeTests.cpp index 04c23300e..9d471f689 100644 --- a/tests/unit/rpc/handlers/UnsubscribeTests.cpp +++ b/tests/unit/rpc/handlers/UnsubscribeTests.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -438,7 +439,7 @@ generateTestValuesForParametersTest() ] })JSON", .expectedError = "badMarket", - .expectedErrorMessage = "badMarket" + .expectedErrorMessage = "No such market." }, UnsubscribeParamTestCaseBundle{ .testName = "BooksItemInvalidBoth", @@ -685,7 +686,7 @@ TEST(RPCUnsubscribeSpecTest, DeprecatedFields) {"rt_transactions", {"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"}}, }; auto const spec = UnsubscribeHandler::spec(2); - auto const warnings = spec.check(json); + auto const warnings = rpc::spec::toJsonArray(spec.check(json)); ASSERT_EQ(warnings.size(), 1); ASSERT_TRUE(warnings[0].is_object()); auto const& warning = warnings[0].as_object(); diff --git a/tests/unit/rpc/handlers/VaultInfoTests.cpp b/tests/unit/rpc/handlers/VaultInfoTests.cpp index a7910500c..ef4f94bdb 100644 --- a/tests/unit/rpc/handlers/VaultInfoTests.cpp +++ b/tests/unit/rpc/handlers/VaultInfoTests.cpp @@ -71,7 +71,7 @@ generateTestValuesForParametersTest() "idk": "idk" })JSON", .expectedError = "malformedRequest", - .expectedErrorCode = ClioError::RpcMalformedRequest, + .expectedErrorCode = rpc::ClioError::RpcMalformedRequest, .expectedErrorMessage = "Malformed request." }, VaultInfoParamTestCaseBundle{ @@ -80,7 +80,7 @@ generateTestValuesForParametersTest() "seq": 4 })JSON", .expectedError = "malformedRequest", - .expectedErrorCode = ClioError::RpcMalformedRequest, + .expectedErrorCode = rpc::ClioError::RpcMalformedRequest, .expectedErrorMessage = "Malformed request." }, VaultInfoParamTestCaseBundle{ @@ -89,7 +89,7 @@ generateTestValuesForParametersTest() "owner": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh" })JSON", .expectedError = "malformedRequest", - .expectedErrorCode = ClioError::RpcMalformedRequest, + .expectedErrorCode = rpc::ClioError::RpcMalformedRequest, .expectedErrorMessage = "Malformed request." }, VaultInfoParamTestCaseBundle{ @@ -99,7 +99,7 @@ generateTestValuesForParametersTest() "seq": "asdf" })JSON", .expectedError = "malformedRequest", - .expectedErrorCode = ClioError::RpcMalformedRequest, + .expectedErrorCode = rpc::ClioError::RpcMalformedRequest, .expectedErrorMessage = "Malformed request." }, VaultInfoParamTestCaseBundle{ @@ -109,7 +109,7 @@ generateTestValuesForParametersTest() "seq": 3 })JSON", .expectedError = "malformedRequest", - .expectedErrorCode = ClioError::RpcMalformedRequest, + .expectedErrorCode = rpc::ClioError::RpcMalformedRequest, .expectedErrorMessage = "OwnerNotHexString" }, VaultInfoParamTestCaseBundle{ @@ -119,7 +119,7 @@ generateTestValuesForParametersTest() "seq": 3 })JSON", .expectedError = "malformedRequest", - .expectedErrorCode = ClioError::RpcMalformedRequest, + .expectedErrorCode = rpc::ClioError::RpcMalformedRequest, .expectedErrorMessage = "OwnerNotHexString" }, VaultInfoParamTestCaseBundle{ @@ -128,7 +128,7 @@ generateTestValuesForParametersTest() "vault_id": 3 })JSON", .expectedError = "malformedRequest", - .expectedErrorCode = ClioError::RpcMalformedRequest, + .expectedErrorCode = rpc::ClioError::RpcMalformedRequest, .expectedErrorMessage = "Malformed request." }, VaultInfoParamTestCaseBundle{ @@ -137,7 +137,7 @@ generateTestValuesForParametersTest() "vault_id": "idk" })JSON", .expectedError = "malformedRequest", - .expectedErrorCode = ClioError::RpcMalformedRequest, + .expectedErrorCode = rpc::ClioError::RpcMalformedRequest, .expectedErrorMessage = "Malformed request." }, VaultInfoParamTestCaseBundle{ @@ -151,7 +151,7 @@ generateTestValuesForParametersTest() kAccount ), .expectedError = "malformedRequest", - .expectedErrorCode = ClioError::RpcMalformedRequest, + .expectedErrorCode = rpc::ClioError::RpcMalformedRequest, .expectedErrorMessage = "Malformed request." } }; diff --git a/tests/unit/util/LedgerUtilsTests.cpp b/tests/unit/util/LedgerUtilsTests.cpp index 2a21bf4bd..31688c4bd 100644 --- a/tests/unit/util/LedgerUtilsTests.cpp +++ b/tests/unit/util/LedgerUtilsTests.cpp @@ -45,7 +45,8 @@ TEST(LedgerUtilsTests, LedgerObjectTypeList) JS(loan_broker), JS(loan), JS(nunl), - JS(delegate) + JS(delegate), + JS(sponsorship) }; static_assert(std::size(kTypesList) == kTypes.size()); @@ -85,11 +86,14 @@ TEST(LedgerUtilsTests, DeletionBlockerTypes) xrpl::ltMPTOKEN_ISSUANCE, xrpl::ltMPTOKEN, xrpl::ltPERMISSIONED_DOMAIN, - xrpl::ltLOAN + xrpl::ltLOAN, + xrpl::ltSPONSORSHIP }; static_assert(std::size(kDeletionBlockers) == kTestedTypes.size()); - static_assert(std::ranges::any_of(kTestedTypes, [](auto const& type) { + // all_of, not any_of: with the size check above this pins the set exactly, so a swapped + // or missing blocker fails instead of being masked by the remaining matches. + static_assert(std::ranges::all_of(kTestedTypes, [](auto const& type) { return std::find(std::cbegin(kDeletionBlockers), std::cend(kDeletionBlockers), type) != std::cend(kDeletionBlockers); }));