mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-21 19:18:22 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fe9167bed | ||
|
|
2c5cee2925 | ||
|
|
450d00ad65 | ||
|
|
b515af4d4e | ||
|
|
cf495fde86 | ||
|
|
00bf88afad | ||
|
|
f45dab46d1 | ||
|
|
b87ad0f08e | ||
|
|
50c9e6335e | ||
|
|
78709c1d09 | ||
|
|
b67266aad4 |
No files matched your search
@@ -177,6 +177,7 @@ test.server > xrpld.core
|
||||
test.server > xrpld.rpc
|
||||
test.server > xrpl.json
|
||||
test.server > xrpl.protocol
|
||||
test.server > xrpl.resource
|
||||
test.server > xrpl.server
|
||||
test.unit_test > xrpl.basics
|
||||
test.unit_test > xrpl.protocol
|
||||
|
||||
@@ -24,6 +24,17 @@ This version is supported by all `xrpld` versions. For WebSocket and HTTP JSON-R
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- The `ripplerpc` request field, which selects the shape of the JSON-RPC reply envelope, is now validated, and a value that is not exactly `"1.0"`, `"2.0"` or `"3.0"` is rejected. This reaches the JSON-RPC transport at every API version. It does not reach a WebSocket session, which reads the field only to echo it back. A request sending `"2"`, `" 2.0"`, `"2.00"`, `"02.0"`, `"2.0.0"`, `"10.0"` or any other text, such as `"abc"` or `"x2"`, gets a working reply today. After this ships, such a request sent alone gets HTTP 400 with `ripplerpc is not a supported version`, charged as a malformed request, and such an entry of a `"method": "batch"` body gets that error in its own reply while the batch answers 200. A client that spells the version loosely must therefore be corrected to one of the three exact values, or omit the field. Previously the value was compared as a string, which both accepted values that name no version and ordered multi-digit versions incorrectly: `"abc"` and `"x2"` sorted above `"3.0"` and so selected version 3, and `"10.0"` sorted below `"2.0"` and so selected version 1. Requests that send one of the three supported values, or omit the field, are unaffected.
|
||||
- `subscribe`: every subscription a connection holds must name the same `api_version`. The first `subscribe` establishes it, and a later one naming a different version is refused with `apiVersionConflict`, registering nothing; the message names the version the connection's subscriptions are served at. The HTTP status is 400 where the envelope derives it from the error, with `ripplerpc: "3.0"` or at API version 3, and 200 with `ripplerpc` `"1.0"` or `"2.0"`, as for every error; a WebSocket frame carries no status. A `subscribe` refused for another reason, a stream name the server does not have for one, still fixes that version, since the registrations it makes are spread through the handler. A connection that subscribed one stream at `api_version` 2 and another at 1, in two calls, was previously served both at version 1, the later call's, so the first stream's shape changed under it. The version is fixed for the connection's life: a client that wants another version opens another connection. On the webhook path the subscriber is keyed on its `url` alone, so the refusal reaches a second admin because of a first admin's version, and the message names the version the url's subscriptions are served at, and no remedy: releasing a url means unsubscribing its streams, which would remove that first admin's subscription, and the server cannot tell the two callers apart. Naming the url alone does not release it, a subscriber a stream map still holds not being evicted. This reaches every API version, since version 1 and version 2 content is not interchangeable either: version 2 renames `transaction` to `tx_json` and hoists `hash`, so a version 1 client handed version 2 content finds no `transaction` member.
|
||||
- `batch`: An entry of a `"method": "batch"` request that names `api_version` both inside `params` and at its own top level is now served at the version inside `params`. It was previously served at the top-level version whenever the one inside `params` resolved to version 1, so an entry asking for version 1 explicitly and something else at the top level changes which version answers it. A lone request can name both values too, and for it the version inside `params` has always won; reversing that precedence was confined to a batch entry.
|
||||
|
||||
### Additions
|
||||
|
||||
- A JSON-RPC (HTTP) request may send its parameters as an object, `"params": {"account": "r..."}`, as well as the array of one object that was already accepted. This reaches every API version: such a request was previously rejected with HTTP 400 and `params unparsable`, and is now served. A request that already sends the array form is unaffected. An entry of a `"method": "batch"` request that carries a by-name `params` object is read for `api_version` and credentials from that object. Previously such an entry had its `api_version` read from its top level and its credentials ignored, since credentials were read only from an array-form `params`.
|
||||
- A JSON-RPC (HTTP) request may name its `api_version` at its own top level, beside `method` and `id`, as well as inside `params`. The member is an XRPL extension, which the JSON-RPC 2.0 specification does not define; placing it beside the specification's own members is what lets a version 3 client send a specification-shaped request. This is honored from API version 3 up, so it needs `[beta_rpc_api]`; a version below that named at the top level is ignored, exactly as it always has been. Previously only an entry of a `"method": "batch"` request had its own top level read; a WebSocket request, which is one flat object, has always been read there.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- A request echoed back in an error reply now has every credential-bearing field masked: `admin_password`, `admin_user`, `passphrase`, `password`, `secret`, `seed`, `seed_hex`, `url_password`, `url_username` and `username`. Nesting no longer matters, so a credential inside `params` is masked too. The same masking is applied to every request and reply written to the log, and it covers six further names that only a reply carries: `master_key`, `master_seed`, `master_seed_hex`, `validation_key`, `validation_private_key` and `validation_seed`, which is how `wallet_propose` and `validation_create` used to write a live private key to the log. A request or reply written to the log is truncated at 10,000 characters. An `[rpc_startup]` line that does not parse is reported by its position in the section and the parser's message, never by its text, and no command runs for it.
|
||||
@@ -31,6 +42,13 @@ This version is supported by all `xrpld` versions. For WebSocket and HTTP JSON-R
|
||||
- A WebSocket frame that does not parse, or exceeds the request size limit, is answered `{"type": "error", "error": "jsonInvalid", "size": <bytes>}`. The frame's body is reported by size rather than echoed back in a `value` member, since a body that does not parse has no fields to mask. A client that read `value` gets `size` instead.
|
||||
- Four error codes that named no HTTP status of their own, and so answered 200 on a reply reporting an error, now name one: `actMalformed`, `alreadyMultisig` and `alreadySingleSig` answer 400, and `actNotFound` answers 404. **No shipped envelope reports these four.** A request sending `ripplerpc: "3.0"` still receives 200 for all four, as it always has, so `account_info` on a malformed account or one the ledger does not hold answers 200 exactly as before.
|
||||
- `submit`, `simulate`, `transaction_entry`, `ledger_entry` and `ledger_accept`: Errors from these methods now include `error_code` and `error_message` alongside the `error` token, as every other method already did. Each error now answers the status its code names: 400 for a malformed request, 404 for `transactionNotFound`, 500 for an internal failure, and 501 for `notYetImplemented` and `notStandAlone`. That status change reaches only a request sending `ripplerpc: "3.0"`, which is the envelope that derives the status from the error. With `ripplerpc` `"1.0"` the status stays 200 and the two new members appear beside `error`; with `"2.0"` the status stays 200, `error_code` appears, and the `code` and `message` members carry the code and the message rather than null, since that envelope copies them from `error_code` and `error_message` and drops `error_message`.
|
||||
- A reply reporting HTTP 402 or 502 now carries a status line. Those two statuses named no case in the switch that writes one, so such a reply began with a header instead and did not parse as an HTTP response at all. Both are reachable at any API version with `ripplerpc: "3.0"`, which derives the status from the error code: 402 through `highFee` from `sign`, `sign_for` or `submit` with a low `fee_mult_max`, and 502 through `dbDeserialization` from `tx`. The eleven statuses that already named a case report the same phrase they always have.
|
||||
- An error reply to a request sending `ripplerpc: "2.0"` or `"3.0"` no longer carries a stray `"error_message": null` beside the error it reports. The member appeared only when the `Server` log partition was set to debug or lower, because the log statement read `error_message` after the reply had renamed it to `message`, and reading it put it back as null. So the reply a client received depended on the server's log level, and the log line itself printed an empty message. Both are fixed.
|
||||
- A body the server rejects before it reads a request out of it now says which of four things was wrong. A body over the size limit answers `Request is too large`. A body that parses to `{}`, `[]` or `null` answers `Request is empty`. A body that parses to a non-empty array answers `Request is not a JSON object`; that is the only other document the parser accepts at the top level. All three previously answered `Unable to parse request: ` with nothing after the colon, the parser having recorded no error for them. Any other body does not parse, which includes one that is only whitespace and one whose top-level value is a string, number or boolean; it answers `Unable to parse request: ` followed by the parser's own reason, as it did before. The status is 400 for all four, as before, and this reaches every API version.
|
||||
- `batch`: An entry that is not identified through a secure gateway no longer clears the connection's `X-User` and forwarded-for values for the entries after it, so every entry of one body reports the role and username it would have reported on its own.
|
||||
- `batch`: Every entry of a `"method": "batch"` request is now charged against the sender's resource allowance, including one rejected before it reaches a handler, and the request stops at the first entry the connection is too loaded to serve. A reply array can therefore be shorter than the request array, and its last element depends on where the batch stopped. An entry that is not a JSON object, or names an API version the server does not serve, is charged before its role is known and answered with its own rejection; if that charge took the connection over the drop threshold, the batch stops there and that rejection is the last answer. Every other entry met over the threshold is answered `Server is overloaded` and nothing follows. A client should read any reply array shorter than its request as a connection over the drop threshold, whatever the last element says. A well-behaved client is unaffected; one that sends thousands of entries in a single body no longer gets every one of them answered.
|
||||
- A body the server rejects before it reads a request out of it is now charged against the sender's resource allowance, as a malformed request already was. Five conditions were free: a body over the size limit, one that does not parse, one carrying no document, one that is not a JSON object, and a `"method": "batch"` naming no entry array. The answer to each is unchanged. A well-behaved client is unaffected; one that repeats such a body exhausts its allowance and is refused the next request a handler would have served. A WebSocket frame that does not parse, or exceeds the request size limit, is charged the same way, and a connection that sends only such frames is closed once it crosses the drop threshold.
|
||||
- `subscribe`, `path_find`: A subscription is now served at the API version its own `subscribe` call named, rather than at the version of the most recent `subscribe` or `path_find` on the same connection. A `path_find` no longer changes the version anything that connection has subscribed is served at. An ordinary request is unaffected: it names its own version and is answered at it, so a connection subscribed at `api_version` 3 still calls `account_info` at version 1 and reads the version 1 shape.
|
||||
|
||||
## XRP Ledger server version 3.5.0
|
||||
|
||||
|
||||
@@ -20,7 +20,11 @@ namespace xrpl {
|
||||
* the config the range extends to kApiBetaVersion.
|
||||
*
|
||||
* A request naming no version is served at kApiVersionIfUnspecified, which is 1
|
||||
* because a request wanting any later version states it.
|
||||
* because a request wanting any later version states it. A WebSocket message is
|
||||
* one flat object and names it at its top level at every version. A JSON-RPC
|
||||
* request names it in its parameters, or at its own top level: an entry of a
|
||||
* `"method": "batch"` body at every version, a lone request from
|
||||
* kApiMinimumSpecVersion up.
|
||||
*/
|
||||
|
||||
namespace rpc {
|
||||
@@ -36,6 +40,19 @@ static constexpr auto kApiCommandLineVersion = kApiVersion<1>; // TODO Bump to
|
||||
static constexpr auto kApiBetaVersion = kApiVersion<3>;
|
||||
static constexpr auto kApiMaximumValidVersion = kApiBetaVersion;
|
||||
|
||||
/**
|
||||
* First API version that follows the JSON-RPC 2.0 specification.
|
||||
*
|
||||
* From this version up a lone JSON-RPC request may state its `api_version` at
|
||||
* its own top level, beside the members the specification defines there, as a
|
||||
* WebSocket message always has. The member is an XRPL extension: the
|
||||
* specification defines no version member.
|
||||
*
|
||||
* Distinct from kApiBetaVersion, the same number today, which says instead that
|
||||
* the version needs [beta_rpc_api] to be reachable.
|
||||
*/
|
||||
static constexpr auto kApiMinimumSpecVersion = kApiVersion<3>;
|
||||
|
||||
static_assert(kApiInvalidVersion < kApiMinimumSupportedVersion);
|
||||
static_assert(
|
||||
kApiVersionIfUnspecified >= kApiMinimumSupportedVersion &&
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -155,7 +156,7 @@ enum ErrorCodeI {
|
||||
RpcNotYetImplemented = 105,
|
||||
RpcTransactionNotFound = 106,
|
||||
|
||||
// transaction_entry
|
||||
// transaction_entry + ledger_entry
|
||||
RpcMalformedRequest = 107,
|
||||
|
||||
// ledger_accept
|
||||
@@ -164,7 +165,38 @@ enum ErrorCodeI {
|
||||
// ledger_entry, API version 1 only
|
||||
RpcUnknownOption = 109,
|
||||
|
||||
RpcLast = RpcUnknownOption, // rpcLAST should always equal the last code.
|
||||
// ledger_entry field validation, one code per malformed field. The ledger_entry helpers report
|
||||
// invalidParams (31) for all of them, so each token needs a row of its own. Alphabetical by
|
||||
// token here only: the enum is append-only once a code ships.
|
||||
//
|
||||
// `malformedIssue` duplicates `issueMalformed` (93): same message, same status, two tokens,
|
||||
// both on the wire already. Do not add a third spelling.
|
||||
RpcMalformedAccount = 110,
|
||||
RpcMalformedAddress = 111,
|
||||
RpcMalformedAuthorized = 112,
|
||||
RpcMalformedAuthorizedCredentials = 113,
|
||||
RpcMalformedBridgeAccount = 114,
|
||||
RpcMalformedBroker = 115,
|
||||
RpcMalformedCurrency = 116,
|
||||
RpcMalformedDirRoot = 117,
|
||||
RpcMalformedDocumentID = 118,
|
||||
RpcMalformedIssue = 119,
|
||||
RpcMalformedIssuingChainDoor = 120,
|
||||
RpcMalformedLockingChainDoor = 121,
|
||||
RpcMalformedMPTIssuanceID = 122,
|
||||
RpcMalformedMPTokenIssuance = 123,
|
||||
RpcMalformedOwner = 124,
|
||||
RpcMalformedSeq = 125,
|
||||
RpcMalformedSponsee = 126,
|
||||
RpcMalformedSponsor = 127,
|
||||
RpcMalformedXChainOwnedClaimID = 128,
|
||||
RpcMalformedXChainOwnedCreateAccountClaimID = 129,
|
||||
|
||||
// subscribe
|
||||
RpcApiVersionConflict = 130,
|
||||
|
||||
// RpcLast should always equal the last code.
|
||||
RpcLast = RpcApiVersionConflict,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -217,6 +249,18 @@ struct ErrorInfo
|
||||
ErrorInfo const&
|
||||
getErrorInfo(ErrorCodeI code);
|
||||
|
||||
/**
|
||||
* Returns the error code that owns @p token.
|
||||
*
|
||||
* A linear scan over views measured at compile time, run once per error reply.
|
||||
* A duplicate token is a build error, so the answer is never ambiguous.
|
||||
*
|
||||
* @param token The error token to resolve.
|
||||
* @return The code the table gives @p token, or RpcUnknown if no row names it.
|
||||
*/
|
||||
ErrorCodeI
|
||||
codeForToken(std::string_view token);
|
||||
|
||||
/**
|
||||
* Add or update the json update to reflect the error code.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/json/json_forwards.h>
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::rpc {
|
||||
@@ -17,4 +19,25 @@ namespace xrpl::rpc {
|
||||
*/
|
||||
inline constexpr std::string_view kJsonRpcVersion{"2.0"};
|
||||
|
||||
/**
|
||||
* Codes for the `code` member of a JSON-RPC error object.
|
||||
*
|
||||
* The specification reserves -32768 to -32000 for the protocol and leaves
|
||||
* -32000 to -32099 of it to the implementation.
|
||||
*
|
||||
* kJsonRpcServerError is the code for an error an XRPL handler reports.
|
||||
*
|
||||
* The codes from kJsonRpcServerOverloaded on lie outside the
|
||||
* implementation-defined sub-range, which the specification does not allow.
|
||||
* They are the codes every shipped version reports, so moving one breaks the
|
||||
* clients matching on it.
|
||||
*/
|
||||
inline constexpr json::Int kJsonRpcServerError{-32000};
|
||||
inline constexpr json::Int kJsonRpcInvalidRequest{-32600};
|
||||
inline constexpr json::Int kJsonRpcMethodNotFound{-32601};
|
||||
inline constexpr json::Int kJsonRpcInvalidParams{-32602};
|
||||
inline constexpr json::Int kJsonRpcServerOverloaded{-32604};
|
||||
inline constexpr json::Int kJsonRpcForbidden{-32605};
|
||||
inline constexpr json::Int kJsonRpcWrongVersion{-32606};
|
||||
|
||||
} // namespace xrpl::rpc
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -113,7 +114,11 @@ public:
|
||||
// you get transactions as they occur or once their
|
||||
// results are confirmed
|
||||
virtual void
|
||||
subAccount(Ref ispListener, HashSet<AccountID> const& vnaAccountIDs, bool realTime) = 0;
|
||||
subAccount(
|
||||
Ref ispListener,
|
||||
HashSet<AccountID> const& vnaAccountIDs,
|
||||
bool realTime,
|
||||
unsigned int apiVersion) = 0;
|
||||
|
||||
// for normal use, removes from InfoSub and server
|
||||
virtual void
|
||||
@@ -130,10 +135,12 @@ public:
|
||||
/**
|
||||
* subscribe an account's new transactions and retrieve the account's
|
||||
* historical transactions
|
||||
* @param apiVersion The version the subscription is registered at,
|
||||
* which is the version its messages are shaped for.
|
||||
* @return rpcSUCCESS if successful, otherwise an error code
|
||||
*/
|
||||
virtual ErrorCodeI
|
||||
subAccountHistory(Ref ispListener, AccountID const& account) = 0;
|
||||
subAccountHistory(Ref ispListener, AccountID const& account, unsigned int apiVersion) = 0;
|
||||
|
||||
/**
|
||||
* unsubscribe an account's transactions
|
||||
@@ -196,30 +203,34 @@ public:
|
||||
HashSet<AccountID> historyAccounts) = 0;
|
||||
|
||||
// VFALCO TODO Document the bool return value
|
||||
//
|
||||
// Every sub* below takes the version the subscription is registered at, which is the
|
||||
// version its messages are shaped for. Registering the same subscription again replaces
|
||||
// the entry, so the newest registration decides that subscription's version.
|
||||
virtual bool
|
||||
subLedger(Ref ispListener, json::Value& jvResult) = 0;
|
||||
subLedger(Ref ispListener, json::Value& jvResult, unsigned int apiVersion) = 0;
|
||||
virtual bool
|
||||
unsubLedger(std::uint64_t uListener) = 0;
|
||||
|
||||
virtual bool
|
||||
subBookChanges(Ref ispListener) = 0;
|
||||
subBookChanges(Ref ispListener, unsigned int apiVersion) = 0;
|
||||
virtual bool
|
||||
unsubBookChanges(std::uint64_t uListener) = 0;
|
||||
|
||||
virtual bool
|
||||
subManifests(Ref ispListener) = 0;
|
||||
subManifests(Ref ispListener, unsigned int apiVersion) = 0;
|
||||
virtual bool
|
||||
unsubManifests(std::uint64_t uListener) = 0;
|
||||
virtual void
|
||||
pubManifest(Manifest const&) = 0;
|
||||
|
||||
virtual bool
|
||||
subServer(Ref ispListener, json::Value& jvResult, bool admin) = 0;
|
||||
subServer(Ref ispListener, json::Value& jvResult, bool admin, unsigned int apiVersion) = 0;
|
||||
virtual bool
|
||||
unsubServer(std::uint64_t uListener) = 0;
|
||||
|
||||
virtual bool
|
||||
subBook(Ref ispListener, Book const&) = 0;
|
||||
subBook(Ref ispListener, Book const&, unsigned int apiVersion) = 0;
|
||||
|
||||
/**
|
||||
* Remove a book subscription for a live subscriber.
|
||||
@@ -248,7 +259,8 @@ public:
|
||||
* InfoSub::bookSubscriptions_ because the InfoSub is being destroyed.
|
||||
* Called by ~InfoSub() for each book in bookSubscriptions_.
|
||||
*
|
||||
* @param uListener The sequence number of the subscriber being torn down.
|
||||
* @param uListener The sequence number of the subscriber being torn
|
||||
* down.
|
||||
* @param book The order book entry to remove.
|
||||
* @return true if the entry was present and removed, false otherwise
|
||||
* (e.g., already removed by a concurrent RPC unsubscribe).
|
||||
@@ -259,22 +271,22 @@ public:
|
||||
unsubBookInternal(std::uint64_t uListener, Book const&) = 0;
|
||||
|
||||
virtual bool
|
||||
subTransactions(Ref ispListener) = 0;
|
||||
subTransactions(Ref ispListener, unsigned int apiVersion) = 0;
|
||||
virtual bool
|
||||
unsubTransactions(std::uint64_t uListener) = 0;
|
||||
|
||||
virtual bool
|
||||
subRTTransactions(Ref ispListener) = 0;
|
||||
subRTTransactions(Ref ispListener, unsigned int apiVersion) = 0;
|
||||
virtual bool
|
||||
unsubRTTransactions(std::uint64_t uListener) = 0;
|
||||
|
||||
virtual bool
|
||||
subValidations(Ref ispListener) = 0;
|
||||
subValidations(Ref ispListener, unsigned int apiVersion) = 0;
|
||||
virtual bool
|
||||
unsubValidations(std::uint64_t uListener) = 0;
|
||||
|
||||
virtual bool
|
||||
subPeerStatus(Ref ispListener) = 0;
|
||||
subPeerStatus(Ref ispListener, unsigned int apiVersion) = 0;
|
||||
|
||||
virtual bool
|
||||
unsubPeerStatus(std::uint64_t uListener) = 0;
|
||||
@@ -282,12 +294,12 @@ public:
|
||||
pubPeerStatus(std::function<json::Value()> const&) = 0;
|
||||
|
||||
virtual bool
|
||||
subConsensus(Ref ispListener) = 0;
|
||||
subConsensus(Ref ispListener, unsigned int apiVersion) = 0;
|
||||
virtual bool
|
||||
unsubConsensus(std::uint64_t uListener) = 0;
|
||||
|
||||
virtual void
|
||||
subMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) = 0;
|
||||
subMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs, unsigned int apiVersion) = 0;
|
||||
virtual void
|
||||
unsubMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) = 0;
|
||||
|
||||
@@ -385,8 +397,9 @@ public:
|
||||
/**
|
||||
* Whether this connection already tracks an account-history for @p account.
|
||||
*
|
||||
* `doSubscribe` reads this to charge the cap for an account_history_tx_stream
|
||||
* only when it is net-new, matching the account branches.
|
||||
* `doSubscribe` reads this to charge the cap for an
|
||||
* account_history_tx_stream only when it is net-new, matching the account
|
||||
* branches.
|
||||
*
|
||||
* @param account The account an account_history_tx_stream would add.
|
||||
* @return true if @p account is already in the account-history set.
|
||||
@@ -447,11 +460,26 @@ public:
|
||||
std::shared_ptr<InfoSubRequest> const&
|
||||
getRequest();
|
||||
|
||||
void
|
||||
setApiVersion(unsigned int apiVersion);
|
||||
|
||||
[[nodiscard]] unsigned int
|
||||
getApiVersion() const noexcept;
|
||||
/**
|
||||
* Establishes @p apiVersion as the version every subscription on this
|
||||
* connection is served at.
|
||||
*
|
||||
* A connection serves one version: one message has one shape, and several
|
||||
* of its subscriptions may match it. The first `subscribe` decides it and
|
||||
* it is never cleared. A `subscribe` refused for another reason still
|
||||
* establishes it, the registrations it makes being spread through the
|
||||
* handler.
|
||||
*
|
||||
* For the subscribe path alone. Nothing reads this value back: a publisher
|
||||
* reads the version recorded on the subscription it took the sink from.
|
||||
*
|
||||
* @param apiVersion The version the `subscribe` named.
|
||||
* @return nullopt when @p apiVersion is this connection's version;
|
||||
* otherwise the version already established.
|
||||
* @note Thread-safe: takes `lock_`.
|
||||
*/
|
||||
[[nodiscard]] std::optional<unsigned int>
|
||||
establishSubscriptionVersion(unsigned int apiVersion);
|
||||
|
||||
void
|
||||
insertSubMPTInfo(MPTID const& mptID);
|
||||
@@ -485,9 +513,20 @@ private:
|
||||
HashSet<AccountID> accountHistorySubscriptions_;
|
||||
HashSet<Book> bookSubscriptions_;
|
||||
HashSet<MPTID> mptSubscriptions_;
|
||||
unsigned int apiVersion_ = 0;
|
||||
// The API version every subscription on this connection was registered at, set by the first
|
||||
// `subscribe` and never cleared. Guarded by lock_. Read only by establishSubscriptionVersion,
|
||||
// so no publisher can reach it.
|
||||
std::optional<unsigned int> subscriptionApiVersion_;
|
||||
|
||||
static int
|
||||
/**
|
||||
* The next sequence number, which is what identifies a connection.
|
||||
*
|
||||
* 64 bits wide, as the counter and `seq_` are, so no two live connections
|
||||
* share an identity.
|
||||
*
|
||||
* @return A sequence number no live connection holds.
|
||||
*/
|
||||
static std::uint64_t
|
||||
assignId()
|
||||
{
|
||||
static std::atomic<std::uint64_t> kID(0);
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace xrpl {
|
||||
* credential this library cannot mask, so the caller logs it masked.
|
||||
*
|
||||
* A 401 with an empty body is answered with the fixed authentication page.
|
||||
* The status line carries the phrase Beast's registry gives @p nStatus, except
|
||||
* for 401 and 503, which carry a phrase of this server's own.
|
||||
*
|
||||
* @param nStatus The HTTP status code.
|
||||
* @param strMsg The body.
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl {
|
||||
namespace rpc {
|
||||
@@ -116,6 +118,27 @@ static constexpr ErrorInfo kUnorderedErrorInfos[]{
|
||||
{RpcMalformedRequest, "malformedRequest", "Request is malformed.", 400},
|
||||
{RpcNotStandAlone, "notStandAlone", "Server is not running stand-alone.", 501},
|
||||
{RpcUnknownOption, "unknownOption", "Unknown option.", 400},
|
||||
{RpcMalformedAccount, "malformedAccount", "Account is malformed.", 400},
|
||||
{RpcMalformedAddress, "malformedAddress", "Address is malformed.", 400},
|
||||
{RpcMalformedAuthorized, "malformedAuthorized", "Authorized is malformed.", 400},
|
||||
{RpcMalformedAuthorizedCredentials, "malformedAuthorizedCredentials", "AuthorizedCredentials is malformed.", 400},
|
||||
{RpcMalformedBridgeAccount, "malformedBridgeAccount", "BridgeAccount is malformed.", 400},
|
||||
{RpcMalformedBroker, "malformedBroker", "Broker is malformed.", 400},
|
||||
{RpcMalformedCurrency, "malformedCurrency", "Currency is malformed.", 400},
|
||||
{RpcMalformedDirRoot, "malformedDirRoot", "DirRoot is malformed.", 400},
|
||||
{RpcMalformedDocumentID, "malformedDocumentID", "DocumentID is malformed.", 400},
|
||||
{RpcMalformedIssue, "malformedIssue", "Issue is malformed.", 400},
|
||||
{RpcMalformedIssuingChainDoor, "malformedIssuingChainDoor", "IssuingChainDoor is malformed.", 400},
|
||||
{RpcMalformedLockingChainDoor, "malformedLockingChainDoor", "LockingChainDoor is malformed.", 400},
|
||||
{RpcMalformedMPTIssuanceID, "malformedMPTIssuanceID", "MPTIssuanceID is malformed.", 400},
|
||||
{RpcMalformedMPTokenIssuance, "malformedMPTokenIssuance", "MPTokenIssuance is malformed.", 400},
|
||||
{RpcMalformedOwner, "malformedOwner", "Owner is malformed.", 400},
|
||||
{RpcMalformedSeq, "malformedSeq", "Seq is malformed.", 400},
|
||||
{RpcMalformedSponsee, "malformedSponsee", "Sponsee is malformed.", 400},
|
||||
{RpcMalformedSponsor, "malformedSponsor", "Sponsor is malformed.", 400},
|
||||
{RpcMalformedXChainOwnedClaimID, "malformedXChainOwnedClaimID", "XChainOwnedClaimID is malformed.", 400},
|
||||
{RpcMalformedXChainOwnedCreateAccountClaimID, "malformedXChainOwnedCreateAccountClaimID", "XChainOwnedCreateAccountClaimID is malformed.", 400},
|
||||
{RpcApiVersionConflict, "apiVersionConflict", "Subscriptions on one connection must all name the same api_version.", 400},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
@@ -131,7 +154,7 @@ sortErrorInfos(ErrorInfo const (&unordered)[N]) -> std::array<ErrorInfo, M>
|
||||
if (info.code <= RpcSuccess || info.code > RpcLast)
|
||||
throw(std::out_of_range("Invalid error_code_i"));
|
||||
|
||||
// The first valid code follows rpcSUCCESS immediately.
|
||||
// The first valid code follows RpcSuccess immediately.
|
||||
static_assert(RpcSuccess == 0, "Unexpected error_code_i layout.");
|
||||
int const index{info.code - 1};
|
||||
|
||||
@@ -142,10 +165,10 @@ sortErrorInfos(ErrorInfo const (&unordered)[N]) -> std::array<ErrorInfo, M>
|
||||
}
|
||||
|
||||
// Verify that all entries are filled in starting with 1 and proceeding
|
||||
// to rpcLAST.
|
||||
// to RpcLast.
|
||||
//
|
||||
// It's okay for there to be missing entries; they will contain the code
|
||||
// rpcUNKNOWN. But other than that all entries should match their index.
|
||||
// RpcUnknown. But other than that all entries should match their index.
|
||||
int codeCount{0};
|
||||
int expect{RpcBadSyntax - 1};
|
||||
for (ErrorInfo const& info : ret)
|
||||
@@ -170,6 +193,68 @@ constexpr auto kSortedErrorInfos{sortErrorInfos<RpcLast>(kUnorderedErrorInfos)};
|
||||
|
||||
constexpr ErrorInfo kUnknownError;
|
||||
|
||||
/**
|
||||
* Asserts that no two rows of @p sorted name the same token.
|
||||
*
|
||||
* A duplicate is then a build error rather than a reply reporting the wrong
|
||||
* `error_code`. `consteval` admits no run-time call, so the two `throw`
|
||||
* statements are how a constant evaluation fails and are otherwise unreachable.
|
||||
*
|
||||
* @param sorted The table, ordered by code, with a gap for every unused code.
|
||||
* @return True. It reports failure by failing to be a constant expression.
|
||||
*/
|
||||
template <std::size_t N>
|
||||
consteval bool
|
||||
tokensAreUnique(std::array<ErrorInfo, N> const& sorted)
|
||||
{
|
||||
for (std::size_t i{0}; i < N; ++i)
|
||||
{
|
||||
// Gaps in the table name no error, so they name no token either.
|
||||
if (sorted[i].code == RpcUnknown)
|
||||
continue;
|
||||
|
||||
std::string_view const token{sorted[i].token.cStr()};
|
||||
if (token.empty())
|
||||
throw(std::invalid_argument("Empty token in list"));
|
||||
|
||||
for (std::size_t j{i + 1}; j < N; ++j)
|
||||
{
|
||||
if (sorted[j].code != RpcUnknown && std::string_view{sorted[j].token.cStr()} == token)
|
||||
throw(std::invalid_argument("Duplicate token in list"));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static_assert(tokensAreUnique(kSortedErrorInfos), "Duplicate token in list");
|
||||
|
||||
/**
|
||||
* The tokens of @p sorted, measured at compile time.
|
||||
*
|
||||
* A lookup compares a caller's token against these rather than against the
|
||||
* table's own `char const*`, which it would have to measure first: comparing a
|
||||
* view with a pointer calls strlen on every row the scan passes. Built from the
|
||||
* same table, so the two cannot name different tokens.
|
||||
*
|
||||
* @param sorted The table, ordered by code, with a gap for every unused code.
|
||||
* @return One view per row, the empty view for a gap. No token a caller can
|
||||
* pass is equal to the empty view.
|
||||
*/
|
||||
template <std::size_t N>
|
||||
consteval std::array<std::string_view, N>
|
||||
tokenViews(std::array<ErrorInfo, N> const& sorted)
|
||||
{
|
||||
std::array<std::string_view, N> ret{};
|
||||
for (std::size_t i{0}; i < N; ++i)
|
||||
{
|
||||
if (sorted[i].code != RpcUnknown)
|
||||
ret[i] = std::string_view{sorted[i].token.cStr()};
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
constexpr auto kSortedTokens{tokenViews(kSortedErrorInfos)};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -200,6 +285,21 @@ getErrorInfo(ErrorCodeI code)
|
||||
return detail::kSortedErrorInfos[code - 1];
|
||||
}
|
||||
|
||||
ErrorCodeI
|
||||
codeForToken(std::string_view token)
|
||||
{
|
||||
if (token.empty())
|
||||
return RpcUnknown;
|
||||
|
||||
for (std::size_t i{0}; i < detail::kSortedTokens.size(); ++i)
|
||||
{
|
||||
// A gap holds the empty view, which the token is not, so a gap names no code here either.
|
||||
if (detail::kSortedTokens[i] == token)
|
||||
return detail::kSortedErrorInfos[i].code;
|
||||
}
|
||||
return RpcUnknown;
|
||||
}
|
||||
|
||||
json::Value
|
||||
makeError(ErrorCodeI code)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
@@ -14,6 +13,7 @@
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -306,17 +306,18 @@ InfoSub::getRequest()
|
||||
return request_;
|
||||
}
|
||||
|
||||
void
|
||||
InfoSub::setApiVersion(unsigned int apiVersion)
|
||||
std::optional<unsigned int>
|
||||
InfoSub::establishSubscriptionVersion(unsigned int apiVersion)
|
||||
{
|
||||
apiVersion_ = apiVersion;
|
||||
}
|
||||
std::scoped_lock const sl(lock_);
|
||||
|
||||
unsigned int
|
||||
InfoSub::getApiVersion() const noexcept
|
||||
{
|
||||
XRPL_ASSERT(apiVersion_ > 0, "xrpl::InfoSub::getApiVersion : valid API version");
|
||||
return apiVersion_;
|
||||
if (!subscriptionApiVersion_)
|
||||
subscriptionApiVersion_ = apiVersion;
|
||||
|
||||
if (*subscriptionApiVersion_ == apiVersion)
|
||||
return std::nullopt;
|
||||
|
||||
return subscriptionApiVersion_;
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
#include <xrpl/protocol/BuildInfo.h>
|
||||
#include <xrpl/protocol/SystemParameters.h>
|
||||
|
||||
#include <boost/beast/http/status.hpp>
|
||||
|
||||
#include <ctime>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -72,42 +75,29 @@ httpReply(int nStatus, std::string const& content, json::Output const& output, b
|
||||
return;
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(bugprone-switch-missing-default-case)
|
||||
switch (nStatus)
|
||||
{
|
||||
// The status every successful reply carries, so a literal rather than a format call.
|
||||
case 200:
|
||||
output("HTTP/1.1 200 OK\r\n");
|
||||
break;
|
||||
case 202:
|
||||
output("HTTP/1.1 202 Accepted\r\n");
|
||||
break;
|
||||
case 400:
|
||||
output("HTTP/1.1 400 Bad Request\r\n");
|
||||
break;
|
||||
// Two statuses this server phrases itself rather than taking from the registry.
|
||||
case 401:
|
||||
output("HTTP/1.1 401 Authorization Required\r\n");
|
||||
break;
|
||||
case 403:
|
||||
output("HTTP/1.1 403 Forbidden\r\n");
|
||||
break;
|
||||
case 404:
|
||||
output("HTTP/1.1 404 Not Found\r\n");
|
||||
break;
|
||||
case 405:
|
||||
output("HTTP/1.1 405 Method Not Allowed\r\n");
|
||||
break;
|
||||
case 429:
|
||||
output("HTTP/1.1 429 Too Many Requests\r\n");
|
||||
break;
|
||||
case 500:
|
||||
output("HTTP/1.1 500 Internal Server Error\r\n");
|
||||
break;
|
||||
case 501:
|
||||
output("HTTP/1.1 501 Not Implemented\r\n");
|
||||
break;
|
||||
case 503:
|
||||
output("HTTP/1.1 503 Server is overloaded\r\n");
|
||||
break;
|
||||
default:
|
||||
// A reply whose first line is a header is not an HTTP response. Beast knows the whole
|
||||
// registry, so a status the error table gains needs no case here.
|
||||
output(
|
||||
std::format(
|
||||
"HTTP/1.1 {} {}\r\n",
|
||||
nStatus,
|
||||
boost::beast::http::obsolete_reason(
|
||||
static_cast<boost::beast::http::status>(nStatus))));
|
||||
break;
|
||||
}
|
||||
|
||||
output(getHTTPHeaderTimestamp());
|
||||
|
||||
@@ -3051,6 +3051,87 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
}
|
||||
|
||||
public:
|
||||
/**
|
||||
* The malformed arms of the object-shaped entry types that nothing else
|
||||
* feeds.
|
||||
*
|
||||
* Each arm names its own token, and the tokens and messages asserted here
|
||||
* are literals, so a helper handed the wrong code fails the case that
|
||||
* reaches it.
|
||||
*/
|
||||
void
|
||||
testMalformedTypedFields()
|
||||
{
|
||||
testcase("Malformed typed fields");
|
||||
using namespace test::jtx;
|
||||
Env env{*this};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
|
||||
// A 64-hex string parses as a UInt256, so the `loan` cases below can reach the field
|
||||
// after it.
|
||||
static constexpr char const* kHash =
|
||||
"1111111111111111111111111111111111111111111111111111111111111111";
|
||||
|
||||
{
|
||||
// `dir_root` present but not a hash. The arm above requires exactly one of `owner`
|
||||
// and `dir_root`, so this is reached only with `dir_root` named.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::directory] = json::ValueType::Object;
|
||||
jvParams[jss::directory][jss::dir_root] = "notahash";
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "malformedDirRoot", "Invalid field 'dir_root', not hash.");
|
||||
}
|
||||
{
|
||||
// `loan_broker` as an object naming no `owner`.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::loan_broker] = json::ValueType::Object;
|
||||
jvParams[jss::loan_broker][jss::seq] = 1;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "malformedRequest", "Missing field 'owner'.");
|
||||
}
|
||||
{
|
||||
// `loan_broker` naming an `owner` that parses, so the `seq` below it is reached.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::loan_broker] = json::ValueType::Object;
|
||||
jvParams[jss::loan_broker][jss::owner] = alice.human();
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "malformedRequest", "Missing field 'seq'.");
|
||||
}
|
||||
{
|
||||
// `loan` as an object naming no `loan_broker_id`.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::loan] = json::ValueType::Object;
|
||||
jvParams[jss::loan][jss::loan_seq] = 1;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "malformedRequest", "Missing field 'loan_broker_id'.");
|
||||
}
|
||||
{
|
||||
// `loan` naming a `loan_broker_id` that parses, so the `loan_seq` below it is reached.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::loan] = json::ValueType::Object;
|
||||
jvParams[jss::loan][jss::loan_broker_id] = kHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "malformedRequest", "Missing field 'loan_seq'.");
|
||||
}
|
||||
{
|
||||
// `mpt_issuance` that is not a Hash192. A hash of the wrong width reaches the same
|
||||
// arm as text does.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::mpt_issuance] = kHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(
|
||||
jrr, "malformedMPTokenIssuance", "Invalid field 'mpt_issuance', not Hash192.");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
@@ -3085,6 +3166,7 @@ public:
|
||||
testFixed();
|
||||
testHashes();
|
||||
testCLI();
|
||||
testMalformedTypedFields();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3176,6 +3258,43 @@ class LedgerEntry_XChain_test : public beast::unit_test::Suite,
|
||||
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
{
|
||||
// A `bridge_account` that is neither door. The case above names the issuing chain's
|
||||
// door, which passes this check and fails the lookup instead.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::bridge_account] = mcAlice.human();
|
||||
jvParams[jss::bridge] = jvb;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
mcEnv.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
|
||||
checkErrorValue(jrr, "malformedRequest", "");
|
||||
}
|
||||
{
|
||||
// A `LockingChainIssue` that `issueFromJson` refuses. It is read before the issuing
|
||||
// chain's issue, so a malformed pair reports this arm first.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::bridge_account] = mcDoor.human();
|
||||
jvParams[jss::bridge] = jvb;
|
||||
jvParams[jss::bridge][sfLockingChainIssue.jsonName] = "notanissue";
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
mcEnv.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
|
||||
checkErrorValue(jrr, "malformedIssue", "Invalid field 'LockingChainIssue', not Issue.");
|
||||
}
|
||||
{
|
||||
// The same for `IssuingChainIssue`, reached only once the locking chain issue reads.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::bridge_account] = mcDoor.human();
|
||||
jvParams[jss::bridge] = jvb;
|
||||
jvParams[jss::bridge][sfIssuingChainIssue.jsonName] = "notanissue";
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
mcEnv.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
|
||||
checkErrorValue(jrr, "malformedIssue", "Invalid field 'IssuingChainIssue', not Issue.");
|
||||
}
|
||||
{
|
||||
// create two claim ids and verify that the bridge counter was
|
||||
// incremented
|
||||
|
||||
@@ -2172,6 +2172,264 @@ public:
|
||||
wscB->invoke("unsubscribe", accountsRequest({alice.human()}));
|
||||
}
|
||||
|
||||
/**
|
||||
* A `subscribe` request for one stream at one API version.
|
||||
*
|
||||
* @param stream The stream to name.
|
||||
* @param apiVersion The `api_version` to name.
|
||||
* @return The request parameters.
|
||||
*/
|
||||
static json::Value
|
||||
streamRequest(char const* stream, unsigned apiVersion)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::streams] = json::ValueType::Array;
|
||||
jv[jss::streams].append(stream);
|
||||
jv[jss::api_version] = apiVersion;
|
||||
return jv;
|
||||
}
|
||||
|
||||
/**
|
||||
* A connection holds subscriptions at one API version only.
|
||||
*
|
||||
* The first `subscribe` establishes the version and a later one naming
|
||||
* another is refused, registering nothing, so the first subscription keeps
|
||||
* publishing in the shape it asked for. The version is never cleared, and a
|
||||
* connection that unsubscribed everything is still held to it: the way to
|
||||
* another version is another connection.
|
||||
*
|
||||
* A webhook subscriber is keyed on its `url` alone, so the refusal reaches
|
||||
* a second caller because of a first caller's version. `unsubscribe` with
|
||||
* that `url` evicts the subscriber, after which any version can be named
|
||||
* again, which is the only escape hatch the rule leaves and so is the half
|
||||
* worth pinning.
|
||||
*
|
||||
* Nothing else is refused. Every request names its own API version and is
|
||||
* answered at it, so a connection subscribed at version 3 still calls
|
||||
* `account_info` at version 1.
|
||||
*/
|
||||
void
|
||||
testOneApiVersionPerConnection()
|
||||
{
|
||||
testcase("A connection subscribes at one API version only");
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, singleThreadIo(envconfig())};
|
||||
|
||||
// Two frames at two versions on one connection: the second is refused and registers
|
||||
// nothing, and the first still publishes.
|
||||
{
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
|
||||
auto const first = wsc->invoke("subscribe", streamRequest("ledger", 3u));
|
||||
BEAST_EXPECTS(!first.isMember(jss::error), to_string(first));
|
||||
|
||||
auto const second = wsc->invoke("subscribe", streamRequest("transactions", 1u));
|
||||
BEAST_EXPECTS(second[jss::error] == "apiVersionConflict", to_string(second));
|
||||
// `invoke` nests an error reply under `result` and hoists only the token to the top
|
||||
// level.
|
||||
BEAST_EXPECTS(
|
||||
second[jss::result][jss::error_message] ==
|
||||
"Subscriptions on this connection are served at api_version 3; use a new "
|
||||
"connection.",
|
||||
to_string(second));
|
||||
|
||||
env.fund(XRP(10000), Account{"alice"});
|
||||
BEAST_EXPECT(env.syncClose());
|
||||
|
||||
// The subscription the first frame made still publishes: the refusal registered
|
||||
// nothing and took nothing away. The event names its `type` here, as every version
|
||||
// does.
|
||||
BEAST_EXPECT(wsc->findMsg(
|
||||
5s, [](json::Value const& jv) { return jv[jss::type] == "ledgerClosed"; }));
|
||||
|
||||
// The refused frame registered nothing, so no transaction event arrives.
|
||||
BEAST_EXPECT(!wsc->findMsg(
|
||||
1s, [](json::Value const& jv) { return jv[jss::type] == "transaction"; }));
|
||||
}
|
||||
|
||||
// The version outlives an unsubscribe of everything: it is the connection's for its life.
|
||||
// The unsubscribe names another version and is served, being an ordinary request.
|
||||
{
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
BEAST_EXPECT(
|
||||
wsc->invoke("subscribe", streamRequest("ledger", 3u))[jss::status] == jss::success);
|
||||
|
||||
auto const unsubscribed = wsc->invoke("unsubscribe", streamRequest("ledger", 1u));
|
||||
BEAST_EXPECTS(unsubscribed[jss::status] == jss::success, to_string(unsubscribed));
|
||||
|
||||
BEAST_EXPECT(env.syncClose());
|
||||
BEAST_EXPECT(!wsc->findMsg(
|
||||
1s, [](json::Value const& jv) { return jv[jss::type] == "ledgerClosed"; }));
|
||||
|
||||
auto const again = wsc->invoke("subscribe", streamRequest("ledger", 1u));
|
||||
BEAST_EXPECTS(again[jss::error] == "apiVersionConflict", to_string(again));
|
||||
}
|
||||
|
||||
// Two streams at one version on one connection are both served: the rule is about versions,
|
||||
// not about how many subscriptions a connection holds.
|
||||
{
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
BEAST_EXPECT(
|
||||
wsc->invoke("subscribe", streamRequest("ledger", 3u))[jss::status] == jss::success);
|
||||
BEAST_EXPECT(
|
||||
wsc->invoke("subscribe", streamRequest("transactions", 3u))[jss::status] ==
|
||||
jss::success);
|
||||
}
|
||||
|
||||
// An ordinary request is not refused, whatever version it names, and is answered at its
|
||||
// own version. The version lives on the connection, so only `doSubscribe` checks it.
|
||||
{
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
BEAST_EXPECT(
|
||||
wsc->invoke("subscribe", streamRequest("ledger", 3u))[jss::status] == jss::success);
|
||||
|
||||
// Told apart by content: `tx_history` runs at version 1, writing `index`, and is
|
||||
// absent from version 2, which answers `unknownCmd`.
|
||||
auto const txHistory = [](unsigned apiVersion) {
|
||||
json::Value jv;
|
||||
jv[jss::start] = 0u;
|
||||
jv[jss::api_version] = apiVersion;
|
||||
return jv;
|
||||
};
|
||||
auto const atV1 = wsc->invoke("tx_history", txHistory(1u));
|
||||
BEAST_EXPECTS(atV1[jss::status] == jss::success, to_string(atV1));
|
||||
BEAST_EXPECTS(atV1[jss::result].isMember(jss::index), to_string(atV1));
|
||||
|
||||
auto const atV2 = wsc->invoke("tx_history", txHistory(2u));
|
||||
BEAST_EXPECTS(atV2[jss::error] == "unknownCmd", to_string(atV2));
|
||||
|
||||
// Naming the connection's own version is answered too.
|
||||
json::Value atV3;
|
||||
atV3[jss::api_version] = 3u;
|
||||
BEAST_EXPECT(wsc->invoke("ledger_closed", atV3)[jss::status] == jss::success);
|
||||
}
|
||||
|
||||
// A connection holding no subscription at all is held to nothing, which is what shows the
|
||||
// refusal is keyed on a recorded subscription rather than on the versions a connection has
|
||||
// used.
|
||||
{
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
|
||||
json::Value atV1;
|
||||
atV1[jss::api_version] = 1u;
|
||||
BEAST_EXPECT(wsc->invoke("ledger_closed", atV1)[jss::status] == jss::success);
|
||||
|
||||
json::Value atV3;
|
||||
atV3[jss::api_version] = 3u;
|
||||
BEAST_EXPECT(wsc->invoke("ledger_closed", atV3)[jss::status] == jss::success);
|
||||
|
||||
// And it can then subscribe at either.
|
||||
BEAST_EXPECT(
|
||||
wsc->invoke("subscribe", streamRequest("ledger", 1u))[jss::status] == jss::success);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A webhook subscriber is shared by url, so its version is too.
|
||||
*
|
||||
* Two `subscribe` calls naming one `url` at two versions: the second is
|
||||
* refused, and the message names the url rather than the connection, since
|
||||
* the caller it refuses is not the caller that decided the version.
|
||||
* `unsubscribe` with that url evicts the subscriber, and the url can then
|
||||
* be subscribed at any version.
|
||||
*/
|
||||
void
|
||||
testOneApiVersionPerUrl()
|
||||
{
|
||||
testcase("A webhook url subscribes at one API version only");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this};
|
||||
|
||||
auto const subscribeUrl = [](unsigned apiVersion) {
|
||||
json::Value jv;
|
||||
jv[jss::url] = "http://localhost/events";
|
||||
jv[jss::api_version] = apiVersion;
|
||||
jv[jss::streams] = json::ValueType::Array;
|
||||
jv[jss::streams].append("ledger");
|
||||
return jv;
|
||||
};
|
||||
|
||||
auto const first = env.rpc("json", "subscribe", to_string(subscribeUrl(3u)))[jss::result];
|
||||
BEAST_EXPECTS(first[jss::status] == jss::success, to_string(first));
|
||||
|
||||
auto const second = env.rpc("json", "subscribe", to_string(subscribeUrl(1u)))[jss::result];
|
||||
BEAST_EXPECTS(second[jss::error] == "apiVersionConflict", to_string(second));
|
||||
BEAST_EXPECT(
|
||||
second[jss::error_message] == "Subscriptions on this url are served at api_version 3.");
|
||||
|
||||
// Unsubscribing the url evicts the subscriber, so the version goes with it. The streams
|
||||
// have to go too: `tryRemoveRpcSub` refuses to erase a subscriber any stream map still
|
||||
// holds, which is why this `unsubscribe` names the streams as well as the url.
|
||||
json::Value unsub;
|
||||
unsub[jss::url] = "http://localhost/events";
|
||||
unsub[jss::streams] = json::ValueType::Array;
|
||||
unsub[jss::streams].append("ledger");
|
||||
env.rpc("json", "unsubscribe", to_string(unsub));
|
||||
|
||||
auto const reused = env.rpc("json", "subscribe", to_string(subscribeUrl(1u)))[jss::result];
|
||||
BEAST_EXPECTS(reused[jss::status] == jss::success, to_string(reused));
|
||||
}
|
||||
|
||||
/**
|
||||
* The two streams no test subscribed to, at every API version.
|
||||
*
|
||||
* Each records the version its subscription was registered at, like every
|
||||
* other stream, and no other test registers either of them. Their
|
||||
* publishers are not reached: a consensus phase change needs a consensus
|
||||
* round and a peer status change needs a peer, and a standalone `Env` has
|
||||
* neither. What is held here is that a subscription is accepted and that
|
||||
* `peer_status` stays behind the admin check.
|
||||
*/
|
||||
void
|
||||
testSubscribeToConsensusAndPeerStatus()
|
||||
{
|
||||
testcase("consensus and peer_status accept a subscription at any version");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
for (auto const apiVersion : {1u, 2u, 3u})
|
||||
{
|
||||
auto const label = std::to_string(apiVersion);
|
||||
|
||||
// An admin connection reaches both.
|
||||
{
|
||||
Env env{*this, singleThreadIo(envconfig())};
|
||||
for (auto const* stream : {"consensus", "peer_status"})
|
||||
{
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
auto const reply = wsc->invoke("subscribe", streamRequest(stream, apiVersion));
|
||||
BEAST_EXPECTS(
|
||||
reply[jss::result][jss::status] == jss::success,
|
||||
std::string{stream} + " " + label + " " + to_string(reply));
|
||||
}
|
||||
}
|
||||
|
||||
// Without admin, `peer_status` is refused and `consensus` is not.
|
||||
{
|
||||
Env env{*this, singleThreadIo(noAdmin(envconfig()))};
|
||||
|
||||
auto consensus = makeWSClient(env.app().config());
|
||||
auto const accepted =
|
||||
consensus->invoke("subscribe", streamRequest("consensus", apiVersion));
|
||||
BEAST_EXPECTS(
|
||||
accepted[jss::result][jss::status] == jss::success,
|
||||
label + " " + to_string(accepted));
|
||||
|
||||
auto peers = makeWSClient(env.app().config());
|
||||
auto const refused =
|
||||
peers->invoke("subscribe", streamRequest("peer_status", apiVersion));
|
||||
BEAST_EXPECTS(
|
||||
refused[jss::result][jss::error] == "noPermission",
|
||||
label + " " + to_string(refused));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSubMPT()
|
||||
{
|
||||
@@ -2477,6 +2735,9 @@ public:
|
||||
testMPTReSubscribeNotOvercounted();
|
||||
testMPTSharesCapWithAccounts();
|
||||
testMPTUnsubscribeFreesCap();
|
||||
testOneApiVersionPerConnection();
|
||||
testOneApiVersionPerUrl();
|
||||
testSubscribeToConsensusAndPeerStatus();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -71,3 +71,47 @@ TEST(ErrorCodes, every_code_names_an_http_status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ErrorCodes, code_for_token_resolves_known_tokens)
|
||||
{
|
||||
EXPECT_EQ(rpc::codeForToken("invalidParams"), RpcInvalidParams);
|
||||
EXPECT_EQ(rpc::codeForToken("malformedSeq"), RpcMalformedSeq);
|
||||
|
||||
// The first and last entries, so a probe that wraps or stops short is caught. The static
|
||||
// assert is what keeps the second one the last entry as codes are appended.
|
||||
EXPECT_EQ(rpc::codeForToken("badSyntax"), RpcBadSyntax);
|
||||
EXPECT_EQ(rpc::codeForToken("apiVersionConflict"), RpcApiVersionConflict);
|
||||
static_assert(RpcApiVersionConflict == RpcLast);
|
||||
}
|
||||
|
||||
TEST(ErrorCodes, code_for_token_rejects_unnamed_tokens)
|
||||
{
|
||||
EXPECT_EQ(rpc::codeForToken("notATokenAnyEntryNames"), RpcUnknown);
|
||||
EXPECT_EQ(rpc::codeForToken(""), RpcUnknown);
|
||||
|
||||
// Tokens are matched whole, not by prefix, and the comparison is case-sensitive.
|
||||
EXPECT_EQ(rpc::codeForToken("invalidParam"), RpcUnknown);
|
||||
EXPECT_EQ(rpc::codeForToken("invalidParamsX"), RpcUnknown);
|
||||
EXPECT_EQ(rpc::codeForToken("invalidparams"), RpcUnknown);
|
||||
|
||||
// The placeholder that gaps in the table report names no error, so it resolves to nothing.
|
||||
EXPECT_EQ(rpc::codeForToken("unknown"), RpcUnknown);
|
||||
}
|
||||
|
||||
TEST(ErrorCodes, every_token_round_trips_to_its_own_code)
|
||||
{
|
||||
// The views the lookup scans and the rows it answers from are two arrays built from one table.
|
||||
// Every row's token resolving to that row's code is what proves the two are index-aligned,
|
||||
// which no static assertion checks.
|
||||
for (int i = RpcBadSyntax; i <= RpcLast; ++i)
|
||||
{
|
||||
auto const code = static_cast<ErrorCodeI>(i);
|
||||
auto const& info = rpc::getErrorInfo(code);
|
||||
|
||||
// Gaps report the placeholder, which is covered above.
|
||||
if (info.code == RpcUnknown)
|
||||
continue;
|
||||
|
||||
EXPECT_EQ(rpc::codeForToken(info.token.cStr()), code) << "token: " << info.token.cStr();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
#include <xrpl/server/detail/JSONRPCUtil.h>
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
using namespace xrpl;
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* The reply httpReply writes, collected as one string.
|
||||
*
|
||||
* @param status The HTTP status to write a reply for.
|
||||
* @return The whole reply, headers and body.
|
||||
*/
|
||||
std::string
|
||||
reply(int status)
|
||||
{
|
||||
std::string out;
|
||||
beast::Journal const journal{beast::Journal::getNullSink()};
|
||||
httpReply(status, "{}", [&out](std::string_view s) { out += s; }, journal);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* The reply's status line, without the trailing CRLF.
|
||||
*
|
||||
* @param reply A whole HTTP reply.
|
||||
* @return The first line, or the whole reply when it holds no CRLF.
|
||||
*/
|
||||
std::string_view
|
||||
statusLine(std::string const& reply)
|
||||
{
|
||||
auto const end = reply.find("\r\n");
|
||||
return std::string_view{reply}.substr(0, end == std::string::npos ? reply.size() : end);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(JSONRPCUtil, status_line_names_the_status)
|
||||
{
|
||||
// Every status this server sends but the two the last test names. The phrase comes from the
|
||||
// registry but for the two below.
|
||||
EXPECT_EQ(statusLine(reply(200)), "HTTP/1.1 200 OK");
|
||||
EXPECT_EQ(statusLine(reply(202)), "HTTP/1.1 202 Accepted");
|
||||
EXPECT_EQ(statusLine(reply(400)), "HTTP/1.1 400 Bad Request");
|
||||
EXPECT_EQ(statusLine(reply(403)), "HTTP/1.1 403 Forbidden");
|
||||
EXPECT_EQ(statusLine(reply(404)), "HTTP/1.1 404 Not Found");
|
||||
EXPECT_EQ(statusLine(reply(405)), "HTTP/1.1 405 Method Not Allowed");
|
||||
EXPECT_EQ(statusLine(reply(429)), "HTTP/1.1 429 Too Many Requests");
|
||||
EXPECT_EQ(statusLine(reply(500)), "HTTP/1.1 500 Internal Server Error");
|
||||
EXPECT_EQ(statusLine(reply(501)), "HTTP/1.1 501 Not Implemented");
|
||||
|
||||
// The two whose phrase is this server's own, not the registry's.
|
||||
EXPECT_EQ(statusLine(reply(401)), "HTTP/1.1 401 Authorization Required");
|
||||
EXPECT_EQ(statusLine(reply(503)), "HTTP/1.1 503 Server is overloaded");
|
||||
}
|
||||
|
||||
TEST(JSONRPCUtil, every_status_the_error_table_names_gets_a_status_line)
|
||||
{
|
||||
// httpReply sees a status, not the table, so only this can check every status the table names.
|
||||
std::set<int> statuses;
|
||||
for (int i = RpcBadSyntax; i <= RpcLast; ++i)
|
||||
{
|
||||
auto const& info = rpc::getErrorInfo(static_cast<ErrorCodeI>(i));
|
||||
|
||||
// Gaps in the table name no error, so they report no status of their own.
|
||||
if (info.code == RpcUnknown)
|
||||
continue;
|
||||
|
||||
statuses.insert(info.httpStatus);
|
||||
}
|
||||
EXPECT_FALSE(statuses.empty());
|
||||
|
||||
for (int const status : statuses)
|
||||
{
|
||||
auto const line = std::string{statusLine(reply(status))};
|
||||
auto const expectedPrefix = "HTTP/1.1 " + std::to_string(status) + " ";
|
||||
EXPECT_TRUE(line.starts_with(expectedPrefix)) << "status: " << status << ", line: " << line;
|
||||
|
||||
// A placeholder phrase means the table names something that is not a status.
|
||||
EXPECT_GT(line.size(), expectedPrefix.size()) << "status: " << status;
|
||||
EXPECT_EQ(line.find("unknown-status"), std::string::npos) << "status: " << status;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(JSONRPCUtil, statuses_without_a_case_take_the_registry_phrase)
|
||||
{
|
||||
// The two statuses the error table names and the switch spells no case for.
|
||||
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcHighFee), 402);
|
||||
EXPECT_EQ(statusLine(reply(402)), "HTTP/1.1 402 Payment Required");
|
||||
|
||||
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcDbDeserialization), 502);
|
||||
EXPECT_EQ(statusLine(reply(502)), "HTTP/1.1 502 Bad Gateway");
|
||||
|
||||
// A number the registry does not know still gets a line, with the placeholder phrase the table
|
||||
// test above refuses.
|
||||
EXPECT_EQ(statusLine(reply(999)), "HTTP/1.1 999 <unknown-status>");
|
||||
}
|
||||
@@ -562,7 +562,11 @@ public:
|
||||
// InfoSub::Source.
|
||||
//
|
||||
void
|
||||
subAccount(InfoSub::Ref ispListener, HashSet<AccountID> const& vnaAccountIDs, bool rt) override;
|
||||
subAccount(
|
||||
InfoSub::Ref ispListener,
|
||||
HashSet<AccountID> const& vnaAccountIDs,
|
||||
bool rt,
|
||||
unsigned int apiVersion) override;
|
||||
void
|
||||
unsubAccount(InfoSub::Ref ispListener, HashSet<AccountID> const& vnaAccountIDs, bool rt)
|
||||
override;
|
||||
@@ -573,15 +577,17 @@ public:
|
||||
unsubAccountInternal(std::uint64_t seq, HashSet<AccountID> const& vnaAccountIDs, bool rt)
|
||||
override;
|
||||
|
||||
ErrorCodeI
|
||||
subAccountHistory(InfoSub::Ref ispListener, AccountID const& account, unsigned int apiVersion)
|
||||
override;
|
||||
void
|
||||
subMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) override;
|
||||
subMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs, unsigned int apiVersion)
|
||||
override;
|
||||
void
|
||||
unsubMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) override;
|
||||
void
|
||||
unsubMPTInternal(std::uint64_t seq, MPTID const& mptID) override;
|
||||
|
||||
ErrorCodeI
|
||||
subAccountHistory(InfoSub::Ref ispListener, AccountID const& account) override;
|
||||
void
|
||||
unsubAccountHistory(InfoSub::Ref ispListener, AccountID const& account, bool historyOnly)
|
||||
override;
|
||||
@@ -598,58 +604,59 @@ public:
|
||||
HashSet<AccountID> historyAccounts) override;
|
||||
|
||||
bool
|
||||
subLedger(InfoSub::Ref ispListener, json::Value& jvResult) override;
|
||||
subLedger(InfoSub::Ref ispListener, json::Value& jvResult, unsigned int apiVersion) override;
|
||||
bool
|
||||
unsubLedger(std::uint64_t uListener) override;
|
||||
|
||||
bool
|
||||
subBookChanges(InfoSub::Ref ispListener) override;
|
||||
subBookChanges(InfoSub::Ref ispListener, unsigned int apiVersion) override;
|
||||
bool
|
||||
unsubBookChanges(std::uint64_t uListener) override;
|
||||
|
||||
bool
|
||||
subServer(InfoSub::Ref ispListener, json::Value& jvResult, bool admin) override;
|
||||
subServer(InfoSub::Ref ispListener, json::Value& jvResult, bool admin, unsigned int apiVersion)
|
||||
override;
|
||||
bool
|
||||
unsubServer(std::uint64_t uListener) override;
|
||||
|
||||
bool
|
||||
subBook(InfoSub::Ref ispListener, Book const&) override;
|
||||
subBook(InfoSub::Ref ispListener, Book const&, unsigned int apiVersion) override;
|
||||
bool
|
||||
unsubBook(InfoSub::Ref ispListener, Book const&) override;
|
||||
bool
|
||||
unsubBookInternal(std::uint64_t uListener, Book const&) override;
|
||||
|
||||
bool
|
||||
subManifests(InfoSub::Ref ispListener) override;
|
||||
subManifests(InfoSub::Ref ispListener, unsigned int apiVersion) override;
|
||||
bool
|
||||
unsubManifests(std::uint64_t uListener) override;
|
||||
void
|
||||
pubManifest(Manifest const&) override;
|
||||
|
||||
bool
|
||||
subTransactions(InfoSub::Ref ispListener) override;
|
||||
subTransactions(InfoSub::Ref ispListener, unsigned int apiVersion) override;
|
||||
bool
|
||||
unsubTransactions(std::uint64_t uListener) override;
|
||||
|
||||
bool
|
||||
subRTTransactions(InfoSub::Ref ispListener) override;
|
||||
subRTTransactions(InfoSub::Ref ispListener, unsigned int apiVersion) override;
|
||||
bool
|
||||
unsubRTTransactions(std::uint64_t uListener) override;
|
||||
|
||||
bool
|
||||
subValidations(InfoSub::Ref ispListener) override;
|
||||
subValidations(InfoSub::Ref ispListener, unsigned int apiVersion) override;
|
||||
bool
|
||||
unsubValidations(std::uint64_t uListener) override;
|
||||
|
||||
bool
|
||||
subPeerStatus(InfoSub::Ref ispListener) override;
|
||||
subPeerStatus(InfoSub::Ref ispListener, unsigned int apiVersion) override;
|
||||
bool
|
||||
unsubPeerStatus(std::uint64_t uListener) override;
|
||||
void
|
||||
pubPeerStatus(std::function<json::Value()> const&) override;
|
||||
|
||||
bool
|
||||
subConsensus(InfoSub::Ref ispListener) override;
|
||||
subConsensus(InfoSub::Ref ispListener, unsigned int apiVersion) override;
|
||||
bool
|
||||
unsubConsensus(std::uint64_t uListener) override;
|
||||
|
||||
@@ -816,7 +823,30 @@ private:
|
||||
getHostId(bool forAdmin);
|
||||
|
||||
private:
|
||||
using SubMapType = HashMap<std::uint64_t, InfoSub::Wptr>;
|
||||
/**
|
||||
* One subscription in a seq-keyed subscription map.
|
||||
*
|
||||
* Carries the API version the subscription was registered at, so a
|
||||
* publisher reads the version from the entry it took the sink from rather
|
||||
* than off the sink itself. The sink is shared: one connection holds every
|
||||
* subscription made on it, and one webhook (keyed on its url alone) is
|
||||
* shared by every admin that named it. Every entry a sink appears in names
|
||||
* the same version, `doSubscribe` refusing a `subscribe` that would make it
|
||||
* otherwise.
|
||||
*
|
||||
* Registering the same subscription again replaces the entry, so the
|
||||
* newest registration decides that subscription's version. An unrelated
|
||||
* subscription on the same connection is a separate entry and keeps the
|
||||
* version it named.
|
||||
*/
|
||||
struct Subscriber
|
||||
{
|
||||
InfoSub::Wptr sink;
|
||||
// Every registration site names it; the zero satisfies pro-type-member-init.
|
||||
unsigned int apiVersion{};
|
||||
};
|
||||
|
||||
using SubMapType = HashMap<std::uint64_t, Subscriber>;
|
||||
using SubInfoMapType = HashMap<AccountID, SubMapType>;
|
||||
using SubRpcMapType = HashMap<std::string, InfoSub::pointer>;
|
||||
using SubMPTInfoMapType = HashMap<MPTID, SubMapType>;
|
||||
@@ -850,11 +880,13 @@ private:
|
||||
{
|
||||
InfoSub::pointer sink;
|
||||
std::shared_ptr<SubAccountHistoryIndex> index;
|
||||
unsigned int apiVersion{};
|
||||
};
|
||||
struct SubAccountHistoryInfoWeak
|
||||
{
|
||||
InfoSub::Wptr sinkWptr;
|
||||
std::shared_ptr<SubAccountHistoryIndex> index;
|
||||
unsigned int apiVersion{};
|
||||
};
|
||||
using SubAccountHistoryMapType =
|
||||
HashMap<AccountID, HashMap<std::uint64_t, SubAccountHistoryInfoWeak>>;
|
||||
@@ -1103,6 +1135,46 @@ std::array<json::StaticString const, 5> const NetworkOPsImp::StateAccounting::kS
|
||||
static auto const kGenesisAccountId =
|
||||
calcAccountID(generateKeyPair(KeyType::Secp256k1, generateSeed("masterpassphrase")).first);
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* One connection that is to receive one message, with the API version that
|
||||
* message is selected for.
|
||||
*
|
||||
* Keyed on the connection's `getSeq()`, so a connection matched by several
|
||||
* subscriptions at once is named once. One transaction can match the same
|
||||
* connection through both the accepted and the proposed account map, or
|
||||
* through two of the accounts it affects, and one message goes out, as every
|
||||
* version has sent.
|
||||
*/
|
||||
struct Notified
|
||||
{
|
||||
InfoSub::pointer sink;
|
||||
unsigned int apiVersion{};
|
||||
};
|
||||
|
||||
using NotifyMap = HashMap<std::uint64_t, Notified>;
|
||||
|
||||
/**
|
||||
* Notes @p subscriber as needing one message, selected for @p apiVersion.
|
||||
*
|
||||
* The first entry a walk reaches decides, and every later one for the same
|
||||
* connection is dropped. That is unambiguous because a connection holds
|
||||
* subscriptions at one API version only, which `doSubscribe` enforces, so every
|
||||
* entry this could reach for one connection names the same version.
|
||||
*
|
||||
* @param notify Where the connection is noted.
|
||||
* @param subscriber The connection.
|
||||
* @param apiVersion The version the matched subscription was registered at.
|
||||
*/
|
||||
void
|
||||
noteSubscriber(NotifyMap& notify, InfoSub::pointer const& subscriber, unsigned int apiVersion)
|
||||
{
|
||||
notify.try_emplace(subscriber->getSeq(), subscriber, apiVersion);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
inline OperatingMode
|
||||
NetworkOPsImp::getOperatingMode() const
|
||||
@@ -2439,7 +2511,7 @@ NetworkOPsImp::pubManifest(Manifest const& mo)
|
||||
|
||||
for (auto i = streamMaps_[SManifests].begin(); i != streamMaps_[SManifests].end();)
|
||||
{
|
||||
if (auto p = i->second.lock())
|
||||
if (auto p = i->second.sink.lock())
|
||||
{
|
||||
p->send(jvObj, true);
|
||||
toRelease.push_back(std::move(p));
|
||||
@@ -2541,7 +2613,7 @@ NetworkOPsImp::pubServer()
|
||||
|
||||
for (auto i = streamMaps_[SServer].begin(); i != streamMaps_[SServer].end();)
|
||||
{
|
||||
InfoSub::pointer p = i->second.lock();
|
||||
InfoSub::pointer p = i->second.sink.lock();
|
||||
|
||||
// VFALCO TODO research the possibility of using thread queues and
|
||||
// linearizing the deletion of subscribers with the
|
||||
@@ -2579,7 +2651,7 @@ NetworkOPsImp::pubConsensus(ConsensusPhase phase)
|
||||
|
||||
for (auto i = streamMap.begin(); i != streamMap.end();)
|
||||
{
|
||||
if (auto p = i->second.lock())
|
||||
if (auto p = i->second.sink.lock())
|
||||
{
|
||||
p->send(jvObj, true);
|
||||
toRelease.push_back(std::move(p));
|
||||
@@ -2689,10 +2761,10 @@ NetworkOPsImp::pubValidation(std::shared_ptr<STValidation> const& val)
|
||||
|
||||
for (auto i = streamMaps_[SValidations].begin(); i != streamMaps_[SValidations].end();)
|
||||
{
|
||||
if (auto p = i->second.lock())
|
||||
if (auto p = i->second.sink.lock())
|
||||
{
|
||||
multiObj.visit(
|
||||
p->getApiVersion(), //
|
||||
i->second.apiVersion, //
|
||||
[&](json::Value const& jv) { p->send(jv, true); });
|
||||
toRelease.push_back(std::move(p));
|
||||
++i;
|
||||
@@ -2723,7 +2795,7 @@ NetworkOPsImp::pubPeerStatus(std::function<json::Value()> const& func)
|
||||
|
||||
for (auto i = streamMaps_[SPeerStatus].begin(); i != streamMaps_[SPeerStatus].end();)
|
||||
{
|
||||
InfoSub::pointer p = i->second.lock();
|
||||
InfoSub::pointer p = i->second.sink.lock();
|
||||
|
||||
if (p)
|
||||
{
|
||||
@@ -3290,12 +3362,12 @@ NetworkOPsImp::pubProposedTransaction(
|
||||
auto it = streamMaps_[SRtTransactions].begin();
|
||||
while (it != streamMaps_[SRtTransactions].end())
|
||||
{
|
||||
InfoSub::pointer p = it->second.lock();
|
||||
InfoSub::pointer p = it->second.sink.lock();
|
||||
|
||||
if (p)
|
||||
{
|
||||
jvObj.visit(
|
||||
p->getApiVersion(), //
|
||||
it->second.apiVersion, //
|
||||
[&](json::Value const& jv) { p->send(jv, true); });
|
||||
toRelease.push_back(std::move(p));
|
||||
++it;
|
||||
@@ -3387,7 +3459,7 @@ NetworkOPsImp::publishLedgerStreams(
|
||||
auto it = streamMaps_[SLedger].begin();
|
||||
while (it != streamMaps_[SLedger].end())
|
||||
{
|
||||
InfoSub::pointer p = it->second.lock();
|
||||
InfoSub::pointer p = it->second.sink.lock();
|
||||
if (p)
|
||||
{
|
||||
p->send(jvObj, true);
|
||||
@@ -3408,7 +3480,7 @@ NetworkOPsImp::publishLedgerStreams(
|
||||
auto it = streamMaps_[SBookChanges].begin();
|
||||
while (it != streamMaps_[SBookChanges].end())
|
||||
{
|
||||
InfoSub::pointer p = it->second.lock();
|
||||
InfoSub::pointer p = it->second.sink.lock();
|
||||
if (p)
|
||||
{
|
||||
p->send(jvObj, true);
|
||||
@@ -3618,12 +3690,12 @@ NetworkOPsImp::pubValidatedTransaction(
|
||||
auto it = streamMaps_[STransactions].begin();
|
||||
while (it != streamMaps_[STransactions].end())
|
||||
{
|
||||
InfoSub::pointer p = it->second.lock();
|
||||
InfoSub::pointer p = it->second.sink.lock();
|
||||
|
||||
if (p)
|
||||
{
|
||||
jvObj.visit(
|
||||
p->getApiVersion(), //
|
||||
it->second.apiVersion, //
|
||||
[&](json::Value const& jv) { p->send(jv, true); });
|
||||
toRelease.push_back(std::move(p));
|
||||
++it;
|
||||
@@ -3638,12 +3710,12 @@ NetworkOPsImp::pubValidatedTransaction(
|
||||
|
||||
while (it != streamMaps_[SRtTransactions].end())
|
||||
{
|
||||
InfoSub::pointer p = it->second.lock();
|
||||
InfoSub::pointer p = it->second.sink.lock();
|
||||
|
||||
if (p)
|
||||
{
|
||||
jvObj.visit(
|
||||
p->getApiVersion(), //
|
||||
it->second.apiVersion, //
|
||||
[&](json::Value const& jv) { p->send(jv, true); });
|
||||
toRelease.push_back(std::move(p));
|
||||
++it;
|
||||
@@ -3687,7 +3759,9 @@ NetworkOPsImp::pubBookTransaction(AcceptedLedgerTx const& alTx, MultiApiJson con
|
||||
// ~InfoSub() reacquires bookLock_ via unsubBook() on its own and serializes
|
||||
// safely with concurrent traffic.
|
||||
|
||||
std::vector<InfoSub::pointer> listeners;
|
||||
// `seen` names the connections already noted, so a connection subscribed to two of the
|
||||
// affected books receives one message, for the reason noteSubscriber gives.
|
||||
std::vector<Notified> listeners;
|
||||
HashSet<std::uint64_t> seen;
|
||||
|
||||
// Sized for the common case where every affected book has at most
|
||||
@@ -3708,7 +3782,7 @@ NetworkOPsImp::pubBookTransaction(AcceptedLedgerTx const& alTx, MultiApiJson con
|
||||
|
||||
for (auto sit = it->second.begin(); sit != it->second.end();)
|
||||
{
|
||||
if (auto p = sit->second.lock())
|
||||
if (auto p = sit->second.sink.lock())
|
||||
{
|
||||
// Defensive: subBook_ entries are normally cleared by
|
||||
// ~InfoSub() -> unsubBook(), so we rarely see expired
|
||||
@@ -3716,7 +3790,9 @@ NetworkOPsImp::pubBookTransaction(AcceptedLedgerTx const& alTx, MultiApiJson con
|
||||
// where the last strong ref is dropped between insertion
|
||||
// and our lock() call.
|
||||
if (seen.emplace(p->getSeq()).second)
|
||||
listeners.emplace_back(std::move(p));
|
||||
{
|
||||
listeners.emplace_back(std::move(p), sit->second.apiVersion);
|
||||
}
|
||||
++sit;
|
||||
}
|
||||
else
|
||||
@@ -3732,9 +3808,11 @@ NetworkOPsImp::pubBookTransaction(AcceptedLedgerTx const& alTx, MultiApiJson con
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const& p : listeners)
|
||||
for (auto const& entry : listeners)
|
||||
{
|
||||
jvObj.visit(p->getApiVersion(), [&](json::Value const& jv) { p->send(jv, true); });
|
||||
jvObj.visit(
|
||||
entry.apiVersion, //
|
||||
[&](json::Value const& jv) { entry.sink->send(jv, true); });
|
||||
}
|
||||
// listeners destructs here, outside bookLock_; ~InfoSub (if any fires)
|
||||
// will reacquire bookLock_ via unsubBook with no iterator hazard.
|
||||
@@ -3746,7 +3824,8 @@ NetworkOPsImp::pubAccountTransaction(
|
||||
AcceptedLedgerTx const& transaction,
|
||||
bool last)
|
||||
{
|
||||
HashSet<InfoSub::pointer> notify;
|
||||
// See noteSubscriber.
|
||||
NotifyMap notify;
|
||||
int iProposed = 0;
|
||||
int iAccepted = 0;
|
||||
|
||||
@@ -3766,11 +3845,11 @@ NetworkOPsImp::pubAccountTransaction(
|
||||
|
||||
while (it != simiIt->second.end())
|
||||
{
|
||||
InfoSub::pointer const p = it->second.lock();
|
||||
InfoSub::pointer const p = it->second.sink.lock();
|
||||
|
||||
if (p)
|
||||
{
|
||||
notify.insert(p);
|
||||
noteSubscriber(notify, p, it->second.apiVersion);
|
||||
++it;
|
||||
++iProposed;
|
||||
}
|
||||
@@ -3787,11 +3866,11 @@ NetworkOPsImp::pubAccountTransaction(
|
||||
auto it = simiIt->second.begin();
|
||||
while (it != simiIt->second.end())
|
||||
{
|
||||
InfoSub::pointer const p = it->second.lock();
|
||||
InfoSub::pointer const p = it->second.sink.lock();
|
||||
|
||||
if (p)
|
||||
{
|
||||
notify.insert(p);
|
||||
noteSubscriber(notify, p, it->second.apiVersion);
|
||||
++it;
|
||||
++iAccepted;
|
||||
}
|
||||
@@ -3819,7 +3898,11 @@ NetworkOPsImp::pubAccountTransaction(
|
||||
if (auto const isSptr = info.sinkWptr.lock(); isSptr)
|
||||
{
|
||||
accountHistoryNotify.emplace_back(
|
||||
SubAccountHistoryInfo{.sink = isSptr, .index = info.index});
|
||||
SubAccountHistoryInfo{
|
||||
.sink = isSptr,
|
||||
.index = info.index,
|
||||
.apiVersion = info.apiVersion,
|
||||
});
|
||||
++it;
|
||||
}
|
||||
else
|
||||
@@ -3846,11 +3929,12 @@ NetworkOPsImp::pubAccountTransaction(
|
||||
auto const trResult = transaction.getResult();
|
||||
MultiApiJson jvObj = transJson(stTxn, trResult, true, ledger, metaRef);
|
||||
|
||||
for (InfoSub::Ref isrListener : notify)
|
||||
{
|
||||
jvObj.visit(
|
||||
isrListener->getApiVersion(), //
|
||||
[&](json::Value const& jv) { isrListener->send(jv, true); });
|
||||
for (auto const& [seq, entry] : notify)
|
||||
{
|
||||
jvObj.visit(
|
||||
entry.apiVersion, [&](json::Value const& jv) { entry.sink->send(jv, true); });
|
||||
}
|
||||
}
|
||||
|
||||
if (last)
|
||||
@@ -3869,7 +3953,7 @@ NetworkOPsImp::pubAccountTransaction(
|
||||
jvObj.set(jss::account_history_tx_index, index->forwardTxIndex++);
|
||||
|
||||
jvObj.visit(
|
||||
info.sink->getApiVersion(), //
|
||||
info.apiVersion, //
|
||||
[&](json::Value const& jv) { info.sink->send(jv, true); });
|
||||
}
|
||||
}
|
||||
@@ -3881,7 +3965,8 @@ NetworkOPsImp::pubProposedAccountTransaction(
|
||||
std::shared_ptr<STTx const> const& tx,
|
||||
TER result)
|
||||
{
|
||||
HashSet<InfoSub::pointer> notify;
|
||||
// See noteSubscriber.
|
||||
NotifyMap notify;
|
||||
int iProposed = 0;
|
||||
|
||||
{
|
||||
@@ -3899,11 +3984,11 @@ NetworkOPsImp::pubProposedAccountTransaction(
|
||||
|
||||
while (it != simiIt->second.end())
|
||||
{
|
||||
InfoSub::pointer const p = it->second.lock();
|
||||
InfoSub::pointer const p = it->second.sink.lock();
|
||||
|
||||
if (p)
|
||||
{
|
||||
notify.insert(p);
|
||||
noteSubscriber(notify, p, it->second.apiVersion);
|
||||
++it;
|
||||
++iProposed;
|
||||
}
|
||||
@@ -3923,11 +4008,12 @@ NetworkOPsImp::pubProposedAccountTransaction(
|
||||
// Create two different Json objects, for different API versions
|
||||
MultiApiJson const jvObj = transJson(tx, result, false, ledger, std::nullopt);
|
||||
|
||||
for (InfoSub::Ref isrListener : notify)
|
||||
{
|
||||
jvObj.visit(
|
||||
isrListener->getApiVersion(), //
|
||||
[&](json::Value const& jv) { isrListener->send(jv, true); });
|
||||
for (auto const& [seq, entry] : notify)
|
||||
{
|
||||
jvObj.visit(
|
||||
entry.apiVersion, [&](json::Value const& jv) { entry.sink->send(jv, true); });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3940,7 +4026,8 @@ void
|
||||
NetworkOPsImp::subAccount(
|
||||
InfoSub::Ref isrListener,
|
||||
HashSet<AccountID> const& vnaAccountIDs,
|
||||
bool rt)
|
||||
bool rt,
|
||||
unsigned int apiVersion)
|
||||
{
|
||||
SubInfoMapType& subMap = rt ? subRTAccount_ : subAccount_;
|
||||
|
||||
@@ -3953,6 +4040,8 @@ NetworkOPsImp::subAccount(
|
||||
|
||||
std::scoped_lock const sl(accountLock_);
|
||||
|
||||
Subscriber const entry{.sink = isrListener, .apiVersion = apiVersion};
|
||||
|
||||
for (auto const& naAccountID : vnaAccountIDs)
|
||||
{
|
||||
auto const simIterator = subMap.find(naAccountID);
|
||||
@@ -3960,14 +4049,14 @@ NetworkOPsImp::subAccount(
|
||||
{
|
||||
// Not found, note that account has a new single listener.
|
||||
SubMapType usisElement;
|
||||
usisElement[isrListener->getSeq()] = isrListener;
|
||||
usisElement[isrListener->getSeq()] = entry;
|
||||
// VFALCO NOTE This is making a needless copy of naAccountID
|
||||
subMap.insert(simIterator, make_pair(naAccountID, usisElement));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Found, note that the account has another listener.
|
||||
simIterator->second[isrListener->getSeq()] = isrListener;
|
||||
simIterator->second.insert_or_assign(isrListener->getSeq(), entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4064,7 +4153,7 @@ NetworkOPsImp::cleanupAccountSubscriptions(
|
||||
SubInfoMapType& subMap)
|
||||
{
|
||||
// Plain account maps need no per-entry teardown before erase.
|
||||
cleanupSubscriptionMap(seq, accounts, subMap, [](InfoSub::Wptr const&) {});
|
||||
cleanupSubscriptionMap(seq, accounts, subMap, [](Subscriber const&) {});
|
||||
}
|
||||
|
||||
void
|
||||
@@ -4150,8 +4239,8 @@ NetworkOPsImp::pubMPTTransaction(AcceptedLedgerTx const& alTx, MultiApiJson cons
|
||||
return;
|
||||
|
||||
// Declared before the lock so a last-reference ~InfoSub runs after
|
||||
// mptLock_ is released (see the deferred-destruction rule).
|
||||
HashSet<InfoSub::pointer> notify;
|
||||
// mptLock_ is released (see the deferred-destruction rule). See noteSubscriber.
|
||||
NotifyMap notify;
|
||||
|
||||
{
|
||||
std::scoped_lock const sl(mptLock_);
|
||||
@@ -4163,11 +4252,11 @@ NetworkOPsImp::pubMPTTransaction(AcceptedLedgerTx const& alTx, MultiApiJson cons
|
||||
auto it = simiIt->second.begin();
|
||||
while (it != simiIt->second.end())
|
||||
{
|
||||
InfoSub::pointer const p = it->second.lock();
|
||||
InfoSub::pointer const p = it->second.sink.lock();
|
||||
|
||||
if (p)
|
||||
{
|
||||
notify.insert(p);
|
||||
noteSubscriber(notify, p, it->second.apiVersion);
|
||||
++it;
|
||||
}
|
||||
else
|
||||
@@ -4182,16 +4271,19 @@ NetworkOPsImp::pubMPTTransaction(AcceptedLedgerTx const& alTx, MultiApiJson cons
|
||||
if (notify.empty())
|
||||
return;
|
||||
|
||||
for (InfoSub::Ref isrListener : notify)
|
||||
for (auto const& [seq, entry] : notify)
|
||||
{
|
||||
jvObj.visit(isrListener->getApiVersion(), [&](json::Value const& jv) {
|
||||
isrListener->send(jv, true);
|
||||
});
|
||||
jvObj.visit(
|
||||
entry.apiVersion, //
|
||||
[&](json::Value const& jv) { entry.sink->send(jv, true); });
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
NetworkOPsImp::subMPT(InfoSub::Ref isrListener, HashSet<MPTID> const& mptIDs)
|
||||
NetworkOPsImp::subMPT(
|
||||
InfoSub::Ref isrListener,
|
||||
HashSet<MPTID> const& mptIDs,
|
||||
unsigned int apiVersion)
|
||||
{
|
||||
// Insert into subMPT_ before the InfoSub (unsubMPT removes in reverse), as
|
||||
// subBook does, so a racing unsubMPT cannot leave a subMPT_ entry that the
|
||||
@@ -4199,6 +4291,8 @@ NetworkOPsImp::subMPT(InfoSub::Ref isrListener, HashSet<MPTID> const& mptIDs)
|
||||
{
|
||||
std::scoped_lock const sl(mptLock_);
|
||||
|
||||
Subscriber const entry{.sink = isrListener, .apiVersion = apiVersion};
|
||||
|
||||
for (auto const& mptID : mptIDs)
|
||||
{
|
||||
JLOG(journal_.trace()) << "subMPT: MPT: " << to_string(mptID);
|
||||
@@ -4208,13 +4302,13 @@ NetworkOPsImp::subMPT(InfoSub::Ref isrListener, HashSet<MPTID> const& mptIDs)
|
||||
{
|
||||
// Not found, note that the MPT issuance has a new single listener.
|
||||
SubMapType usisElement;
|
||||
usisElement[isrListener->getSeq()] = isrListener;
|
||||
usisElement[isrListener->getSeq()] = entry;
|
||||
subMPT_.insert(simIterator, make_pair(mptID, usisElement));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Found, note that the MPT issuance has another listener.
|
||||
simIterator->second[isrListener->getSeq()] = isrListener;
|
||||
simIterator->second.insert_or_assign(isrListener->getSeq(), entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4314,7 +4408,7 @@ NetworkOPsImp::addAccountHistoryJob(SubAccountHistoryInfoWeak subInfo)
|
||||
if (auto sptr = subInfo.sinkWptr.lock())
|
||||
{
|
||||
jvObj.visit(
|
||||
sptr->getApiVersion(), //
|
||||
subInfo.apiVersion, //
|
||||
[&](json::Value const& jv) { sptr->send(jv, true); });
|
||||
|
||||
if (unsubscribe)
|
||||
@@ -4528,7 +4622,10 @@ NetworkOPsImp::subAccountHistoryStart(
|
||||
}
|
||||
|
||||
ErrorCodeI
|
||||
NetworkOPsImp::subAccountHistory(InfoSub::Ref isrListener, AccountID const& accountId)
|
||||
NetworkOPsImp::subAccountHistory(
|
||||
InfoSub::Ref isrListener,
|
||||
AccountID const& accountId,
|
||||
unsigned int apiVersion)
|
||||
{
|
||||
if (!isrListener->insertSubAccountHistory(accountId))
|
||||
{
|
||||
@@ -4541,17 +4638,18 @@ NetworkOPsImp::subAccountHistory(InfoSub::Ref isrListener, AccountID const& acco
|
||||
SubAccountHistoryInfoWeak ahi{
|
||||
.sinkWptr = isrListener,
|
||||
.index = std::make_shared<SubAccountHistoryIndex>(accountId),
|
||||
.apiVersion = apiVersion,
|
||||
};
|
||||
auto const simIterator = subAccountHistory_.find(accountId);
|
||||
if (simIterator == subAccountHistory_.end())
|
||||
{
|
||||
HashMap<std::uint64_t, SubAccountHistoryInfoWeak> inner;
|
||||
inner.emplace(isrListener->getSeq(), ahi);
|
||||
inner.insert_or_assign(isrListener->getSeq(), ahi);
|
||||
subAccountHistory_.insert(simIterator, std::make_pair(accountId, inner));
|
||||
}
|
||||
else
|
||||
{
|
||||
simIterator->second.emplace(isrListener->getSeq(), ahi);
|
||||
simIterator->second.insert_or_assign(isrListener->getSeq(), ahi);
|
||||
}
|
||||
|
||||
auto const ledger = registry_.get().getLedgerMaster().getValidatedLedger();
|
||||
@@ -4612,7 +4710,7 @@ NetworkOPsImp::unsubAccountHistoryInternal(
|
||||
}
|
||||
|
||||
bool
|
||||
NetworkOPsImp::subBook(InfoSub::Ref isrListener, Book const& book)
|
||||
NetworkOPsImp::subBook(InfoSub::Ref isrListener, Book const& book, unsigned int apiVersion)
|
||||
{
|
||||
// Server-side insert first, then InfoSub bookkeeping. If the InfoSub-side
|
||||
// insert throws, the orphan in subBook_ is cleared by the expired-weak_ptr
|
||||
@@ -4620,7 +4718,8 @@ NetworkOPsImp::subBook(InfoSub::Ref isrListener, Book const& book)
|
||||
// call unsubBookInternal for a key that was never inserted server-side.
|
||||
{
|
||||
std::scoped_lock const sl(bookLock_);
|
||||
subBook_[book].try_emplace(isrListener->getSeq(), isrListener);
|
||||
subBook_[book].insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion});
|
||||
}
|
||||
isrListener->insertBookSubscription(book);
|
||||
return true;
|
||||
@@ -4668,7 +4767,7 @@ NetworkOPsImp::acceptLedger(std::optional<std::chrono::milliseconds> consensusDe
|
||||
|
||||
// <-- bool: true=added, false=already there
|
||||
bool
|
||||
NetworkOPsImp::subLedger(InfoSub::Ref isrListener, json::Value& jvResult)
|
||||
NetworkOPsImp::subLedger(InfoSub::Ref isrListener, json::Value& jvResult, unsigned int apiVersion)
|
||||
{
|
||||
if (auto const lpClosed = ledgerMaster_.getValidatedLedger())
|
||||
{
|
||||
@@ -4690,15 +4789,21 @@ NetworkOPsImp::subLedger(InfoSub::Ref isrListener, json::Value& jvResult)
|
||||
}
|
||||
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
return streamMaps_[SLedger].emplace(isrListener->getSeq(), isrListener).second;
|
||||
return streamMaps_[SLedger]
|
||||
.insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion})
|
||||
.second;
|
||||
}
|
||||
|
||||
// <-- bool: true=added, false=already there
|
||||
bool
|
||||
NetworkOPsImp::subBookChanges(InfoSub::Ref isrListener)
|
||||
NetworkOPsImp::subBookChanges(InfoSub::Ref isrListener, unsigned int apiVersion)
|
||||
{
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
return streamMaps_[SBookChanges].emplace(isrListener->getSeq(), isrListener).second;
|
||||
return streamMaps_[SBookChanges]
|
||||
.insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion})
|
||||
.second;
|
||||
}
|
||||
|
||||
// <-- bool: true=erased, false=was not there
|
||||
@@ -4719,10 +4824,13 @@ NetworkOPsImp::unsubBookChanges(std::uint64_t uSeq)
|
||||
|
||||
// <-- bool: true=added, false=already there
|
||||
bool
|
||||
NetworkOPsImp::subManifests(InfoSub::Ref isrListener)
|
||||
NetworkOPsImp::subManifests(InfoSub::Ref isrListener, unsigned int apiVersion)
|
||||
{
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
return streamMaps_[SManifests].emplace(isrListener->getSeq(), isrListener).second;
|
||||
return streamMaps_[SManifests]
|
||||
.insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion})
|
||||
.second;
|
||||
}
|
||||
|
||||
// <-- bool: true=erased, false=was not there
|
||||
@@ -4735,7 +4843,11 @@ NetworkOPsImp::unsubManifests(std::uint64_t uSeq)
|
||||
|
||||
// <-- bool: true=added, false=already there
|
||||
bool
|
||||
NetworkOPsImp::subServer(InfoSub::Ref isrListener, json::Value& jvResult, bool admin)
|
||||
NetworkOPsImp::subServer(
|
||||
InfoSub::Ref isrListener,
|
||||
json::Value& jvResult,
|
||||
bool admin,
|
||||
unsigned int apiVersion)
|
||||
{
|
||||
UInt256 uRandom;
|
||||
|
||||
@@ -4755,7 +4867,10 @@ NetworkOPsImp::subServer(InfoSub::Ref isrListener, json::Value& jvResult, bool a
|
||||
toBase58(TokenType::NodePublic, registry_.get().getApp().nodeIdentity().first);
|
||||
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
return streamMaps_[SServer].emplace(isrListener->getSeq(), isrListener).second;
|
||||
return streamMaps_[SServer]
|
||||
.insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion})
|
||||
.second;
|
||||
}
|
||||
|
||||
// <-- bool: true=erased, false=was not there
|
||||
@@ -4768,10 +4883,13 @@ NetworkOPsImp::unsubServer(std::uint64_t uSeq)
|
||||
|
||||
// <-- bool: true=added, false=already there
|
||||
bool
|
||||
NetworkOPsImp::subTransactions(InfoSub::Ref isrListener)
|
||||
NetworkOPsImp::subTransactions(InfoSub::Ref isrListener, unsigned int apiVersion)
|
||||
{
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
return streamMaps_[STransactions].emplace(isrListener->getSeq(), isrListener).second;
|
||||
return streamMaps_[STransactions]
|
||||
.insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion})
|
||||
.second;
|
||||
}
|
||||
|
||||
// <-- bool: true=erased, false=was not there
|
||||
@@ -4784,10 +4902,13 @@ NetworkOPsImp::unsubTransactions(std::uint64_t uSeq)
|
||||
|
||||
// <-- bool: true=added, false=already there
|
||||
bool
|
||||
NetworkOPsImp::subRTTransactions(InfoSub::Ref isrListener)
|
||||
NetworkOPsImp::subRTTransactions(InfoSub::Ref isrListener, unsigned int apiVersion)
|
||||
{
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
return streamMaps_[SRtTransactions].emplace(isrListener->getSeq(), isrListener).second;
|
||||
return streamMaps_[SRtTransactions]
|
||||
.insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion})
|
||||
.second;
|
||||
}
|
||||
|
||||
// <-- bool: true=erased, false=was not there
|
||||
@@ -4800,10 +4921,13 @@ NetworkOPsImp::unsubRTTransactions(std::uint64_t uSeq)
|
||||
|
||||
// <-- bool: true=added, false=already there
|
||||
bool
|
||||
NetworkOPsImp::subValidations(InfoSub::Ref isrListener)
|
||||
NetworkOPsImp::subValidations(InfoSub::Ref isrListener, unsigned int apiVersion)
|
||||
{
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
return streamMaps_[SValidations].emplace(isrListener->getSeq(), isrListener).second;
|
||||
return streamMaps_[SValidations]
|
||||
.insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion})
|
||||
.second;
|
||||
}
|
||||
|
||||
void
|
||||
@@ -4822,10 +4946,13 @@ NetworkOPsImp::unsubValidations(std::uint64_t uSeq)
|
||||
|
||||
// <-- bool: true=added, false=already there
|
||||
bool
|
||||
NetworkOPsImp::subPeerStatus(InfoSub::Ref isrListener)
|
||||
NetworkOPsImp::subPeerStatus(InfoSub::Ref isrListener, unsigned int apiVersion)
|
||||
{
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
return streamMaps_[SPeerStatus].emplace(isrListener->getSeq(), isrListener).second;
|
||||
return streamMaps_[SPeerStatus]
|
||||
.insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion})
|
||||
.second;
|
||||
}
|
||||
|
||||
// <-- bool: true=erased, false=was not there
|
||||
@@ -4838,10 +4965,13 @@ NetworkOPsImp::unsubPeerStatus(std::uint64_t uSeq)
|
||||
|
||||
// <-- bool: true=added, false=already there
|
||||
bool
|
||||
NetworkOPsImp::subConsensus(InfoSub::Ref isrListener)
|
||||
NetworkOPsImp::subConsensus(InfoSub::Ref isrListener, unsigned int apiVersion)
|
||||
{
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
return streamMaps_[SConsensusPhase].emplace(isrListener->getSeq(), isrListener).second;
|
||||
return streamMaps_[SConsensusPhase]
|
||||
.insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion})
|
||||
.second;
|
||||
}
|
||||
|
||||
// <-- bool: true=erased, false=was not there
|
||||
|
||||
@@ -53,12 +53,14 @@ PathRequest::PathRequest(
|
||||
std::shared_ptr<InfoSub> const& subscriber,
|
||||
int id,
|
||||
PathRequestManager& owner,
|
||||
beast::Journal journal)
|
||||
beast::Journal journal,
|
||||
unsigned int apiVersion)
|
||||
: app_(app)
|
||||
, journal_(journal)
|
||||
, owner_(owner)
|
||||
, wpSubscriber_(subscriber)
|
||||
, consumer_(subscriber->getConsumer())
|
||||
, apiVersion_(apiVersion)
|
||||
, jvStatus_(json::ValueType::Object)
|
||||
, lastIndex_(0)
|
||||
, inProgress_(false)
|
||||
@@ -82,6 +84,9 @@ PathRequest::PathRequest(
|
||||
, owner_(owner)
|
||||
, fCompletion_(std::move(completion))
|
||||
, consumer_(consumer)
|
||||
// A ripple_path_find request answers its caller once and pushes no update, so it names no
|
||||
// version.
|
||||
, apiVersion_(0)
|
||||
, jvStatus_(json::ValueType::Object)
|
||||
, lastIndex_(0)
|
||||
, inProgress_(false)
|
||||
|
||||
@@ -57,7 +57,8 @@ public:
|
||||
std::shared_ptr<InfoSub> const& subscriber,
|
||||
int id,
|
||||
PathRequestManager&,
|
||||
beast::Journal journal);
|
||||
beast::Journal journal,
|
||||
unsigned int apiVersion);
|
||||
|
||||
// ripple_path_find semantics
|
||||
// Completion function is called after path update is complete
|
||||
@@ -98,6 +99,19 @@ public:
|
||||
std::function<bool()> const& continueCallback = {});
|
||||
InfoSub::pointer
|
||||
getSubscriber() const;
|
||||
|
||||
/**
|
||||
* The API version the `path_find create` request named, recorded for the
|
||||
* publisher of its updates.
|
||||
*
|
||||
* @return The version, or zero for a `ripple_path_find` request, which
|
||||
* produces no update to shape.
|
||||
*/
|
||||
[[nodiscard]] unsigned int
|
||||
apiVersion() const noexcept
|
||||
{
|
||||
return apiVersion_;
|
||||
}
|
||||
bool
|
||||
hasCompletion();
|
||||
|
||||
@@ -139,6 +153,8 @@ private:
|
||||
std::function<void()> fCompletion_;
|
||||
resource::Consumer& consumer_; // Charge according to source currencies
|
||||
|
||||
unsigned int const apiVersion_;
|
||||
|
||||
json::Value jvId_;
|
||||
json::Value jvStatus_; // Last result
|
||||
|
||||
|
||||
@@ -235,10 +235,11 @@ json::Value
|
||||
PathRequestManager::makePathRequest(
|
||||
std::shared_ptr<InfoSub> const& subscriber,
|
||||
std::shared_ptr<ReadView const> const& inLedger,
|
||||
json::Value const& requestJson)
|
||||
json::Value const& requestJson,
|
||||
unsigned int apiVersion)
|
||||
{
|
||||
auto const req =
|
||||
std::make_shared<PathRequest>(app_, subscriber, ++lastIdentifier_, *this, journal_);
|
||||
auto const req = std::make_shared<PathRequest>(
|
||||
app_, subscriber, ++lastIdentifier_, *this, journal_, apiVersion);
|
||||
|
||||
auto [valid, jvRes] = req->doCreate(getAssetCache(inLedger, false), requestJson);
|
||||
|
||||
|
||||
@@ -50,13 +50,22 @@ public:
|
||||
std::shared_ptr<AssetCache>
|
||||
getAssetCache(std::shared_ptr<ReadView const> const& ledger, bool authoritative);
|
||||
|
||||
// Create a new-style path request that pushes
|
||||
// updates to a subscriber
|
||||
/**
|
||||
* Creates a path request that pushes updates to @p subscriber.
|
||||
*
|
||||
* @param subscriber The connection the updates go to.
|
||||
* @param ledger The ledger the first search runs against.
|
||||
* @param request The `path_find create` parameters.
|
||||
* @param apiVersion The API version the request named, recorded on the
|
||||
* request.
|
||||
* @return The first result, or an error when the request is malformed.
|
||||
*/
|
||||
json::Value
|
||||
makePathRequest(
|
||||
std::shared_ptr<InfoSub> const& subscriber,
|
||||
std::shared_ptr<ReadView const> const& ledger,
|
||||
json::Value const& request);
|
||||
json::Value const& request,
|
||||
unsigned int apiVersion);
|
||||
|
||||
// Create an old-style path request that is
|
||||
// managed by a coroutine and updated by
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/BuildInfo.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/JsonRpc.h>
|
||||
#include <xrpl/protocol/RPCErr.h>
|
||||
#include <xrpl/protocol/SystemParameters.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
@@ -67,6 +68,7 @@
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -105,6 +107,78 @@ statusRequestResponse(HttpRequestType const& request, boost::beast::http::status
|
||||
return handoff;
|
||||
}
|
||||
|
||||
/**
|
||||
* The object a request presents its parameters in.
|
||||
*
|
||||
* `params` is an array holding one object for the by-position form and is that
|
||||
* object itself for the by-name form. Anything else reads as an empty object,
|
||||
* since a caller reads the version and the credentials out of it before the
|
||||
* shape is validated.
|
||||
*
|
||||
* Answers an Object or a Null and nothing else: Role.cpp reads `admin_password`
|
||||
* out of the result through the const `operator[]`, which asserts on any other
|
||||
* type, so widening this makes that line a remotely reachable assert.
|
||||
*
|
||||
* @param request The request, which need not be an object.
|
||||
* @return The parameters, or an empty object when the request presents none.
|
||||
* Borrowed from @p request, so it lives as long as the request does.
|
||||
*/
|
||||
static json::Value const&
|
||||
requestParams(json::Value const& request)
|
||||
{
|
||||
static json::Value const kNoParams{json::ValueType::Object};
|
||||
if (!request.isObject())
|
||||
return kNoParams;
|
||||
|
||||
auto const& params = request[jss::params];
|
||||
if (params.isArray() && params.size() > 0 && params[json::UInt(0)].isObjectOrNull())
|
||||
return params[json::UInt(0)];
|
||||
if (params.isObject())
|
||||
return params;
|
||||
|
||||
return kNoParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the API version a JSON-RPC @p request asks for.
|
||||
*
|
||||
* The version lives with the request's parameters, where a client puts it, and
|
||||
* @p honorTopLevelFrom also accepts it beside the method.
|
||||
*
|
||||
* @param request The request to read.
|
||||
* @param honorTopLevelFrom The lowest version a value beside the method may
|
||||
* select. kApiInvalidVersion honors any of them, including one the
|
||||
* server cannot serve; kApiMinimumSpecVersion leaves such a request at
|
||||
* version 1.
|
||||
* @param betaEnabled Whether the beta version counts as supported.
|
||||
* @return The version asked for. kApiInvalidVersion if the parameters name one
|
||||
* the server cannot serve, or if the top level does and
|
||||
* @p honorTopLevelFrom is kApiInvalidVersion. kApiVersionIfUnspecified
|
||||
* if the request names none, and also if the only value is beside the
|
||||
* method and below @p honorTopLevelFrom: a value the server cannot
|
||||
* serve reads as kApiInvalidVersion, so it is below any other
|
||||
* threshold, and such a request is answered as if it had named none,
|
||||
* as it always has been.
|
||||
*/
|
||||
static unsigned
|
||||
apiVersionOf(json::Value const& request, unsigned honorTopLevelFrom, bool betaEnabled)
|
||||
{
|
||||
// Whichever object carries the member decides, so a request naming the version explicitly is
|
||||
// told apart from one naming none.
|
||||
auto const& params = requestParams(request);
|
||||
if (params.isMember(jss::api_version))
|
||||
return rpc::getAPIVersionNumber(params, betaEnabled);
|
||||
|
||||
if (request.isMember(jss::api_version))
|
||||
{
|
||||
auto const version = rpc::getAPIVersionNumber(request, betaEnabled);
|
||||
if (version >= honorTopLevelFrom)
|
||||
return version;
|
||||
}
|
||||
|
||||
return rpc::kApiVersionIfUnspecified;
|
||||
}
|
||||
|
||||
// VFALCO TODO Rewrite to use boost::beast::http::fields
|
||||
static bool
|
||||
authorized(Port const& port, std::map<std::string, std::string> const& h)
|
||||
@@ -343,6 +417,16 @@ ServerHandler::onWSMessage(
|
||||
auto const size = boost::asio::buffer_size(buffers);
|
||||
if (size > rpc::tuning::kMaxRequestSize || !json::Reader{}.parse(jv, buffers) || !jv.isObject())
|
||||
{
|
||||
// The only place that can account for an unreadable frame: it never reaches
|
||||
// processSession, where every other request is charged.
|
||||
auto const is = std::static_pointer_cast<WSInfoSub>(session->appDefined);
|
||||
is->getConsumer().charge(resource::kFeeMalformedRpc);
|
||||
if (is->getConsumer().disconnect(journal_))
|
||||
{
|
||||
session->close({boost::beast::websocket::policy_error, "threshold exceeded"});
|
||||
return;
|
||||
}
|
||||
|
||||
// An unparsed body cannot be masked field-wise, so its size goes instead of its content.
|
||||
// Clamped rather than narrowed: json has no integer wider than 32 bits.
|
||||
json::Value jvResult(json::ValueType::Object);
|
||||
@@ -597,10 +681,40 @@ makeJsonError(json::Int code, json::Value&& message)
|
||||
return r;
|
||||
}
|
||||
|
||||
constexpr json::Int kMethodNotFound = -32601;
|
||||
constexpr json::Int kServerOverloaded = -32604;
|
||||
constexpr json::Int kForbidden = -32605;
|
||||
constexpr json::Int kWrongVersion = -32606;
|
||||
// Version of the JSON-RPC reply envelope, selected by the `ripplerpc` request parameter. It shapes
|
||||
// a completed result into a reply; no handler observes it.
|
||||
enum class RpcVersion {
|
||||
// Errors are returned under `result`, keyed by `error_message`, and echo the (secret-masked)
|
||||
// request. HTTP status is always 200.
|
||||
V1,
|
||||
// Errors are returned under `error`, keyed by `message`, and the request is not echoed. HTTP
|
||||
// status is always 200.
|
||||
V2,
|
||||
// As V2, but the error code selects a 4xx/5xx HTTP status.
|
||||
V3,
|
||||
};
|
||||
|
||||
constexpr RpcVersion kRpcVersionIfUnspecified = RpcVersion::V1;
|
||||
|
||||
/**
|
||||
* Maps a `ripplerpc` value onto the envelope version it selects.
|
||||
*
|
||||
* The value is unauthenticated, so anything but an exact match is refused.
|
||||
*
|
||||
* @param value The `ripplerpc` member, as sent.
|
||||
* @return The envelope version, or nullopt for the caller to reject.
|
||||
*/
|
||||
static std::optional<RpcVersion>
|
||||
rpcVersion(std::string_view value)
|
||||
{
|
||||
if (value == rpc::kRippleRpcVersion1)
|
||||
return RpcVersion::V1;
|
||||
if (value == rpc::kRippleRpcVersion2)
|
||||
return RpcVersion::V2;
|
||||
if (value == rpc::kRippleRpcVersion3)
|
||||
return RpcVersion::V3;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* The HTTP status the `ripplerpc: "3.0"` envelope reports for @p code.
|
||||
@@ -627,6 +741,84 @@ legacyHttpStatus(ErrorCodeI code)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shapes a handler @p result into the reply envelope for @p version, appending
|
||||
* it to @p reply.
|
||||
*
|
||||
* @param version The envelope the reply takes.
|
||||
* @param result A handler result, consumed.
|
||||
* @param request The request's parameters, echoed on a version 1 error only,
|
||||
* and where the `jsonrpc`, `ripplerpc` and `id` members are read.
|
||||
* @param batch Whether to append to @p reply rather than assign it.
|
||||
* @param reply The reply this writes.
|
||||
* @param journal Where an error is logged.
|
||||
* @return The HTTP status the reply is sent with. Only version 3 derives it
|
||||
* from the error code; the others report 200. A batch discards it.
|
||||
*/
|
||||
static int
|
||||
shapeReply(
|
||||
RpcVersion version,
|
||||
json::Value result,
|
||||
json::Value const& request,
|
||||
bool batch,
|
||||
json::Value& reply,
|
||||
beast::Journal journal)
|
||||
{
|
||||
json::Value r(json::ValueType::Object);
|
||||
int status = 200;
|
||||
|
||||
if (!result.isMember(jss::error))
|
||||
{
|
||||
result[jss::status] = jss::success;
|
||||
r[jss::result] = std::move(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Read through a const reference: the non-const `operator[]` inserts a null member for an
|
||||
// absent name, and the reply below is built out of `result`.
|
||||
json::Value const& reported = result;
|
||||
|
||||
JLOG(journal.debug()) << "rpcError: " << reported[jss::error] << ": "
|
||||
<< reported[jss::error_message];
|
||||
|
||||
if (version == RpcVersion::V3 && reported[jss::error_code].isInt())
|
||||
status = legacyHttpStatus(static_cast<ErrorCodeI>(reported[jss::error_code].asInt()));
|
||||
|
||||
result[jss::status] = jss::error;
|
||||
|
||||
if (version == RpcVersion::V1)
|
||||
{
|
||||
result[jss::request] = rpc::maskSecrets(request);
|
||||
r[jss::result] = std::move(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
result[jss::code] = result[jss::error_code];
|
||||
result[jss::message] = result[jss::error_message];
|
||||
result.removeMember(jss::error_message);
|
||||
r[jss::error] = std::move(result);
|
||||
}
|
||||
}
|
||||
|
||||
if (request.isMember(jss::jsonrpc))
|
||||
r[jss::jsonrpc] = request[jss::jsonrpc];
|
||||
if (request.isMember(jss::ripplerpc))
|
||||
r[jss::ripplerpc] = request[jss::ripplerpc];
|
||||
if (request.isMember(jss::id))
|
||||
r[jss::id] = request[jss::id];
|
||||
|
||||
if (batch)
|
||||
{
|
||||
reply.append(std::move(r));
|
||||
}
|
||||
else
|
||||
{
|
||||
reply = std::move(r);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
void
|
||||
ServerHandler::processRequest(
|
||||
Port const& port,
|
||||
@@ -639,12 +831,55 @@ ServerHandler::processRequest(
|
||||
{
|
||||
auto const rpcJ = app_.getJournal("RPC");
|
||||
|
||||
// What a request presents when the server cannot read what it presents: an unparsable body or a
|
||||
// malformed entry carries no credentials to act on. A connection privileged by its address is
|
||||
// still privileged, `requestRole` reading the port rather than the request for that.
|
||||
static json::Value const kUnreadable(json::ValueType::Object);
|
||||
|
||||
// The resource entry a request is charged against before its own role is read. `Role::GUEST` is
|
||||
// the requirement asked for, no method having been read that could ask for more.
|
||||
auto const usageFor = [&](json::Value const& presented) {
|
||||
return requestInboundEndpoint(
|
||||
resourceManager_,
|
||||
remoteIPAddress,
|
||||
requestRole(Role::GUEST, port, presented, remoteIPAddress, user),
|
||||
user,
|
||||
forwardedFor);
|
||||
};
|
||||
|
||||
// Whether a charge also asks if the connection is over the drop threshold.
|
||||
enum class Threshold { Ignore, Ask };
|
||||
|
||||
// Charges a request rejected before its own role is read, and reports whether the connection is
|
||||
// over the drop threshold. Only a caller that can act on the answer passes `Threshold::Ask`,
|
||||
// since asking is itself a charge: see chargeUnreadBody below.
|
||||
auto const chargeWithoutRole = [&](json::Value const& presented, Threshold threshold) {
|
||||
auto usage = usageFor(presented);
|
||||
usage.charge(resource::kFeeMalformedRpc);
|
||||
return threshold == Threshold::Ask && usage.disconnect(journal_);
|
||||
};
|
||||
|
||||
// Charges a body the server rejects before it reads a request out of it. The drop threshold is
|
||||
// not asked about: `disconnect` is a mutator, charging the drop fee and counting a drop on
|
||||
// every call made while the balance is at or above the drop threshold, and this rejection
|
||||
// answers the whole body.
|
||||
auto const chargeUnreadBody = [&] { usageFor(kUnreadable).charge(resource::kFeeMalformedRpc); };
|
||||
|
||||
json::Value jsonOrig;
|
||||
{
|
||||
json::Reader reader;
|
||||
if ((request.size() > rpc::tuning::kMaxRequestSize) || !reader.parse(request, jsonOrig) ||
|
||||
!jsonOrig || !jsonOrig.isObject())
|
||||
// Only a parse failure can report the reader's reason, getFormattedErrorMessages being
|
||||
// built from what the reader recorded. The other three name their own cause.
|
||||
if (request.size() > rpc::tuning::kMaxRequestSize)
|
||||
{
|
||||
chargeUnreadBody();
|
||||
httpReply(400, "Request is too large", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
|
||||
json::Reader reader;
|
||||
if (!reader.parse(request, jsonOrig))
|
||||
{
|
||||
chargeUnreadBody();
|
||||
httpReply(
|
||||
400,
|
||||
"Unable to parse request: " + reader.getFormattedErrorMessages(),
|
||||
@@ -652,6 +887,23 @@ ServerHandler::processRequest(
|
||||
rpcJ);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!jsonOrig)
|
||||
{
|
||||
// A well-formed document that carries nothing: `{}`, `[]` or `null`.
|
||||
chargeUnreadBody();
|
||||
httpReply(400, "Request is empty", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!jsonOrig.isObject())
|
||||
{
|
||||
// A non-empty array, the only value the reader accepts that is neither null nor an
|
||||
// object. A number, string or boolean is a parse failure, answered above.
|
||||
chargeUnreadBody();
|
||||
httpReply(400, "Request is not a JSON object", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool batch = false;
|
||||
@@ -663,6 +915,7 @@ ServerHandler::processRequest(
|
||||
batch = true;
|
||||
if (!jsonOrig.isMember(jss::params) || !jsonOrig[jss::params].isArray())
|
||||
{
|
||||
chargeUnreadBody();
|
||||
httpReply(400, "Malformed batch request", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
@@ -670,45 +923,85 @@ ServerHandler::processRequest(
|
||||
}
|
||||
|
||||
json::Value reply(batch ? json::ValueType::Array : json::ValueType::Object);
|
||||
// Only a lone request selects the HTTP status: a batch may mix versions and reports each
|
||||
// entry's outcome in its own reply, so the batch itself always succeeds.
|
||||
int httpStatus = 200;
|
||||
auto const start(std::chrono::high_resolution_clock::now());
|
||||
for (unsigned i = 0; i < size; ++i)
|
||||
{
|
||||
json::Value const& jsonRPC = batch ? jsonOrig[jss::params][i] : jsonOrig;
|
||||
|
||||
// Only an entry of a batch can be a non-object; a lone request was checked before the loop.
|
||||
// Inline rather than through `reject` below: an entry with no members carries the copy
|
||||
// under `request` whatever a caller asks for.
|
||||
if (!jsonRPC.isObject())
|
||||
{
|
||||
// Only a batch reaches here, so the loop can act on the threshold. The entry presents
|
||||
// no credentials, so the connection pays.
|
||||
bool const overloaded = chargeWithoutRole(kUnreadable, Threshold::Ask);
|
||||
|
||||
json::Value r(json::ValueType::Object);
|
||||
r[jss::request] = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "Method not found");
|
||||
r[jss::error] = makeJsonError(rpc::kJsonRpcMethodNotFound, "Method not found");
|
||||
reply.append(r);
|
||||
|
||||
if (overloaded)
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
unsigned apiVersion = rpc::kApiVersionIfUnspecified;
|
||||
if (jsonRPC.isMember(jss::params) && jsonRPC[jss::params].isArray() &&
|
||||
jsonRPC[jss::params].size() > 0 && jsonRPC[jss::params][0u].isObject())
|
||||
{
|
||||
apiVersion = rpc::getAPIVersionNumber(
|
||||
jsonRPC[jss::params][json::UInt(0)], app_.config().betaRpcApi);
|
||||
}
|
||||
// A `method: "batch"` entry names its version beside its method, that form having no
|
||||
// parameters to nest inside. A lone request may spell it there too, but only a value naming
|
||||
// a specification version is honored: a top-level `api_version: 2` answers as version 1,
|
||||
// and honoring it would change a shipped reply shape.
|
||||
unsigned const apiVersion = apiVersionOf(
|
||||
jsonRPC,
|
||||
batch ? unsigned{rpc::kApiInvalidVersion} : unsigned{rpc::kApiMinimumSpecVersion},
|
||||
app_.config().betaRpcApi);
|
||||
|
||||
if (apiVersion == rpc::kApiVersionIfUnspecified && batch)
|
||||
{
|
||||
// for batch request, api_version may be at a different level
|
||||
apiVersion = rpc::getAPIVersionNumber(jsonRPC, app_.config().betaRpcApi);
|
||||
}
|
||||
// Answers a request rejected before it reaches a handler. A lone request receives the
|
||||
// HTTP status and `message` as the whole body; a batch entry receives an error object
|
||||
// beside a copy of the entry, one reply array having no other place to name the entry that
|
||||
// failed. `wrapRequest` puts that copy under `request` instead.
|
||||
//
|
||||
// Returns whether the loop continues, which it does only for a batch.
|
||||
auto const reject =
|
||||
[&](int status, json::Int code, char const* message, bool wrapRequest = false) {
|
||||
if (!batch)
|
||||
{
|
||||
httpReply(status, message, output, rpcJ);
|
||||
return false;
|
||||
}
|
||||
json::Value r(json::ValueType::Object);
|
||||
if (!wrapRequest)
|
||||
{
|
||||
r = rpc::maskSecrets(jsonRPC);
|
||||
}
|
||||
else
|
||||
{
|
||||
r[jss::request] = rpc::maskSecrets(jsonRPC);
|
||||
}
|
||||
r[jss::error] = makeJsonError(code, message);
|
||||
reply.append(std::move(r));
|
||||
return true;
|
||||
};
|
||||
|
||||
if (apiVersion == rpc::kApiInvalidVersion)
|
||||
{
|
||||
if (!batch)
|
||||
bool const overloaded = chargeWithoutRole(
|
||||
requestParams(jsonRPC), batch ? Threshold::Ask : Threshold::Ignore);
|
||||
// An object-shaped rejection returns this entry under `request`, where a client
|
||||
// correlating by `reply[i].request` finds it.
|
||||
if (!reject(
|
||||
400,
|
||||
rpc::kJsonRpcWrongVersion,
|
||||
jss::invalid_API_version.cStr(),
|
||||
/*wrapRequest=*/true))
|
||||
{
|
||||
httpReply(400, jss::invalid_API_version.cStr(), output, rpcJ);
|
||||
return;
|
||||
}
|
||||
json::Value r(json::ValueType::Object);
|
||||
r[jss::request] = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kWrongVersion, jss::invalid_API_version.cStr());
|
||||
reply.append(r);
|
||||
if (overloaded)
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -721,65 +1014,37 @@ ServerHandler::processRequest(
|
||||
apiVersion, app_.config().betaRpcApi, jsonRPC[jss::method].asString());
|
||||
}
|
||||
|
||||
if (jsonRPC.isMember(jss::params) && jsonRPC[jss::params].isArray() &&
|
||||
jsonRPC[jss::params].size() > 0 && jsonRPC[jss::params][json::UInt(0)].isObjectOrNull())
|
||||
{
|
||||
role = requestRole(
|
||||
required, port, jsonRPC[jss::params][json::UInt(0)], remoteIPAddress, user);
|
||||
}
|
||||
else
|
||||
{
|
||||
role = requestRole(required, port, json::ValueType::Object, remoteIPAddress, user);
|
||||
}
|
||||
// The role comes from the credentials the request presents, which sit with its parameters
|
||||
// whichever form they take.
|
||||
role = requestRole(required, port, requestParams(jsonRPC), remoteIPAddress, user);
|
||||
|
||||
resource::Consumer usage;
|
||||
if (isUnlimited(role))
|
||||
auto usage =
|
||||
requestInboundEndpoint(resourceManager_, remoteIPAddress, role, user, forwardedFor);
|
||||
|
||||
// An overloaded server sheds the request without charging for it; disconnect() has already
|
||||
// accounted for the load that got it here. The loop then stops: every later entry of the
|
||||
// same body would be shed too, and answering each one is itself work an overloaded server
|
||||
// must not do.
|
||||
if (!isUnlimited(role) && usage.disconnect(journal_))
|
||||
{
|
||||
usage = resourceManager_.newUnlimitedEndpoint(remoteIPAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
usage = resourceManager_.newInboundEndpoint(
|
||||
remoteIPAddress, role == Role::PROXY, forwardedFor);
|
||||
if (usage.disconnect(journal_))
|
||||
{
|
||||
if (!batch)
|
||||
{
|
||||
httpReply(503, "Server is overloaded", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kServerOverloaded, "Server is overloaded");
|
||||
reply.append(r);
|
||||
continue;
|
||||
}
|
||||
if (!reject(503, rpc::kJsonRpcServerOverloaded, "Server is overloaded"))
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
if (role == Role::FORBID)
|
||||
{
|
||||
usage.charge(resource::kFeeMalformedRpc);
|
||||
if (!batch)
|
||||
{
|
||||
httpReply(403, "Forbidden", output, rpcJ);
|
||||
if (!reject(403, rpc::kJsonRpcForbidden, "Forbidden"))
|
||||
return;
|
||||
}
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kForbidden, "Forbidden");
|
||||
reply.append(r);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!jsonRPC.isMember(jss::method) || jsonRPC[jss::method].isNull())
|
||||
{
|
||||
usage.charge(resource::kFeeMalformedRpc);
|
||||
if (!batch)
|
||||
{
|
||||
httpReply(400, "Null method", output, rpcJ);
|
||||
if (!reject(400, rpc::kJsonRpcMethodNotFound, "Null method"))
|
||||
return;
|
||||
}
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "Null method");
|
||||
reply.append(r);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -787,14 +1052,8 @@ ServerHandler::processRequest(
|
||||
if (!method.isString())
|
||||
{
|
||||
usage.charge(resource::kFeeMalformedRpc);
|
||||
if (!batch)
|
||||
{
|
||||
httpReply(400, "method is not string", output, rpcJ);
|
||||
if (!reject(400, rpc::kJsonRpcMethodNotFound, "method is not string"))
|
||||
return;
|
||||
}
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "method is not string");
|
||||
reply.append(r);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -802,23 +1061,13 @@ ServerHandler::processRequest(
|
||||
if (strMethod.empty())
|
||||
{
|
||||
usage.charge(resource::kFeeMalformedRpc);
|
||||
if (!batch)
|
||||
{
|
||||
httpReply(400, "method is empty", output, rpcJ);
|
||||
if (!reject(400, rpc::kJsonRpcMethodNotFound, "method is empty"))
|
||||
return;
|
||||
}
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "method is empty");
|
||||
reply.append(r);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract request parameters from the request Json as `params`.
|
||||
//
|
||||
// If the field "params" is empty, `params` is an empty object.
|
||||
//
|
||||
// Otherwise, that field must be an array of length 1 (why?)
|
||||
// and we take that first entry and validate that it's an object.
|
||||
// The `params` field carries the one object a handler reads, in either of the two forms the
|
||||
// specification defines: the object itself, or an array holding it.
|
||||
json::Value params;
|
||||
if (!batch)
|
||||
{
|
||||
@@ -827,14 +1076,15 @@ ServerHandler::processRequest(
|
||||
{
|
||||
params = json::Value(json::ValueType::Object);
|
||||
}
|
||||
else if (!params.isArray() || params.size() != 1)
|
||||
{
|
||||
usage.charge(resource::kFeeMalformedRpc);
|
||||
httpReply(400, "params unparsable", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
else
|
||||
else if (!params.isObject())
|
||||
{
|
||||
if (!params.isArray() || params.size() != 1)
|
||||
{
|
||||
usage.charge(resource::kFeeMalformedRpc);
|
||||
httpReply(400, "params unparsable", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
|
||||
params = std::move(params[0u]);
|
||||
if (!params.isObjectOrNull())
|
||||
{
|
||||
@@ -849,35 +1099,36 @@ ServerHandler::processRequest(
|
||||
params = jsonRPC;
|
||||
}
|
||||
|
||||
std::string ripplerpc{rpc::kRippleRpcVersion1};
|
||||
RpcVersion envelope = kRpcVersionIfUnspecified;
|
||||
if (params.isMember(jss::ripplerpc))
|
||||
{
|
||||
// A `ripplerpc` the server cannot honor is a bad parameter, so the second check reports
|
||||
// that code. The first keeps the method-not-found code shipped versions report.
|
||||
if (!params[jss::ripplerpc].isString())
|
||||
{
|
||||
usage.charge(resource::kFeeMalformedRpc);
|
||||
if (!batch)
|
||||
{
|
||||
httpReply(400, "ripplerpc is not a string", output, rpcJ);
|
||||
if (!reject(400, rpc::kJsonRpcMethodNotFound, "ripplerpc is not a string"))
|
||||
return;
|
||||
}
|
||||
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "ripplerpc is not a string");
|
||||
reply.append(r);
|
||||
continue;
|
||||
}
|
||||
ripplerpc = params[jss::ripplerpc].asString();
|
||||
|
||||
auto const parsed = rpcVersion(params[jss::ripplerpc].asString());
|
||||
if (!parsed)
|
||||
{
|
||||
usage.charge(resource::kFeeMalformedRpc);
|
||||
if (!reject(
|
||||
400, rpc::kJsonRpcInvalidParams, "ripplerpc is not a supported version"))
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
envelope = *parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear header-assigned values if not positively identified from a
|
||||
* secureGateway.
|
||||
*/
|
||||
if (role != Role::IDENTIFIED && role != Role::PROXY)
|
||||
{
|
||||
forwardedFor.remove_suffix(forwardedFor.size());
|
||||
user.remove_suffix(user.size());
|
||||
}
|
||||
// Header-assigned values belong to the connection, so an entry not identified from a
|
||||
// secureGateway drops them for itself only rather than clearing them in place.
|
||||
bool const identified = role == Role::IDENTIFIED || role == Role::PROXY;
|
||||
std::string_view const entryForwardedFor = identified ? forwardedFor : std::string_view{};
|
||||
std::string_view const entryUser = identified ? user : std::string_view{};
|
||||
|
||||
JLOG(journal_.debug()) << "Query: " << strMethod << rpc::loggable(params);
|
||||
|
||||
@@ -901,7 +1152,7 @@ ServerHandler::processRequest(
|
||||
.apiVersion = apiVersion,
|
||||
},
|
||||
params,
|
||||
{.user = user, .forwardedFor = forwardedFor},
|
||||
{.user = entryUser, .forwardedFor = entryForwardedFor},
|
||||
};
|
||||
json::Value result;
|
||||
|
||||
@@ -926,58 +1177,9 @@ ServerHandler::processRequest(
|
||||
if (usage.warn())
|
||||
result[jss::warning] = jss::load;
|
||||
|
||||
json::Value r(json::ValueType::Object);
|
||||
if (ripplerpc >= rpc::kRippleRpcVersion2)
|
||||
{
|
||||
if (result.isMember(jss::error))
|
||||
{
|
||||
result[jss::status] = jss::error;
|
||||
result["code"] = result[jss::error_code];
|
||||
result["message"] = result[jss::error_message];
|
||||
result.removeMember(jss::error_message);
|
||||
JLOG(journal_.debug())
|
||||
<< "rpcError: " << result[jss::error] << ": " << result[jss::error_message];
|
||||
r[jss::error] = std::move(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
result[jss::status] = jss::success;
|
||||
r[jss::result] = std::move(result);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Always report "status". On an error report the request as
|
||||
// received.
|
||||
if (result.isMember(jss::error))
|
||||
{
|
||||
result[jss::status] = jss::error;
|
||||
result[jss::request] = rpc::maskSecrets(params);
|
||||
|
||||
JLOG(journal_.debug())
|
||||
<< "rpcError: " << result[jss::error] << ": " << result[jss::error_message];
|
||||
}
|
||||
else
|
||||
{
|
||||
result[jss::status] = jss::success;
|
||||
}
|
||||
r[jss::result] = std::move(result);
|
||||
}
|
||||
|
||||
if (params.isMember(jss::jsonrpc))
|
||||
r[jss::jsonrpc] = params[jss::jsonrpc];
|
||||
if (params.isMember(jss::ripplerpc))
|
||||
r[jss::ripplerpc] = params[jss::ripplerpc];
|
||||
if (params.isMember(jss::id))
|
||||
r[jss::id] = params[jss::id];
|
||||
if (batch)
|
||||
{
|
||||
reply.append(std::move(r));
|
||||
}
|
||||
else
|
||||
{
|
||||
reply = std::move(r);
|
||||
}
|
||||
int const status = shapeReply(envelope, std::move(result), params, batch, reply, journal_);
|
||||
if (!batch)
|
||||
httpStatus = status;
|
||||
|
||||
if (reply.isMember(jss::result) && reply[jss::result].isMember(jss::result))
|
||||
{
|
||||
@@ -990,25 +1192,6 @@ ServerHandler::processRequest(
|
||||
}
|
||||
}
|
||||
|
||||
// If we're returning an error_code, use that to determine the HTTP status.
|
||||
int const httpStatus = [&reply] {
|
||||
// This feature is enabled with ripplerpc version 3.0 and above.
|
||||
// Before ripplerpc version 3.0 always return 200.
|
||||
if (reply.isMember(jss::ripplerpc) && reply[jss::ripplerpc].isString() &&
|
||||
reply[jss::ripplerpc].asString() >= rpc::kRippleRpcVersion3)
|
||||
{
|
||||
// If there's an error_code, use that to determine the HTTP Status.
|
||||
if (reply.isMember(jss::error) && reply[jss::error].isMember(jss::error_code) &&
|
||||
reply[jss::error][jss::error_code].isInt())
|
||||
{
|
||||
int const errCode = reply[jss::error][jss::error_code].asInt();
|
||||
return legacyHttpStatus(static_cast<ErrorCodeI>(errCode));
|
||||
}
|
||||
}
|
||||
// Return OK.
|
||||
return 200;
|
||||
}();
|
||||
|
||||
auto response = to_string(reply);
|
||||
|
||||
rpcTime_.notify(
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
#include <xrpl/core/NetworkIDService.h>
|
||||
#include <xrpl/json/json_writer.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -455,9 +454,25 @@ struct TransactionPreProcessResult
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks a sign or submit request and builds the transaction it describes.
|
||||
*
|
||||
* @param params The request's parameters, which `tx_json` is read out of and
|
||||
* autofilled fields are written into.
|
||||
* @param apiVersion The API version the request is served at, which selects
|
||||
* the error reported for a ledger that is not current.
|
||||
* @param role The caller's role.
|
||||
* @param signingArgs Who signs, and for whom, filled in as the request is
|
||||
* read.
|
||||
* @param validatedLedgerAge How old the last validated ledger is.
|
||||
* @param app The application.
|
||||
* @param rules The rules of the ledger the transaction is built against.
|
||||
* @return The transaction, or the error to report.
|
||||
*/
|
||||
static TransactionPreProcessResult
|
||||
transactionPreProcessImpl(
|
||||
json::Value& params,
|
||||
unsigned apiVersion,
|
||||
Role role,
|
||||
SigningForParams& signingArgs,
|
||||
std::chrono::seconds validatedLedgerAge,
|
||||
@@ -506,13 +521,7 @@ transactionPreProcessImpl(
|
||||
|
||||
// Check tx_json fields, but don't add any.
|
||||
auto [txJsonResult, srcAddressID] = checkTxJsonFields(
|
||||
txJson,
|
||||
role,
|
||||
verify,
|
||||
validatedLedgerAge,
|
||||
app.config(),
|
||||
app.getFeeTrack(),
|
||||
getAPIVersionNumber(params, app.config().betaRpcApi));
|
||||
txJson, role, verify, validatedLedgerAge, app.config(), app.getFeeTrack(), apiVersion);
|
||||
|
||||
if (rpc::containsError(txJsonResult))
|
||||
return std::move(txJsonResult);
|
||||
@@ -1025,7 +1034,7 @@ transactionSign(
|
||||
// Add and amend fields based on the transaction type.
|
||||
SigningForParams signForParams;
|
||||
TransactionPreProcessResult const preprocResult = transactionPreProcessImpl(
|
||||
jvRequest, role, signForParams, validatedLedgerAge, app, ledger->rules());
|
||||
jvRequest, apiVersion, role, signForParams, validatedLedgerAge, app, ledger->rules());
|
||||
|
||||
if (!preprocResult.second)
|
||||
return preprocResult.first;
|
||||
@@ -1062,7 +1071,7 @@ transactionSubmit(
|
||||
// Add and amend fields based on the transaction type.
|
||||
SigningForParams signForParams;
|
||||
TransactionPreProcessResult const preprocResult = transactionPreProcessImpl(
|
||||
jvRequest, role, signForParams, validatedLedgerAge, app, ledger->rules());
|
||||
jvRequest, apiVersion, role, signForParams, validatedLedgerAge, app, ledger->rules());
|
||||
|
||||
if (!preprocResult.second)
|
||||
return preprocResult.first;
|
||||
@@ -1230,7 +1239,7 @@ transactionSignFor(
|
||||
SigningForParams signForParams(*signerAccountID);
|
||||
|
||||
TransactionPreProcessResult const preprocResult = transactionPreProcessImpl(
|
||||
jvRequest, role, signForParams, validatedLedgerAge, app, ledger->rules());
|
||||
jvRequest, apiVersion, role, signForParams, validatedLedgerAge, app, ledger->rules());
|
||||
|
||||
if (!preprocResult.second)
|
||||
return preprocResult.first;
|
||||
@@ -1317,13 +1326,7 @@ transactionSubmitMultiSigned(
|
||||
json::Value& txJson(jvRequest["tx_json"]);
|
||||
|
||||
auto [txJsonResult, srcAddressID] = checkTxJsonFields(
|
||||
txJson,
|
||||
role,
|
||||
true,
|
||||
validatedLedgerAge,
|
||||
app.config(),
|
||||
app.getFeeTrack(),
|
||||
getAPIVersionNumber(jvRequest, app.config().betaRpcApi));
|
||||
txJson, role, true, validatedLedgerAge, app.config(), app.getFeeTrack(), apiVersion);
|
||||
|
||||
if (rpc::containsError(txJsonResult))
|
||||
return std::move(txJsonResult);
|
||||
|
||||
@@ -71,7 +71,7 @@ parseObjectID(
|
||||
{
|
||||
return *uNodeIndex;
|
||||
}
|
||||
return ledger_entry_helpers::invalidFieldError("malformedRequest", fieldName, expectedType);
|
||||
return ledger_entry_helpers::invalidFieldError(RpcMalformedRequest, fieldName, expectedType);
|
||||
}
|
||||
|
||||
static std::expected<UInt256, json::Value>
|
||||
@@ -107,7 +107,7 @@ parseAccountRoot(
|
||||
return keylet::account(*account).key;
|
||||
}
|
||||
|
||||
return ledger_entry_helpers::invalidFieldError("malformedAddress", fieldName, "AccountID");
|
||||
return ledger_entry_helpers::invalidFieldError(RpcMalformedAddress, fieldName, "AccountID");
|
||||
}
|
||||
|
||||
auto const parseAmendments = fixed(keylet::amendments());
|
||||
@@ -129,12 +129,12 @@ parseAMM(
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
|
||||
auto const asset = ledger_entry_helpers::requiredAsset(params, jss::asset, "malformedRequest");
|
||||
auto const asset = ledger_entry_helpers::requiredAsset(params, jss::asset, RpcMalformedRequest);
|
||||
if (!asset)
|
||||
return std::unexpected(asset.error());
|
||||
|
||||
auto const asset2 =
|
||||
ledger_entry_helpers::requiredAsset(params, jss::asset2, "malformedRequest");
|
||||
ledger_entry_helpers::requiredAsset(params, jss::asset2, RpcMalformedRequest);
|
||||
if (!asset2)
|
||||
return std::unexpected(asset2.error());
|
||||
|
||||
@@ -162,14 +162,14 @@ parseBridge(
|
||||
return std::unexpected(bridge.error());
|
||||
|
||||
auto const account = ledger_entry_helpers::requiredAccountID(
|
||||
params, jss::bridge_account, "malformedBridgeAccount");
|
||||
params, jss::bridge_account, RpcMalformedBridgeAccount);
|
||||
if (!account)
|
||||
return std::unexpected(account.error());
|
||||
|
||||
STXChainBridge::ChainType const chainType =
|
||||
STXChainBridge::srcChain(account.value() == bridge->lockingChainDoor());
|
||||
if (account.value() != bridge->door(chainType))
|
||||
return ledger_entry_helpers::malformedError("malformedRequest", "");
|
||||
return ledger_entry_helpers::malformedError(RpcMalformedRequest, "");
|
||||
|
||||
return keylet::bridge(*bridge, chainType).key;
|
||||
}
|
||||
@@ -186,12 +186,12 @@ parseCheck(
|
||||
}
|
||||
|
||||
auto const account =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, RpcMalformedAddress);
|
||||
if (!account)
|
||||
return std::unexpected(account.error());
|
||||
|
||||
auto const sequence =
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest");
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::seq, RpcMalformedRequest);
|
||||
if (!sequence)
|
||||
return std::unexpected(sequence.error());
|
||||
|
||||
@@ -211,17 +211,17 @@ parseCredential(
|
||||
}
|
||||
|
||||
auto const subject =
|
||||
ledger_entry_helpers::requiredAccountID(cred, jss::subject, "malformedRequest");
|
||||
ledger_entry_helpers::requiredAccountID(cred, jss::subject, RpcMalformedRequest);
|
||||
if (!subject)
|
||||
return std::unexpected(subject.error());
|
||||
|
||||
auto const issuer =
|
||||
ledger_entry_helpers::requiredAccountID(cred, jss::issuer, "malformedRequest");
|
||||
ledger_entry_helpers::requiredAccountID(cred, jss::issuer, RpcMalformedRequest);
|
||||
if (!issuer)
|
||||
return std::unexpected(issuer.error());
|
||||
|
||||
auto const credType = ledger_entry_helpers::requiredHexBlob(
|
||||
cred, jss::credential_type, kMaxCredentialTypeLength, "malformedRequest");
|
||||
cred, jss::credential_type, kMaxCredentialTypeLength, RpcMalformedRequest);
|
||||
if (!credType)
|
||||
return std::unexpected(credType.error());
|
||||
|
||||
@@ -240,12 +240,12 @@ parseDelegate(
|
||||
}
|
||||
|
||||
auto const account =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, RpcMalformedAddress);
|
||||
if (!account)
|
||||
return std::unexpected(account.error());
|
||||
|
||||
auto const authorize =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::authorize, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::authorize, RpcMalformedAddress);
|
||||
if (!authorize)
|
||||
return std::unexpected(authorize.error());
|
||||
|
||||
@@ -258,7 +258,7 @@ parseAuthorizeCredentials(json::Value const& jv)
|
||||
if (!jv.isArray())
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedAuthorizedCredentials", jss::authorized_credentials, "array");
|
||||
RpcMalformedAuthorizedCredentials, jss::authorized_credentials, "array");
|
||||
}
|
||||
|
||||
std::uint32_t const n = jv.size();
|
||||
@@ -266,7 +266,7 @@ parseAuthorizeCredentials(json::Value const& jv)
|
||||
{
|
||||
return std::unexpected(
|
||||
ledger_entry_helpers::malformedError(
|
||||
"malformedAuthorizedCredentials",
|
||||
RpcMalformedAuthorizedCredentials,
|
||||
"Invalid field '" + std::string(jss::authorized_credentials) +
|
||||
"', array too long."));
|
||||
}
|
||||
@@ -275,7 +275,7 @@ parseAuthorizeCredentials(json::Value const& jv)
|
||||
{
|
||||
return std::unexpected(
|
||||
ledger_entry_helpers::malformedError(
|
||||
"malformedAuthorizedCredentials",
|
||||
RpcMalformedAuthorizedCredentials,
|
||||
"Invalid field '" + std::string(jss::authorized_credentials) + "', array empty."));
|
||||
}
|
||||
|
||||
@@ -285,23 +285,23 @@ parseAuthorizeCredentials(json::Value const& jv)
|
||||
if (!jo.isObject())
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedAuthorizedCredentials", jss::authorized_credentials, "array of objects");
|
||||
RpcMalformedAuthorizedCredentials, jss::authorized_credentials, "array of objects");
|
||||
}
|
||||
|
||||
if (auto const value = ledger_entry_helpers::hasRequired(
|
||||
jo, {jss::issuer, jss::credential_type}, "malformedAuthorizedCredentials");
|
||||
jo, {jss::issuer, jss::credential_type}, RpcMalformedAuthorizedCredentials);
|
||||
!value)
|
||||
{
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
|
||||
auto const issuer = ledger_entry_helpers::requiredAccountID(
|
||||
jo, jss::issuer, "malformedAuthorizedCredentials");
|
||||
jo, jss::issuer, RpcMalformedAuthorizedCredentials);
|
||||
if (!issuer)
|
||||
return std::unexpected(issuer.error());
|
||||
|
||||
auto const credentialType = ledger_entry_helpers::requiredHexBlob(
|
||||
jo, jss::credential_type, kMaxCredentialTypeLength, "malformedAuthorizedCredentials");
|
||||
jo, jss::credential_type, kMaxCredentialTypeLength, RpcMalformedAuthorizedCredentials);
|
||||
if (!credentialType)
|
||||
return std::unexpected(credentialType.error());
|
||||
|
||||
@@ -328,12 +328,12 @@ parseDepositPreauth(
|
||||
if ((dp.isMember(jss::authorized) == dp.isMember(jss::authorized_credentials)))
|
||||
{
|
||||
return ledger_entry_helpers::malformedError(
|
||||
"malformedRequest",
|
||||
RpcMalformedRequest,
|
||||
"Must have exactly one of `authorized` and "
|
||||
"`authorized_credentials`.");
|
||||
}
|
||||
|
||||
auto const owner = ledger_entry_helpers::requiredAccountID(dp, jss::owner, "malformedOwner");
|
||||
auto const owner = ledger_entry_helpers::requiredAccountID(dp, jss::owner, RpcMalformedOwner);
|
||||
if (!owner)
|
||||
{
|
||||
return std::unexpected(owner.error());
|
||||
@@ -346,7 +346,7 @@ parseDepositPreauth(
|
||||
return keylet::depositPreauth(*owner, *authorized).key;
|
||||
}
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedAuthorized", jss::authorized, "AccountID");
|
||||
RpcMalformedAuthorized, jss::authorized, "AccountID");
|
||||
}
|
||||
|
||||
auto const& ac(dp[jss::authorized_credentials]);
|
||||
@@ -359,7 +359,7 @@ parseDepositPreauth(
|
||||
{
|
||||
// TODO: this error message is bad/inaccurate
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedAuthorizedCredentials", jss::authorized_credentials, "array");
|
||||
RpcMalformedAuthorizedCredentials, jss::authorized_credentials, "array");
|
||||
}
|
||||
|
||||
return keylet::depositPreauth(*owner, sorted).key;
|
||||
@@ -374,7 +374,7 @@ parseDID(
|
||||
auto const account = ledger_entry_helpers::parse<AccountID>(params);
|
||||
if (!account)
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError("malformedAddress", fieldName, "AccountID");
|
||||
return ledger_entry_helpers::invalidFieldError(RpcMalformedAddress, fieldName, "AccountID");
|
||||
}
|
||||
|
||||
return keylet::did(*account).key;
|
||||
@@ -396,13 +396,13 @@ parseDirectoryNode(
|
||||
params[jss::sub_index].isBool()))
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedRequest", jss::sub_index, "number");
|
||||
RpcMalformedRequest, jss::sub_index, "number");
|
||||
}
|
||||
|
||||
if (params.isMember(jss::owner) == params.isMember(jss::dir_root))
|
||||
{
|
||||
return ledger_entry_helpers::malformedError(
|
||||
"malformedRequest", "Must have exactly one of `owner` and `dir_root` fields.");
|
||||
RpcMalformedRequest, "Must have exactly one of `owner` and `dir_root` fields.");
|
||||
}
|
||||
|
||||
std::uint64_t const uSubIndex = params.get(jss::sub_index, 0).asUInt();
|
||||
@@ -414,22 +414,19 @@ parseDirectoryNode(
|
||||
return keylet::page(*uDirRoot, uSubIndex).key;
|
||||
}
|
||||
|
||||
return ledger_entry_helpers::invalidFieldError("malformedDirRoot", jss::dir_root, "hash");
|
||||
return ledger_entry_helpers::invalidFieldError(RpcMalformedDirRoot, jss::dir_root, "hash");
|
||||
}
|
||||
|
||||
if (params.isMember(jss::owner))
|
||||
// Exactly one of `owner` and `dir_root` is present, checked above, and the `dir_root` arm
|
||||
// returned, so this is the `owner` arm.
|
||||
auto const ownerID = ledger_entry_helpers::parse<AccountID>(params[jss::owner]);
|
||||
if (!ownerID)
|
||||
{
|
||||
auto const ownerID = ledger_entry_helpers::parse<AccountID>(params[jss::owner]);
|
||||
if (!ownerID)
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedAddress", jss::owner, "AccountID");
|
||||
}
|
||||
|
||||
return keylet::page(keylet::ownerDir(*ownerID), uSubIndex).key;
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
RpcMalformedAddress, jss::owner, "AccountID");
|
||||
}
|
||||
|
||||
return ledger_entry_helpers::malformedError("malformedRequest", "");
|
||||
return keylet::page(keylet::ownerDir(*ownerID), uSubIndex).key;
|
||||
}
|
||||
|
||||
static std::expected<UInt256, json::Value>
|
||||
@@ -443,10 +440,10 @@ parseEscrow(
|
||||
return parseObjectID(params, fieldName);
|
||||
}
|
||||
|
||||
auto const id = ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner");
|
||||
auto const id = ledger_entry_helpers::requiredAccountID(params, jss::owner, RpcMalformedOwner);
|
||||
if (!id)
|
||||
return std::unexpected(id.error());
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedSeq");
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, RpcMalformedSeq);
|
||||
if (!seq)
|
||||
return std::unexpected(seq.error());
|
||||
|
||||
@@ -469,7 +466,7 @@ parseFixed(
|
||||
}
|
||||
if (!params.asBool())
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError("invalidParams", fieldName, "true");
|
||||
return ledger_entry_helpers::invalidFieldError(RpcInvalidParams, fieldName, "true");
|
||||
}
|
||||
|
||||
return keylet.key;
|
||||
@@ -506,10 +503,10 @@ parseLoanBroker(
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
|
||||
auto const id = ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner");
|
||||
auto const id = ledger_entry_helpers::requiredAccountID(params, jss::owner, RpcMalformedOwner);
|
||||
if (!id)
|
||||
return std::unexpected(id.error());
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedSeq");
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, RpcMalformedSeq);
|
||||
if (!seq)
|
||||
return std::unexpected(seq.error());
|
||||
|
||||
@@ -529,10 +526,10 @@ parseLoan(
|
||||
}
|
||||
|
||||
auto const id =
|
||||
ledger_entry_helpers::requiredUInt256(params, jss::loan_broker_id, "malformedBroker");
|
||||
ledger_entry_helpers::requiredUInt256(params, jss::loan_broker_id, RpcMalformedBroker);
|
||||
if (!id)
|
||||
return std::unexpected(id.error());
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::loan_seq, "malformedSeq");
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::loan_seq, RpcMalformedSeq);
|
||||
if (!seq)
|
||||
return std::unexpected(seq.error());
|
||||
|
||||
@@ -552,12 +549,12 @@ parseMPToken(
|
||||
}
|
||||
|
||||
auto const mptIssuanceID = ledger_entry_helpers::requiredUInt192(
|
||||
params, jss::mpt_issuance_id, "malformedMPTIssuanceID");
|
||||
params, jss::mpt_issuance_id, RpcMalformedMPTIssuanceID);
|
||||
if (!mptIssuanceID)
|
||||
return std::unexpected(mptIssuanceID.error());
|
||||
|
||||
auto const account =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAccount");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, RpcMalformedAccount);
|
||||
if (!account)
|
||||
return std::unexpected(account.error());
|
||||
|
||||
@@ -574,7 +571,7 @@ parseMPTokenIssuance(
|
||||
if (!mptIssuanceID)
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedMPTokenIssuance", fieldName, "Hash192");
|
||||
RpcMalformedMPTokenIssuance, fieldName, "Hash192");
|
||||
}
|
||||
|
||||
return keylet::mptokenIssuance(*mptIssuanceID).key;
|
||||
@@ -592,12 +589,12 @@ parseNFTokenOffer(
|
||||
}
|
||||
|
||||
auto const owner =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::owner, RpcMalformedOwner);
|
||||
if (!owner)
|
||||
return std::unexpected(owner.error());
|
||||
|
||||
auto const sequence =
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest");
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::seq, RpcMalformedRequest);
|
||||
if (!sequence)
|
||||
return std::unexpected(sequence.error());
|
||||
|
||||
@@ -628,11 +625,11 @@ parseOffer(
|
||||
}
|
||||
|
||||
auto const id =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, RpcMalformedAddress);
|
||||
if (!id)
|
||||
return std::unexpected(id.error());
|
||||
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest");
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, RpcMalformedRequest);
|
||||
if (!seq)
|
||||
return std::unexpected(seq.error());
|
||||
|
||||
@@ -652,12 +649,12 @@ parseOracle(
|
||||
}
|
||||
|
||||
auto const id =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAccount");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, RpcMalformedAccount);
|
||||
if (!id)
|
||||
return std::unexpected(id.error());
|
||||
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(
|
||||
params, jss::oracle_document_id, "malformedDocumentID");
|
||||
params, jss::oracle_document_id, RpcMalformedDocumentID);
|
||||
if (!seq)
|
||||
return std::unexpected(seq.error());
|
||||
|
||||
@@ -676,17 +673,17 @@ parsePayChannel(
|
||||
}
|
||||
|
||||
auto const account =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, RpcMalformedAddress);
|
||||
if (!account)
|
||||
return std::unexpected(account.error());
|
||||
|
||||
auto const destination =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::destination, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::destination, RpcMalformedAddress);
|
||||
if (!destination)
|
||||
return std::unexpected(destination.error());
|
||||
|
||||
auto const sequence =
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest");
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::seq, RpcMalformedRequest);
|
||||
if (!sequence)
|
||||
return std::unexpected(sequence.error());
|
||||
|
||||
@@ -708,15 +705,15 @@ parsePermissionedDomain(
|
||||
if (!pd.isObject())
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedRequest", fieldName, "hex string or object");
|
||||
RpcMalformedRequest, fieldName, "hex string or object");
|
||||
}
|
||||
|
||||
auto const account =
|
||||
ledger_entry_helpers::requiredAccountID(pd, jss::account, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(pd, jss::account, RpcMalformedAddress);
|
||||
if (!account)
|
||||
return std::unexpected(account.error());
|
||||
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(pd, jss::seq, "malformedRequest");
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(pd, jss::seq, RpcMalformedRequest);
|
||||
if (!seq)
|
||||
return std::unexpected(seq.error());
|
||||
|
||||
@@ -747,7 +744,7 @@ parseRippleState(
|
||||
if (!jvRippleState[jss::accounts].isArray() || jvRippleState[jss::accounts].size() != 2)
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedRequest", jss::accounts, "length-2 array of Accounts");
|
||||
RpcMalformedRequest, jss::accounts, "length-2 array of Accounts");
|
||||
}
|
||||
|
||||
auto const id1 = ledger_entry_helpers::parse<AccountID>(jvRippleState[jss::accounts][0u]);
|
||||
@@ -755,19 +752,19 @@ parseRippleState(
|
||||
if (!id1 || !id2)
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedAddress", jss::accounts, "array of Accounts");
|
||||
RpcMalformedAddress, jss::accounts, "array of Accounts");
|
||||
}
|
||||
if (id1 == id2)
|
||||
{
|
||||
return ledger_entry_helpers::malformedError(
|
||||
"malformedRequest", "Cannot have a trustline to self.");
|
||||
RpcMalformedRequest, "Cannot have a trustline to self.");
|
||||
}
|
||||
|
||||
if (!jvRippleState[jss::currency].isString() || jvRippleState[jss::currency] == "" ||
|
||||
!toCurrency(uCurrency, jvRippleState[jss::currency].asString()))
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedCurrency", jss::currency, "Currency");
|
||||
RpcMalformedCurrency, jss::currency, "Currency");
|
||||
}
|
||||
|
||||
return keylet::trustLine(*id1, *id2, uCurrency).key;
|
||||
@@ -785,7 +782,7 @@ parseSignerList(
|
||||
}
|
||||
|
||||
auto const id =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, RpcMalformedAddress);
|
||||
if (!id)
|
||||
return std::unexpected(id.error());
|
||||
|
||||
@@ -802,12 +799,12 @@ parseSponsorship(
|
||||
return parseObjectID(params, fieldName);
|
||||
|
||||
auto const sponsorID =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::sponsor, "malformedSponsor");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::sponsor, RpcMalformedSponsor);
|
||||
if (!sponsorID)
|
||||
return std::unexpected(sponsorID.error());
|
||||
|
||||
auto const sponseeID =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::sponsee, "malformedSponsee");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::sponsee, RpcMalformedSponsee);
|
||||
if (!sponseeID)
|
||||
return std::unexpected(sponseeID.error());
|
||||
|
||||
@@ -829,14 +826,14 @@ parseTransactionProposal(
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
|
||||
auto const targetID =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, RpcMalformedAddress);
|
||||
if (!targetID)
|
||||
return std::unexpected(targetID.error());
|
||||
|
||||
// The proposed transaction's TicketSequence (a proposed transaction is
|
||||
// ticket-only), mirroring how parseTicket looks up a Ticket object.
|
||||
auto const ticketSequence =
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::ticket_seq, "malformedRequest");
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::ticket_seq, RpcMalformedRequest);
|
||||
if (!ticketSequence)
|
||||
return std::unexpected(ticketSequence.error());
|
||||
|
||||
@@ -855,12 +852,12 @@ parseTicket(
|
||||
}
|
||||
|
||||
auto const id =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, RpcMalformedAddress);
|
||||
if (!id)
|
||||
return std::unexpected(id.error());
|
||||
|
||||
auto const seq =
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::ticket_seq, "malformedRequest");
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::ticket_seq, RpcMalformedRequest);
|
||||
if (!seq)
|
||||
return std::unexpected(seq.error());
|
||||
|
||||
@@ -879,11 +876,11 @@ parseVault(
|
||||
return parseObjectID(params, fieldName);
|
||||
}
|
||||
|
||||
auto const id = ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner");
|
||||
auto const id = ledger_entry_helpers::requiredAccountID(params, jss::owner, RpcMalformedOwner);
|
||||
if (!id)
|
||||
return std::unexpected(id.error());
|
||||
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest");
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, RpcMalformedRequest);
|
||||
if (!seq)
|
||||
return std::unexpected(seq.error());
|
||||
|
||||
@@ -907,7 +904,7 @@ parseXChainOwnedClaimID(
|
||||
return std::unexpected(bridgeSpec.error());
|
||||
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(
|
||||
claimId, jss::xchain_owned_claim_id, "malformedXChainOwnedClaimID");
|
||||
claimId, jss::xchain_owned_claim_id, RpcMalformedXChainOwnedClaimID);
|
||||
if (!seq)
|
||||
{
|
||||
return std::unexpected(seq.error());
|
||||
@@ -935,7 +932,7 @@ parseXChainOwnedCreateAccountClaimID(
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(
|
||||
claimId,
|
||||
jss::xchain_owned_create_account_claim_id,
|
||||
"malformedXChainOwnedCreateAccountClaimID");
|
||||
RpcMalformedXChainOwnedCreateAccountClaimID);
|
||||
if (!seq)
|
||||
{
|
||||
return std::unexpected(seq.error());
|
||||
|
||||
@@ -24,50 +24,90 @@
|
||||
|
||||
namespace xrpl::ledger_entry_helpers {
|
||||
|
||||
// These helpers name the malformed field in the `error` token and report `invalidParams` as the
|
||||
// code, whatever the token is. A client has read 31 for every one of these tokens for years, so the
|
||||
// code is deliberately not derived from the token here.
|
||||
/**
|
||||
* Reports @p field as absent, under the token @p code names.
|
||||
*
|
||||
* `error_code` is `invalidParams` (31) whatever @p code is: clients match on 31
|
||||
* for these tokens, so a token whose own code differs disagrees with the code
|
||||
* beside it, and reconciling the two is the reply envelope's concern.
|
||||
*
|
||||
* @param field The field the request did not name.
|
||||
* @param code The code whose token names the field.
|
||||
* @return The error, as an unexpected value.
|
||||
*/
|
||||
inline std::unexpected<json::Value>
|
||||
missingFieldError(json::StaticString const field, std::optional<std::string> err = std::nullopt)
|
||||
missingFieldError(json::StaticString const field, ErrorCodeI code = RpcMalformedRequest)
|
||||
{
|
||||
json::Value json = json::ValueType::Object;
|
||||
json[jss::error] = err.value_or("malformedRequest");
|
||||
json[jss::error] = rpc::getErrorInfo(code).token;
|
||||
json[jss::error_code] = RpcInvalidParams;
|
||||
json[jss::error_message] = rpc::missingFieldMessage(std::string(field.cStr()));
|
||||
return std::unexpected(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports @p field as holding something that is not @p type, under the token
|
||||
* @p code names.
|
||||
*
|
||||
* Reports `invalidParams` as the code, for the reason missingFieldError gives.
|
||||
*
|
||||
* @param code The code whose token names the field.
|
||||
* @param field The field whose value is wrong.
|
||||
* @param type The type the field was expected to hold, named in the message.
|
||||
* @return The error, as an unexpected value.
|
||||
*/
|
||||
inline std::unexpected<json::Value>
|
||||
invalidFieldError(std::string const& err, json::StaticString const field, std::string const& type)
|
||||
invalidFieldError(ErrorCodeI code, json::StaticString const field, std::string const& type)
|
||||
{
|
||||
json::Value json = json::ValueType::Object;
|
||||
json[jss::error] = err;
|
||||
json[jss::error] = rpc::getErrorInfo(code).token;
|
||||
json[jss::error_code] = RpcInvalidParams;
|
||||
json[jss::error_message] = rpc::expectedFieldMessage(field, type);
|
||||
return std::unexpected(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports @p message under the token @p code names.
|
||||
*
|
||||
* For a value of the right type that is wrong in a way only the caller can
|
||||
* state. Reports `invalidParams` as the code, for the reason
|
||||
* missingFieldError gives.
|
||||
*
|
||||
* @param code The code whose token names the error. Callers pass a field's
|
||||
* token or a condition's, such as a trust line to self.
|
||||
* @param message What is wrong.
|
||||
* @return The error, as an unexpected value.
|
||||
*/
|
||||
inline std::unexpected<json::Value>
|
||||
malformedError(std::string const& err, std::string const& message)
|
||||
malformedError(ErrorCodeI code, std::string const& message)
|
||||
{
|
||||
json::Value json = json::ValueType::Object;
|
||||
json[jss::error] = err;
|
||||
json[jss::error] = rpc::getErrorInfo(code).token;
|
||||
json[jss::error_code] = RpcInvalidParams;
|
||||
json[jss::error_message] = message;
|
||||
return std::unexpected(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the first of @p fields the request does not name, if any.
|
||||
*
|
||||
* @param params The request's parameters.
|
||||
* @param fields The fields the request must name.
|
||||
* @param code The code whose token names a missing field. Defaults to
|
||||
* `RpcMalformedRequest`, as missingFieldError's does.
|
||||
* @return true if every field is named, otherwise the error.
|
||||
*/
|
||||
inline std::expected<bool, json::Value>
|
||||
hasRequired(
|
||||
json::Value const& params,
|
||||
std::initializer_list<json::StaticString> fields,
|
||||
std::optional<std::string> err = std::nullopt)
|
||||
ErrorCodeI code = RpcMalformedRequest)
|
||||
{
|
||||
for (auto const field : fields)
|
||||
{
|
||||
if (!params.isMember(field) || params[field].isNull())
|
||||
{
|
||||
return missingFieldError(field, err);
|
||||
return missingFieldError(field, code);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -77,12 +117,25 @@ template <class T>
|
||||
std::optional<T>
|
||||
parse(json::Value const& param);
|
||||
|
||||
/**
|
||||
* Reads @p fieldName out of @p params, parsed as T.
|
||||
*
|
||||
* Absent or null is a missing field; present but unparsable is an invalid one
|
||||
* of @p expectedType.
|
||||
*
|
||||
* @param params The request parameters.
|
||||
* @param fieldName The field to read.
|
||||
* @param code The code whose token names the field in an error. `error_code`
|
||||
* stays `invalidParams` whatever it is.
|
||||
* @param expectedType The type named in an invalid-field message.
|
||||
* @return The parsed value, or the error to report.
|
||||
*/
|
||||
template <class T>
|
||||
std::expected<T, json::Value>
|
||||
required(
|
||||
json::Value const& params,
|
||||
json::StaticString const fieldName,
|
||||
std::string const& err,
|
||||
ErrorCodeI code,
|
||||
std::string const& expectedType)
|
||||
{
|
||||
if (!params.isMember(fieldName) || params[fieldName].isNull())
|
||||
@@ -93,7 +146,7 @@ required(
|
||||
{
|
||||
return *obj;
|
||||
}
|
||||
return invalidFieldError(err, fieldName, expectedType);
|
||||
return invalidFieldError(code, fieldName, expectedType);
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -112,13 +165,14 @@ parse(json::Value const& param)
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param code The code whose token names the field in an error. `error_code`
|
||||
* stays `invalidParams` whatever it is.
|
||||
*/
|
||||
inline std::expected<AccountID, json::Value>
|
||||
requiredAccountID(
|
||||
json::Value const& params,
|
||||
json::StaticString const fieldName,
|
||||
std::string const& err)
|
||||
requiredAccountID(json::Value const& params, json::StaticString const fieldName, ErrorCodeI code)
|
||||
{
|
||||
return required<AccountID>(params, fieldName, err, "AccountID");
|
||||
return required<AccountID>(params, fieldName, code, "AccountID");
|
||||
}
|
||||
|
||||
inline std::optional<Blob>
|
||||
@@ -134,12 +188,16 @@ parseHexBlob(json::Value const& param, std::size_t maxLength)
|
||||
return blob;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param code The code whose token names the field in an error. `error_code`
|
||||
* stays `invalidParams` whatever it is.
|
||||
*/
|
||||
inline std::expected<Blob, json::Value>
|
||||
requiredHexBlob(
|
||||
json::Value const& params,
|
||||
json::StaticString const fieldName,
|
||||
std::size_t maxLength,
|
||||
std::string const& err)
|
||||
ErrorCodeI code)
|
||||
{
|
||||
if (!params.isMember(fieldName) || params[fieldName].isNull())
|
||||
{
|
||||
@@ -149,7 +207,7 @@ requiredHexBlob(
|
||||
{
|
||||
return *blob;
|
||||
}
|
||||
return invalidFieldError(err, fieldName, "hex string");
|
||||
return invalidFieldError(code, fieldName, "hex string");
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -169,13 +227,14 @@ parse(json::Value const& param)
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param code The code whose token names the field in an error. `error_code`
|
||||
* stays `invalidParams` whatever it is.
|
||||
*/
|
||||
inline std::expected<std::uint32_t, json::Value>
|
||||
requiredUInt32(
|
||||
json::Value const& params,
|
||||
json::StaticString const fieldName,
|
||||
std::string const& err)
|
||||
requiredUInt32(json::Value const& params, json::StaticString const fieldName, ErrorCodeI code)
|
||||
{
|
||||
return required<std::uint32_t>(params, fieldName, err, "number");
|
||||
return required<std::uint32_t>(params, fieldName, code, "number");
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -191,13 +250,14 @@ parse(json::Value const& param)
|
||||
return uNodeIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param code The code whose token names the field in an error. `error_code`
|
||||
* stays `invalidParams` whatever it is.
|
||||
*/
|
||||
inline std::expected<UInt256, json::Value>
|
||||
requiredUInt256(
|
||||
json::Value const& params,
|
||||
json::StaticString const fieldName,
|
||||
std::string const& err)
|
||||
requiredUInt256(json::Value const& params, json::StaticString const fieldName, ErrorCodeI code)
|
||||
{
|
||||
return required<UInt256>(params, fieldName, err, "Hash256");
|
||||
return required<UInt256>(params, fieldName, code, "Hash256");
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -213,13 +273,14 @@ parse(json::Value const& param)
|
||||
return field;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param code The code whose token names the field in an error. `error_code`
|
||||
* stays `invalidParams` whatever it is.
|
||||
*/
|
||||
inline std::expected<UInt192, json::Value>
|
||||
requiredUInt192(
|
||||
json::Value const& params,
|
||||
json::StaticString const fieldName,
|
||||
std::string const& err)
|
||||
requiredUInt192(json::Value const& params, json::StaticString const fieldName, ErrorCodeI code)
|
||||
{
|
||||
return required<UInt192>(params, fieldName, err, "Hash192");
|
||||
return required<UInt192>(params, fieldName, code, "Hash192");
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -236,12 +297,24 @@ parse(json::Value const& param)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param code The code whose token names the field in an error. `error_code`
|
||||
* stays `invalidParams` whatever it is.
|
||||
*/
|
||||
inline std::expected<Asset, json::Value>
|
||||
requiredAsset(json::Value const& params, json::StaticString const fieldName, std::string const& err)
|
||||
requiredAsset(json::Value const& params, json::StaticString const fieldName, ErrorCodeI code)
|
||||
{
|
||||
return required<Asset>(params, fieldName, err, "Asset");
|
||||
return required<Asset>(params, fieldName, code, "Asset");
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the bridge the four `*ChainDoor` and `*ChainIssue` fields of @p params
|
||||
* describe.
|
||||
*
|
||||
* @param params The request object naming the four fields.
|
||||
* @return The bridge, or the error of the first field that is missing or
|
||||
* malformed.
|
||||
*/
|
||||
inline std::expected<STXChainBridge, json::Value>
|
||||
parseBridgeFields(json::Value const& params)
|
||||
{
|
||||
@@ -259,14 +332,14 @@ parseBridgeFields(json::Value const& params)
|
||||
}
|
||||
|
||||
auto const lockingChainDoor =
|
||||
requiredAccountID(params, jss::LockingChainDoor, "malformedLockingChainDoor");
|
||||
requiredAccountID(params, jss::LockingChainDoor, RpcMalformedLockingChainDoor);
|
||||
if (!lockingChainDoor)
|
||||
{
|
||||
return std::unexpected(lockingChainDoor.error());
|
||||
}
|
||||
|
||||
auto const issuingChainDoor =
|
||||
requiredAccountID(params, jss::IssuingChainDoor, "malformedIssuingChainDoor");
|
||||
requiredAccountID(params, jss::IssuingChainDoor, RpcMalformedIssuingChainDoor);
|
||||
if (!issuingChainDoor)
|
||||
{
|
||||
return std::unexpected(issuingChainDoor.error());
|
||||
@@ -279,7 +352,7 @@ parseBridgeFields(json::Value const& params)
|
||||
}
|
||||
catch (std::runtime_error const& ex)
|
||||
{
|
||||
return invalidFieldError("malformedIssue", jss::LockingChainIssue, "Issue");
|
||||
return invalidFieldError(RpcMalformedIssue, jss::LockingChainIssue, "Issue");
|
||||
}
|
||||
|
||||
Issue issuingChainIssue;
|
||||
@@ -289,7 +362,7 @@ parseBridgeFields(json::Value const& params)
|
||||
}
|
||||
catch (std::runtime_error const& ex)
|
||||
{
|
||||
return invalidFieldError("malformedIssue", jss::IssuingChainIssue, "Issue");
|
||||
return invalidFieldError(RpcMalformedIssue, jss::IssuingChainIssue, "Issue");
|
||||
}
|
||||
|
||||
return STXChainBridge(
|
||||
|
||||
@@ -12,6 +12,16 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Serves `path_find`: `create` opens a path request that pushes updates to the
|
||||
* connection, recording the API version this request named on it, `close` ends
|
||||
* the connection's open request and `status` reports it.
|
||||
*
|
||||
* @param context The request and the connection it arrived on.
|
||||
* @return The subcommand's result, or an error when pathfinding is disabled,
|
||||
* the subcommand is missing or unknown, no connection can receive
|
||||
* updates, or there is no open request to close or report.
|
||||
*/
|
||||
json::Value
|
||||
doPathFind(rpc::JsonContext& context)
|
||||
{
|
||||
@@ -28,8 +38,6 @@ doPathFind(rpc::JsonContext& context)
|
||||
if (!context.infoSub)
|
||||
return rpcError(RpcNoEvents);
|
||||
|
||||
context.infoSub->setApiVersion(context.apiVersion);
|
||||
|
||||
auto const sSubCommand = context.params[jss::subcommand].asString();
|
||||
|
||||
if (sSubCommand == "create")
|
||||
@@ -37,7 +45,7 @@ doPathFind(rpc::JsonContext& context)
|
||||
context.loadType = resource::kFeeHeavyBurdenRpc;
|
||||
context.infoSub->clearRequest();
|
||||
return context.app.getPathRequestManager().makePathRequest(
|
||||
context.infoSub, lpLedger, context.params);
|
||||
context.infoSub, lpLedger, context.params, context.apiVersion);
|
||||
}
|
||||
|
||||
if (sSubCommand == "close")
|
||||
|
||||
@@ -46,6 +46,18 @@ wouldExceedSubscriptionCap(InfoSub::Ref ispSub, std::size_t additional, std::siz
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* Registers the streams, accounts, books and MPT issuances @p context names
|
||||
* for the connection the request arrived on, or for the webhook url it names.
|
||||
*
|
||||
* Every subscription one subscriber holds is served at one API version, so a
|
||||
* request naming a version other than the one already established is refused
|
||||
* with `apiVersionConflict` before anything is registered.
|
||||
*
|
||||
* @param context The request, its connection and the server it runs on.
|
||||
* @return The registrations' first messages, for the streams that send one, or
|
||||
* an error.
|
||||
*/
|
||||
json::Value
|
||||
doSubscribe(rpc::JsonContext& context)
|
||||
{
|
||||
@@ -59,6 +71,29 @@ doSubscribe(rpc::JsonContext& context)
|
||||
return rpcError(RpcInvalidParams);
|
||||
}
|
||||
|
||||
// Every subscription on a subscriber is served at one API version; see
|
||||
// establishSubscriptionVersion. Checked before any stream is registered, so a refused call
|
||||
// still fixes the version, which is why the message says "served at". An ordinary request is
|
||||
// answered at its own version.
|
||||
auto const refuseVersionConflict = [&] -> std::optional<json::Value> {
|
||||
if (auto const established = ispSub->establishSubscriptionVersion(context.apiVersion))
|
||||
{
|
||||
// The url message states the condition and no remedy: the subscriber is keyed on its
|
||||
// url alone, so releasing it would remove whoever subscribed it first, and the server
|
||||
// cannot tell the two callers apart. The connection message does name its remedy, a
|
||||
// second connection harming nobody.
|
||||
auto const subject = context.params.isMember(jss::url)
|
||||
? "Subscriptions on this url are served at api_version " +
|
||||
std::to_string(*established) + "."
|
||||
: "Subscriptions on this connection are served at api_version " +
|
||||
std::to_string(*established) + "; use a new connection.";
|
||||
|
||||
JLOG(context.j.debug()) << "doSubscribe: " << subject;
|
||||
return rpc::makeError(RpcApiVersionConflict, subject);
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
if (context.params.isMember(jss::url))
|
||||
{
|
||||
if (context.role != Role::ADMIN)
|
||||
@@ -106,6 +141,13 @@ doSubscribe(rpc::JsonContext& context)
|
||||
{
|
||||
JLOG(context.j.trace()) << "doSubscribe: reusing: " << strUrl;
|
||||
|
||||
// Checked before the credentials are written, because this subscriber belongs to
|
||||
// whoever subscribed the url first: a refused subscribe that had already overwritten
|
||||
// their username would leave their endpoint rejecting the stream it still receives,
|
||||
// with nothing to tell them why.
|
||||
if (auto const conflict = refuseVersionConflict())
|
||||
return *conflict;
|
||||
|
||||
if (auto const rpcSub = std::dynamic_pointer_cast<RPCSub>(ispSub))
|
||||
{
|
||||
// Why do we need to check isMember against jss::username and
|
||||
@@ -123,7 +165,9 @@ doSubscribe(rpc::JsonContext& context)
|
||||
{
|
||||
ispSub = context.infoSub;
|
||||
}
|
||||
ispSub->setApiVersion(context.apiVersion);
|
||||
|
||||
if (auto const conflict = refuseVersionConflict())
|
||||
return *conflict;
|
||||
|
||||
// Effective per-connection subscription cap: a configured override if set,
|
||||
// otherwise the built-in default. Resolved once and reused by every branch.
|
||||
@@ -146,43 +190,44 @@ doSubscribe(rpc::JsonContext& context)
|
||||
std::string const streamName = it.asString();
|
||||
if (streamName == "server")
|
||||
{
|
||||
context.netOps.subServer(ispSub, jvResult, context.role == Role::ADMIN);
|
||||
context.netOps.subServer(
|
||||
ispSub, jvResult, context.role == Role::ADMIN, context.apiVersion);
|
||||
}
|
||||
else if (streamName == "ledger")
|
||||
{
|
||||
context.netOps.subLedger(ispSub, jvResult);
|
||||
context.netOps.subLedger(ispSub, jvResult, context.apiVersion);
|
||||
}
|
||||
else if (streamName == "book_changes")
|
||||
{
|
||||
context.netOps.subBookChanges(ispSub);
|
||||
context.netOps.subBookChanges(ispSub, context.apiVersion);
|
||||
}
|
||||
else if (streamName == "manifests")
|
||||
{
|
||||
context.netOps.subManifests(ispSub);
|
||||
context.netOps.subManifests(ispSub, context.apiVersion);
|
||||
}
|
||||
else if (streamName == "transactions")
|
||||
{
|
||||
context.netOps.subTransactions(ispSub);
|
||||
context.netOps.subTransactions(ispSub, context.apiVersion);
|
||||
}
|
||||
else if (
|
||||
streamName == "transactions_proposed" ||
|
||||
streamName == "rt_transactions") // DEPRECATED
|
||||
{
|
||||
context.netOps.subRTTransactions(ispSub);
|
||||
context.netOps.subRTTransactions(ispSub, context.apiVersion);
|
||||
}
|
||||
else if (streamName == "validations")
|
||||
{
|
||||
context.netOps.subValidations(ispSub);
|
||||
context.netOps.subValidations(ispSub, context.apiVersion);
|
||||
}
|
||||
else if (streamName == "peer_status")
|
||||
{
|
||||
if (context.role != Role::ADMIN)
|
||||
return rpcError(RpcNoPermission);
|
||||
context.netOps.subPeerStatus(ispSub);
|
||||
context.netOps.subPeerStatus(ispSub, context.apiVersion);
|
||||
}
|
||||
else if (streamName == "consensus")
|
||||
{
|
||||
context.netOps.subConsensus(ispSub);
|
||||
context.netOps.subConsensus(ispSub, context.apiVersion);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -238,11 +283,11 @@ doSubscribe(rpc::JsonContext& context)
|
||||
}
|
||||
|
||||
if (hasProposed)
|
||||
context.netOps.subAccount(ispSub, proposedIds, true);
|
||||
context.netOps.subAccount(ispSub, proposedIds, true, context.apiVersion);
|
||||
|
||||
if (hasAccounts)
|
||||
{
|
||||
context.netOps.subAccount(ispSub, accountIds, false);
|
||||
context.netOps.subAccount(ispSub, accountIds, false, context.apiVersion);
|
||||
JLOG(context.j.debug()) << "doSubscribe: accounts: " << accountIds.size();
|
||||
}
|
||||
|
||||
@@ -267,7 +312,8 @@ doSubscribe(rpc::JsonContext& context)
|
||||
if (wouldExceedSubscriptionCap(ispSub, historyCharge, subscriptionCap))
|
||||
return rpc::makeParamError("Too many subscriptions for this connection.");
|
||||
|
||||
if (auto const result = context.netOps.subAccountHistory(ispSub, *id); result != RpcSuccess)
|
||||
if (auto const result = context.netOps.subAccountHistory(ispSub, *id, context.apiVersion);
|
||||
result != RpcSuccess)
|
||||
{
|
||||
return rpcError(result);
|
||||
}
|
||||
@@ -337,14 +383,14 @@ doSubscribe(rpc::JsonContext& context)
|
||||
return rpcError(RpcBadMarket);
|
||||
}
|
||||
|
||||
context.netOps.subBook(ispSub, book);
|
||||
context.netOps.subBook(ispSub, book, context.apiVersion);
|
||||
|
||||
// both_sides is deprecated.
|
||||
bool const both = (j.isMember(jss::both) && j[jss::both].asBool()) ||
|
||||
(j.isMember(jss::both_sides) && j[jss::both_sides].asBool());
|
||||
|
||||
if (both)
|
||||
context.netOps.subBook(ispSub, reversed(book));
|
||||
context.netOps.subBook(ispSub, reversed(book), context.apiVersion);
|
||||
|
||||
// state_now is deprecated.
|
||||
if ((j.isMember(jss::snapshot) && j[jss::snapshot].asBool()) ||
|
||||
@@ -409,7 +455,7 @@ doSubscribe(rpc::JsonContext& context)
|
||||
if (!ispSub->tryReserveMPTSubscriptions(ids, subscriptionCap))
|
||||
return rpc::makeParamError("Too many subscriptions for this connection.");
|
||||
|
||||
context.netOps.subMPT(ispSub, ids);
|
||||
context.netOps.subMPT(ispSub, ids, context.apiVersion);
|
||||
JLOG(context.j.debug()) << "doSubscribe: mpts: " << ids.size();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user