Compare commits

...
Author SHA1 Message Date
Bart 2a0c8ba982 feat: Accept a JSON-RPC 2.0 batch from API version 3
A top-level JSON array is the batch form the specification defines, accepted
from API version 3. Each entry is a request in its own right, nesting its
parameters as a lone request does and correlating by its own `id`. The
answer is an array of one reply per entry at HTTP 200, as the `method:
"batch"` form answers. All entries of one body must name the same
`api_version`, or name none and inherit the body's, and a body whose entries
disagree is refused whole. For a top-level array the first entry to name
a served version of 3 or above decides, and for a `method: "batch"` body
the first entry to name a version the server serves does, wherever that
entry sits, so the order of the entries does not matter; a `method:
"batch"` body naming a version the server cannot serve at its own top level
is refused rather than served at version 1. A body refused before it has
named a version is answered as version 1, so neither a malformed `params`
nor the first entry decides the shape of the refusal. One body then has
one version, which is what makes a 100-entry cap answerable: a cap that read a
version off one entry could be side-stepped by reordering the entries, or by
naming the version where the cap did not look, so the cap reads the body's
one version, found wherever dispatch would find it.

Refusing a mixed body is a break a version 1 or 2 client can see, recorded
as such in `API-CHANGELOG.md`. A `method: "batch"` body whose entries name
different versions is served today, each entry at its own version, and is
refused from now on, as is one whose top level names a version its entries
disagree with, where the top-level value was ignored; one whose entries
agree, with each other and with any top-level version, keeps working, and
below version 3 that form stays uncapped.

Five conditions reject a whole array: empty, over the cap, holding nothing
that could be a request, naming no served version of 3 or above, and
naming two different served versions across its entries. Four answer one
specification error object with a null `id`, an array naming none; an array
holding nothing that could be a request answers one such object per entry,
as the specification shows. Each reaches every API version, since no earlier
version accepts an array.
2026-10-14 18:42:02 +09:00
Bart 4d30ee7ec3 feat: Return the JSON-RPC 2.0 envelope on the WebSocket transport
The specification envelope reaches only the JSON-RPC transport, so API
version 3 answers in two shapes depending on which transport carried the
request. Shape the WebSocket reply the same way, reading `id` from the flat
message object rather than from `params`. The reply keeps one extra member,
`"type": "response"`, since a session also receives server-initiated
messages on the same connection and `type` is how a client tells the two
apart. The load notice is written inside the result at this version, which
`shapeSpecReply` hoists back to the top level.

Six conditions reject a request before it reaches a handler, and from
version 3 five answer in the specification envelope under the code the
JSON-RPC transport reports for the same condition: a request naming no
method, one naming a method that is not a string, one whose `command` is
not a string, and one naming two different methods are each an invalid
request, and a role the server refuses is forbidden. Those first four
collapse into one bare `missingCommand` token today, so a client naming two
methods is told the command entry is missing. An `id` that is neither a
string, a number nor null is rejected here too, answered with a null `id`
and judged before the role, as on the JSON-RPC transport.

The sixth condition, an unsupported `api_version`, keeps the legacy shape,
since the server cannot know which envelope such a client speaks. Below
version 3 all six are unchanged. The jtx WebSocket client gains `invokeRaw`,
which returns a reply as it arrived, and `invoke` rewrites a version 3 reply
into the legacy shape so a test can assert one shape across versions.
2026-10-14 18:42:02 +09:00
Bart d25df82c14 feat: Return a JSON-RPC 2.0 envelope from API version 3
API version 3 answers the JSON-RPC 2.0 specification's shape on the
JSON-RPC transport: `jsonrpc` and the request's `id` at the top level, the
payload under `result`, and a failure under `error` as {code, message,
data}. Three helpers write it, one for the members every reply carries, one
for a pre-dispatch rejection and one for a handler result, so a completed
request and a rejection name the protocol and correlate with their request
the same way.

An XRPL error is an application-level failure of a well-formed call, so it
reports the reserved implementation-defined code and carries its own token
and code inside `data`. A method the server does not have is the exception:
the specification defines a code for that condition, so the token selects
it. An `id` the specification does not allow is answered with a null one
rather than echoed, and is judged before the role and method checks, so a
client is told about its id whatever it asked for. `reject` charges the fee
itself, and the two rejections that must not charge say so. `removeMember`
moves the erased value out rather than copying it, which `hoistNotices`
relies on for every notice it moves.

Versions 1 and 2 are untouched, the envelope being selected by
`api_version` and version 3 needing `[beta_rpc_api]`. Four jtx helpers let
a test assert one payload shape across every version, and
`xrpl.jtx.TestHelpers` pins them.
2026-10-14 18:42:02 +09:00
Bart 0fe9167bed feat: Read a request's version at its top level from API version 3
The version a request asks for is read from its parameters, where a client
puts it, and for a `method: "batch"` entry from the entry's own top level
too, since that form carries it beside its method. The JSON-RPC 2.0
specification puts a request's own members beside `method`, so a lone
request may spell `api_version` there as well. One helper states where a
request keeps its version, so the two reads share one answer. The sign and
submit paths that read the version out of the parameters again take the
one already selected instead, so a handler and its checks agree.

Two things a client can observe. A lone request's top-level `api_version`
is now honored, but only from version 3 up: a top-level `api_version: 2`
answers as version 1, as does one the server cannot serve, so honoring
either would change a shipped reply shape. And for a `method: "batch"`
entry, the one shape whose top-level version was already read, the
precedence reverses: the value with the parameters now decides, where the
top-level one previously decided whenever the nested one resolved to
version 1.
2026-10-14 18:42:01 +09:00
Bart 2c5cee2925 fix: Serve every subscription on a connection at one API version
`InfoSub` holds one `apiVersion_` field and every `subscribe` or
`path_find` call overwrites it, so a connection subscribing two streams in
two calls at two `api_version`s has both served at whichever ran last. A
webhook subscriber is keyed on its url alone, so two admins naming one url
share one `InfoSub` and each call reselects the other's content. Two changes
together: a subscription records the version its `subscribe` named, so no
publisher reads a version off a shared object, and `doSubscribe` refuses a
`subscribe` naming a version different from the one this connection
established, answering `apiVersionConflict`, at HTTP 400 where the envelope
derives the status from the error, and registering nothing.

Refusing removes an impossible choice at publish time: one message can reach
a connection through several of its subscriptions, and one message has one
shape. The version is established and compared inside `doSubscribe` and
nowhere else, so a check in the general request path would pin every request
a connection sends. The check runs before any stream is registered, so a
`subscribe` refused further down, for a stream name the server does not
have, still fixes the version; the refusal message speaks of what the
subscriber is served at rather than of what it holds. `path_find` records
its version on the `PathRequest` instead; nothing reads that record until a
later commit shapes the update. `InfoSub::assignId` returns the 64-bit
counter it reads, where it narrowed the value to `int`.

A client sees a second `subscribe` at another version refused where it used
to succeed and reshape the first subscription, and a shared webhook url is
now first-writer-wins. An ordinary request is unaffected, naming its own
version and being answered at it.
2026-10-14 18:42:01 +09:00
Bart 450d00ad65 refactor: Take an ErrorCodeI in LedgerEntryHelpers, not a token string
The `ledger_entry` helpers report a `malformed*` token as a bare string a
caller types by hand, so a typo compiles cleanly and is caught only by
`LedgerEntry_test.cpp`'s token-to-code map failing at run time. Take an
`ErrorCodeI` and derive the token from it through `getErrorInfo`, so a
token cannot be misspelled and comes from one table rather than a literal
per call site. Choosing the wrong code remains the caller's
responsibility, the parameter being the whole unscoped enum, and that map
is what catches it.

No reply changes. These helpers still report `RpcInvalidParams` (31) for
every token they name, because a version 1 or 2 client has read 31 for all
of them for years. Reconciling a token with the code that belongs to it
will be the version 3 envelope's job, through `codeForToken`, which a
later branch gives its first caller.

The three helpers that build an error and the nine that forward to them
gain docstrings, replacing the block comment that covered the family. The
guard on `parseDirectoryNode`'s `owner` arm goes: the check above it
requires exactly one of `owner` and `dir_root` and the `dir_root` arm
returns, so the guard was always true and the error after it unreachable.
2026-10-14 18:42:01 +09:00
Bart b515af4d4e feat: Look up the error code that owns a token
One handler reports an error token whose own code it cannot use, because
changing `error_code` breaks clients matching on the old value: the
`ledger_entry` helpers name twenty-one `malformed*` tokens and report
`invalidParams` (31) for all of them. Reconciling that at the reply
envelope needs the reverse of `getErrorInfo`, and twenty of the twenty-one
have no `ErrorCodeI` entry at all, so `getErrorInfo` resolves none of them.
`malformedRequest` is the exception, already holding 107.

Add a code for each of the twenty, alphabetical by token, since an
append-only enum can only be ordered at introduction. Then add
`codeForToken`, which scans the table and answers `RpcUnknown` for a token
no entry names. The scan compares against views measured at compile time
rather than the table's own `char const*`, since comparing a view with a
pointer measures the pointer first and would call strlen on every entry it
passes. A compile-time assertion makes a duplicate token a build error
rather than a lookup reporting one of two codes. Four comments in the two
files spelled `rpcLAST`, `rpcSUCCESS` and `rpcUNKNOWN` where the
enumerators are `RpcLast`, `RpcSuccess` and `RpcUnknown`; they spell the
enumerators now.

Nothing on the wire changes: the helpers still report `invalidParams`, so
this only reserves the numbers a later API version reports. The first
caller of `codeForToken` is the version 3 reply envelope, which a later
branch adds.
2026-10-14 18:42:00 +09:00
Bart cf495fde86 fix: Charge for a request the server rejects before it can read it
Seven conditions reject a request without charging for it. Five reject a
whole body before the server reads a request out of it: over the size
limit, unparsable, carrying no document, not a JSON object, or a
`method: "batch"` naming no entry array. Two sit inside the batch loop: an
entry that is not an object, and one naming an API version the server does
not serve. Free is what makes a rejection worth repeating, so a client can
send nothing else and never exhaust its allowance. The WebSocket transport
has the same hole for a frame that does not parse, which is answered before
`processSession`, where every other frame is charged.

Charge all of them what a malformed request costs: the HTTP conditions
through two helpers over a third naming the resource entry a request pays
from before its role is read, and the WebSocket frame where it is answered.
The role comes from whatever credentials the request presents, so a
privileged connection stays unlimited. Only one helper reports whether the
charge crossed the drop threshold, and it asks only where its caller can
act on the answer: `disconnect` is a mutator that charges the drop fee and
counts a drop on every call made while the balance is at or above the drop
threshold.

The answer to each rejection is unchanged, except that a WebSocket
connection over the drop threshold is closed rather than answered. What
changes is that a `method: "batch"` body now stops at the first entry the
connection is too loaded to serve, where it previously answered every
entry, so the reply array can be shorter than the request array. That form
is uncapped, and one body within the size limit holds around 333,000
entries.
2026-10-14 18:42:00 +09:00
Bart 00bf88afad fix: Accept a request's parameters by name
A JSON-RPC 2.0 request names its parameters in one of two ways: by
position, where `params` is an array holding the one object a handler
reads, and by name, where `params` is that object itself. Only the
positional form is accepted, so the named form is rejected with HTTP 400
and `params unparsable`. Accept both. One helper states where a request
keeps its parameters, so the version read, the role check and the handler
share one answer.

Two things a client can observe. The named `params` form is served, at
every API version, and the `api_version` and credentials it carries are
read as the positional form's are. And a `method: "batch"` entry carrying
a by-name `params` object is read for its version and credentials from
that object; before, only its `api_version` was read from the entry's top
level and its credentials were ignored.
2026-10-14 18:42:00 +09:00
Bart f45dab46d1 fix: Keep the connection's identity for every entry of a batch
`X-User` and the forwarded-for address are assigned by the connection's
header, so they belong to the connection rather than to one entry of a
`method: "batch"` body. The loop clears them in place for an entry whose
own role is neither identified nor proxied, and a `std::string_view`
shortened in place stays shortened, so the first such entry decides them
for every entry after it. A later entry's role is read from that same
value, so it is demoted along with the username.

Read them into two entry-scoped views instead and hand those to the
handler's context, so the clearing reaches only the entry it belongs to. A
lone request is unaffected, there being no entry after it.

What a client can observe: a body whose first entry presents admin
credentials made the second report no username, where the same entry sent
alone reports the connection's. Every entry of one body now reports the
role and username it would report alone.
2026-10-14 18:41:59 +09:00
Bart b87ad0f08e fix: Name the reason a body is rejected before it is read
Four conditions reject a request body before the server reads a request out
of it, and all four answer `Unable to parse request: ` followed by whatever
the reader recorded. Only one is a parse failure, so for the other three
the reader recorded nothing, the text ends at the colon, and the reply
names a cause that is not theirs while giving no reason at all.

Answer each with its own reason instead. A body over the size limit answers
`Request is too large`, a body that does not parse keeps the parse heading
and the reader's reason after it, a body that parses but carries no
document answers `Request is empty`, and a document that is not a JSON
object answers `Request is not a JSON object`. The status stays 400 for all
four, and this reaches every API version, an unreadable body naming none.

Splitting one condition into four also makes the existing order visible:
the size is checked before the parse, so an oversized body is rejected
without being read.
2026-10-14 18:41:59 +09:00
Bart 50c9e6335e fix: Select the reply envelope from a ripplerpc the server supports
The `ripplerpc` version is read in three places: twice inside the dispatch
loop to pick the error shape, and once after it to derive the HTTP status
by re-reading the version off the finished reply. Replace all three with
one `shapeReply` keyed by an `RpcVersion` enum naming the three envelopes.
The version is read once per request and passed in, and the status is
returned rather than read back, since only the function that wrote the
shape knows where the code went.

The version is now matched exactly against the three valid spellings, where
the `ripplerpc` string was compared with `>=` against "2.0" and "3.0", so
"abc" read as version 3 and "10.0" as version 1. Junk from an
unauthenticated client therefore chose an envelope, and at version 3 chose
real HTTP error codes. Anything but the three exact values is rejected with
a 400, the malformed-RPC fee and -32602, which is a break for API versions
1 and 2 on the JSON-RPC transport, listed under a breaking-changes heading
of its own in the changelog.

Hoisting the log statement above the shape branch fixes a second defect. It
read `error_message` after the version 2 and 3 branch renamed that member
to `message`, and reading a missing member non-const puts it back as an
explicit null, so an error reply carried `"error_message": null` exactly
when the `Server` log partition wrote at debug.
2026-10-14 18:41:59 +09:00
Bart 78709c1d09 refactor: Answer every pre-dispatch rejection through one helper
Nine conditions reject a request before it reaches a handler, and seven
of them write their reply twice over: once as a plain-text body with an
HTTP status for a lone request, and once as an error object appended to
the reply array for a batch entry. The same fix has to be made seven
times, and another condition means another copy. Collapse them into one
`reject` helper that answers either shape and reports whether the loop has
more to do, so a caller reads `if (!reject(...)) return; continue;`.

Each shape is preserved exactly, including the asymmetry. Two of the nine
return the entry under `request`: the one naming an `api_version` the
server cannot serve, which asks for that shape through `wrapRequest`, and
an entry that is not an object, which stays inline, having no members to
carry an error. Six carry the error beside the entry's own members, and
`params unparsable` reaches a lone request only, that form's entries being
flat.

No reply changes. The codes these paths report are declared in
protocol/JsonRpc.h beside the protocol version, so the four file-local
copies go. The header declares every JSON-RPC error code this server
reports as one set, so three of them, `kJsonRpcInvalidRequest`,
`kJsonRpcInvalidParams` and `kJsonRpcServerError`, have no user until a
later branch reports them. The consumer now comes from
`requestInboundEndpoint`, which the WebSocket upgrade path already uses,
leaving the overload check as a condition beside the others rather than
nested inside endpoint construction. Two tests pin the shape a rejected
batch entry keeps: a null `method` reports `-32601` with `Null method`,
and an entry that is not an object is echoed under `request`. The suite
gains `overloadEndpoint`, which charges an address past the drop
threshold, for the privileged-request case.
2026-10-14 18:41:58 +09:00
Bart b67266aad4 fix: Write a status line for every HTTP status a reply can report
`httpReply` names each status in a switch with no `default:` arm, and the
error table names two statuses that switch does not spell out: 402 for
`highFee` and 502 for `dbDeserialization`. A reply reporting either writes
no status line at all, so its first line is a header and the whole thing
is not an HTTP response. A client parsing it reads a protocol error rather
than the error the server meant to report.

Add the arm, taking the reason phrase from beast, which knows the whole
status registry, and drop the `bugprone-switch-missing-default-case`
suppression the omission needed. Eight of the arms above it then spell out
exactly what that arm produces, so they go. Three stay: 401 and 503 report
a phrase of this server's own, and 200 is what every successful reply
carries, so its line stays a compile-time literal rather than a
`std::format` call on the server's most common path.

Both statuses are reachable today through the `ripplerpc: "3.0"` envelope,
which derives the status from the error code. A gtest pins the status line
for every status this server sends, walks the error table, and reads the
placeholder line for a number the registry does not know, so a row added
with a new status cannot reintroduce the defect. A `sign` request whose
fee ceiling is zero reports `highFee` through that envelope, so the server
suite reads the 402 line end to end.
2026-10-14 18:41:58 +09:00
41 changed files with 6188 additions and 767 deletions

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
+23 -2
View File
@@ -24,13 +24,34 @@ 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. Two things it does not reach: a WebSocket session, which reads the field only to echo it back, and API version 3, where the field selects nothing and so is neither read nor checked, `ripplerpc: "banana"` and no `ripplerpc` at all being served alike. 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.
- `batch`: every entry of one batch request is now served at the same API version. An entry may name the version the body names, or name none and inherit it, and a body whose entries name two different versions is refused whole with `Batch entries name different versions` at HTTP 400, charged as a malformed request. **This is visible to a version 1 or 2 client:** a `"method": "batch"` body could mix versions across its entries, each entry being dispatched at its own, and the difference is observable because the reply shape changes with the version. Three bodies change. One whose entries name two different versions is now refused instead of answered. One where some entries name a version and others name none now serves all of them at the named version, where an entry naming none was served at version 1; the same applies to a version named on the request's own top level, which the entries now inherit. One whose top level names a version and whose entries name a different one is now refused, where the top-level value was ignored and each entry was served at the version it named. A body whose top level names no version or the same version as its entries, and whose entries agree with one another or name none at all, is unaffected. A `"method": "batch"` body naming at its own top level a version the server does not serve is refused with `invalid_API_version` at HTTP 400, where it was previously served at version 1 as if it had named none. No test pinned the mixing behavior and the line that produced it was a fallback about where the field sits rather than a decision that entries may differ, so this is a correction rather than a removal of a feature: a per-entry version made the entry cap above unanswerable, since a cap can only ask about a body that has one version.
### 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.
- The command line client no longer prints a credential the operator did not type. A failing command echoes the request it built under `request_sent`, which carries the `admin_password` the client copies out of `[port_rpc]` in the config, so `./xrpld account_info rBogus` printed that password to stdout and into any captured output. `request_sent` is now masked. The `rpc` member beside it, which echoes the arguments as they were typed, is unchanged. The command line client also no longer writes an unparsed `json` or `ripple_path_find` argument to its trace log before parsing it, where a `secret` inside that argument could not be masked; it logs the parsed request instead, masked. The reply it receives is logged the same way, parsed and masked, where the raw body was written before, a `validation_create` answer included.
- 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`.
- 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. **Only API version 3 reports these four.** A request sending `ripplerpc: "3.0"` at API version 1 or 2 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 for those clients 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"`, or API version 3, which are the envelopes that derive 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`. From API version 3 the twenty-one `malformed*` tokens `ledger_entry` names each report the code they own instead of 31.
- 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 what was wrong. A body over the size limit answers `Request is too large`, and a body that parses to `{}` or `null` answers `Request is empty`; both previously answered `Unable to parse request: ` with nothing after the colon, the parser having recorded no error for them. Any other body that is not an array 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 three, as before, and this reaches every API version. A top-level array, the one other body nothing can read a request from, is answered with a JSON error object instead, which the entry on top-level arrays below describes.
- `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. Ten conditions were free: a body over the size limit, one that does not parse, one carrying no document, one that is neither a JSON object nor an array, a `"method": "batch"` naming no entry array, and six ways a top-level array is not a batch this server can serve. The answer to the first five is unchanged; an array's answer changes as the entry on top-level arrays below describes, the one generic rejection it received having become six. 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.
- `batch`: A batch request served at API version 3 or above is now refused when it holds more than 100 entries, before any entry is dispatched. The cap reads the one version the body is served at, so no ordering of the entries evades it. A body served below version 3 is unaffected and stays bounded only by the request size limit.
- A top-level JSON array, which is the JSON-RPC 2.0 batch form accepted from API version 3, is now rejected with a JSON-RPC error object rather than a plain text body when it cannot be served: when it is empty, holds more than 100 entries, holds nothing that could be a request, names no API version of 3 or above that the server serves, or names two different versions the server serves. An array naming no served version of 3 or above is refused for requiring version 3, or, where every version it names is one the server does not serve, with `invalid_API_version`. One entry naming a version the server does not serve does not reject an array in which another entry names a served version: it is answered on its own, inside the reply array, with the same `-32606` code. This reaches every API version, since no earlier version accepts an array at all: a client that sent one received HTTP 400 with `Unable to parse request: ` and now receives the same status with a JSON body. The message also names the version rather than the shape where the version is what is wrong, which is what a client with `[beta_rpc_api]` disabled receives.
## XRP Ledger server version 3.5.0
+30 -1
View File
@@ -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,31 @@ 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>;
/**
* Whether @p apiVersion follows the JSON-RPC 2.0 specification.
*
* @param apiVersion The version a request asked for.
* @return Whether that version follows the specification.
*/
[[nodiscard]] constexpr bool
isSpecVersion(unsigned apiVersion)
{
return apiVersion >= kApiMinimumSpecVersion;
}
static_assert(kApiInvalidVersion < kApiMinimumSupportedVersion);
static_assert(
kApiVersionIfUnspecified >= kApiMinimumSupportedVersion &&
+46 -2
View File
@@ -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.
*/
+25
View File
@@ -1,5 +1,7 @@
#pragma once
#include <xrpl/json/json_forwards.h>
#include <string_view>
namespace xrpl::rpc {
@@ -17,4 +19,27 @@ 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
* XRPL token and code travel in the error's `data` member, so renumbering an
* XRPL error cannot change a JSON-RPC code.
*
* 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
+62 -23
View File
@@ -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);
+2
View File
@@ -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.
+2 -1
View File
@@ -1039,7 +1039,8 @@ Value::removeMember(char const* key)
if (it == value_.mapVal->end())
return kNull;
Value old(it->second);
// The entry is erased on the next line, so nothing observes what the move leaves behind.
Value old(std::move(it->second));
value_.mapVal->erase(it);
return old;
}
+103 -3
View File
@@ -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)
{
+11 -10
View File
@@ -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
+15 -25
View File
@@ -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());
+1 -1
View File
@@ -1248,7 +1248,7 @@ struct PayChan_test : public beast::unit_test::Suite
testcase("PayChan Channel_Auth RPC Api " + std::to_string(apiVersion));
args[jss::api_version] = apiVersion;
auto const rs =
env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
jtx::rpcPayload(env.rpc("json", "channel_authorize", args.toStyledString()));
auto const error = apiVersion < 2u ? "invalidParams" : "badKeyType";
BEAST_EXPECT(rs[jss::error] == error);
});
+127
View File
@@ -317,6 +317,133 @@ auto const kData = JTxFieldWrapper<BlobField>(sfData);
auto const kAmount = JTxFieldWrapper<StAmountField>(sfAmount);
/**
* Reports whether an RPC reply arrived in the JSON-RPC 2.0 envelope.
*
* No single member identifies it. `jsonrpc` does not, since a legacy WebSocket
* reply echoes whatever `jsonrpc` the request sent. An object under `error`
* does not either: the ripplerpc version 2 and 3 envelopes move the whole
* result there, so they carry an object too.
*
* What is unique to an error is `code` beside `message` with neither of the two
* members a legacy envelope always writes, `error_code` and `status`. `data` is
* no part of the test: the specification makes it optional, and a pre-dispatch
* rejection carries none, so testing for it would read every such rejection as
* a legacy reply and hand a caller a null payload.
*
* For a success it is a `result` with no `status` at either level.
*
* @param reply A whole reply, as received.
* @return Whether the specification envelope carried it.
*/
inline bool
isSpecEnvelope(json::Value const& reply)
{
if (reply.isMember(jss::error))
{
auto const& err = reply[jss::error];
return err.isObject() && err.isMember(jss::code) && err.isMember(jss::message) &&
!err.isMember(jss::error_code) && !err.isMember(jss::status);
}
return reply.isMember(jss::jsonrpc) && reply.isMember(jss::result) &&
!reply.isMember(jss::status) && !reply[jss::result].isMember(jss::status);
}
/**
* Reports whether a reply carries a failure under a top-level `error` object
* in the legacy shape, which is how the `ripplerpc` 2.0 and 3.0 envelopes
* answer a failed request: the payload sits under `error` instead of `result`,
* `code` and `message` are added beside `error_code`, and `error_message` is
* removed.
*
* @param reply A whole reply, as received.
* @return Whether `error` is an object and the reply is not a specification
* envelope.
*/
inline bool
isLegacyErrorObject(json::Value const& reply)
{
return reply.isMember(jss::error) && reply[jss::error].isObject() && !isSpecEnvelope(reply);
}
/**
* Returns the payload of an RPC reply in the legacy shape, whichever envelope
* carried it.
*
* From API version 3 a reply follows the JSON-RPC 2.0 specification: a failure
* arrives under `error` as {code, message, data}, with the XRPL token and code
* inside `data`. Version 1 puts everything under `result`, and the `ripplerpc`
* 2.0 and 3.0 envelopes put a failure under `error` with `message` in place of
* `error_message`. A test that only cares which error a request produced can
* call this and assert against one shape across every version.
*
* A test that exists to pin the envelope itself must not use this, the envelope
* difference being what it is checking.
*
* @param reply A whole reply, as received.
* @return The payload, in the shape a version 1 reply carries.
*/
inline json::Value
rpcPayload(json::Value const& reply)
{
bool const spec = isSpecEnvelope(reply);
if (spec && reply.isMember(jss::error))
{
auto const& err = reply[jss::error];
auto payload =
err.isMember(jss::data) ? err[jss::data] : json::Value(json::ValueType::Object);
payload[jss::error_message] = err[jss::message];
payload[jss::status] = jss::error;
return payload;
}
if (spec)
{
// The specification has no `status`, since `result` versus `error` already says which it
// was. Restore it so a caller can assert the same way on either envelope.
auto payload = reply[jss::result];
payload[jss::status] = jss::success;
return payload;
}
if (isLegacyErrorObject(reply))
{
// The `ripplerpc` 2.0 and 3.0 envelopes keep the payload whole under `error`, with
// `message` in place of `error_message`. Restore the name a version 1 payload carries.
auto payload = reply[jss::error];
payload[jss::error_message] = payload[jss::message];
return payload;
}
return reply[jss::result];
}
/**
* Returns a whole RPC reply rewritten into the legacy envelope, whichever one
* carried it.
*
* Where rpcPayload returns just the payload, this keeps the reply's outer
* shape, so an assertion that indexes `[jss::result]` and reads a top-level
* `status` holds across every version. A `ripplerpc` 2.0 or 3.0 failure, which
* sits under `error`, is moved under `result` the same way.
*
* @param reply A whole reply, as received.
* @return The reply, in the shape a version 1 reply carries.
*/
inline json::Value
rpcLegacyReply(json::Value const& reply)
{
if (!isSpecEnvelope(reply) && !isLegacyErrorObject(reply))
return reply;
json::Value legacy(json::ValueType::Object);
legacy[jss::result] = rpcPayload(reply);
legacy[jss::status] = legacy[jss::result][jss::status];
return legacy;
}
// Functions used in debugging
json::Value
getAccountOffers(Env& env, AccountID const& acct, bool current = false);
+259
View File
@@ -0,0 +1,259 @@
#include <test/jtx/TestHelpers.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/JsonRpc.h>
#include <xrpl/protocol/jss.h>
namespace xrpl::test {
/**
* Pins the rule by which a reply's envelope is recognized.
*
* `isSpecEnvelope` is read by `rpcPayload` and `rpcLegacyReply`, which dozens
* of assertions across the suites index through. A misreading there does not
* fail loudly: it hands every one of those assertions a payload from the wrong
* place, so they compare a null against a null and pass. The rule is also the
* opposite of obvious, no single member identifying the envelope, so it is
* pinned here against replies written out rather than obtained from a server.
*
* A server-driven test cannot do this job. It can only show the helper agreeing
* with the envelopes the server happens to send, where what needs holding is
* that the near misses are told apart: a legacy reply that echoes `jsonrpc`,
* and a ripplerpc version 2 error, which is an object under `error` as well.
*/
class TestHelpers_test : public beast::unit_test::Suite
{
/**
* A version 3 failure: {code, message} under `error`, and no legacy member.
*
* @return The reply, as a client receives it.
*/
static json::Value
specError()
{
json::Value reply(json::ValueType::Object);
reply[jss::jsonrpc] = rpc::kJsonRpcVersion;
reply[jss::id] = 1;
reply[jss::error] = json::ValueType::Object;
reply[jss::error][jss::code] = rpc::kJsonRpcInvalidRequest;
reply[jss::error][jss::message] = "Request is not a JSON object";
return reply;
}
/**
* A version 3 success: a `result` with no `status` at either level.
*
* @return The reply, as a client receives it.
*/
static json::Value
specSuccess()
{
json::Value reply(json::ValueType::Object);
reply[jss::jsonrpc] = rpc::kJsonRpcVersion;
reply[jss::id] = 2;
reply[jss::result] = json::ValueType::Object;
reply[jss::result][jss::ledger_index] = 3;
return reply;
}
/**
* A `ripplerpc: "2.0"` failure as `shapeReply` writes it: the legacy payload
* under `error`, with `code` and `message` beside the legacy members and no
* `error_message`.
*
* @return The reply, as a client receives it.
*/
static json::Value
ripplerpcError()
{
json::Value reply(json::ValueType::Object);
reply[jss::ripplerpc] = "2.0";
reply[jss::error] = json::ValueType::Object;
reply[jss::error][jss::status] = jss::error;
reply[jss::error][jss::error] = "invalidParams";
reply[jss::error][jss::error_code] = RpcInvalidParams;
reply[jss::error][jss::code] = RpcInvalidParams;
reply[jss::error][jss::message] = "Invalid parameters.";
return reply;
}
public:
/**
* A version 3 failure and a version 3 success are both recognized.
*/
void
testSpecEnvelopeIsRecognized()
{
testcase("The specification envelope is recognized");
using namespace jtx;
BEAST_EXPECT(isSpecEnvelope(specError()));
BEAST_EXPECT(isSpecEnvelope(specSuccess()));
}
/**
* The three near misses read as legacy: a `status` at either level, an
* echoed `jsonrpc`, and the ripplerpc version 2 error object.
*/
void
testLegacyEnvelopeIsNotMistakenForIt()
{
testcase("A legacy envelope is not mistaken for the specification");
using namespace jtx;
// The `status` a legacy reply always writes, at whichever level.
{
json::Value reply(json::ValueType::Object);
reply[jss::result] = json::ValueType::Object;
reply[jss::result][jss::status] = jss::success;
BEAST_EXPECTS(!isSpecEnvelope(reply), to_string(reply));
json::Value outer(json::ValueType::Object);
outer[jss::status] = jss::success;
outer[jss::result] = json::ValueType::Object;
BEAST_EXPECTS(!isSpecEnvelope(outer), to_string(outer));
}
// A legacy WebSocket reply echoes the `jsonrpc` its request sent, so that member alone
// decides nothing. This is the near miss the docstring names first.
{
json::Value reply(json::ValueType::Object);
reply[jss::jsonrpc] = rpc::kJsonRpcVersion;
reply[jss::status] = jss::success;
reply[jss::type] = jss::response;
reply[jss::result] = json::ValueType::Object;
BEAST_EXPECTS(!isSpecEnvelope(reply), to_string(reply));
}
// The ripplerpc version 2 error envelope moves the whole result under `error`, so an object
// there decides nothing either. It is told apart by the two members it keeps.
{
json::Value reply(json::ValueType::Object);
reply[jss::error] = json::ValueType::Object;
reply[jss::error][jss::error] = "invalidParams";
reply[jss::error][jss::error_code] = RpcInvalidParams;
reply[jss::error][jss::code] = RpcInvalidParams;
reply[jss::error][jss::message] = "Invalid parameters.";
reply[jss::error][jss::status] = jss::error;
BEAST_EXPECTS(!isSpecEnvelope(reply), to_string(reply));
}
}
/**
* `rpcPayload` hands a caller the legacy payload shape from either
* envelope, with and without `data`, restores `error_message` to a
* `ripplerpc` failure, and leaves a version 1 reply as it is.
*/
void
testPayloadIsOneShapeAcrossEnvelopes()
{
testcase("A payload reads the same whichever envelope carried it");
using namespace jtx;
// A specification failure carries the token and code inside `data`, which is what a caller
// asserting on the legacy shape expects to find at the top level of the payload.
{
auto reply = specError();
reply[jss::error][jss::data] = json::ValueType::Object;
reply[jss::error][jss::data][jss::error] = "invalidParams";
reply[jss::error][jss::data][jss::error_code] = RpcInvalidParams;
auto const payload = rpcPayload(reply);
BEAST_EXPECTS(payload[jss::error] == "invalidParams", to_string(payload));
BEAST_EXPECTS(payload[jss::error_code] == RpcInvalidParams, to_string(payload));
BEAST_EXPECTS(
payload[jss::error_message] == "Request is not a JSON object", to_string(payload));
BEAST_EXPECTS(payload[jss::status] == jss::error, to_string(payload));
}
// A pre-dispatch rejection carries no `data`, the specification making it optional. The
// payload is then the message and the status alone rather than a null.
{
auto const payload = rpcPayload(specError());
BEAST_EXPECTS(!payload.isMember(jss::error), to_string(payload));
BEAST_EXPECTS(
payload[jss::error_message] == "Request is not a JSON object", to_string(payload));
BEAST_EXPECTS(payload[jss::status] == jss::error, to_string(payload));
}
// A success regains the `status` the specification has no place for.
{
auto const payload = rpcPayload(specSuccess());
BEAST_EXPECTS(payload[jss::ledger_index] == 3, to_string(payload));
BEAST_EXPECTS(payload[jss::status] == jss::success, to_string(payload));
}
// A legacy reply is returned as it stands, so a caller reads one shape either way.
{
json::Value reply(json::ValueType::Object);
reply[jss::result] = json::ValueType::Object;
reply[jss::result][jss::status] = jss::success;
reply[jss::result][jss::ledger_index] = 3;
BEAST_EXPECT(rpcPayload(reply) == reply[jss::result]);
}
// A `ripplerpc` 2.0 or 3.0 failure sits under `error` with `message` in place of
// `error_message`. The payload regains that name, so it reads as a version 1 failure.
{
auto const payload = rpcPayload(ripplerpcError());
BEAST_EXPECTS(payload[jss::error] == "invalidParams", to_string(payload));
BEAST_EXPECTS(payload[jss::error_code] == RpcInvalidParams, to_string(payload));
BEAST_EXPECTS(payload[jss::error_message] == "Invalid parameters.", to_string(payload));
BEAST_EXPECTS(payload[jss::status] == jss::error, to_string(payload));
}
}
/**
* `rpcLegacyReply` keeps the outer legacy shape, `status` beside `result`,
* moves a `ripplerpc` failure under `result`, and returns a version 1
* reply unchanged.
*/
void
testLegacyReplyKeepsTheOuterShape()
{
testcase("A rewritten reply keeps the outer legacy shape");
using namespace jtx;
auto const legacy = rpcLegacyReply(specSuccess());
BEAST_EXPECTS(legacy[jss::status] == jss::success, to_string(legacy));
BEAST_EXPECTS(legacy[jss::result][jss::ledger_index] == 3, to_string(legacy));
auto const failed = rpcLegacyReply(specError());
BEAST_EXPECTS(failed[jss::status] == jss::error, to_string(failed));
BEAST_EXPECTS(
failed[jss::result][jss::error_message] == "Request is not a JSON object",
to_string(failed));
// A reply already in the legacy envelope is returned unchanged, not rewritten twice.
json::Value reply(json::ValueType::Object);
reply[jss::status] = jss::success;
reply[jss::result] = json::ValueType::Object;
reply[jss::result][jss::status] = jss::success;
BEAST_EXPECT(rpcLegacyReply(reply) == reply);
// A `ripplerpc` failure moves from `error` to `result`, with `status` beside it.
auto const moved = rpcLegacyReply(ripplerpcError());
BEAST_EXPECTS(moved[jss::status] == jss::error, to_string(moved));
BEAST_EXPECTS(moved[jss::result][jss::error] == "invalidParams", to_string(moved));
}
void
run() override
{
testSpecEnvelopeIsRecognized();
testLegacyEnvelopeIsNotMistakenForIt();
testPayloadIsOneShapeAcrossEnvelopes();
testLegacyReplyKeepsTheOuterShape();
}
};
BEAST_DEFINE_TESTSUITE(TestHelpers, jtx, xrpl);
} // namespace xrpl::test
+28 -1
View File
@@ -31,16 +31,43 @@ public:
findMsg(
std::chrono::milliseconds const& timeout,
std::function<bool(json::Value const&)> pred) = 0;
/**
* Send a command and return its reply exactly as it arrived.
*
* `invoke` reshapes a reply into the legacy envelope so that a caller can
* assert one shape across API versions. A test checking the envelope itself
* needs what the server actually sent.
*
* At version 3 `invoke` keeps `id` and `jsonrpc` beside the legacy members,
* as a legacy success reply does. A legacy error reply nests the whole
* reply under `result`, `id` included, as it always has.
*
* @param cmd The method to call.
* @param params The parameters to send with it.
* @return The reply, or nullopt if none arrived.
*/
virtual std::optional<json::Value>
invokeRaw(std::string const& cmd, json::Value const& params) = 0;
};
/**
* Returns a client operating through WebSockets/S.
*
* @param cfg The server configuration to read the port from.
* @param v2 Whether to connect to the `ws2` port rather than `ws`.
* @param rpcVersion The legacy `ripplerpc` envelope to select.
* @param headers Extra headers to send with the upgrade request.
* @param apiVersion Sent as `api_version` on every request. From version 3 it
* selects the JSON-RPC 2.0 envelope.
* @return The client.
*/
std::unique_ptr<WSClient>
makeWSClient(
Config const& cfg,
bool v2 = true,
unsigned rpcVersion = 2,
std::unordered_map<std::string, std::string> const& headers = {});
std::unordered_map<std::string, std::string> const& headers = {},
std::optional<unsigned> apiVersion = std::nullopt);
} // namespace xrpl::test
+10 -3
View File
@@ -2,6 +2,7 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/multisign.h>
#include <test/jtx/seq.h>
#include <test/jtx/ter.h>
@@ -233,12 +234,18 @@ AMM::ammRpcInfo(
if (ammAccount)
jv[jss::amm_account] = *ammAccount;
}
auto jr =
auto const jr =
(apiVersion == rpc::kApiInvalidVersion
? env_.rpc("json", "amm_info", to_string(jv))
: env_.rpc(apiVersion, "json", "amm_info", to_string(jv)));
if (jr.isObject() && jr.isMember(jss::result) && jr[jss::result].isMember(jss::status))
return jr[jss::result];
if (!jr.isObject())
return json::ValueType::Null;
// rpcPayload normalizes the specification envelope that API version 3 returns, which has no
// `status` member, back to the shape callers assert against.
auto payload = rpcPayload(jr);
if (payload.isMember(jss::status))
return payload;
return json::ValueType::Null;
}
+56 -7
View File
@@ -1,5 +1,7 @@
#include <test/jtx/WSClient.h>
#include <test/jtx/TestHelpers.h>
#include <xrpld/core/Config.h>
#include <xrpl/basics/Mutex.hpp>
@@ -127,6 +129,7 @@ class WSClientImpl : public WSClient
std::list<std::shared_ptr<Msg>> msgs_;
unsigned rpcVersion_;
std::optional<unsigned> apiVersion_;
void
cleanup()
@@ -156,17 +159,28 @@ class WSClientImpl : public WSClient
}
public:
/**
* Connects to the server's WebSocket port and starts reading messages.
*
* @param cfg The server configuration to read the port from.
* @param v2 Whether to connect to the `ws2` port rather than `ws`.
* @param rpcVersion The legacy `ripplerpc` envelope to select.
* @param headers Extra headers to send with the upgrade request.
* @param apiVersion Sent as `api_version` on every request, when given.
*/
WSClientImpl(
Config const& cfg,
bool v2,
unsigned rpcVersion,
std::unordered_map<std::string, std::string> const& headers = {})
std::unordered_map<std::string, std::string> const& headers = {},
std::optional<unsigned> apiVersion = std::nullopt)
: work_(std::in_place, boost::asio::make_work_guard(ios_))
, strand_(boost::asio::make_strand(ios_))
, thread_([&] { ios_.run(); })
, stream_(ios_)
, ws_(stream_)
, rpcVersion_(rpcVersion)
, apiVersion_(apiVersion)
{
try
{
@@ -196,8 +210,8 @@ public:
cleanup();
}
json::Value
invoke(std::string const& cmd, json::Value const& params) override
std::optional<json::Value>
invokeRaw(std::string const& cmd, json::Value const& params) override
{
using boost::asio::buffer;
using namespace std::chrono_literals;
@@ -217,6 +231,8 @@ public:
{
jp[jss::command] = cmd;
}
if (apiVersion_)
jp[jss::api_version] = *apiVersion_;
auto const s = to_string(jp);
// Use the error_code overload to avoid an unhandled exception
@@ -228,12 +244,44 @@ public:
return {};
}
auto jv =
findMsg(5s, [&](json::Value const& jval) { return jval[jss::type] == jss::response; });
return findMsg(
5s, [&](json::Value const& jval) { return jval[jss::type] == jss::response; });
}
json::Value
invoke(std::string const& cmd, json::Value const& params) override
{
auto jv = invokeRaw(cmd, params);
if (jv)
{
// Normalize JSON output
jv->removeMember(jss::type);
// Restore the legacy shape so a caller can assert the same way on either envelope,
// whatever `api_version` the client was built with.
if (jtx::isSpecEnvelope(*jv))
{
// rpcLegacyReply does the same rewrite, minus the hoist below: this client's legacy
// branch puts `error` at the top level as well as under `result`, and callers read
// it there. Kept apart, since hoisting it in rpcLegacyReply would change what the
// HTTP callers of that helper see. A rejection without `data` has no token to
// hoist.
auto ret = jtx::rpcLegacyReply(*jv);
if (ret[jss::result].isMember(jss::error))
ret[jss::error] = ret[jss::result][jss::error];
// The notices the server hoists beside `result` stay beside it, which is where a
// legacy WebSocket reply carries them too, and so do `id` and `jsonrpc`, which a
// legacy success reply keeps at the top level. `ripplerpc` and `api_version` are
// not in a specification reply at all, so there is nothing to carry for them.
for (auto const& name :
{jss::warning, jss::warnings, jss::deprecated, jss::id, jss::jsonrpc})
{
if (jv->isMember(name))
ret[name] = (*jv)[name];
}
return ret;
}
if ((*jv).isMember(jss::status) && (*jv)[jss::status] == jss::error)
{
json::Value ret;
@@ -374,9 +422,10 @@ makeWSClient(
Config const& cfg,
bool v2,
unsigned rpcVersion,
std::unordered_map<std::string, std::string> const& headers)
std::unordered_map<std::string, std::string> const& headers,
std::optional<unsigned> apiVersion)
{
return std::make_unique<WSClientImpl>(cfg, v2, rpcVersion, headers);
return std::make_unique<WSClientImpl>(cfg, v2, rpcVersion, headers, apiVersion);
}
} // namespace xrpl::test
+20 -16
View File
@@ -145,7 +145,8 @@ class AccountTx_test : public beast::unit_test::Suite
// Ledger 3 has the two txs associated with funding the account
// All other ledgers have no txs
auto const hasTxs = [apiVersion](json::Value const& j) {
auto const hasTxs = [apiVersion](json::Value const& reply) {
auto const j = jtx::rpcLegacyReply(reply);
switch (apiVersion)
{
case 1:
@@ -190,12 +191,14 @@ class AccountTx_test : public beast::unit_test::Suite
}
};
auto const noTxs = [](json::Value const& j) {
auto const noTxs = [](json::Value const& reply) {
auto const j = jtx::rpcLegacyReply(reply);
return j.isMember(jss::result) && (j[jss::result][jss::status] == "success") &&
(j[jss::result][jss::transactions].size() == 0);
};
auto const isErr = [](json::Value const& j, ErrorCodeI code) {
auto const isErr = [](json::Value const& reply, ErrorCodeI code) {
auto const j = jtx::rpcLegacyReply(reply);
return j.isMember(jss::result) && j[jss::result].isMember(jss::error) &&
j[jss::result][jss::error] == rpc::getErrorInfo(code).token;
};
@@ -410,7 +413,7 @@ class AccountTx_test : public beast::unit_test::Suite
}
p[jss::binary] = true;
json::Value result{env.rpc("json", "account_tx", to_string(p))};
json::Value result{jtx::rpcLegacyReply(env.rpc("json", "account_tx", to_string(p)))};
BEAST_EXPECT(result[jss::result][jss::status] == "success");
p[jss::forward] = "true";
@@ -424,7 +427,7 @@ class AccountTx_test : public beast::unit_test::Suite
}
p[jss::forward] = false;
result = env.rpc("json", "account_tx", to_string(p));
result = jtx::rpcLegacyReply(env.rpc("json", "account_tx", to_string(p)));
BEAST_EXPECT(result[jss::result][jss::status] == "success");
}
// test limit with malformed values
@@ -438,56 +441,56 @@ class AccountTx_test : public beast::unit_test::Suite
// Test case: limit = 1.2 should fail (not an integer)
p[jss::limit] = 1.2;
BEAST_EXPECT(
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
// Test case: limit = "10" should fail (string instead of integer)
p[jss::limit] = "10";
BEAST_EXPECT(
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
// Test case: limit = true should fail (boolean instead of integer)
p[jss::limit] = true;
BEAST_EXPECT(
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
// Test case: limit = false should fail (boolean instead of integer)
p[jss::limit] = false;
BEAST_EXPECT(
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
// Test case: limit = -1 should fail (negative number)
p[jss::limit] = -1;
BEAST_EXPECT(
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
// Test case: limit = [] should fail (array instead of integer)
p[jss::limit] = json::Value(json::ValueType::Array);
BEAST_EXPECT(
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
// Test case: limit = {} should fail (object instead of integer)
p[jss::limit] = json::Value(json::ValueType::Object);
BEAST_EXPECT(
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
// Test case: limit = "malformed" should fail (malformed string)
p[jss::limit] = "malformed";
BEAST_EXPECT(
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
// Test case: limit = ["limit"] should fail (array with string)
p[jss::limit] = json::Value(json::ValueType::Array);
p[jss::limit].append("limit");
BEAST_EXPECT(
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
// Test case: limit = {"limit": 10} should fail (object with
@@ -495,13 +498,14 @@ class AccountTx_test : public beast::unit_test::Suite
p[jss::limit] = json::Value(json::ValueType::Object);
p[jss::limit][jss::limit] = 10;
BEAST_EXPECT(
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
// Test case: limit = 10 should succeed (valid integer)
p[jss::limit] = 10;
BEAST_EXPECT(
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::status] == "success");
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::status] ==
"success");
}
}
+303 -172
View File
@@ -59,7 +59,6 @@
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <source_location>
#include <stdexcept>
#include <string>
@@ -151,12 +150,66 @@ getTypeName(FieldType typeID)
class LedgerEntry_test : public beast::unit_test::Suite
{
/**
* Calls `ledger_entry` with @p params and returns the payload in the legacy
* shape, whichever envelope the version selects.
*
* @param env The environment to call.
* @param params The request parameters.
* @param apiVersion The API version to name, or none to let the client
* choose.
* @return The reply payload, as rpcPayload shapes it.
*/
static json::Value
ledgerEntry(
jtx::Env& env,
json::Value const& params,
std::optional<unsigned> apiVersion = std::nullopt)
{
if (apiVersion)
return jtx::rpcPayload(env.rpc(*apiVersion, "json", "ledger_entry", to_string(params)));
return jtx::rpcPayload(env.rpc("json", "ledger_entry", to_string(params)));
}
/**
* Asserts that @p jv reports the error @p err with the message @p msg and
* the code the token carries below API version 3, which is what a request
* naming no `api_version` gets.
*
* @param jv The reply payload, in the legacy shape.
* @param err The expected error token.
* @param msg The expected error message.
* @param location The caller, named in a failure.
*/
void
checkErrorValue(
json::Value const& jv,
std::string const& err,
std::string const& msg,
std::source_location const location = std::source_location::current())
{
checkErrorValue(1, jv, err, msg, location);
}
/**
* Asserts that @p jv reports the error @p err with the message @p msg and
* the code that token carries at @p apiVersion. The version leads the
* parameter list so that @p location stays last and still captures the
* caller.
*
* @param apiVersion The API version the request named.
* @param jv The reply payload, in the legacy shape.
* @param err The expected error token.
* @param msg The expected error message.
* @param location The caller, named in a failure.
*/
void
checkErrorValue(
unsigned apiVersion,
json::Value const& jv,
std::string const& err,
std::string const& msg,
std::source_location const location = std::source_location::current())
{
if (BEAST_EXPECT(jv.isMember(jss::status)))
BEAST_EXPECTS(jv[jss::status] == "error", std::to_string(location.line()));
@@ -169,7 +222,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
// Wire values pinned as literals: read from the error table, the expectation would
// agree with any change to the row that produced the reply.
// These are raised through injectError and carry their own code.
// These are raised through injectError and carry their own code at every version.
static std::map<std::string, int> const kCodes{
{"entryNotFound", 98},
{"invalidParams", 31},
@@ -178,37 +231,38 @@ class LedgerEntry_test : public beast::unit_test::Suite
{"unknownOption", 109},
};
// The tokens the ledger_entry helpers name, all reporting the generic invalidParams;
// see LedgerEntryHelpers.h. Listed, so a misspelled token reaches the else below.
static std::set<std::string> const kMalformedTokens{
"malformedAccount",
"malformedAddress",
"malformedAuthorized",
"malformedAuthorizedCredentials",
"malformedBridgeAccount",
"malformedBroker",
"malformedCurrency",
"malformedDirRoot",
"malformedDocumentID",
"malformedIssue",
"malformedIssuingChainDoor",
"malformedLockingChainDoor",
"malformedMPTIssuanceID",
"malformedMPTokenIssuance",
"malformedOwner",
"malformedRequest",
"malformedSeq",
"malformedSponsee",
"malformedSponsor",
"malformedXChainOwnedClaimID",
"malformedXChainOwnedCreateAccountClaimID",
// The tokens the ledger_entry helpers name. Below version 3 every one of them reports
// the generic invalidParams (31); from version 3 each reports the code it owns.
static std::map<std::string, int> const kMalformedCodes{
{"malformedAccount", 110},
{"malformedAddress", 111},
{"malformedAuthorized", 112},
{"malformedAuthorizedCredentials", 113},
{"malformedBridgeAccount", 114},
{"malformedBroker", 115},
{"malformedCurrency", 116},
{"malformedDirRoot", 117},
{"malformedDocumentID", 118},
{"malformedIssue", 119},
{"malformedIssuingChainDoor", 120},
{"malformedLockingChainDoor", 121},
{"malformedMPTIssuanceID", 122},
{"malformedMPTokenIssuance", 123},
{"malformedOwner", 124},
{"malformedRequest", 107},
{"malformedSeq", 125},
{"malformedSponsee", 126},
{"malformedSponsor", 127},
{"malformedXChainOwnedClaimID", 128},
{"malformedXChainOwnedCreateAccountClaimID", 129},
};
auto const expectCode = [&](int expected) {
BEAST_EXPECTS(
jv[jss::error_code] == expected,
"Expected error_code " + std::to_string(expected) + " for " + err +
", received " + jv[jss::error_code].toStyledString() + ", at line " +
" at version " + std::to_string(apiVersion) + ", received " +
jv[jss::error_code].toStyledString() + ", at line " +
std::to_string(location.line()));
};
@@ -216,13 +270,16 @@ class LedgerEntry_test : public beast::unit_test::Suite
{
expectCode(it->second);
}
else if (kMalformedTokens.contains(err))
else if (auto const mit = kMalformedCodes.find(err); mit != kMalformedCodes.end())
{
expectCode(RpcInvalidParams);
expectCode(apiVersion < 3 ? int{RpcInvalidParams} : mit->second);
}
else
{
// A token in neither list is a typo; reporting 31 like the rest would let it pass.
// Every token either map names is checked above. A token in neither is one no
// error table row names, which reports its neighbors' generic code at version 3
// and so would pass an assertion derived from it. Fail instead: the missing row is
// the defect.
BEAST_EXPECTS(
false,
"Token " + err + " names no error code, at line " +
@@ -400,25 +457,27 @@ class LedgerEntry_test : public beast::unit_test::Suite
if (required)
{
correctRequest.removeMember(fieldName);
json::Value const jrr = env.rpc(
apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result];
json::Value const jrr = ledgerEntry(env, correctRequest, apiVersion);
if (apiVersion < 2u)
{
checkErrorValue(jrr, "unknownOption", "Unknown option.", location);
checkErrorValue(apiVersion, jrr, "unknownOption", "Unknown option.", location);
}
else
{
checkErrorValue(
jrr, "invalidParams", "No ledger_entry params provided.", location);
apiVersion,
jrr,
"invalidParams",
"No ledger_entry params provided.",
location);
}
}
auto const tryField = [&](json::Value fieldValue) -> void {
correctRequest[fieldName] = fieldValue;
json::Value const jrr = env.rpc(
apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result];
json::Value const jrr = ledgerEntry(env, correctRequest, apiVersion);
auto const expectedErrMsg = rpc::expectedFieldMessage(
fieldName, typeNameOverride.value_or(getTypeName(typeID)));
checkErrorValue(jrr, expectedError, expectedErrMsg, location);
checkErrorValue(apiVersion, jrr, expectedError, expectedErrMsg, location);
};
auto const& badValues = getBadValues(typeID);
@@ -448,23 +507,29 @@ class LedgerEntry_test : public beast::unit_test::Suite
if (required)
{
correctRequest[parentFieldName].removeMember(fieldName);
json::Value const jrr = env.rpc(
apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result];
json::Value const jrr = ledgerEntry(env, correctRequest, apiVersion);
checkErrorValue(
jrr, "malformedRequest", rpc::missingFieldMessage(fieldName.cStr()), location);
apiVersion,
jrr,
"malformedRequest",
rpc::missingFieldMessage(fieldName.cStr()),
location);
correctRequest[parentFieldName][fieldName] = json::ValueType::Null;
json::Value const jrr2 = env.rpc(
apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result];
json::Value const jrr2 = ledgerEntry(env, correctRequest, apiVersion);
checkErrorValue(
jrr2, "malformedRequest", rpc::missingFieldMessage(fieldName.cStr()), location);
apiVersion,
jrr2,
"malformedRequest",
rpc::missingFieldMessage(fieldName.cStr()),
location);
}
auto const tryField = [&](json::Value fieldValue) -> void {
correctRequest[parentFieldName][fieldName] = fieldValue;
json::Value const jrr = env.rpc(
apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result];
json::Value const jrr = ledgerEntry(env, correctRequest, apiVersion);
checkErrorValue(
apiVersion,
jrr,
expectedError,
rpc::expectedFieldMessage(fieldName, getTypeName(typeID)),
@@ -560,7 +625,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[jss::ledger_hash] =
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AA";
auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
auto const jrr = ledgerEntry(env, jvParams);
checkErrorValue(jrr, "lgrNotFound", "ledgerNotFound");
}
{
@@ -572,10 +637,12 @@ class LedgerEntry_test : public beast::unit_test::Suite
forAllApiVersions([&, this](unsigned apiVersion) {
auto const tryField = [&](json::Value fieldValue) -> void {
jvParams[jss::ledger_hash] = fieldValue;
json::Value const jrr = env.rpc(
apiVersion, "json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams, apiVersion);
checkErrorValue(
jrr, "invalidParams", "Invalid field 'ledger_hash', not hex string.");
apiVersion,
jrr,
"invalidParams",
"Invalid field 'ledger_hash', not hex string.");
};
auto const& badValues = getBadValues(typeId);
@@ -593,7 +660,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[jss::index] =
"00000000000000000000000000000000000000000000000000000000000000"
"00";
auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
auto const jrr = ledgerEntry(env, jvParams);
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
}
@@ -605,16 +672,16 @@ class LedgerEntry_test : public beast::unit_test::Suite
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAA";
jvParams[jss::api_version] = apiVersion;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
if (apiVersion < 2u)
{
checkErrorValue(jrr, "unknownOption", "Unknown option.");
checkErrorValue(apiVersion, jrr, "unknownOption", "Unknown option.");
}
else
{
checkErrorValue(jrr, "invalidParams", "No ledger_entry params provided.");
checkErrorValue(
apiVersion, jrr, "invalidParams", "No ledger_entry params provided.");
}
}
});
@@ -637,7 +704,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
std::string const ledgerHash{to_string(env.closed()->header().hash)};
{
// Exercise ledger_closed along the way.
json::Value const jrr = env.rpc("ledger_closed")[jss::result];
json::Value const jrr = jtx::rpcPayload(env.rpc("ledger_closed"));
BEAST_EXPECT(jrr[jss::ledger_hash] == ledgerHash);
BEAST_EXPECT(jrr[jss::ledger_index] == 3);
}
@@ -648,8 +715,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::account_root] = alice.human();
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr.isMember(jss::node));
BEAST_EXPECT(jrr[jss::node][jss::Account] == alice.human());
BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName] == "10000000000");
@@ -667,8 +733,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[jss::account_root] = alice.human();
jvParams[jss::binary] = 1;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr.isMember(jss::node_binary));
BEAST_EXPECT(jrr[jss::node_binary] == kAliceAcctRootBinary);
}
@@ -676,8 +741,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
// Request alice's account root using the index.
json::Value jvParams;
jvParams[jss::index] = accountRootIndex;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(!jrr.isMember(jss::node_binary));
BEAST_EXPECT(jrr.isMember(jss::node));
BEAST_EXPECT(jrr[jss::node][jss::Account] == alice.human());
@@ -688,8 +752,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::index] = accountRootIndex;
jvParams[jss::binary] = 0;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr.isMember(jss::node));
BEAST_EXPECT(jrr[jss::node][jss::Account] == alice.human());
BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName] == "10000000000");
@@ -699,8 +762,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::account] = alice.human();
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr.isMember(jss::node));
BEAST_EXPECT(jrr[jss::node][jss::Account] == alice.human());
BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName] == "10000000000");
@@ -717,8 +779,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::account_root] = Account("bob").human();
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
}
}
@@ -791,8 +852,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::amendments] = to_string(keylet.key);
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Amendments);
}
@@ -891,8 +951,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::check] = to_string(checkId.key);
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Check);
BEAST_EXPECT(jrr[jss::node][sfSendMax.jsonName] == "100000000");
}
@@ -927,15 +986,13 @@ class LedgerEntry_test : public beast::unit_test::Suite
{
json::Value jvParams;
jvParams[jss::account_root] = alice.human();
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
accountRootIndex = jrr[jss::index].asString();
}
json::Value jvParams;
jvParams[jss::check] = accountRootIndex;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
checkErrorValue(jrr, "unexpectedLedgerType", "Unexpected ledger type.");
}
{
@@ -1033,8 +1090,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[jss::delegate][jss::account] = alice.human();
jvParams[jss::delegate][jss::authorize] = bob.human();
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Delegate);
BEAST_EXPECT(jrr[jss::node][sfAccount.jsonName] == alice.human());
BEAST_EXPECT(jrr[jss::node][sfAuthorize.jsonName] == bob.human());
@@ -1045,8 +1101,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::delegate] = delegateIndex;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Delegate);
BEAST_EXPECT(jrr[jss::node][sfAccount.jsonName] == alice.human());
BEAST_EXPECT(jrr[jss::node][sfAuthorize.jsonName] == bob.human());
@@ -1089,8 +1144,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[jss::deposit_preauth][jss::owner] = alice.human();
jvParams[jss::deposit_preauth][jss::authorized] = becky.human();
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::DepositPreauth);
BEAST_EXPECT(jrr[jss::node][sfAccount.jsonName] == alice.human());
@@ -1102,8 +1156,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::deposit_preauth] = depositPreauthIndex;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::DepositPreauth);
BEAST_EXPECT(jrr[jss::node][sfAccount.jsonName] == alice.human());
@@ -1185,8 +1238,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
arr.append(jo);
jvParams[jss::deposit_preauth][jss::authorized_credentials] = arr;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
auto const expectedErrMsg = fieldValue.isNull()
? rpc::missingFieldMessage(jss::issuer.cStr())
: rpc::expectedFieldMessage(jss::issuer, "AccountID");
@@ -1236,8 +1288,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
arr.append(jo);
jvParams[jss::deposit_preauth][jss::authorized_credentials] = arr;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
auto const expectedErrMsg = fieldValue.isNull()
? rpc::missingFieldMessage(jss::credential_type.cStr())
: rpc::expectedFieldMessage(jss::credential_type, "hex string");
@@ -1402,7 +1453,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
std::string const ledgerHash{to_string(env.closed()->header().hash)};
{
// Exercise ledger_closed along the way.
json::Value const jrr = env.rpc("ledger_closed")[jss::result];
json::Value const jrr = jtx::rpcPayload(env.rpc("ledger_closed"));
BEAST_EXPECT(jrr[jss::ledger_hash] == ledgerHash);
BEAST_EXPECT(jrr[jss::ledger_index] == 5);
}
@@ -1414,8 +1465,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::directory] = dirRootIndex;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][sfIndexes.jsonName].size() == 32);
}
{
@@ -1423,8 +1473,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::directory] = json::ValueType::Object;
jvParams[jss::directory][jss::dir_root] = dirRootIndex;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::index] == dirRootIndex);
}
{
@@ -1433,8 +1482,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[jss::directory] = json::ValueType::Object;
jvParams[jss::directory][jss::owner] = alice.human();
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::index] == dirRootIndex);
}
{
@@ -1443,8 +1491,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[jss::directory] = json::ValueType::Object;
jvParams[jss::directory][jss::dir_root] = dirRootIndex;
jvParams[jss::directory][jss::sub_index] = 1;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::index] != dirRootIndex);
BEAST_EXPECT(jrr[jss::node][sfIndexes.jsonName].size() == 2);
}
@@ -1455,8 +1502,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[jss::directory][jss::owner] = alice.human();
jvParams[jss::directory][jss::sub_index] = 1;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::index] != dirRootIndex);
BEAST_EXPECT(jrr[jss::node][sfIndexes.jsonName].size() == 2);
}
@@ -1504,8 +1550,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[jss::directory][jss::owner] = alice.human();
jvParams[jss::directory][jss::dir_root] = dirRootIndex;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
checkErrorValue(
jrr, "malformedRequest", "Must have exactly one of `owner` and `dir_root` fields.");
}
@@ -1515,8 +1560,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[jss::directory] = json::ValueType::Object;
jvParams[jss::directory][jss::sub_index] = 1;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
checkErrorValue(
jrr, "malformedRequest", "Must have exactly one of `owner` and `dir_root` fields.");
}
@@ -1558,8 +1602,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[jss::escrow] = json::ValueType::Object;
jvParams[jss::escrow][jss::owner] = alice.human();
jvParams[jss::escrow][jss::seq] = env.seq(alice) - 1;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][jss::Amount] == XRP(333).value().getText());
escrowIndex = jrr[jss::index].asString();
}
@@ -1568,8 +1611,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::escrow] = escrowIndex;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][jss::Amount] == XRP(333).value().getText());
}
{
@@ -1596,8 +1638,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
Keylet const keylet = keylet::feeSettings();
json::Value jvParams;
jvParams[jss::fee] = to_string(keylet.key);
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::FeeSettings);
}
@@ -1618,8 +1659,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
Keylet const keylet = keylet::skip();
json::Value jvParams;
jvParams[jss::hashes] = to_string(keylet.key);
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::LedgerHashes);
}
@@ -1652,8 +1692,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
{
json::Value jvParams;
jvParams[jss::nft_offer] = to_string(offerID);
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::NFTokenOffer);
BEAST_EXPECT(jrr[jss::node][sfOwner.jsonName] == issuer.human());
BEAST_EXPECT(jrr[jss::node][sfNFTokenID.jsonName] == to_string(nftokenID0));
@@ -1714,8 +1753,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
{
json::Value jvParams;
jvParams[jss::nft_page] = to_string(nftpage.key);
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::NFTokenPage);
}
@@ -1767,8 +1805,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::nunl] = to_string(keylet.key);
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::NegativeUNL);
}
@@ -1801,8 +1838,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[jss::offer][jss::account] = alice.human();
jvParams[jss::offer][jss::seq] = env.seq(alice) - 1;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][jss::TakerGets] == "322000000");
offerIndex = jrr[jss::index].asString();
}
@@ -1810,8 +1846,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
// Request the offer using its index.
json::Value jvParams;
jvParams[jss::offer] = offerIndex;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][jss::TakerGets] == "322000000");
}
@@ -1868,8 +1903,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::payment_channel] = to_string(payChanIndex);
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][sfAmount.jsonName] == "57000000");
BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName] == "0");
BEAST_EXPECT(jrr[jss::node][sfSettleDelay.jsonName] == 18);
@@ -1906,8 +1940,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::payment_channel] = ledgerHash;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
}
@@ -1955,8 +1988,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[fieldName][jss::accounts][1u] = gw.human();
jvParams[fieldName][jss::currency] = "USD";
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName][jss::value] == "-97");
BEAST_EXPECT(jrr[jss::node][sfHighLimit.jsonName][jss::value] == "999");
}
@@ -1978,8 +2010,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[fieldName][jss::accounts][0u] = alice.human();
jvParams[fieldName][jss::currency] = "USD";
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
checkErrorValue(
jrr,
"malformedRequest",
@@ -1996,8 +2027,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[fieldName][jss::accounts][2u] = alice.human();
jvParams[fieldName][jss::currency] = "USD";
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
checkErrorValue(
jrr,
"malformedRequest",
@@ -2016,8 +2046,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[fieldName][jss::accounts][1u] = gw.human();
jvParams[fieldName][jss::currency] = "USD";
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
checkErrorValue(
jrr,
"malformedAddress",
@@ -2031,8 +2060,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[fieldName][jss::accounts][1u] = badAccount;
jvParams[fieldName][jss::currency] = "USD";
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
checkErrorValue(
jrr,
"malformedAddress",
@@ -2056,8 +2084,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[fieldName][jss::accounts][1u] = alice.human();
jvParams[fieldName][jss::currency] = "USD";
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
checkErrorValue(jrr, "malformedRequest", "Cannot have a trustline to self.");
}
}
@@ -2158,7 +2185,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[jss::sponsorship][jss::sponsor] = alice.human();
jvParams[jss::sponsorship][jss::sponsee] = bob.human();
jvParams[jss::ledger_hash] = ledgerHash;
auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
auto const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Sponsorship);
BEAST_EXPECT(jrr[jss::node][sfOwner.jsonName] == alice.human());
BEAST_EXPECT(jrr[jss::node][sfSponsee.jsonName] == bob.human());
@@ -2169,8 +2196,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::sponsorship] = sponsorshipIndex;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Sponsorship);
BEAST_EXPECT(jrr[jss::node][sfOwner.jsonName] == alice.human());
BEAST_EXPECT(jrr[jss::node][sfSponsee.jsonName] == bob.human());
@@ -2210,8 +2236,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::ticket] = to_string(keylet::ticket(env.master, tkt1).key);
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
}
{
@@ -2220,8 +2245,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
tkt1.advanceBy(1);
jvParams[jss::ticket] = to_string(keylet::ticket(env.master, tkt1).key);
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Ticket);
BEAST_EXPECT(jrr[jss::node][sfTicketSequence.jsonName] == tkt1.value());
}
@@ -2233,8 +2257,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[jss::ticket][jss::account] = env.master.human();
jvParams[jss::ticket][jss::ticket_seq] = tkt1.value();
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(
jrr[jss::node][jss::index] == to_string(keylet::ticket(env.master, tkt1).key));
}
@@ -2246,8 +2269,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[jss::ticket][jss::account] = env.master.human();
jvParams[jss::ticket][jss::ticket_seq] = tkt1.value();
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
}
{
@@ -2255,8 +2277,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::ticket] = to_string(keylet::account(env.master).key);
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
checkErrorValue(jrr, "unexpectedLedgerType", "Unexpected ledger type.");
}
@@ -2385,8 +2406,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::did] = alice.human();
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][sfDIDDocument.jsonName] == strHex(std::string{"data"}));
BEAST_EXPECT(jrr[jss::node][sfURI.jsonName] == strHex(std::string{"uri"}));
}
@@ -2395,8 +2415,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::did] = env.master.human();
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
}
{
@@ -2507,8 +2526,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::mpt_issuance] = strHex(mptAlice.issuanceID());
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(jrr[jss::node][sfMPTokenMetadata.jsonName] == strHex(std::string{"123"}));
BEAST_EXPECT(jrr[jss::node][jss::mpt_issuance_id] == strHex(mptAlice.issuanceID()));
}
@@ -2517,8 +2535,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value jvParams;
jvParams[jss::mpt_issuance] = badMptID;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
}
{
@@ -2528,8 +2545,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[jss::mptoken][jss::account] = bob.human();
jvParams[jss::mptoken][jss::mpt_issuance_id] = strHex(mptAlice.issuanceID());
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
BEAST_EXPECT(
jrr[jss::node][sfMPTokenIssuanceID.jsonName] == strHex(mptAlice.issuanceID()));
}
@@ -2540,8 +2556,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
jvParams[jss::mptoken][jss::account] = bob.human();
jvParams[jss::mptoken][jss::mpt_issuance_id] = badMptID;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
json::Value const jrr = ledgerEntry(env, jvParams);
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
}
{
@@ -2657,23 +2672,20 @@ class LedgerEntry_test : public beast::unit_test::Suite
json::Value const& jv,
json::StaticString const& expectedType,
std::optional<std::string> const& expectedError = {}) {
auto const payload = jtx::rpcPayload(jv);
if (good)
{
BEAST_EXPECTS(
jv.isObject() && jv.isMember(jss::result) &&
!jv[jss::result].isMember(jss::error) &&
jv[jss::result].isMember(jss::node) &&
jv[jss::result][jss::node].isMember(sfLedgerEntryType.jsonName) &&
jv[jss::result][jss::node][sfLedgerEntryType.jsonName] == expectedType,
jv.isObject() && !payload.isMember(jss::error) && payload.isMember(jss::node) &&
payload[jss::node].isMember(sfLedgerEntryType.jsonName) &&
payload[jss::node][sfLedgerEntryType.jsonName] == expectedType,
to_string(jv));
}
else
{
BEAST_EXPECTS(
jv.isObject() && jv.isMember(jss::result) &&
jv[jss::result].isMember(jss::error) &&
!jv[jss::result].isMember(jss::node) &&
jv[jss::result][jss::error] == expectedError.value_or("entryNotFound"),
jv.isObject() && payload.isMember(jss::error) && !payload.isMember(jss::node) &&
payload[jss::error] == expectedError.value_or("entryNotFound"),
to_string(jv));
}
};
@@ -2798,7 +2810,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
params[jss::api_version] = 3;
auto const jv = env.rpc("json", "ledger_entry", to_string(params));
// Index is correct either way
BEAST_EXPECT(jv[jss::result][jss::index].asString() == hexKey);
BEAST_EXPECT(jtx::rpcPayload(jv)[jss::index].asString() == hexKey);
checkResult(good, jv, expectedType);
}
@@ -3045,12 +3057,93 @@ class LedgerEntry_test : public beast::unit_test::Suite
env.close();
// Request a check.
json::Value const jrr = env.rpc("ledger_entry", to_string(checkId.key))[jss::result];
json::Value const jrr = jtx::rpcPayload(env.rpc("ledger_entry", to_string(checkId.key)));
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Check);
BEAST_EXPECT(jrr[jss::node][sfSendMax.jsonName] == "100000000");
}
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 +3178,7 @@ public:
testFixed();
testHashes();
testCLI();
testMalformedTypedFields();
}
};
@@ -3176,6 +3270,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
+2 -1
View File
@@ -2,6 +2,7 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/envconfig.h>
@@ -280,7 +281,7 @@ public:
// the purpose in this test is to force the ledger expiration/out of
// date check to trigger
env.timeKeeper().adjustCloseTime(Weeks{3});
auto const result = env.rpc(apiVersion, "ledger_request", "1")[jss::result];
auto const result = test::jtx::rpcPayload(env.rpc(apiVersion, "ledger_request", "1"));
BEAST_EXPECT(result[jss::status] == "error");
if (apiVersion == 1)
{
+4 -3
View File
@@ -248,14 +248,15 @@ public:
params[jss::role] = "gateway";
params[jss::transactions] = "asdf";
auto lines = env.rpc("json", "noripple_check", to_string(params));
auto const lines =
test::jtx::rpcPayload(env.rpc("json", "noripple_check", to_string(params)));
if (apiVersion < 2u)
{
BEAST_EXPECT(lines[jss::result][jss::status] == "success");
BEAST_EXPECT(lines[jss::status] == "success");
}
else
{
BEAST_EXPECT(lines[jss::result][jss::error] == "invalidParams");
BEAST_EXPECT(lines[jss::error] == "invalidParams");
}
}
}
+86
View File
@@ -12,10 +12,12 @@
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/config/Constants.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ApiVersion.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/jss.h>
#include <memory>
#include <optional>
#include <string>
#include <utility>
@@ -73,11 +75,95 @@ public:
BEAST_EXPECT(warned && booted);
}
/**
* The load warning reaches the client on both transports at every
* version, at the place that version puts it. From API version 3 that is
* the top level of the specification envelope, which reserves no place
* for a notice and so carries it beside its own members. Below version 3
* it is beside `result` on the WebSocket transport and inside `result`
* on the JSON-RPC one.
*
* @param useWS True to test the WebSocket transport, false for JSON-RPC.
* @param apiVersion The API version to request the warning under.
*/
void
testWarningLocation(bool useWS, unsigned apiVersion)
{
testcase << "Load warning on " << (useWS ? "WS" : "HTTP") << " at API version "
<< apiVersion;
using namespace jtx;
Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->loadFromString(std::string("[") + Sections::kSigningSupport + "]\ntrue");
return noAdmin(std::move(cfg));
})};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
// invokeRaw is needed on the WebSocket transport: invoke rewrites a version 3 reply into
// the legacy envelope, which is the very difference under test.
auto const ws = useWS ? makeWSClient(env.app().config(), true, 2, {}, apiVersion) : nullptr;
auto const http = useWS ? nullptr : makeJSONRPCClient(env.app().config());
json::Value tx = json::ValueType::Object;
tx[jss::tx_json] = pay(alice, bob, XRP(1));
tx[jss::secret] = toBase58(generateSeed("alice"));
// A WebSocket request carries api_version in the same flat object, which makeWSClient adds.
if (!useWS)
tx[jss::api_version] = apiVersion;
auto const send = [&] -> std::optional<json::Value> {
if (useWS)
return ws->invokeRaw("sign", tx);
auto jv = http->invoke("sign", tx);
if (jv.isNull())
return std::nullopt;
return jv;
};
// Signing is charged heavily enough that a limited consumer is warned within a few calls.
// Stop at the first reply that carries a warning anywhere: that reply is the one under
// test, and continuing would eventually get the client booted instead.
std::optional<json::Value> warned;
for (int i = 0; i < 500 && !warned; ++i)
{
auto const reply = send();
// A booted client gets nothing back, which means no reply ever carried the warning.
if (!reply)
break;
if (reply->isMember(jss::warning) || (*reply)[jss::result].isMember(jss::warning))
warned = reply;
}
if (!BEAST_EXPECT(warned.has_value()))
return;
// NOLINTNEXTLINE(bugprone-unchecked-optional-access) checked above
json::Value const& reply = *warned;
if (apiVersion >= rpc::kApiMinimumSpecVersion || useWS)
{
BEAST_EXPECT(reply[jss::warning] == jss::load);
BEAST_EXPECT(!reply[jss::result].isMember(jss::warning));
}
else
{
BEAST_EXPECT(reply[jss::result][jss::warning] == jss::load);
BEAST_EXPECT(!reply.isMember(jss::warning));
}
}
void
run() override
{
testOverload(false /* http */);
testOverload(true /* ws */);
for (auto const useWS : {false, true})
{
for (auto const apiVersion : {1u, 2u, 3u})
testWarningLocation(useWS, apiVersion);
}
}
};
+392
View File
@@ -32,8 +32,10 @@
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/JsonRpc.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
@@ -2172,6 +2174,392 @@ public:
wscB->invoke("unsubscribe", accountsRequest({alice.human()}));
}
/**
* A reply to a request follows the specification envelope on the
* WebSocket transport as well, so an API version selects one shape
* whichever transport carries it. The reply keeps a `type` member, which
* is how a client tells a reply apart from the asynchronous messages a
* session also receives, and which the specification has no place for.
*/
void
testWebsocketSpecEnvelope()
{
testcase("WebSocket replies conform to JSON-RPC 2.0 from API version 3");
using namespace std::chrono_literals;
using namespace jtx;
Env env{*this, singleThreadIo(envconfig())};
// invokeRaw returns the message as it arrived, before the client normalizes it, so the
// envelope itself can be asserted.
auto const rawReply = [&](unsigned apiVersion) -> std::optional<json::Value> {
auto wsc = makeWSClient(env.app().config(), true, 2, {}, apiVersion);
json::Value req;
req[jss::api_version] = apiVersion;
return wsc->invokeRaw("ledger_closed", req);
};
for (auto const version : {1u, 2u})
{
auto const msg = rawReply(version);
if (BEAST_EXPECTS(msg.has_value(), std::to_string(version)))
{
// NOLINTNEXTLINE(bugprone-unchecked-optional-access) checked above
json::Value const& reply = *msg;
BEAST_EXPECTS(reply[jss::type] == jss::response, std::to_string(version));
// The legacy envelope keeps `status` at the top level. It cannot be told apart by
// `jsonrpc`, which it echoes back from the request the client sent.
BEAST_EXPECTS(reply.isMember(jss::status), std::to_string(version));
BEAST_EXPECTS(
!reply.isMember(jss::result) || !reply[jss::result].isMember(jss::error),
std::to_string(version));
}
}
{
auto const msg = rawReply(3u);
if (BEAST_EXPECT(msg.has_value()))
{
// NOLINTNEXTLINE(bugprone-unchecked-optional-access) checked above
json::Value const& reply = *msg;
BEAST_EXPECT(reply[jss::jsonrpc] == rpc::kJsonRpcVersion);
// The client sends `id: 5` in its JSON-RPC form, and the reply echoes it.
BEAST_EXPECT(reply.isMember(jss::id) && reply[jss::id] == 5);
BEAST_EXPECT(reply.isMember(jss::result));
// No `status` at either level: `result` versus `error` already says which it was.
BEAST_EXPECT(!reply.isMember(jss::status));
BEAST_EXPECT(!reply[jss::result].isMember(jss::status));
BEAST_EXPECT(reply[jss::type] == jss::response);
}
}
// The client's `command` form names no id, and the reply names a null one rather than
// leaving the member out.
{
auto wsc = makeWSClient(env.app().config(), true, 1, {}, 3u);
json::Value req;
req[jss::api_version] = 3u;
auto const msg = wsc->invokeRaw("ledger_closed", req);
if (BEAST_EXPECT(msg.has_value()))
{
// NOLINTNEXTLINE(bugprone-unchecked-optional-access) checked above
json::Value const& reply = *msg;
BEAST_EXPECT(reply[jss::jsonrpc] == rpc::kJsonRpcVersion);
BEAST_EXPECT(reply.isMember(jss::id) && reply[jss::id].isNull());
BEAST_EXPECT(reply.isMember(jss::result));
}
}
{
auto wsc = makeWSClient(env.app().config(), true, 2, {}, 3u);
json::Value req;
req[jss::api_version] = 3u;
req[jss::account] = "bogus";
auto const msg = wsc->invokeRaw("account_info", req);
if (BEAST_EXPECT(msg.has_value()))
{
// NOLINTNEXTLINE(bugprone-unchecked-optional-access) checked above
json::Value const& reply = *msg;
BEAST_EXPECT(reply[jss::jsonrpc] == rpc::kJsonRpcVersion);
BEAST_EXPECT(reply[jss::error][jss::code] == rpc::kJsonRpcServerError);
BEAST_EXPECT(reply[jss::error][jss::data][jss::error] == "actMalformed");
BEAST_EXPECT(reply[jss::error][jss::data][jss::error_code] == RpcActMalformed);
BEAST_EXPECT(!reply.isMember(jss::status));
}
}
// `path_find` is the one handler that sets `status` inside its result, so it is the only
// command that can show the member is dropped there too.
{
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
env.close();
auto wsc = makeWSClient(env.app().config(), true, 2, {}, 3u);
json::Value create;
create[jss::api_version] = 3u;
create[jss::subcommand] = "create";
create[jss::source_account] = alice.human();
create[jss::destination_account] = bob.human();
create[jss::destination_amount] = "100000000";
wsc->invokeRaw("path_find", create);
json::Value status;
status[jss::api_version] = 3u;
status[jss::subcommand] = "status";
auto const msg = wsc->invokeRaw("path_find", status);
if (BEAST_EXPECT(msg.has_value()))
{
// NOLINTNEXTLINE(bugprone-unchecked-optional-access) checked above
json::Value const& reply = *msg;
BEAST_EXPECT(reply.isMember(jss::result));
BEAST_EXPECT(!reply.isMember(jss::status));
BEAST_EXPECT(!reply[jss::result].isMember(jss::status));
}
}
}
/**
* 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;
};
// Read through rpcPayload: the version 3 reply arrives in the specification envelope.
auto const first = rpcPayload(env.rpc("json", "subscribe", to_string(subscribeUrl(3u))));
BEAST_EXPECTS(first[jss::status] == jss::success, to_string(first));
auto const second = rpcPayload(env.rpc("json", "subscribe", to_string(subscribeUrl(1u))));
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 = rpcPayload(env.rpc("json", "subscribe", to_string(subscribeUrl(1u))));
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 +2865,10 @@ public:
testMPTReSubscribeNotOvercounted();
testMPTSharesCapWithAccounts();
testMPTUnsubscribeFreesCap();
testWebsocketSpecEnvelope();
testOneApiVersionPerConnection();
testOneApiVersionPerUrl();
testSubscribeToConsensusAndPeerStatus();
}
};
+3 -2
View File
@@ -1,5 +1,6 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/envconfig.h>
#include <test/jtx/multisign.h>
@@ -785,7 +786,7 @@ class Transaction_test : public beast::unit_test::Suite
params[jss::transaction] = to_string(txn->getTransactionID());
params[jss::binary] = false;
params[jss::api_version] = apiVersion;
return env.client().invoke("tx", params);
return test::jtx::rpcLegacyReply(env.client().invoke("tx", params));
}(),
};
@@ -853,7 +854,7 @@ class Transaction_test : public beast::unit_test::Suite
params[jss::transaction] = to_string(txn->getTransactionID());
params[jss::binary] = true;
params[jss::api_version] = apiVersion;
return env.client().invoke("tx", params);
return test::jtx::rpcLegacyReply(env.client().invoke("tx", params));
}();
if (BEAST_EXPECT(result[jss::status] == "success"))
+16 -15
View File
@@ -128,21 +128,22 @@ class Version_test : public beast::unit_test::Suite
using namespace test::jtx;
Env env{*this};
auto const withoutApiVerion = std::string("{ ") +
"\"jsonrpc\": \"2.0\", "
"\"ripplerpc\": \"2.0\", "
"\"id\": 5, "
"\"method\": \"version\", "
"\"params\": {}}";
auto const withApiVerion = std::string("{ ") +
"\"jsonrpc\": \"2.0\", "
"\"ripplerpc\": \"2.0\", "
"\"id\": 6, "
"\"method\": \"version\", "
"\"params\": { "
"\"api_version\": " +
std::to_string(rpc::kApiMaximumSupportedVersion) + "}}";
auto re = env.rpc("json2", '[' + withoutApiVerion + ", " + withApiVerion + ']');
// Both entries name the same version: the command-line client fills kApiCommandLineVersion
// into an entry naming none, and one batch is served at one version, so an entry naming
// none beside one naming another version would be a mixed body and be refused whole.
auto const entry = [](int id) {
return std::string("{ ") +
"\"jsonrpc\": \"2.0\", "
"\"ripplerpc\": \"2.0\", "
"\"id\": " +
std::to_string(id) +
", "
"\"method\": \"version\", "
"\"params\": { "
"\"api_version\": " +
std::to_string(rpc::kApiMaximumSupportedVersion) + "}}";
};
auto re = env.rpc("json2", '[' + entry(5) + ", " + entry(6) + ']');
if (!BEAST_EXPECT(re.isArray()))
return;
File diff suppressed because it is too large. Load diff
+44
View File
@@ -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();
}
}
+105
View File
@@ -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>");
}
+221 -91
View File
@@ -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
+6 -1
View File
@@ -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)
+17 -1
View File
@@ -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
+4 -3
View File
@@ -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);
+12 -3
View File
@@ -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
+7
View File
@@ -2148,6 +2148,13 @@ rpcClient(
{
nRet = std::stoi(jvOutput[jss::error][jss::error_code].asString());
}
else if (
jvOutput[jss::error].isMember(jss::data) &&
jvOutput[jss::error][jss::data].isMember(jss::error_code))
{
// The specification envelope, from API version 3, carries the XRPL code in `data`.
nRet = std::stoi(jvOutput[jss::error][jss::data][jss::error_code].asString());
}
else
{
nRet = RpcBadSyntax;
File diff suppressed because it is too large. Load diff
+21 -18
View File
@@ -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);
+12
View File
@@ -69,6 +69,18 @@ static constexpr int kMaxJobQueueClients = 500;
constexpr auto kMaxValidatedLedgerAge = std::chrono::minutes{2};
static constexpr int kMaxRequestSize = 1000000;
/**
* Maximum number of entries in a batch, in either of its two forms, from API
* version 3 up.
*
* A batch dispatches one command per entry, and an entry can be small, so the
* body size limit bounds the bytes rather than the work.
*
* Below version 3 the `method: "batch"` form is bounded by the body size limit
* and the resource drop threshold only.
*/
static constexpr unsigned kMaxBatchEntries = 100;
/**
* Maximum number of pages in one response from a binary LedgerData request.
*/
+72 -75
View File
@@ -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(
+11 -3
View File
@@ -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")
+62 -16
View File
@@ -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();
}