Compare commits

...
Author SHA1 Message Date
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
Bart 79a8b4849e fix: Give handler-specific RPC errors a code and message
Thirteen sites across five handlers assign `jss::error` a bare token,
skipping the `error_code`/`error_message` pair `rpc::injectError` sets.
Both consequences are visible on the wire: with no `error_code` the HTTP
status defaults to 200, so a load balancer sees success for a failed
request, and the version 2 envelope copies the pair unconditionally, so a
missing source produces an explicit `"code":null`/`"message":null`. Give
those tokens rows of their own, codes 100 to 109, and route every site
through `injectError`, preserving the `error_exception` detail `submit`
and `simulate` attach. `transaction_entry` keeps its four errors in the
handler's output as an rpc-spec `Status`, and `writeResult` reports each
through the status bridge, so the code and message land beside the ledger
fields the reply carries.

The `ledger_entry` helpers still report `invalidParams` (31) rather than
each token's own code. A version 1 or 2 client has read 31 for those
tokens for years, so changing it would break a client matching on the old
value; a comment on the helpers says so. `checkErrorValue` checks
`error_code` beside the token and the message, pinning each token's code
and failing on a token it does not know. The server test reaches a coded
error on four of the five handlers. `ledger_accept` reports
`notStandAlone` only on a networked server that is synced, which no
test harness provides, so that arm joins the test list below.

The `submit` and `simulate` arms reporting an internal error take no
coverage exclusion. Those arms are live, reached once
`NetworkOPs::processTransaction` or `Transaction::getJson` throws, and no
injection seam exists today, so the gap belongs in the test list rather
than behind a marker that hides it. Two more exclusions in `Simulate.cpp`
go, on arms that are live as well: the `Account` type check, which a
numeric `Account` reaches and a new `simulate` case sends, and the
fallback `engine_result` arm, which gets a comment saying why it stays.
2026-10-14 18:41:58 +09:00
Bart 49410ad867 fix: Give every error code an HTTP status
Four rows of the error table name no HTTP status, so the `ErrorInfo`
constructor defaults them to 200. The `ripplerpc: "3.0"` envelope derives
the status from the code, so a reply reporting an error claims success
and anything reading the status, a load balancer above all, reads success
too. Give all four one: `actNotFound` answers 404, matching every
`*NotFound` sibling but `entryNotFound`, and `actMalformed`,
`alreadyMultisig` and `alreadySingleSig` answer 400. Then drop the
constructor that defaulted a status, so no row can omit one again. A gtest
lists by hand the codes that have no row, so an enumerator added without
one fails it, and asserts that every other code names a status other than
200.

No client reads a new status here. `legacyHttpStatus` reports 200 for
exactly those four rows, so the 3.0 envelope answers what it always has.
That list is closed, naming the rows that had no status of their own, so
a row added later reports whatever the table says. The test suite names
the two statuses it compares against most, 200 and 400, as `kOk` and
`kBadRequest`, and every existing assertion on them uses the name.
2026-10-14 10:14:03 +09:00
Bart 3689dca688 style: Realign the error table
The columns of the error table had drifted apart as rows were added, so a reader scanning it follows a ragged edge and a new row has no alignment to copy. Realign all four columns on one set of widths inside the existing `clang-format off` guard, changing no row's content.
2026-10-14 10:14:02 +09:00
Bart 671a00b1ac fix: Mask every credential the server echoes, logs or prints
An error reply echoes the request that caused it, and masking covers four
fields at the top level of an object only. A `secret` nested inside
`params`, where the JSON-RPC transport puts it, comes back in the clear,
as does every other credential field at any depth, and only two sites
mask at all. Collect the names in one list and mask recursively, so
nesting stops mattering and a new field is added once. The list covers
the six names only a reply carries, which is how `wallet_propose` and
`validation_create` wrote a live key to the log; `validation_key` is the
same seed as `validation_seed` in RFC1751 words.

A log is an echo that outlives the reply, so one `loggable()` helper
masks and truncates together and every site that writes a request or a
reply out uses it, the `[rpc_startup]` command and its result included,
and the command line client logs the reply it receives parsed and
masked where it wrote the raw body, a `validation_create` answer among
them, and caps a body it cannot parse at the same length. An
`[rpc_startup]` line that does not parse was echoed raw at fatal, the
one case masking cannot reach, and then run as whatever the parser had
left; it is now reported by position and parser message only, and
skipped. The `HTTP Reply` trace line in libxrpl, which cannot reach the
masker, now carries the status only; the body is logged beside it at
debug, masked when it carries a credential and otherwise as the string
already built for the wire, so a reply with no credential is serialized
once. No site logs an inbound request body uncapped, so the method name,
bounded by the request size limit, is the one thing a client chooses the
length of in the log. The request-duration line used to climb to warn
and error for a slow request with the request in it; the duration alone
still climbs, and the request stays at debug, so the duration line never
lifts text an anonymous client wrote to the default severity.

Three changes are visible to a caller. A credential in an echoed request
reads `<masked>` wherever it appears, nested inside `params` or a batch
entry too. The command line client masks `request_sent`, which carries
the `admin_password` it copies out of the config, so a failing
`./xrpld account_info rBogus` no longer prints a credential the operator
never typed. And a WebSocket frame that does not parse is answered with
its `size` rather than its body.
2026-10-14 10:14:02 +09:00
Bart 9096ec03a7 refactor: Declare the JSON-RPC and ripplerpc version constants
`ripplerpc`, which selects the reply envelope and accepts three values,
and `jsonrpc`, which names the JSON-RPC protocol version, are spelled as
literals at every call site, so neither field has one place stating what
it accepts. Declare `kRippleRpcVersion1/2/3` beside the `api_version`
constants in ApiVersion.h and `kJsonRpcVersion` in a new
protocol/JsonRpc.h, and use them at every production site that reads
a value: the one in RPCCall.cpp that reads the pair, and the three in
ServerHandler.cpp that default, compare and gate on `ripplerpc`. Every
test site spelling a value as a C++ expression follows. A literal inside
a JSON string fixture is left alone, since substituting there means
assembling the JSON by concatenation. The three `ripplerpc` constants
are declared as one set, so the header states every value the field
accepts, and each has a production user in ServerHandler.cpp.

The two fields keep separate constants and separate headers although
both spell "2.0" today. The specification fixes `jsonrpc` at that value
while `ripplerpc` accepts three, and the `ripplerpc` constants go when
support for API versions 1 and 2 goes, where JSON-RPC is a protocol this
server keeps speaking.

ApiVersion.h's header comment named five constants by unprefixed
spellings that no longer exist, and read as a complete map of the file's
version constants, which it stops being here. Two jtx helpers,
`hasEnvelope2` and `setEnvelope2` in TestHelpers.h, replace the
assertion pair and the request pair that every rewritten test site
repeated. No value changes, on the wire or in a test.
2026-10-14 10:14:02 +09:00
Bart 5fec493960 refactor: Let json::Value be compared and constructed from a string view
`json::Value` accepts `char const*`, `std::string` and
`json::StaticString`, so a caller holding a `std::string_view` has to
materialize a `std::string` whose characters are then copied a second
time into the value's own storage. Add the missing constructor, which
the `std::string` one delegates to, and an `operator==` that reads the
value's characters in place.

The comparison is constrained to `std::string_view` exactly rather than
taking a view by plain overload. A string literal converts equally well
to a view and to a Value, so a plain overload makes every
`value == "literal"` ambiguous, and a literal `0`, which converts to a
view through `char const*` as well as to a Value, with it.

No reply changes. The two spellings differ only for a view holding an
embedded NUL, which the Value comparison stops at. The `method: "batch"`
check in `ServerHandler.cpp` is the operator's first production user, so
that comparison stops building a `Value` from the literal on every
request.
2026-10-13 18:45:16 +09:00
Bart a6a4fff2aa refactor: Remove a publish loop nothing can enter
`pubProposedAccountTransaction` declares an `accountHistoryNotify` vector,
never inserts into it, and then tests it and iterates it. The vector is a
local, so the proof is the function itself: between the declaration and the
loop there are exactly two mentions of it, the condition and the loop, and
neither adds an element. Its sibling `pubAccountTransaction` fills its own
copy, which is what makes the empty one here read as live code.

The compiler corroborates it: with the loop gone the message becomes
`MultiApiJson const`, which is possible only because that loop was its sole
mutator. Nothing is lost with the assertion above the loop either. It held
that a `transJson` result carries no member named `account_history_tx_stream`.
No code writes one: outside the two assertions, the token appears in the
`subscribe` and `unsubscribe` request handlers only. The identical assertion
stays where the loop it guards is live.

No subscriber sees a difference. An account-history subscription is
registered in `subAccountHistory_` alone, so it was never in the map this
function reads, and it receives its transactions from
`pubAccountTransaction`. The same function's guard on three subscription maps
also goes: an earlier return leaves `subRTAccount_` non-empty, so the
condition cannot be false.
2026-10-13 18:45:15 +09:00
48 changed files with 4244 additions and 778 deletions

No files matched your search

@@ -174,8 +174,10 @@ test.server > xrpl.basics
test.server > xrpl.config
test.server > xrpld.app
test.server > xrpld.core
test.server > xrpld.rpc
test.server > xrpl.json
test.server > xrpl.protocol
test.server > xrpl.resource
test.server > xrpl.server
test.unit_test > xrpl.basics
test.unit_test > xrpl.protocol
+24
View File
@@ -22,6 +22,30 @@ API version 2 is available in `xrpld` version 2.0.0 and later. See [API-VERSION-
This version is supported by all `xrpld` versions. For WebSocket and HTTP JSON-RPC requests, it is currently the default API version used when no `api_version` is specified.
## Unreleased
### Breaking changes
- The `ripplerpc` request field, which selects the shape of the JSON-RPC reply envelope, is now validated, and a value that is not exactly `"1.0"`, `"2.0"` or `"3.0"` is rejected. This reaches the JSON-RPC transport at every API version. It does not reach a WebSocket session, which reads the field only to echo it back. A request sending `"2"`, `" 2.0"`, `"2.00"`, `"02.0"`, `"2.0.0"`, `"10.0"` or any other text, such as `"abc"` or `"x2"`, gets a working reply today. After this ships, such a request sent alone gets HTTP 400 with `ripplerpc is not a supported version`, charged as a malformed request, and such an entry of a `"method": "batch"` body gets that error in its own reply while the batch answers 200. A client that spells the version loosely must therefore be corrected to one of the three exact values, or omit the field. Previously the value was compared as a string, which both accepted values that name no version and ordered multi-digit versions incorrectly: `"abc"` and `"x2"` sorted above `"3.0"` and so selected version 3, and `"10.0"` sorted below `"2.0"` and so selected version 1. Requests that send one of the three supported values, or omit the field, are unaffected.
### 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`.
### 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`.
- A reply reporting HTTP 402 or 502 now carries a status line. Those two statuses named no case in the switch that writes one, so such a reply began with a header instead and did not parse as an HTTP response at all. Both are reachable at any API version with `ripplerpc: "3.0"`, which derives the status from the error code: 402 through `highFee` from `sign`, `sign_for` or `submit` with a low `fee_mult_max`, and 502 through `dbDeserialization` from `tx`. The eleven statuses that already named a case report the same phrase they always have.
- An error reply to a request sending `ripplerpc: "2.0"` or `"3.0"` no longer carries a stray `"error_message": null` beside the error it reports. The member appeared only when the `Server` log partition was set to debug or lower, because the log statement read `error_message` after the reply had renamed it to `message`, and reading it put it back as null. So the reply a client received depended on the server's log level, and the log line itself printed an empty message. Both are fixed.
- A body the server rejects before it reads a request out of it now says which of four things was wrong. A body over the size limit answers `Request is too large`. A body that parses to `{}`, `[]` or `null` answers `Request is empty`. A body that parses to a non-empty array answers `Request is not a JSON object`; that is the only other document the parser accepts at the top level. All three previously answered `Unable to parse request: ` with nothing after the colon, the parser having recorded no error for them. Any other body does not parse, which includes one that is only whitespace and one whose top-level value is a string, number or boolean; it answers `Unable to parse request: ` followed by the parser's own reason, as it did before. The status is 400 for all four, as before, and this reaches every API version.
- `batch`: An entry that is not identified through a secure gateway no longer clears the connection's `X-User` and forwarded-for values for the entries after it, so every entry of one body reports the role and username it would have reported on its own.
- `batch`: Every entry of a `"method": "batch"` request is now charged against the sender's resource allowance, including one rejected before it reaches a handler, and the request stops at the first entry the connection is too loaded to serve. A reply array can therefore be shorter than the request array, and its last element depends on where the batch stopped. An entry that is not a JSON object, or names an API version the server does not serve, is charged before its role is known and answered with its own rejection; if that charge took the connection over the drop threshold, the batch stops there and that rejection is the last answer. Every other entry met over the threshold is answered `Server is overloaded` and nothing follows. A client should read any reply array shorter than its request as a connection over the drop threshold, whatever the last element says. A well-behaved client is unaffected; one that sends thousands of entries in a single body no longer gets every one of them answered.
- A body the server rejects before it reads a request out of it is now charged against the sender's resource allowance, as a malformed request already was. Five conditions were free: a body over the size limit, one that does not parse, one carrying no document, one that is not a JSON object, and a `"method": "batch"` naming no entry array. The answer to each is unchanged. A well-behaved client is unaffected; one that repeats such a body exhausts its allowance and is refused the next request a handler would have served. A WebSocket frame that does not parse, or exceeds the request size limit, is charged the same way, and a connection that sends only such frames is closed once it crosses the drop threshold.
## XRP Ledger server version 3.5.0
Version 3.5.0 is not yet released.
+37
View File
@@ -3,11 +3,13 @@
#include <xrpl/basics/Number.h>
#include <xrpl/json/json_forwards.h>
#include <concepts>
#include <cstring>
#include <iterator>
#include <limits>
#include <map>
#include <string>
#include <string_view>
#include <vector>
/**
@@ -198,6 +200,15 @@ public:
*/
Value(StaticString const& value);
Value(std::string const& value);
/**
* @brief Constructs a value from a string view.
*
* The characters are copied, so the view need not outlive the call and need
* not be NUL-terminated.
*
* @param value The characters to copy.
*/
Value(std::string_view value);
Value(bool value);
Value(Value const& other);
~Value();
@@ -472,6 +483,32 @@ toJson(xrpl::Number const& number)
bool
operator==(Value const&, Value const&);
/**
* Compares a value with a string view, reading the value's characters in place
* rather than building a Value from the view.
*
* Constrained to the exact type: a string literal converts equally well to a
* view and to a Value, so a plain overload makes `value == "literal"`
* ambiguous.
*
* @param x The value to compare.
* @param y The characters to compare it against.
* @return Whether `x` is a string whose characters up to its first NUL are
* exactly the characters of `y`.
*/
template <class T>
requires std::same_as<T, std::string_view>
bool
operator==(Value const& x, T y)
{
if (!x.isString())
return false;
// A string `Value` can hold a null pointer, which names no characters, so it equals no view.
char const* const s = x.asCString();
return s != nullptr && std::string_view{s} == y;
}
bool
operator<(Value const&, Value const&);
+17 -17
View File
@@ -6,31 +6,21 @@
#include <xrpl/protocol/jss.h>
#include <cstddef>
#include <string_view>
#include <type_traits>
#include <utility>
namespace xrpl {
/**
* API version numbers used in later API versions
* The `api_version` numbers this server serves.
*
* Requests with a version number in the range
* [apiMinimumSupportedVersion, apiMaximumSupportedVersion]
* are supported.
* A request naming a version in [kApiMinimumSupportedVersion,
* kApiMaximumSupportedVersion] is served. With `[beta_rpc_api]` set to `1` in
* the config the range extends to kApiBetaVersion.
*
* If [beta_rpc_api] is enabled in config, the version numbers
* in the range [apiMinimumSupportedVersion, apiBetaVersion]
* are supported.
*
* Network Requests without explicit version numbers use
* apiVersionIfUnspecified. apiVersionIfUnspecified is 1,
* because all the RPC requests with a version >= 2 must
* explicitly specify the version in the requests.
* Note that apiVersionIfUnspecified will be lower than
* apiMinimumSupportedVersion when we stop supporting API
* version 1.
*
* Command line Requests use apiCommandLineVersion.
* A request naming no version is served at kApiVersionIfUnspecified, which is 1
* because a request wanting any later version states it.
*/
namespace rpc {
@@ -57,6 +47,16 @@ static_assert(kApiMaximumSupportedVersion >= kApiMinimumSupportedVersion);
static_assert(kApiBetaVersion >= kApiMaximumSupportedVersion);
static_assert(kApiMaximumValidVersion >= kApiMaximumSupportedVersion);
/**
* Values accepted in the `ripplerpc` request field, which selects the shape of
* the JSON-RPC reply envelope. Distinct from `kJsonRpcVersion` in JsonRpc.h,
* which names the JSON-RPC protocol itself, and from the `api_version`
* constants above, which select the content of the response.
*/
inline constexpr std::string_view kRippleRpcVersion1{"1.0"};
inline constexpr std::string_view kRippleRpcVersion2{"2.0"};
inline constexpr std::string_view kRippleRpcVersion3{"3.0"};
inline void
setVersion(json::Value& parent, unsigned int apiVersion, bool betaEnabled)
{
+62 -6
View File
@@ -3,6 +3,7 @@
#include <xrpl/json/json_value.h>
#include <string>
#include <string_view>
namespace xrpl {
@@ -144,7 +145,55 @@ enum ErrorCodeI {
RpcEntryNotFound = 98,
RpcUnexpectedLedgerType = 99,
RpcLast = RpcUnexpectedLedgerType, // rpcLAST should always equal the last code.
// submit + simulate
RpcInvalidTransaction = 100,
RpcInternalSubmit = 101,
RpcInternalJson = 102,
RpcInternalSimulate = 103,
// transaction_entry
RpcFieldNotFoundTransaction = 104,
RpcNotYetImplemented = 105,
RpcTransactionNotFound = 106,
// transaction_entry
RpcMalformedRequest = 107,
// ledger_accept
RpcNotStandAlone = 108,
// ledger_entry, API version 1 only
RpcUnknownOption = 109,
// 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,
// RpcLast should always equal the last code.
RpcLast = RpcMalformedXChainOwnedCreateAccountClaimID,
};
/**
@@ -180,11 +229,6 @@ struct ErrorInfo
{
}
constexpr ErrorInfo(ErrorCodeI code, char const* token, char const* message)
: code(code), token(token), message(message), httpStatus(200)
{
}
constexpr ErrorInfo(ErrorCodeI code, char const* token, char const* message, int httpStatus)
: code(code), token(token), message(message), httpStatus(httpStatus)
{
@@ -202,6 +246,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.
*/
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <xrpl/json/json_forwards.h>
#include <string_view>
namespace xrpl::rpc {
/**
* Constants of the JSON-RPC 2.0 protocol itself.
*
* Kept apart from the `api_version` and `ripplerpc` constants in ApiVersion.h,
* which go when support for API versions 1 and 2 goes.
*/
/**
* Value of the `jsonrpc` member of a request and of its reply, fixed at "2.0"
* by the JSON-RPC specification.
*/
inline constexpr std::string_view kJsonRpcVersion{"2.0"};
/**
* Codes for the `code` member of a JSON-RPC error object.
*
* The specification reserves -32768 to -32000 for the protocol and leaves
* -32000 to -32099 of it to the implementation.
*
* kJsonRpcServerError is the code for an error an XRPL handler reports.
*
* The codes from kJsonRpcServerOverloaded on lie outside the
* implementation-defined sub-range, which the specification does not allow.
* They are the codes every shipped version reports, so moving one breaks the
* clients matching on it.
*/
inline constexpr json::Int kJsonRpcServerError{-32000};
inline constexpr json::Int kJsonRpcInvalidRequest{-32600};
inline constexpr json::Int kJsonRpcMethodNotFound{-32601};
inline constexpr json::Int kJsonRpcInvalidParams{-32602};
inline constexpr json::Int kJsonRpcServerOverloaded{-32604};
inline constexpr json::Int kJsonRpcForbidden{-32605};
inline constexpr json::Int kJsonRpcWrongVersion{-32606};
} // namespace xrpl::rpc
+2 -2
View File
@@ -245,7 +245,7 @@ JSS(ephemeral_key); // out: ValidatorInfo
JSS(error); // out: error
JSS(errored); //
JSS(error_code); // out: error
JSS(error_exception); // out: Submit
JSS(error_exception); // out: Submit, Simulate
JSS(error_message); // out: error
JSS(expand); // in: handler/Ledger
JSS(expected_date); // out: any (warnings)
@@ -559,7 +559,7 @@ JSS(signing_key); // out: NetworkOPs
JSS(signing_keys); // out: ValidatorList
JSS(signing_time); // out: NetworkOPs
JSS(signer_lists); // in/out: AccountInfo
JSS(size); // out: get_aggregate_price
JSS(size); // out: get_aggregate_price, ServerHandler
JSS(snapshot); // in: Subscribe
JSS(source_account); // in: PathRequest, RipplePathFind
JSS(source_amount); // in: PathRequest, RipplePathFind
+14
View File
@@ -7,6 +7,20 @@
namespace xrpl {
/**
* Writes an HTTP reply carrying @p strMsg with status @p nStatus to @p output,
* and logs the status at trace. The body is not logged here: it may carry a
* 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.
* @param output Where the reply bytes are written.
* @param j The journal the status is logged to.
*/
void
httpReply(int nStatus, std::string const& strMsg, json::Output const&, beast::Journal j);
+8 -3
View File
@@ -15,6 +15,7 @@
#include <cstring>
#include <limits>
#include <string>
#include <string_view>
#include <utility>
namespace json {
@@ -241,10 +242,14 @@ Value::Value(xrpl::Number const& value) : type_(ValueType::String), allocated_(t
value_.stringVal = valueAllocator()->duplicateStringValue(tmp.c_str(), tmp.length());
}
Value::Value(std::string const& value) : type_(ValueType::String), allocated_(true)
Value::Value(std::string const& value) : Value(std::string_view{value})
{
value_.stringVal =
valueAllocator()->duplicateStringValue(value.c_str(), (unsigned int)value.length());
}
Value::Value(std::string_view value) : type_(ValueType::String), allocated_(true)
{
value_.stringVal = valueAllocator()->duplicateStringValue(
value.data(), static_cast<unsigned int>(value.length()));
}
Value::Value(StaticString const& value) : type_(ValueType::String)
+190 -77
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 {
@@ -20,88 +22,122 @@ namespace detail {
// will remain in the object file. But the string literals will remain.
//
// There's a certain amount of tension in determining the correct HTTP
// status to associate with a given RPC error. Initially all RPC errors
// returned 200 (OK). And that's the default behavior if no HTTP status code
// is specified below.
// status to associate with a given RPC error. Every row below names its
// HTTP status, and a row that omits it does not compile, since ErrorInfo has
// no constructor that leaves the status out.
//
// The codes currently selected target the load balancer fail-over use case.
// If a query fails on one node but is likely to have a positive outcome
// on a different node, then the failure should return a 4xx/5xx range
// status code.
// The rows below are aligned by hand into four columns, so that a wrong status or a mistyped token
// is visible by scanning one column. That cannot be had inside the 100-column limit once the
// longest enumerator and the longest message sit in one row. So this region is exempt from the
// limit by design rather than by oversight.
// clang-format off
static constexpr ErrorInfo kUnorderedErrorInfos[]{
{RpcActMalformed, "actMalformed", "Account malformed."},
{RpcActNotFound, "actNotFound", "Account not found."},
{RpcAlreadyMultisig, "alreadyMultisig", "Already multisigned."},
{RpcAlreadySingleSig, "alreadySingleSig", "Already single-signed."},
{RpcAmendmentBlocked, "amendmentBlocked", "Amendment blocked, need upgrade.", 503},
{RpcExpiredValidatorList, "unlBlocked", "Validator list expired.", 503},
{RpcAtxDeprecated, "deprecated", "Use the new API or specify a ledger range.", 400},
{RpcBadKeyType, "badKeyType", "Bad key type.", 400},
{RpcBadFeature, "badFeature", "Feature unknown or invalid.", 500},
{RpcBadIssuer, "badIssuer", "Issuer account malformed.", 400},
{RpcBadMarket, "badMarket", "No such market.", 404},
{RpcBadSecret, "badSecret", "Secret does not match account.", 403},
{RpcBadSeed, "badSeed", "Disallowed seed.", 403},
{RpcBadSyntax, "badSyntax", "Syntax error.", 400},
{RpcChannelMalformed, "channelMalformed", "Payment channel is malformed.", 400},
{RpcChannelAmtMalformed, "channelAmtMalformed", "Payment channel amount is malformed.", 400},
{RpcCommandMissing, "commandMissing", "Missing command entry.", 400},
{RpcDbDeserialization, "dbDeserialization", "Database deserialization error.", 502},
{RpcDstActMalformed, "dstActMalformed", "Destination account is malformed.", 400},
{RpcDstActMissing, "dstActMissing", "Destination account not provided.", 400},
{RpcDstActNotFound, "dstActNotFound", "Destination account not found.", 404},
{RpcDstAmtMalformed, "dstAmtMalformed", "Destination amount/currency/issuer is malformed.", 400},
{RpcDstAmtMissing, "dstAmtMissing", "Destination amount/currency/issuer is missing.", 400},
{RpcDstIsrMalformed, "dstIsrMalformed", "Destination issuer is malformed.", 400},
{RpcExcessiveLgrRange, "excessiveLgrRange", "Ledger range exceeds 1000.", 400},
{RpcForbidden, "forbidden", "Bad credentials.", 403},
{RpcHighFee, "highFee", "Current transaction fee exceeds your limit.", 402},
{RpcInternal, "internal", "Internal error.", 500},
{RpcInvalidLgrRange, "invalidLgrRange", "Ledger range is invalid.", 400},
{RpcInvalidParams, "invalidParams", "Invalid parameters.", 400},
{RpcInvalidHotwallet, "invalidHotWallet", "Invalid hotwallet.", 400},
{RpcIssueMalformed, "issueMalformed", "Issue is malformed.", 400},
{RpcJsonRpc, "json_rpc", "JSON-RPC transport error.", 500},
{RpcLgrIdxsInvalid, "lgrIdxsInvalid", "Ledger indexes invalid.", 400},
{RpcLgrIdxMalformed, "lgrIdxMalformed", "Ledger index malformed.", 400},
{RpcLgrNotFound, "lgrNotFound", "Ledger not found.", 404},
{RpcLgrNotValidated, "lgrNotValidated", "Ledger not validated.", 202},
{RpcMasterDisabled, "masterDisabled", "Master key is disabled.", 403},
{RpcNotEnabled, "notEnabled", "Not enabled in configuration.", 501},
{RpcNotImpl, "notImpl", "Not implemented.", 501},
{RpcNotReady, "notReady", "Not ready to handle this request.", 503},
{RpcNotSupported, "notSupported", "Operation not supported.", 501},
{RpcNoClosed, "noClosed", "Closed ledger is unavailable.", 503},
{RpcNoCurrent, "noCurrent", "Current ledger is unavailable.", 503},
{RpcNotSynced, "notSynced", "Not synced to the network.", 503},
{RpcNoEvents, "noEvents", "Current transport does not support events.", 405},
{RpcNoNetwork, "noNetwork", "Not synced to the network.", 503},
{RpcWrongNetwork, "wrongNetwork", "Wrong network.", 503},
{RpcNoPermission, "noPermission", "You don't have permission for this command.", 401},
{RpcNoPfRequest, "noPathRequest", "No pathfinding request in progress.", 404},
{RpcObjectNotFound, "objectNotFound", "The requested object was not found.", 404},
{RpcPublicMalformed, "publicMalformed", "Public key is malformed.", 400},
{RpcSendmaxMalformed, "sendMaxMalformed", "SendMax amount malformed.", 400},
{RpcSigningMalformed, "signingMalformed", "Signing of transaction is malformed.", 400},
{RpcSlowDown, "slowDown", "You are placing too much load on the server.", 429},
{RpcSrcActMalformed, "srcActMalformed", "Source account is malformed.", 400},
{RpcSrcActMissing, "srcActMissing", "Source account not provided.", 400},
{RpcSrcActNotFound, "srcActNotFound", "Source account not found.", 404},
{RpcDelegateActNotFound, "delegateActNotFound", "Delegate account not found.", 404},
{RpcSrcCurMalformed, "srcCurMalformed", "Source currency is malformed.", 400},
{RpcSrcIsrMalformed, "srcIsrMalformed", "Source issuer is malformed.", 400},
{RpcStreamMalformed, "malformedStream", "Stream malformed.", 400},
{RpcTooBusy, "tooBusy", "The server is too busy to help you now.", 503},
{RpcTxnNotFound, "txnNotFound", "Transaction not found.", 404},
{RpcUnknownCommand, "unknownCmd", "Unknown method.", 405},
{RpcOracleMalformed, "oracleMalformed", "Oracle request is malformed.", 400},
{RpcBadCredentials, "badCredentials", "Credentials do not exist, are not accepted, or have expired.", 400},
{RpcTxSigned, "transactionSigned", "Transaction should not be signed.", 400},
{RpcDomainMalformed, "domainMalformed", "Domain is malformed.", 400},
{RpcEntryNotFound, "entryNotFound", "Entry not found.", 400},
{RpcUnexpectedLedgerType, "unexpectedLedgerType", "Unexpected ledger type.", 400},
{RpcActMalformed, "actMalformed", "Account malformed.", 400},
{RpcActNotFound, "actNotFound", "Account not found.", 404},
{RpcAlreadyMultisig, "alreadyMultisig", "Already multisigned.", 400},
{RpcAlreadySingleSig, "alreadySingleSig", "Already single-signed.", 400},
{RpcAmendmentBlocked, "amendmentBlocked", "Amendment blocked, need upgrade.", 503},
{RpcExpiredValidatorList, "unlBlocked", "Validator list expired.", 503},
{RpcAtxDeprecated, "deprecated", "Use the new API or specify a ledger range.", 400},
{RpcBadKeyType, "badKeyType", "Bad key type.", 400},
{RpcBadFeature, "badFeature", "Feature unknown or invalid.", 500},
{RpcBadIssuer, "badIssuer", "Issuer account malformed.", 400},
{RpcBadMarket, "badMarket", "No such market.", 404},
{RpcBadSecret, "badSecret", "Secret does not match account.", 403},
{RpcBadSeed, "badSeed", "Disallowed seed.", 403},
{RpcBadSyntax, "badSyntax", "Syntax error.", 400},
{RpcChannelMalformed, "channelMalformed", "Payment channel is malformed.", 400},
{RpcChannelAmtMalformed, "channelAmtMalformed", "Payment channel amount is malformed.", 400},
{RpcCommandMissing, "commandMissing", "Missing command entry.", 400},
{RpcDbDeserialization, "dbDeserialization", "Database deserialization error.", 502},
{RpcDstActMalformed, "dstActMalformed", "Destination account is malformed.", 400},
{RpcDstActMissing, "dstActMissing", "Destination account not provided.", 400},
{RpcDstActNotFound, "dstActNotFound", "Destination account not found.", 404},
{RpcDstAmtMalformed, "dstAmtMalformed", "Destination amount/currency/issuer is malformed.", 400},
{RpcDstAmtMissing, "dstAmtMissing", "Destination amount/currency/issuer is missing.", 400},
{RpcDstIsrMalformed, "dstIsrMalformed", "Destination issuer is malformed.", 400},
{RpcExcessiveLgrRange, "excessiveLgrRange", "Ledger range exceeds 1000.", 400},
{RpcForbidden, "forbidden", "Bad credentials.", 403},
{RpcHighFee, "highFee", "Current transaction fee exceeds your limit.", 402},
{RpcInternal, "internal", "Internal error.", 500},
{RpcInvalidLgrRange, "invalidLgrRange", "Ledger range is invalid.", 400},
{RpcInvalidParams, "invalidParams", "Invalid parameters.", 400},
{RpcInvalidHotwallet, "invalidHotWallet", "Invalid hotwallet.", 400},
{RpcIssueMalformed, "issueMalformed", "Issue is malformed.", 400},
{RpcJsonRpc, "json_rpc", "JSON-RPC transport error.", 500},
{RpcLgrIdxsInvalid, "lgrIdxsInvalid", "Ledger indexes invalid.", 400},
{RpcLgrIdxMalformed, "lgrIdxMalformed", "Ledger index malformed.", 400},
{RpcLgrNotFound, "lgrNotFound", "Ledger not found.", 404},
{RpcLgrNotValidated, "lgrNotValidated", "Ledger not validated.", 202},
{RpcMasterDisabled, "masterDisabled", "Master key is disabled.", 403},
{RpcNotEnabled, "notEnabled", "Not enabled in configuration.", 501},
{RpcNotImpl, "notImpl", "Not implemented.", 501},
{RpcNotReady, "notReady", "Not ready to handle this request.", 503},
{RpcNotSupported, "notSupported", "Operation not supported.", 501},
{RpcNoClosed, "noClosed", "Closed ledger is unavailable.", 503},
{RpcNoCurrent, "noCurrent", "Current ledger is unavailable.", 503},
{RpcNotSynced, "notSynced", "Not synced to the network.", 503},
{RpcNoEvents, "noEvents", "Current transport does not support events.", 405},
{RpcNoNetwork, "noNetwork", "Not synced to the network.", 503},
{RpcWrongNetwork, "wrongNetwork", "Wrong network.", 503},
{RpcNoPermission, "noPermission", "You don't have permission for this command.", 401},
{RpcNoPfRequest, "noPathRequest", "No pathfinding request in progress.", 404},
{RpcObjectNotFound, "objectNotFound", "The requested object was not found.", 404},
{RpcPublicMalformed, "publicMalformed", "Public key is malformed.", 400},
{RpcSendmaxMalformed, "sendMaxMalformed", "SendMax amount malformed.", 400},
{RpcSigningMalformed, "signingMalformed", "Signing of transaction is malformed.", 400},
{RpcSlowDown, "slowDown", "You are placing too much load on the server.", 429},
{RpcSrcActMalformed, "srcActMalformed", "Source account is malformed.", 400},
{RpcSrcActMissing, "srcActMissing", "Source account not provided.", 400},
{RpcSrcActNotFound, "srcActNotFound", "Source account not found.", 404},
{RpcDelegateActNotFound, "delegateActNotFound", "Delegate account not found.", 404},
{RpcSrcCurMalformed, "srcCurMalformed", "Source currency is malformed.", 400},
{RpcSrcIsrMalformed, "srcIsrMalformed", "Source issuer is malformed.", 400},
{RpcStreamMalformed, "malformedStream", "Stream malformed.", 400},
{RpcTooBusy, "tooBusy", "The server is too busy to help you now.", 503},
{RpcTxnNotFound, "txnNotFound", "Transaction not found.", 404},
{RpcUnknownCommand, "unknownCmd", "Unknown method.", 405},
{RpcOracleMalformed, "oracleMalformed", "Oracle request is malformed.", 400},
{RpcBadCredentials, "badCredentials", "Credentials do not exist, are not accepted, or have expired.", 400},
{RpcTxSigned, "transactionSigned", "Transaction should not be signed.", 400},
{RpcDomainMalformed, "domainMalformed", "Domain is malformed.", 400},
{RpcEntryNotFound, "entryNotFound", "Entry not found.", 400},
{RpcUnexpectedLedgerType, "unexpectedLedgerType", "Unexpected ledger type.", 400},
{RpcInvalidTransaction, "invalidTransaction", "Transaction is invalid.", 400},
{RpcInternalSubmit, "internalSubmit", "Internal error during submit.", 500},
{RpcInternalJson, "internalJson", "Internal error during JSON handling.", 500},
{RpcInternalSimulate, "internalSimulate", "Internal error during simulate.", 500},
{RpcFieldNotFoundTransaction, "fieldNotFoundTransaction", "Missing required field.", 400},
{RpcNotYetImplemented, "notYetImplemented", "Not yet implemented.", 501},
{RpcTransactionNotFound, "transactionNotFound", "Transaction not found.", 404},
{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},
};
// clang-format on
@@ -117,7 +153,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};
@@ -128,10 +164,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)
@@ -156,6 +192,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
//------------------------------------------------------------------------------
@@ -186,6 +284,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)
{
+19 -26
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 {
@@ -33,7 +36,10 @@ getHTTPHeaderTimestamp()
void
httpReply(int nStatus, std::string const& content, json::Output const& output, beast::Journal j)
{
JLOG(j.trace()) << "HTTP Reply " << nStatus << " " << content;
// The status only. The body is a reply, which carries a credential when the
// command is a keygen, and this library cannot mask one: the caller logs the
// masked body at debug.
JLOG(j.trace()) << "HTTP Reply " << nStatus;
if (content.empty() && nStatus == 401)
{
@@ -69,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 -2
View File
@@ -2419,8 +2419,7 @@ public:
BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "tesSUCCESS");
if (wsc->version() == 2)
{
BEAST_EXPECT(jrr.isMember(jss::jsonrpc) && jrr[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jrr.isMember(jss::ripplerpc) && jrr[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jrr));
BEAST_EXPECT(jrr.isMember(jss::id) && jrr[jss::id] == 5);
}
+1 -2
View File
@@ -2170,8 +2170,7 @@ public:
BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "tesSUCCESS");
if (wsc->version() == 2)
{
BEAST_EXPECT(jrr.isMember(jss::jsonrpc) && jrr[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jrr.isMember(jss::ripplerpc) && jrr[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jrr));
BEAST_EXPECT(jrr.isMember(jss::id) && jrr[jss::id] == 5);
}
+31
View File
@@ -16,10 +16,12 @@
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ApiVersion.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/JsonRpc.h>
#include <xrpl/protocol/PathAsset.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/SField.h>
@@ -363,6 +365,35 @@ getAccountLines(Env& env, AccountID const& acctId, IOU... ious)
[[nodiscard]] bool
checkArraySize(json::Value const& val, unsigned int size);
/**
* Reports whether @p reply carries the version 2 envelope: `jsonrpc` equal to
* rpc::kJsonRpcVersion and `ripplerpc` equal to rpc::kRippleRpcVersion2, both
* present.
*
* @param reply The reply to inspect.
* @return True when both members are present with those values.
*/
[[nodiscard]]
inline bool
hasEnvelope2(json::Value const& reply)
{
return reply.isMember(jss::jsonrpc) && reply[jss::jsonrpc] == rpc::kJsonRpcVersion &&
reply.isMember(jss::ripplerpc) && reply[jss::ripplerpc] == rpc::kRippleRpcVersion2;
}
/**
* Sets the version 2 envelope on @p request: `jsonrpc` to rpc::kJsonRpcVersion
* and `ripplerpc` to rpc::kRippleRpcVersion2.
*
* @param request The request to stamp.
*/
inline void
setEnvelope2(json::Value& request)
{
request[jss::jsonrpc] = rpc::kJsonRpcVersion;
request[jss::ripplerpc] = rpc::kRippleRpcVersion2;
}
// Helper function that returns the owner count on an account.
std::uint32_t
ownerCount(test::jtx::Env const& env, test::jtx::Account const& account);
+4 -2
View File
@@ -10,6 +10,8 @@
#include <xrpl/json/json_reader.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/ApiVersion.h>
#include <xrpl/protocol/JsonRpc.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/server/Port.h>
@@ -163,8 +165,8 @@ public:
jr[jss::method] = cmd;
if (rpcVersion_ == 2)
{
jr[jss::jsonrpc] = "2.0";
jr[jss::ripplerpc] = "2.0";
jr[jss::jsonrpc] = rpc::kJsonRpcVersion;
jr[jss::ripplerpc] = rpc::kRippleRpcVersion2;
jr[jss::id] = 5;
}
if (params)
+4 -2
View File
@@ -9,6 +9,8 @@
#include <xrpl/json/json_reader.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/ApiVersion.h>
#include <xrpl/protocol/JsonRpc.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/server/Port.h>
@@ -207,8 +209,8 @@ public:
if (rpcVersion_ == 2)
{
jp[jss::method] = cmd;
jp[jss::jsonrpc] = "2.0";
jp[jss::ripplerpc] = "2.0";
jp[jss::jsonrpc] = rpc::kJsonRpcVersion;
jp[jss::ripplerpc] = rpc::kRippleRpcVersion2;
jp[jss::id] = 5;
}
else
+8 -14
View File
@@ -1,6 +1,7 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/flags.h>
#include <test/jtx/multisign.h>
@@ -371,8 +372,7 @@ public:
BEAST_EXPECT(
info.isMember(jss::result) && info[jss::result].isMember(jss::account_data));
BEAST_EXPECT(!info[jss::result][jss::account_data].isMember(jss::signer_lists));
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(info));
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 5);
}
{
@@ -385,8 +385,7 @@ public:
auto const& signerLists = data[jss::signer_lists];
BEAST_EXPECT(signerLists.isArray());
BEAST_EXPECT(signerLists.size() == 0);
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(info));
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 6);
}
{
@@ -396,8 +395,7 @@ public:
info[0u].isMember(jss::result) &&
info[0u][jss::result].isMember(jss::account_data));
BEAST_EXPECT(!info[0u][jss::result][jss::account_data].isMember(jss::signer_lists));
BEAST_EXPECT(info[0u].isMember(jss::jsonrpc) && info[0u][jss::jsonrpc] == "2.0");
BEAST_EXPECT(info[0u].isMember(jss::ripplerpc) && info[0u][jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(info[0u]));
BEAST_EXPECT(info[0u].isMember(jss::id) && info[0u][jss::id] == 5);
BEAST_EXPECT(
@@ -408,8 +406,7 @@ public:
auto const& signerLists = data[jss::signer_lists];
BEAST_EXPECT(signerLists.isArray());
BEAST_EXPECT(signerLists.size() == 0);
BEAST_EXPECT(info[1u].isMember(jss::jsonrpc) && info[1u][jss::jsonrpc] == "2.0");
BEAST_EXPECT(info[1u].isMember(jss::ripplerpc) && info[1u][jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(info[1u]));
BEAST_EXPECT(info[1u].isMember(jss::id) && info[1u][jss::id] == 6);
}
@@ -424,8 +421,7 @@ public:
BEAST_EXPECT(
info.isMember(jss::result) && info[jss::result].isMember(jss::account_data));
BEAST_EXPECT(!info[jss::result][jss::account_data].isMember(jss::signer_lists));
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(info));
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 5);
}
{
@@ -445,8 +441,7 @@ public:
BEAST_EXPECT(signerEntries.size() == 1);
auto const& entry0 = signerEntries[0u][sfSignerEntry.jsonName];
BEAST_EXPECT(entry0[sfSignerWeight.jsonName] == 3);
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(info));
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 6);
}
@@ -495,8 +490,7 @@ public:
BEAST_EXPECT(entry.isMember(sfAccount.jsonName));
BEAST_EXPECT(entry[sfSignerWeight.jsonName] == 1);
}
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(info));
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 6);
}
}
+77 -129
View File
@@ -797,25 +797,21 @@ public:
// account_lines with mal-formed json2 (missing id field).
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
}
{
// account_lines with no account.
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(
lines[jss::error][jss::message] ==
rpc::missingFieldError(jss::account)[jss::error_message]);
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -824,16 +820,14 @@ public:
params[jss::account] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj";
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(
lines[jss::error][jss::message] ==
rpc::makeError(RpcActMalformed)[jss::error_message]);
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
Account const alice{"alice"};
@@ -843,16 +837,14 @@ public:
params[jss::account] = alice.human();
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(
lines[jss::error][jss::message] ==
rpc::makeError(RpcActNotFound)[jss::error_message]);
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
env.fund(XRP(10000), alice);
@@ -869,16 +861,14 @@ public:
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines[jss::error][jss::error] == "invalidParams");
BEAST_EXPECT(lines[jss::error][jss::message] == "Invalid field 'peer'.");
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
};
@@ -895,15 +885,13 @@ public:
params[jss::account] = alice.human();
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
BEAST_EXPECT(lines[jss::result][jss::lines].size() == 0);
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -913,16 +901,14 @@ public:
params[jss::ledger_index] = "nonsense";
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(
lines[jss::error][jss::message] ==
"Invalid field 'ledger_index', not string or number.");
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -932,14 +918,12 @@ public:
params[jss::ledger_index] = 50000;
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines[jss::error][jss::message] == "ledgerNotFound");
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
// Create trust lines to share with alice.
@@ -992,44 +976,38 @@ public:
BEAST_EXPECT(ledger58Info.seq == 58);
// A re-usable test for historic ledgers.
auto const testAccountLinesHistory = [this, &env](
Account const& account,
LedgerHeader const& info,
int count) {
// Get account_lines by ledger index.
json::Value paramsSeq;
paramsSeq[jss::account] = account.human();
paramsSeq[jss::ledger_index] = info.seq;
json::Value requestSeq;
requestSeq[jss::method] = "account_lines";
requestSeq[jss::jsonrpc] = "2.0";
requestSeq[jss::ripplerpc] = "2.0";
requestSeq[jss::id] = 5;
requestSeq[jss::params] = paramsSeq;
auto const linesSeq = env.rpc("json2", to_string(requestSeq));
BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray());
BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count);
BEAST_EXPECT(linesSeq.isMember(jss::jsonrpc) && linesSeq[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesSeq.isMember(jss::ripplerpc) && linesSeq[jss::ripplerpc] == "2.0");
BEAST_EXPECT(linesSeq.isMember(jss::id) && linesSeq[jss::id] == 5);
auto const testAccountLinesHistory =
[this, &env](Account const& account, LedgerHeader const& info, int count) {
// Get account_lines by ledger index.
json::Value paramsSeq;
paramsSeq[jss::account] = account.human();
paramsSeq[jss::ledger_index] = info.seq;
json::Value requestSeq;
requestSeq[jss::method] = "account_lines";
setEnvelope2(requestSeq);
requestSeq[jss::id] = 5;
requestSeq[jss::params] = paramsSeq;
auto const linesSeq = env.rpc("json2", to_string(requestSeq));
BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray());
BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count);
BEAST_EXPECT(hasEnvelope2(linesSeq));
BEAST_EXPECT(linesSeq.isMember(jss::id) && linesSeq[jss::id] == 5);
// Get account_lines by ledger hash.
json::Value paramsHash;
paramsHash[jss::account] = account.human();
paramsHash[jss::ledger_hash] = to_string(info.hash);
json::Value requestHash;
requestHash[jss::method] = "account_lines";
requestHash[jss::jsonrpc] = "2.0";
requestHash[jss::ripplerpc] = "2.0";
requestHash[jss::id] = 5;
requestHash[jss::params] = paramsHash;
auto const linesHash = env.rpc("json2", to_string(requestHash));
BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray());
BEAST_EXPECT(linesHash[jss::result][jss::lines].size() == count);
BEAST_EXPECT(linesHash.isMember(jss::jsonrpc) && linesHash[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesHash.isMember(jss::ripplerpc) && linesHash[jss::ripplerpc] == "2.0");
BEAST_EXPECT(linesHash.isMember(jss::id) && linesHash[jss::id] == 5);
};
// Get account_lines by ledger hash.
json::Value paramsHash;
paramsHash[jss::account] = account.human();
paramsHash[jss::ledger_hash] = to_string(info.hash);
json::Value requestHash;
requestHash[jss::method] = "account_lines";
setEnvelope2(requestHash);
requestHash[jss::id] = 5;
requestHash[jss::params] = paramsHash;
auto const linesHash = env.rpc("json2", to_string(requestHash));
BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray());
BEAST_EXPECT(linesHash[jss::result][jss::lines].size() == count);
BEAST_EXPECT(hasEnvelope2(linesHash));
BEAST_EXPECT(linesHash.isMember(jss::id) && linesHash[jss::id] == 5);
};
// Alice should have no trust lines in ledger 3.
testAccountLinesHistory(alice, ledger3Info, 0);
@@ -1049,8 +1027,7 @@ public:
params[jss::ledger_index] = ledger58Info.seq;
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
@@ -1059,8 +1036,7 @@ public:
lines[jss::error][jss::message] ==
"Exactly one of 'ledger_hash' or 'ledger_index' can be "
"specified.");
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -1069,15 +1045,13 @@ public:
params[jss::account] = alice.human();
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
BEAST_EXPECT(lines[jss::result][jss::lines].size() == 52);
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -1087,15 +1061,13 @@ public:
params[jss::peer] = gw1.human();
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -1105,16 +1077,14 @@ public:
params[jss::peer] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj";
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(
lines[jss::error][jss::message] ==
rpc::makeError(RpcActMalformed)[jss::error_message]);
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -1124,16 +1094,14 @@ public:
params[jss::limit] = -1;
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(
lines[jss::error][jss::message] ==
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -1143,15 +1111,13 @@ public:
paramsA[jss::limit] = 1;
json::Value requestA;
requestA[jss::method] = "account_lines";
requestA[jss::jsonrpc] = "2.0";
requestA[jss::ripplerpc] = "2.0";
setEnvelope2(requestA);
requestA[jss::id] = 5;
requestA[jss::params] = paramsA;
auto const linesA = env.rpc("json2", to_string(requestA));
BEAST_EXPECT(linesA[jss::result][jss::lines].isArray());
BEAST_EXPECT(linesA[jss::result][jss::lines].size() == 1);
BEAST_EXPECT(linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesA.isMember(jss::ripplerpc) && linesA[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(linesA));
BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5);
// Pick up from where the marker left off. We should get 51.
@@ -1161,15 +1127,13 @@ public:
paramsB[jss::marker] = marker;
json::Value requestB;
requestB[jss::method] = "account_lines";
requestB[jss::jsonrpc] = "2.0";
requestB[jss::ripplerpc] = "2.0";
setEnvelope2(requestB);
requestB[jss::id] = 5;
requestB[jss::params] = paramsB;
auto const linesB = env.rpc("json2", to_string(requestB));
BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 51);
BEAST_EXPECT(linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesB.isMember(jss::ripplerpc) && linesB[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(linesB));
BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5);
// Go again from where the marker left off, but set a limit of 3.
@@ -1179,15 +1143,13 @@ public:
paramsC[jss::marker] = marker;
json::Value requestC;
requestC[jss::method] = "account_lines";
requestC[jss::jsonrpc] = "2.0";
requestC[jss::ripplerpc] = "2.0";
setEnvelope2(requestC);
requestC[jss::id] = 5;
requestC[jss::params] = paramsC;
auto const linesC = env.rpc("json2", to_string(requestC));
BEAST_EXPECT(linesC[jss::result][jss::lines].isArray());
BEAST_EXPECT(linesC[jss::result][jss::lines].size() == 3);
BEAST_EXPECT(linesC.isMember(jss::jsonrpc) && linesC[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesC.isMember(jss::ripplerpc) && linesC[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(linesC));
BEAST_EXPECT(linesC.isMember(jss::id) && linesC[jss::id] == 5);
// Mess with the marker so it becomes bad and check for the error.
@@ -1197,16 +1159,14 @@ public:
paramsD[jss::marker] = marker;
json::Value requestD;
requestD[jss::method] = "account_lines";
requestD[jss::jsonrpc] = "2.0";
requestD[jss::ripplerpc] = "2.0";
setEnvelope2(requestD);
requestD[jss::id] = 5;
requestD[jss::params] = paramsD;
auto const linesD = env.rpc("json2", to_string(requestD));
BEAST_EXPECT(
linesD[jss::error][jss::message] ==
rpc::makeError(RpcInvalidParams)[jss::error_message]);
BEAST_EXPECT(linesD.isMember(jss::jsonrpc) && linesD[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesD.isMember(jss::ripplerpc) && linesD[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(linesD));
BEAST_EXPECT(linesD.isMember(jss::id) && linesD[jss::id] == 5);
}
{
@@ -1216,16 +1176,14 @@ public:
params[jss::marker] = true;
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(
lines[jss::error][jss::message] ==
rpc::expectedFieldMessage(jss::marker, "string"));
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -1236,8 +1194,7 @@ public:
params[jss::peer] = gw2.human();
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
setEnvelope2(request);
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
@@ -1246,8 +1203,7 @@ public:
BEAST_EXPECT(line[jss::deep_freeze].asBool() == true);
BEAST_EXPECT(line[jss::no_ripple].asBool() == true);
BEAST_EXPECT(line[jss::peer_authorized].asBool() == true);
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(lines));
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
}
{
@@ -1258,8 +1214,7 @@ public:
paramsA[jss::peer] = alice.human();
json::Value requestA;
requestA[jss::method] = "account_lines";
requestA[jss::jsonrpc] = "2.0";
requestA[jss::ripplerpc] = "2.0";
setEnvelope2(requestA);
requestA[jss::id] = 5;
requestA[jss::params] = paramsA;
auto const linesA = env.rpc("json2", to_string(requestA));
@@ -1268,8 +1223,7 @@ public:
BEAST_EXPECT(lineA[jss::deep_freeze_peer].asBool() == true);
BEAST_EXPECT(lineA[jss::no_ripple_peer].asBool() == true);
BEAST_EXPECT(lineA[jss::authorized].asBool() == true);
BEAST_EXPECT(linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesA.isMember(jss::ripplerpc) && linesA[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(linesA));
BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5);
// Continue from the returned marker to make sure that works.
@@ -1282,16 +1236,14 @@ public:
paramsB[jss::peer] = alice.human();
json::Value requestB;
requestB[jss::method] = "account_lines";
requestB[jss::jsonrpc] = "2.0";
requestB[jss::ripplerpc] = "2.0";
setEnvelope2(requestB);
requestB[jss::id] = 5;
requestB[jss::params] = paramsB;
auto const linesB = env.rpc("json2", to_string(requestB));
BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 25);
BEAST_EXPECT(!linesB[jss::result].isMember(jss::marker));
BEAST_EXPECT(linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesB.isMember(jss::ripplerpc) && linesB[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(linesB));
BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5);
}
}
@@ -1351,15 +1303,13 @@ public:
linesBegParams[jss::limit] = 2;
json::Value linesBegRequest;
linesBegRequest[jss::method] = "account_lines";
linesBegRequest[jss::jsonrpc] = "2.0";
linesBegRequest[jss::ripplerpc] = "2.0";
setEnvelope2(linesBegRequest);
linesBegRequest[jss::id] = 5;
linesBegRequest[jss::params] = linesBegParams;
auto const linesBeg = env.rpc("json2", to_string(linesBegRequest));
BEAST_EXPECT(linesBeg[jss::result][jss::lines][0u][jss::currency] == "USD");
BEAST_EXPECT(linesBeg[jss::result].isMember(jss::marker));
BEAST_EXPECT(linesBeg.isMember(jss::jsonrpc) && linesBeg[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesBeg.isMember(jss::ripplerpc) && linesBeg[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(linesBeg));
BEAST_EXPECT(linesBeg.isMember(jss::id) && linesBeg[jss::id] == 5);
// alice pays 100 USD to cheri.
@@ -1373,16 +1323,14 @@ public:
linesEndParams[jss::marker] = linesBeg[jss::result][jss::marker];
json::Value linesEndRequest;
linesEndRequest[jss::method] = "account_lines";
linesEndRequest[jss::jsonrpc] = "2.0";
linesEndRequest[jss::ripplerpc] = "2.0";
setEnvelope2(linesEndRequest);
linesEndRequest[jss::id] = 5;
linesEndRequest[jss::params] = linesEndParams;
auto const linesEnd = env.rpc("json2", to_string(linesEndRequest));
BEAST_EXPECT(
linesEnd[jss::error][jss::message] ==
rpc::makeError(RpcInvalidParams)[jss::error_message]);
BEAST_EXPECT(linesEnd.isMember(jss::jsonrpc) && linesEnd[jss::jsonrpc] == "2.0");
BEAST_EXPECT(linesEnd.isMember(jss::ripplerpc) && linesEnd[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(linesEnd));
BEAST_EXPECT(linesEnd.isMember(jss::id) && linesEnd[jss::id] == 5);
}
+21 -40
View File
@@ -1,4 +1,5 @@
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/WSClient.h>
#include <test/jtx/amount.h>
#include <test/jtx/domain.h>
@@ -88,8 +89,7 @@ public:
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
@@ -126,8 +126,7 @@ public:
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
@@ -165,8 +164,7 @@ public:
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
@@ -209,8 +207,7 @@ public:
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
@@ -242,8 +239,7 @@ public:
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
@@ -288,8 +284,7 @@ public:
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
@@ -328,8 +323,7 @@ public:
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
@@ -386,8 +380,7 @@ public:
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
@@ -428,8 +421,7 @@ public:
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
@@ -487,8 +479,7 @@ public:
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
@@ -542,8 +533,7 @@ public:
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
@@ -613,8 +603,7 @@ public:
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
@@ -657,8 +646,7 @@ public:
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
@@ -731,8 +719,7 @@ public:
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
@@ -789,8 +776,7 @@ public:
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
@@ -887,8 +873,7 @@ public:
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
@@ -921,8 +906,7 @@ public:
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
@@ -952,8 +936,7 @@ public:
auto jv = wsc->invoke("book_offers", jvParams);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
auto jrr = jv[jss::result];
@@ -997,8 +980,7 @@ public:
jv = wsc->invoke("book_offers", jvParams);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
jrr = jv[jss::result];
@@ -1023,8 +1005,7 @@ public:
jv = wsc->invoke("unsubscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
+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/WSClient.h>
#include <test/jtx/amount.h>
#include <test/jtx/balance.h>
@@ -22,6 +23,7 @@
#include <xrpl/protocol/jss.h>
#include <chrono>
#include <string>
#include <utility>
namespace xrpl::test {
@@ -254,8 +256,7 @@ class DeliveredAmount_test : public beast::unit_test::Suite
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::ledger_index] == 3);
+2 -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/WSClient.h>
#include <test/jtx/amount.h>
#include <test/jtx/escrow.h>
@@ -85,8 +86,7 @@ public:
expect(jv[jss::status] == "success");
if (wsc->version() == 2)
{
expect(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
expect(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
expect(hasEnvelope2(jv));
expect(jv.isMember(jss::id) && jv[jss::id] == 5);
}
+66 -2
View File
@@ -56,8 +56,10 @@
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <source_location>
#include <stdexcept>
#include <string>
@@ -164,6 +166,68 @@ class LedgerEntry_test : public beast::unit_test::Suite
jv[jss::error] == err,
"Expected error " + err + ", received " + jv[jss::error].asString() + ", at line " +
std::to_string(location.line()) + ", " + jv.toStyledString());
// 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.
static std::map<std::string, int> const kCodes{
{"entryNotFound", 98},
{"invalidParams", 31},
{"lgrNotFound", 21},
{"unexpectedLedgerType", 99},
{"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",
};
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 " +
std::to_string(location.line()));
};
if (auto const it = kCodes.find(err); it != kCodes.end())
{
expectCode(it->second);
}
else if (kMalformedTokens.contains(err))
{
expectCode(RpcInvalidParams);
}
else
{
// A token in neither list is a typo; reporting 31 like the rest would let it pass.
BEAST_EXPECTS(
false,
"Token " + err + " names no error code, at line " +
std::to_string(location.line()));
}
}
if (msg.empty())
{
@@ -340,7 +404,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result];
if (apiVersion < 2u)
{
checkErrorValue(jrr, "unknownOption", "", location);
checkErrorValue(jrr, "unknownOption", "Unknown option.", location);
}
else
{
@@ -546,7 +610,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
if (apiVersion < 2u)
{
checkErrorValue(jrr, "unknownOption", "");
checkErrorValue(jrr, "unknownOption", "Unknown option.");
}
else
{
+397
View File
@@ -0,0 +1,397 @@
#include <test/jtx/Env.h>
#include <xrpld/rpc/detail/MaskSecrets.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_reader.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/jss.h>
#include <algorithm>
#include <array>
#include <string>
#include <string_view>
namespace xrpl::test {
class MaskSecrets_test : public beast::unit_test::Suite
{
static constexpr char const* kMasked = "<masked>";
static constexpr char const* kSensitive = "sensitive";
public:
/**
* Every field in `kCredentialFields` is masked, at the top level.
*/
void
testEveryCredentialField()
{
testcase("Every credential field is masked");
json::Value request(json::ValueType::Object);
for (auto const field : rpc::kCredentialFields)
request[std::string{field}] = kSensitive;
auto const masked = rpc::maskSecrets(request);
for (auto const field : rpc::kCredentialFields)
BEAST_EXPECTS(masked[std::string{field}] == kMasked, std::string{field});
// An empty list would let every assertion above pass while masking nothing.
BEAST_EXPECT(!rpc::kCredentialFields.empty());
}
/**
* A field the list does not name is left as it was.
*/
void
testNonCredentialsSurvive()
{
testcase("Fields that are not credentials are left alone");
json::Value request(json::ValueType::Object);
request[jss::method] = "sign";
request[jss::account] = "rSomeAccount";
request[jss::secret] = kSensitive;
auto const masked = rpc::maskSecrets(request);
BEAST_EXPECT(masked[jss::secret] == kMasked);
BEAST_EXPECT(masked[jss::method] == "sign");
BEAST_EXPECT(masked[jss::account] == "rSomeAccount");
// Masking replaces members rather than adding or removing them.
BEAST_EXPECT(masked.size() == request.size());
}
/**
* A member name is matched whole, not as a substring or case-insensitively.
*/
void
testNamesAreMatchedExactly()
{
testcase("Only an exact field name is masked");
json::Value request(json::ValueType::Object);
for (auto const* name : {"secrets", "Secret", "my_secret", "seedling", "sec", ""})
request[name] = kSensitive;
auto const masked = rpc::maskSecrets(request);
for (auto const* name : {"secrets", "Secret", "my_secret", "seedling", "sec", ""})
BEAST_EXPECTS(masked[name] == kSensitive, name);
}
/**
* A credential is masked wherever it sits, not only at the top level.
*
* The JSON-RPC transport nests a request's fields inside `params`, which is
* where a `secret` arrives.
*/
void
testNesting()
{
testcase("A nested credential is masked at any depth");
// Where the JSON-RPC transport carries a credential: inside `params`.
{
json::Value params(json::ValueType::Object);
params[jss::secret] = kSensitive;
params[jss::account] = "rSomeAccount";
json::Value request(json::ValueType::Object);
request[jss::method] = "sign";
request[jss::params] = json::ValueType::Array;
request[jss::params][0u] = params;
auto const masked = rpc::maskSecrets(request);
BEAST_EXPECT(masked[jss::params][0u][jss::secret] == kMasked);
BEAST_EXPECT(masked[jss::params][0u][jss::account] == "rSomeAccount");
BEAST_EXPECT(masked[jss::method] == "sign");
}
// Where a batch carries one: inside an entry, itself inside `params`.
{
json::Value entry(json::ValueType::Object);
entry[jss::id] = 2;
entry[jss::seed] = kSensitive;
json::Value batch(json::ValueType::Object);
batch[jss::method] = "batch";
batch[jss::params] = json::ValueType::Array;
batch[jss::params][0u] = entry;
auto const masked = rpc::maskSecrets(batch);
BEAST_EXPECT(masked[jss::params][0u][jss::seed] == kMasked);
BEAST_EXPECT(masked[jss::params][0u][jss::id] == 2);
}
// A credential in an array element, which names no member of its own.
{
json::Value inner(json::ValueType::Object);
inner[jss::passphrase] = kSensitive;
json::Value request(json::ValueType::Array);
request[0u] = inner;
auto const masked = rpc::maskSecrets(request);
BEAST_EXPECT(masked[0u][jss::passphrase] == kMasked);
}
// Up to the parser's nesting limit, which is what bounds the recursion.
{
json::Value deep(json::ValueType::Object);
deep[jss::secret] = kSensitive;
for (unsigned i{0}; i < json::Reader::kNestLimit; ++i)
{
json::Value outer(json::ValueType::Object);
outer[jss::params] = deep;
deep = outer;
}
auto const masked = rpc::maskSecrets(deep);
auto const* level = &masked;
for (unsigned i{0}; i < json::Reader::kNestLimit; ++i)
level = &(*level)[jss::params];
BEAST_EXPECT((*level)[jss::secret] == kMasked);
}
}
/**
* A credential's own value is replaced whole, whatever shape it has.
*
* An object or array under `secret` is not walked into: the whole value is
* the credential.
*/
void
testCredentialIsNotDescendedInto()
{
testcase("A credential is replaced whatever it holds");
json::Value nested(json::ValueType::Object);
nested["inner"] = kSensitive;
json::Value request(json::ValueType::Object);
request[jss::secret] = nested;
request[jss::seed] = json::ValueType::Array;
request[jss::seed][0u] = kSensitive;
auto const masked = rpc::maskSecrets(request);
BEAST_EXPECT(masked[jss::secret] == kMasked);
BEAST_EXPECT(masked[jss::seed] == kMasked);
BEAST_EXPECT(!to_string(masked).contains(kSensitive));
}
/**
* A credential that is not a string is masked too, the value being replaced
* whole.
*/
void
testNonStringCredentials()
{
testcase("A credential of any type is masked");
json::Value request(json::ValueType::Object);
request["secret"] = 12345;
request["seed"] = true;
request["passphrase"] = 1.5;
request["password"] = json::ValueType::Null;
auto const masked = rpc::maskSecrets(request);
for (auto const* field : {"secret", "seed", "passphrase", "password"})
BEAST_EXPECTS(masked[field] == kMasked, field);
}
/**
* A value with no members is returned unchanged, masking keying on member
* names.
*/
void
testValuesWithoutMembers()
{
testcase("A value that has no members is returned unchanged");
BEAST_EXPECT(rpc::maskSecrets(json::Value{}).isNull());
BEAST_EXPECT(rpc::maskSecrets(json::Value{kSensitive}) == kSensitive);
BEAST_EXPECT(rpc::maskSecrets(json::Value{42}) == 42);
BEAST_EXPECT(rpc::maskSecrets(json::Value{json::ValueType::Object}).size() == 0);
BEAST_EXPECT(rpc::maskSecrets(json::Value{json::ValueType::Array}).size() == 0);
}
/**
* The request the caller passed in is not modified.
*
* Every call site goes on to use the request it holds, so the mask copies.
*/
void
testRequestIsNotModified()
{
testcase("The request passed in is left unchanged");
json::Value request(json::ValueType::Object);
request[jss::secret] = kSensitive;
request[jss::params] = json::ValueType::Array;
request[jss::params][0u] = json::ValueType::Object;
request[jss::params][0u][jss::seed] = kSensitive;
auto const before = to_string(request);
auto const masked = rpc::maskSecrets(request);
BEAST_EXPECT(to_string(request) == before);
BEAST_EXPECT(request[jss::secret] == kSensitive);
BEAST_EXPECT(request[jss::params][0u][jss::seed] == kSensitive);
BEAST_EXPECT(masked[jss::secret] == kMasked);
}
/**
* `hasSecret` reports a credential at any depth and nothing else.
*
* Every name on the list is reported at the top level, one nested where the
* JSON-RPC transport puts it and one inside an array element are reported
* too, and a value naming none, or having no members at all, is not.
*/
void
testHasSecret()
{
testcase("A credential is detected at any depth");
for (auto const field : rpc::kCredentialFields)
{
json::Value request(json::ValueType::Object);
request[std::string{field}] = kSensitive;
BEAST_EXPECTS(rpc::hasSecret(request), std::string{field});
}
// Inside `params`, where the JSON-RPC transport carries a credential.
{
json::Value request(json::ValueType::Object);
request[jss::method] = "sign";
request[jss::params] = json::ValueType::Array;
request[jss::params][0u] = json::ValueType::Object;
request[jss::params][0u][jss::account] = "rSomeAccount";
request[jss::params][0u][jss::secret] = kSensitive;
BEAST_EXPECT(rpc::hasSecret(request));
}
// Inside an array element, which names no member of its own.
{
json::Value request(json::ValueType::Array);
request[0u] = json::ValueType::Object;
request[0u][jss::passphrase] = kSensitive;
BEAST_EXPECT(rpc::hasSecret(request));
}
// No credential at any depth, and a near miss is not one.
{
json::Value request(json::ValueType::Object);
request[jss::method] = "account_info";
request[jss::params] = json::ValueType::Array;
request[jss::params][0u] = json::ValueType::Object;
request[jss::params][0u][jss::account] = "rSomeAccount";
request[jss::params][0u]["secrets"] = kSensitive;
BEAST_EXPECT(!rpc::hasSecret(request));
}
// A value with no members carries none.
BEAST_EXPECT(!rpc::hasSecret(json::Value{}));
BEAST_EXPECT(!rpc::hasSecret(json::Value{kSensitive}));
BEAST_EXPECT(!rpc::hasSecret(json::Value{42}));
BEAST_EXPECT(!rpc::hasSecret(json::Value{json::ValueType::Object}));
BEAST_EXPECT(!rpc::hasSecret(json::Value{json::ValueType::Array}));
}
/**
* `loggable` masks a value that carries a credential, then truncates at
* `kMaxLoggedChars`; one that carries none is rendered as it is.
*
* The mask has to survive the cap, so the credential is placed where a
* sorted member order puts it first.
*/
void
testLoggable()
{
testcase("A rendering for a log line is masked and capped");
// Sorts before the filler, so the mask falls inside the cap.
json::Value request;
request[jss::secret] = kSensitive;
request["zfiller"] = std::string(2 * rpc::kMaxLoggedChars, 'z');
auto const rendered = rpc::loggable(request);
BEAST_EXPECT(rendered.size() == rpc::kMaxLoggedChars);
BEAST_EXPECT(rendered.contains(kMasked));
BEAST_EXPECT(!rendered.contains(kSensitive));
// A short request is not padded.
json::Value small;
small[jss::seed] = kSensitive;
BEAST_EXPECT(rpc::loggable(small) == to_string(rpc::maskSecrets(small)));
// A value with no credential is the serialized value itself, cut at the cap.
json::Value plain;
plain[jss::account] = "rSomeAccount";
plain["zfiller"] = std::string(2 * rpc::kMaxLoggedChars, 'z');
BEAST_EXPECT(rpc::loggable(plain) == to_string(plain).substr(0, rpc::kMaxLoggedChars));
BEAST_EXPECT(rpc::loggable(small[jss::seed]) == to_string(small[jss::seed]));
}
/**
* A keygen reply is rendered for the log with no key in it.
*
* Every field of each reply is checked, not only the names on the list, so
* a field the list is missing fails here. Only the fields a client may read
* in public are allowed through.
*/
void
testKeygenRepliesAreMasked()
{
testcase("A keygen reply is logged with every key masked");
static constexpr std::array<std::string_view, 6> kPublicFields{
"account_id",
"key_type",
"public_key",
"public_key_hex",
"status",
"validation_public_key",
};
jtx::Env env{*this};
for (auto const command : {"wallet_propose", "validation_create"})
{
auto const reply = env.rpc(command);
auto const& result = reply[jss::result];
BEAST_EXPECT(result[jss::status] == "success");
auto const rendered = rpc::loggable(reply);
for (auto const& name : result.getMemberNames())
{
if (std::ranges::find(kPublicFields, name) != kPublicFields.end())
continue;
auto const& value = result[name];
BEAST_EXPECT(value.isString() && !value.asString().empty());
BEAST_EXPECTS(!rendered.contains(value.asString()), command + (": " + name));
}
}
}
void
run() override
{
testEveryCredentialField();
testNonCredentialsSurvive();
testNamesAreMatchedExactly();
testNesting();
testCredentialIsNotDescendedInto();
testNonStringCredentials();
testValuesWithoutMembers();
testRequestIsNotModified();
testHasSecret();
testLoggable();
testKeygenRepliesAreMasked();
}
};
BEAST_DEFINE_TESTSUITE(MaskSecrets, rpc, xrpl);
} // namespace xrpl::test
+22 -42
View File
@@ -1,4 +1,5 @@
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/WSClient.h>
#include <test/jtx/amount.h>
#include <test/jtx/balance.h> // IWYU pragma: keep
@@ -38,8 +39,7 @@ public:
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
@@ -53,8 +53,7 @@ public:
auto jv = wsc->invoke("submit", payment);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tefMAX_LEDGER");
@@ -65,8 +64,7 @@ public:
jv = wsc->invoke("submit", payment);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tefPAST_SEQ");
@@ -76,8 +74,7 @@ public:
jv = wsc->invoke("submit", payment);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "terPRE_SEQ");
@@ -87,8 +84,7 @@ public:
jv = wsc->invoke("submit", payment);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
@@ -100,8 +96,7 @@ public:
jv = wsc->invoke("ledger_accept");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
@@ -128,8 +123,7 @@ public:
jv = wsc->invoke("unsubscribe", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -164,8 +158,7 @@ public:
jv = wsc->invoke("submit", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
@@ -187,8 +180,7 @@ public:
jv = wsc->invoke("account_tx", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
@@ -218,8 +210,7 @@ public:
jv = wsc->invoke("submit", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
@@ -228,8 +219,7 @@ public:
jv = wsc->invoke("ledger_accept");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
@@ -247,8 +237,7 @@ public:
jv = wsc->invoke("subscribe", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -260,8 +249,7 @@ public:
auto jv = wsc->invoke("ledger_accept");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
@@ -281,8 +269,7 @@ public:
jv = wsc->invoke("unsubscribe", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -300,8 +287,7 @@ public:
jv = wsc->invoke("subscribe", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -313,8 +299,7 @@ public:
auto jv = wsc->invoke("ledger_accept");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
@@ -334,8 +319,7 @@ public:
jv = wsc->invoke("unsubscribe", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -352,8 +336,7 @@ public:
jv = wsc->invoke("account_tx", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
@@ -383,8 +366,7 @@ public:
jv = wsc->invoke("subscribe", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -399,8 +381,7 @@ public:
jv = wsc->invoke("submit", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
@@ -421,8 +402,7 @@ public:
jv = wsc->invoke("unsubscribe", jv);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
+15
View File
@@ -328,6 +328,21 @@ class Simulate_test : public beast::unit_test::Suite
resp[jss::result][jss::error].toStyledString());
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'tx.Account'.");
}
{
// A non-string `Account`, which no earlier check rejects: the `tx_json` arm validates
// only that it is an object, and the check above only that `Account` is present.
json::Value params;
json::Value txJson = json::ValueType::Object;
txJson[jss::TransactionType] = jss::AccountSet;
txJson[jss::Account] = 123;
params[jss::tx_json] = txJson;
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECTS(
resp[jss::result][jss::error] == "invalidParams",
resp[jss::result][jss::error].toStyledString());
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'tx.Account'.");
}
{
// Account doesn't exist for Sequence autofill
json::Value params;
+15 -28
View File
@@ -1,4 +1,5 @@
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/WSClient.h>
#include <test/jtx/amount.h>
#include <test/jtx/batch.h>
@@ -83,8 +84,7 @@ public:
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -113,8 +113,7 @@ public:
auto jv = wsc->invoke("unsubscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -149,8 +148,7 @@ public:
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::ledger_index] == 2);
@@ -184,8 +182,7 @@ public:
auto jv = wsc->invoke("unsubscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -208,8 +205,7 @@ public:
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -266,8 +262,7 @@ public:
auto jv = wsc->invoke("unsubscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -281,8 +276,7 @@ public:
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -314,8 +308,7 @@ public:
auto jv = wsc->invoke("unsubscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -344,8 +337,7 @@ public:
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -391,8 +383,7 @@ public:
auto jv = wsc->invoke("unsubscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -414,8 +405,7 @@ public:
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -425,8 +415,7 @@ public:
auto jv = wsc->invoke("unsubscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -461,8 +450,7 @@ public:
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
@@ -537,8 +525,7 @@ public:
auto jv = wsc->invoke("unsubscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(hasEnvelope2(jv));
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
+61 -12
View File
@@ -18,6 +18,7 @@
#include <xrpl/protocol/jss.h>
#include <memory>
#include <source_location>
#include <stdexcept>
#include <string>
@@ -25,6 +26,45 @@ namespace xrpl {
class TransactionEntry_test : public beast::unit_test::Suite
{
/**
* Asserts the reply carries the whole error, not just the token.
*
* @param result The reply's payload.
* @param token The `error` token expected.
* @param code The `error_code` expected.
* @param message The `error_message` expected.
* @param location The caller, reported when an assertion fails.
*/
void
checkCodedError(
json::Value const& result,
std::string const& token,
ErrorCodeI code,
std::string const& message,
std::source_location const location = std::source_location::current())
{
auto const at = std::to_string(location.line());
BEAST_EXPECTS(result[jss::error] == token, at);
BEAST_EXPECTS(result[jss::error_code] == code, at);
BEAST_EXPECTS(result[jss::error_message] == message, at);
BEAST_EXPECTS(result[jss::status] == "error", at);
}
/**
* Wire values pinned as literals, so reassigning a code fails here.
*/
void
testErrorCodeValues()
{
testcase("Error code values are stable");
BEAST_EXPECT(static_cast<int>(RpcFieldNotFoundTransaction) == 104);
BEAST_EXPECT(static_cast<int>(RpcNotYetImplemented) == 105);
BEAST_EXPECT(static_cast<int>(RpcTransactionNotFound) == 106);
BEAST_EXPECT(static_cast<int>(RpcMalformedRequest) == 107);
}
void
testBadInput()
{
@@ -38,8 +78,12 @@ class TransactionEntry_test : public beast::unit_test::Suite
{
// no params
auto const result = env.client().invoke("transaction_entry", {})[jss::result];
BEAST_EXPECT(result[jss::error] == "fieldNotFoundTransaction");
BEAST_EXPECT(result[jss::status] == "error");
// The message names the missing field rather than the table's default.
checkCodedError(
result,
"fieldNotFoundTransaction",
RpcFieldNotFoundTransaction,
"Missing field 'tx_hash'.");
}
{
@@ -55,8 +99,8 @@ class TransactionEntry_test : public beast::unit_test::Suite
params[jss::ledger] = "current";
params[jss::tx_hash] = "DEADBEEF";
auto const result = env.client().invoke("transaction_entry", params)[jss::result];
BEAST_EXPECT(result[jss::error] == "notYetImplemented");
BEAST_EXPECT(result[jss::status] == "error");
checkCodedError(
result, "notYetImplemented", RpcNotYetImplemented, "Not yet implemented.");
}
{
@@ -65,8 +109,8 @@ class TransactionEntry_test : public beast::unit_test::Suite
params[jss::tx_hash] =
"E2FE8D4AF3FCC3944DDF6CD8CDDC5E3F0AD50863EF8919AFEF10CB6408CD4D05";
auto const result = env.client().invoke("transaction_entry", params)[jss::result];
BEAST_EXPECT(result[jss::error] == "notYetImplemented");
BEAST_EXPECT(result[jss::status] == "error");
checkCodedError(
result, "notYetImplemented", RpcNotYetImplemented, "Not yet implemented.");
BEAST_EXPECT(result.isMember(jss::ledger_current_index));
BEAST_EXPECT(!result.isMember(jss::ledger_hash));
BEAST_EXPECT(result[jss::validated] == false);
@@ -78,18 +122,19 @@ class TransactionEntry_test : public beast::unit_test::Suite
params[jss::tx_hash] = "DEADBEEF";
auto const result = env.client().invoke("transaction_entry", params)[jss::result];
BEAST_EXPECT(!result[jss::ledger_hash].asString().empty());
BEAST_EXPECT(result[jss::error] == "malformedRequest");
BEAST_EXPECT(result[jss::status] == "error");
checkCodedError(
result, "malformedRequest", RpcMalformedRequest, "Request is malformed.");
}
// A `tx_hash` that is not a string is malformed, the same as one that is not hex.
for (auto const type : {json::ValueType::Object, json::ValueType::Array})
{
json::Value params{json::ValueType::Object};
params[jss::ledger] = "closed";
params[jss::tx_hash] = json::Value{type};
auto const result = env.client().invoke("transaction_entry", params)[jss::result];
BEAST_EXPECT(result[jss::error] == "malformedRequest");
BEAST_EXPECT(result[jss::status] == "error");
checkCodedError(
result, "malformedRequest", RpcMalformedRequest, "Request is malformed.");
}
std::string const txHash{
@@ -150,8 +195,11 @@ class TransactionEntry_test : public beast::unit_test::Suite
// Valid structure, but transaction not found.
json::Value const result{env.rpc("transaction_entry", txHash, "closed")};
BEAST_EXPECT(!result[jss::result][jss::ledger_hash].asString().empty());
BEAST_EXPECT(result[jss::result][jss::error] == "transactionNotFound");
BEAST_EXPECT(result[jss::result][jss::status] == "error");
checkCodedError(
result[jss::result],
"transactionNotFound",
RpcTransactionNotFound,
"Transaction not found.");
}
}
@@ -379,6 +427,7 @@ public:
void
run() override
{
testErrorCodeValues();
testBadInput();
forAllApiVersions([this](unsigned apiVersion) { testRequest(apiVersion); });
}
File diff suppressed because it is too large. Load diff
+85
View File
@@ -598,6 +598,91 @@ TEST(JsonValue, compare_strings)
#pragma pop_macro("DO_COMPARE")
}
TEST(JsonValue, construct_from_string_view)
{
using namespace std::string_view_literals;
// Characters are copied with an explicit length, so a view need not be NUL-terminated.
EXPECT_EQ(json::Value{"2.0"sv}.asString(), "2.0");
EXPECT_EQ(json::Value{"2.0 and more"sv.substr(0, 3)}.asString(), "2.0");
EXPECT_EQ(json::Value{""sv}.asString(), "");
// A default-constructed view has a null `data()` and yields an empty string value.
std::string_view const empty;
json::Value const fromEmpty{empty};
EXPECT_TRUE(fromEmpty.isString());
EXPECT_EQ(fromEmpty.asString(), "");
EXPECT_TRUE(fromEmpty == ""sv);
// The view need not outlive the call.
json::Value copied;
{
std::string const owner{"borrowed"};
copied = json::Value{std::string_view{owner}};
}
EXPECT_EQ(copied.asString(), "borrowed");
// A reader of a string value stops at the first NUL, so a view carrying one reads back short.
json::Value const embedded{"a\0b"sv};
EXPECT_EQ(embedded.asString(), "a");
EXPECT_TRUE(embedded != "a\0b"sv);
EXPECT_TRUE(embedded == "a"sv);
EXPECT_TRUE(embedded == json::Value{"a\0b"sv});
}
TEST(JsonValue, compare_string_view)
{
// Comparing against a view builds no temporary Value.
using namespace std::string_view_literals;
EXPECT_TRUE(json::Value{"2.0"} == "2.0"sv);
EXPECT_FALSE(json::Value{"2.0"} != "2.0"sv);
EXPECT_TRUE(json::Value{"2.0"} != "2.00"sv);
EXPECT_TRUE(json::Value{"2.0"} != ""sv);
// A view need not be NUL-terminated, and the comparison must not read past its end.
EXPECT_TRUE(json::Value{"2.0"} == "2.0 and more"sv.substr(0, 3));
EXPECT_TRUE(json::Value{""} == ""sv);
// A string value holding a null pointer equals nothing, not even the empty view.
EXPECT_TRUE(json::Value{json::ValueType::String} != ""sv);
EXPECT_TRUE(json::Value{json::ValueType::String} != "2.0"sv);
EXPECT_TRUE(json::Value{json::ValueType::String} != json::Value{""});
// Nothing that is not a string equals one. Null is what an absent member reads as.
EXPECT_TRUE(json::Value{json::ValueType::Null} != ""sv);
EXPECT_TRUE(json::Value{json::ValueType::Null} != "2.0"sv);
EXPECT_TRUE(json::Value{2} != "2"sv);
EXPECT_TRUE(json::Value{true} != "true"sv);
EXPECT_TRUE(json::Value{json::ValueType::Object} != ""sv);
// Only a view reaches this overload; every other spelling compares through Value.
static constexpr json::StaticString kVersion{"2.0"};
EXPECT_TRUE(json::Value{"2.0"} == kVersion);
EXPECT_TRUE(json::Value{"2.1"} != kVersion);
EXPECT_TRUE(json::Value{} != kVersion);
EXPECT_TRUE(json::Value{"2.0"} == "2.0");
EXPECT_TRUE(json::Value{"2.0"} != "2.1");
EXPECT_TRUE(json::Value{} != "2.0");
EXPECT_TRUE(json::Value{"2.0"} == std::string{"2.0"});
EXPECT_TRUE(json::Value{"2.0"} != std::string{"2.1"});
EXPECT_TRUE(json::Value{json::ValueType::Null} != std::string{"2.0"});
// A literal `0` also converts to a view through `char const*`, so a plain overload would make
// this comparison ambiguous. It must compare as a number: RPCCall.cpp rejects a
// `transaction_entry` ledger index this way.
EXPECT_TRUE(json::Value{0} == 0);
EXPECT_TRUE(json::Value{4294967295u} != 0);
EXPECT_TRUE(json::Value{""} != 0);
EXPECT_TRUE(json::Value{2} != 0);
// A null and an Int are different types, so the comparison is false.
EXPECT_TRUE(json::Value{} != 0);
}
TEST(JsonValue, bool)
{
EXPECT_FALSE(json::Value());
+118
View File
@@ -0,0 +1,118 @@
#include <xrpl/protocol/ErrorCodes.h>
#include <gtest/gtest.h>
#include <set>
#include <type_traits>
using namespace xrpl;
// A row cannot be written without a status, so no row can default to 200 again.
static_assert(!std::is_constructible_v<rpc::ErrorInfo, ErrorCodeI, char const*, char const*>);
namespace {
/**
* The numbers between `RpcBadSyntax` and `RpcLast` that have no row in the
* error table: those no enumerator uses, and `RpcReportingUnsupported` (91),
* the one enumerator the table does not list.
*/
std::set<int> const kGaps{
5, 8, 20, 24, 25, 26, 27, 28, 34, 38, 39, 54, 55, 56,
59, 60, 61, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91,
};
} // namespace
TEST(ErrorCodes, table_gaps_report_the_unknown_placeholder)
{
// Codes absent from the table report the placeholder, so a scan by code must skip them.
auto const& gap = rpc::getErrorInfo(static_cast<ErrorCodeI>(5));
EXPECT_EQ(gap.code, RpcUnknown);
EXPECT_STREQ(gap.token.cStr(), "unknown");
auto const& real = rpc::getErrorInfo(RpcInvalidParams);
EXPECT_EQ(real.code, RpcInvalidParams);
EXPECT_STREQ(real.token.cStr(), "invalidParams");
}
TEST(ErrorCodes, exactly_the_listed_codes_have_no_row)
{
// The gap set is written out by hand, so an enumerator added without a row fails here, where a
// test asking the table which codes it knows would take the missing row for a gap.
for (int code = RpcBadSyntax; code <= RpcLast; ++code)
{
bool const isGap = rpc::getErrorInfo(static_cast<ErrorCodeI>(code)).code == RpcUnknown;
EXPECT_EQ(isGap, kGaps.contains(code)) << "code " << code;
}
}
TEST(ErrorCodes, every_code_names_an_http_status)
{
// Written out here rather than read from the row: a test reading the row production reads
// cannot tell whether the row is right.
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcActMalformed), 400);
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcActNotFound), 404);
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcAlreadyMultisig), 400);
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcAlreadySingleSig), 400);
// Every row names a status of its own. A code with no row reports the placeholder's, which the
// default constructor fixes at 200.
for (int code = RpcBadSyntax; code <= RpcLast; ++code)
{
auto const status = rpc::errorCodeHttpStatus(static_cast<ErrorCodeI>(code));
if (kGaps.contains(code))
{
EXPECT_EQ(status, 200) << "code " << code;
}
else
{
EXPECT_NE(status, 200) << "code " << code;
}
}
}
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.
EXPECT_EQ(rpc::codeForToken("badSyntax"), RpcBadSyntax);
EXPECT_EQ(
rpc::codeForToken("malformedXChainOwnedCreateAccountClaimID"),
RpcMalformedXChainOwnedCreateAccountClaimID);
static_assert(RpcMalformedXChainOwnedCreateAccountClaimID == 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>");
}
+11 -5
View File
@@ -36,6 +36,7 @@
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/ServerHandler.h>
#include <xrpld/rpc/detail/Handler.h>
#include <xrpld/rpc/detail/MaskSecrets.h>
#include <xrpld/rpc/detail/PathRequestManager.h>
#include <xrpld/rpc/detail/Pathfinder.h>
#include <xrpld/shamap/NodeFamily.h>
@@ -1460,20 +1461,25 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
//
// Execute start up rpc commands.
//
for (auto const& cmd : config_->section(Sections::kRpcStartup).lines())
auto const& startupCommands = config_->section(Sections::kRpcStartup).lines();
for (std::size_t i = 0; i < startupCommands.size(); ++i)
{
json::Reader jrReader;
json::Value jvCommand;
if (!jrReader.parse(cmd, jvCommand))
// A line that does not parse has no field to mask, so it is reported by position and
// parser message only, and no command runs for it.
if (!jrReader.parse(startupCommands[i], jvCommand))
{
JLOG(journal_.fatal())
<< "Couldn't parse entry in [" << Sections::kRpcStartup << "]: '" << cmd;
<< "Couldn't parse entry " << (i + 1) << " in [" << Sections::kRpcStartup
<< "]: " << jrReader.getFormattedErrorMessages();
continue;
}
if (!config_->quiet())
{
JLOG(journal_.fatal()) << "Startup RPC: " << jvCommand << std::endl;
JLOG(journal_.fatal()) << "Startup RPC: " << rpc::loggable(jvCommand) << std::endl;
}
resource::Charge loadType = resource::kFeeReferenceRpc;
@@ -1499,7 +1505,7 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
if (!config_->quiet())
{
JLOG(journal_.fatal()) << "Result: " << jvResult << std::endl;
JLOG(journal_.fatal()) << "Result: " << rpc::loggable(jvResult) << std::endl;
}
}
+17 -37
View File
@@ -3884,37 +3884,32 @@ NetworkOPsImp::pubProposedAccountTransaction(
HashSet<InfoSub::pointer> notify;
int iProposed = 0;
std::vector<SubAccountHistoryInfo> accountHistoryNotify;
{
std::scoped_lock const sl(accountLock_);
if (subRTAccount_.empty())
return;
if (!subAccount_.empty() || !subRTAccount_.empty() || !subAccountHistory_.empty())
for (auto const& affectedAccount : tx->getMentionedAccounts())
{
for (auto const& affectedAccount : tx->getMentionedAccounts())
if (auto const simiIt = subRTAccount_.find(affectedAccount);
simiIt != subRTAccount_.end())
{
if (auto const simiIt = subRTAccount_.find(affectedAccount);
simiIt != subRTAccount_.end())
auto it = simiIt->second.begin();
while (it != simiIt->second.end())
{
auto it = simiIt->second.begin();
InfoSub::pointer const p = it->second.lock();
while (it != simiIt->second.end())
if (p)
{
InfoSub::pointer const p = it->second.lock();
if (p)
{
notify.insert(p);
++it;
++iProposed;
}
else
{
it = simiIt->second.erase(it);
}
notify.insert(p);
++it;
++iProposed;
}
else
{
it = simiIt->second.erase(it);
}
}
}
@@ -3923,10 +3918,10 @@ NetworkOPsImp::pubProposedAccountTransaction(
JLOG(journal_.trace()) << "pubProposedAccountTransaction: " << iProposed;
if (!notify.empty() || !accountHistoryNotify.empty())
if (!notify.empty())
{
// Create two different Json objects, for different API versions
MultiApiJson jvObj = transJson(tx, result, false, ledger, std::nullopt);
MultiApiJson const jvObj = transJson(tx, result, false, ledger, std::nullopt);
for (InfoSub::Ref isrListener : notify)
{
@@ -3934,21 +3929,6 @@ NetworkOPsImp::pubProposedAccountTransaction(
isrListener->getApiVersion(), //
[&](json::Value const& jv) { isrListener->send(jv, true); });
}
XRPL_ASSERT(
jvObj.isMember(jss::account_history_tx_stream) == MultiApiJson::IsMemberResult::None,
"xrpl::NetworkOPs::pubProposedAccountTransaction : "
"account_history_tx_stream not set");
for (auto& info : accountHistoryNotify)
{
auto const& index = info.index;
if (index->forwardTxIndex == 0 && !index->haveHistorical)
jvObj.set(jss::account_history_tx_first, true);
jvObj.set(jss::account_history_tx_index, index->forwardTxIndex++);
jvObj.visit(
info.sink->getApiVersion(), //
[&](json::Value const& jv) { info.sink->send(jv, true); });
}
}
}
+52 -1
View File
@@ -171,6 +171,18 @@ public:
void
onRequest(Session& session);
/**
* Receives one WebSocket frame.
*
* A frame that exceeds the request size limit, does not parse or is not
* an object is answered here with `jsonInvalid` and the frame's `size`;
* its body is never echoed, since an unparsed body has no fields to mask.
* Any other frame is posted to the job queue and answered from
* processSession.
*
* @param session The WebSocket session the frame arrived on.
* @param buffers The frame's bytes.
*/
void
onWSMessage(
std::shared_ptr<WSSession> session,
@@ -183,15 +195,54 @@ public:
onStopped(Server&);
private:
/**
* Serves one parsed WebSocket request.
*
* Closes the connection when its resource balance is past the drop
* threshold. Otherwise checks the API version and the `command` and
* `method` fields, dispatches through rpc::doCommand, charges the
* session's consumer, and shapes the reply as a `response`, echoing the
* masked request on an error.
*
* @param session The session the request arrived on.
* @param coro The coroutine the request runs on.
* @param jv The parsed request.
* @return The reply to send.
*/
json::Value
processSession(
std::shared_ptr<WSSession> const& session,
std::shared_ptr<JobQueue::Coro> const& coro,
json::Value const& jv);
/**
* Serves one HTTP request on a coroutine: hands the body, the client
* address and the forwarding headers to processRequest, then completes
* or closes the session as its keep-alive header asks.
*
* @param session The HTTP session the request arrived on.
* @param coro The coroutine the request runs on.
*/
void
processSession(std::shared_ptr<Session> const&, std::shared_ptr<JobQueue::Coro> coro);
processSession(std::shared_ptr<Session> const& session, std::shared_ptr<JobQueue::Coro> coro);
/**
* Serves one HTTP body: parses it, answers a malformed or unauthorized
* request with a plain-text status, serves a `method: "batch"` body entry
* by entry, dispatches each request through rpc::doCommand, and writes
* the reply with its HTTP status. An error reply echoes the masked
* request.
*
* @param port The port the request arrived on, for its role and limits.
* @param request The raw body.
* @param remoteIPAddress The client address, for resource accounting and
* the role.
* @param output Where the reply bytes are written.
* @param coro The coroutine the request runs on.
* @param forwardedFor The `X-Forwarded-For` header, when the port trusts
* a proxy.
* @param user The `X-User` header.
*/
void
processRequest(
Port const& port,
+88
View File
@@ -0,0 +1,88 @@
#include <xrpld/rpc/detail/MaskSecrets.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <algorithm>
#include <string>
#include <string_view>
namespace xrpl::rpc {
namespace {
/**
* Reports whether @p name is one of kCredentialFields.
*
* An array element's memberName is empty, which no credential is spelled as,
* so an element is never a credential itself and is descended into.
*
* @param name The member name to look up.
* @return True if @p name is a credential-bearing field.
*/
bool
isCredential(std::string_view name)
{
return !name.empty() && std::ranges::find(kCredentialFields, name) != kCredentialFields.end();
}
/**
* Replaces every credential-bearing field of @p value in place, at every depth.
*
* @param value The object or array to mask. Any other value is left alone.
*/
void
maskInPlace(json::Value& value)
{
if (!value.isObject() && !value.isArray())
return;
for (auto it = value.begin(); it != value.end(); ++it)
{
// A credential is replaced whatever it holds, so it is not descended into.
if (isCredential(it.memberName()))
{
*it = json::Value{"<masked>"};
}
else
{
maskInPlace(*it);
}
}
}
} // namespace
bool
hasSecret(json::Value const& value)
{
if (!value.isObject() && !value.isArray())
return false;
for (auto it = value.begin(); it != value.end(); ++it)
{
if (isCredential(it.memberName()) || hasSecret(*it))
return true;
}
return false;
}
json::Value
maskSecrets(json::Value const& request)
{
auto masked = request;
maskInPlace(masked);
return masked;
}
std::string
loggable(json::Value const& value)
{
// The copy the mask takes is paid only when there is a credential to replace.
auto text = hasSecret(value) ? to_string(maskSecrets(value)) : to_string(value);
if (text.size() > kMaxLoggedChars)
text.resize(kMaxLoggedChars);
return text;
}
} // namespace xrpl::rpc
+99
View File
@@ -0,0 +1,99 @@
#pragma once
#include <xrpl/json/json_value.h>
#include <array>
#include <cstddef>
#include <string>
#include <string_view>
namespace xrpl::rpc {
/**
* Fields that carry a credential, in a request or in a reply.
*
* Literals rather than jss entries, since not every one has a jss entry.
*
* `username` and `password` are `subscribe`'s deprecated spellings of
* `url_username` and `url_password`, which is why a name identifying rather
* than authenticating is on the list. The `master_` and `validation_` names
* come from `wallet_propose` and `validation_create` replies, and a reply is
* logged like a request. `master_key` is also the public key that
* `validator_info` and the `manifests` and `validations` streams report, and
* it is masked with the rest.
*/
inline constexpr std::array<std::string_view, 16> kCredentialFields{
"admin_password",
"admin_user",
"master_key",
"master_seed",
"master_seed_hex",
"passphrase",
"password",
"secret",
"seed",
"seed_hex",
"url_password",
"url_username",
"username",
"validation_key",
"validation_private_key",
"validation_seed",
};
/**
* How much of a request or a reply a log line carries.
*
* A client chooses the size of what it sends, so nothing it cannot choose
* bounds the length.
*/
inline constexpr std::size_t kMaxLoggedChars = 10000;
/**
* Returns a copy of @p request with every credential-bearing field replaced by
* a placeholder.
*
* Apply to every request echoed back to a caller. Masks at every depth, since
* the JSON-RPC transport nests a credential inside `params` and a batch nests
* one request per entry. Recursion is bounded by json::Reader::kNestLimit.
*
* @param request The request to mask.
* @return The masked copy.
*/
[[nodiscard]] json::Value
maskSecrets(json::Value const& request);
/**
* Reports whether @p value carries a credential-bearing field at any depth.
*
* Walks objects and arrays without copying and stops at the first field named
* in kCredentialFields, so a caller pays for masking only when there is
* something to mask. A value with no members carries none.
*
* @param value The request or reply to inspect.
* @return True if a credential-bearing field is present at any depth.
*/
[[nodiscard]] bool
hasSecret(json::Value const& value);
/**
* Renders @p value for a log line: masked when it carries a credential, then
* truncated to kMaxLoggedChars.
*
* Every site that writes a request or a reply body to the log goes through
* this. A value with no credential is serialized as it is, so the copy the
* mask takes is paid only when hasSecret() reports one.
*
* Call it inside the JLOG argument and never before the macro: that argument is
* evaluated only when the sink is active, so hoisting the call puts the walk
* and the serialization on every request.
*
* Truncation is by bytes, so a multi-byte character can be split.
*
* @param value The request or reply to render.
* @return The masked, truncated text.
*/
[[nodiscard]] std::string
loggable(json::Value const& value);
} // namespace xrpl::rpc
+82 -17
View File
@@ -3,6 +3,7 @@
#include <xrpld/core/Config.h>
#include <xrpld/rpc/MethodNames.h>
#include <xrpld/rpc/ServerHandler.h>
#include <xrpld/rpc/detail/MaskSecrets.h>
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/basics/Log.h>
@@ -23,6 +24,7 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ApiVersion.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/JsonRpc.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/RPCErr.h>
@@ -614,21 +616,32 @@ private:
return rpcError(RpcInvalidParams);
}
// json <command> <json>
/**
* Parses the `json <command> <json>` form: the second argument is parsed
* as the request and the first becomes its `method`. The request is
* logged masked once parsed; the unparsed text never is.
*
* @param jvParams The command name and the JSON text.
* @return The request, or `invalidParams` when the text does not parse
* to an object.
*/
json::Value
parseJson(json::Value const& jvParams)
{
json::Reader reader;
json::Value jvRequest;
// The command name cannot carry a credential, so it logs whether the JSON parses or not.
JLOG(j_.trace()) << "RPC method: " << jvParams[0u];
JLOG(j_.trace()) << "RPC json: " << jvParams[1u];
if (reader.parse(jvParams[1u].asString(), jvRequest))
{
if (!jvRequest.isObjectOrNull())
return rpcError(RpcInvalidParams);
// Logged only once parsed, so a signing secret among the members can be masked.
JLOG(j_.trace()) << "RPC json: " << rpc::loggable(jvRequest);
jvRequest[jss::method] = jvParams[0u];
return jvRequest;
@@ -637,6 +650,16 @@ private:
return rpcError(RpcInvalidParams);
}
/**
* Reports whether @p jv is a request this client accepts as JSON-RPC 2.0:
* an object whose `jsonrpc` is rpc::kJsonRpcVersion and `ripplerpc` is
* rpc::kRippleRpcVersion2, carrying an `id` and a `method`, with `params`
* absent, null, an array or an object; or a non-empty array of such
* objects.
*
* @param jv The parsed argument.
* @return True when the shape is accepted.
*/
bool
isValidJson2(json::Value const& jv)
{
@@ -651,8 +674,8 @@ private:
}
if (jv.isObject())
{
if (jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0" &&
jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0" &&
if (jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == rpc::kJsonRpcVersion &&
jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == rpc::kRippleRpcVersion2 &&
jv.isMember(jss::id) && jv.isMember(jss::method))
{
return !jv.isMember(jss::params) ||
@@ -1010,7 +1033,14 @@ private:
return jvRequest;
}
// ripple_path_find <json> [<ledger>]
/**
* Parses the `ripple_path_find <json> [<ledger>]` form: the first
* argument is parsed as the request and logged masked once parsed, the
* optional second names the ledger.
*
* @param jvParams The JSON text and, optionally, the ledger.
* @return The request, or `invalidParams` when the text does not parse.
*/
json::Value
parseRipplePathFind(json::Value const& jvParams)
{
@@ -1018,10 +1048,12 @@ private:
json::Value jvRequest{json::ValueType::Object};
bool const bLedger = 2 == jvParams.size();
JLOG(j_.trace()) << "RPC json: " << jvParams[0u];
if (reader.parse(jvParams[0u].asString(), jvRequest))
{
// The JSON is logged only once parsed, so a signing secret among the named members can
// be masked. Unparsed it is a bare string with no members to name.
JLOG(j_.trace()) << "RPC json: " << rpc::loggable(jvRequest);
if (bLedger)
{
jvParseLedger(jvRequest, jvParams[1u].asString());
@@ -1750,11 +1782,7 @@ public:
json::Value
parseCommand(std::string_view strMethod, json::Value const& jvParams, bool allowAnyCommand)
{
if (auto const stream = j_.trace())
{
stream << "Method: '" << strMethod << "'";
stream << "Params: " << jvParams;
}
JLOG(j_.trace()) << "Method: '" << strMethod << "'";
auto const found = std::ranges::lower_bound(kSortedCommands, strMethod, {}, &Command::name);
@@ -1861,15 +1889,26 @@ struct RPCCallImp
"process.");
}
// Parse reply
JLOG(j.debug()) << "RPC reply: " << strData << std::endl;
// Parse reply. A plain text body is a rejection, which carries no field a credential
// could sit in, so it is logged as it is, capped at kMaxLoggedChars.
if (strData.starts_with("Unable to parse request") ||
strData.starts_with(jss::invalid_API_version.cStr()))
{
JLOG(j.debug()) << "RPC reply: "
<< std::string_view{strData}.substr(0, rpc::kMaxLoggedChars);
Throw<RequestNotParsable>(strData);
}
json::Reader reader;
json::Value jvReply;
if (!reader.parse(strData, jvReply))
{
JLOG(j.debug()) << "RPC reply: "
<< std::string_view{strData}.substr(0, rpc::kMaxLoggedChars);
Throw<std::runtime_error>("couldn't parse reply from server");
}
// Logged once parsed, masked: `validation_create` and `wallet_propose` answer with a
// private key, which the fallback scrubber in Log.cpp does not know by every name.
JLOG(j.debug()) << "RPC reply: " << rpc::loggable(jvReply);
if (!jvReply)
Throw<std::runtime_error>("expected reply to have result, error and id properties");
@@ -1911,7 +1950,16 @@ commandLineMethodNames()
return RPCParser::methodNames();
}
// Used internally by rpcClient.
/**
* Translates command line arguments into the request the client sends.
*
* @param args The method name followed by its positional arguments.
* @param retParams Receives the arguments as `method` and `params`, the form
* `rpc` echoes on an error.
* @param apiVersion The `api_version` stamped on each request naming none.
* @param j The journal the built request is logged to, masked.
* @return The request, an array of requests, or the parser's error object.
*/
json::Value
rpcCmdToJson(
std::vector<std::string> const& args,
@@ -1951,12 +1999,27 @@ rpcCmdToJson(
std::for_each(jvRequest.begin(), jvRequest.end(), insertApiVersion);
}
JLOG(j.trace()) << "RPC Request: " << jvRequest << std::endl;
// `sign` and `submit` put the signing secret in the request the command line built.
JLOG(j.trace()) << "RPC Request: " << rpc::loggable(jvRequest) << std::endl;
return jvRequest;
}
//------------------------------------------------------------------------------
/**
* Runs one command line command against the configured server.
*
* Builds the request, adds the admin credentials from the config, sends it
* over HTTP and unwraps the result. On an error the output also carries the
* invocation as `rpc` and the request as `request_sent`, masked.
*
* @param args The method name followed by its arguments.
* @param config The client configuration naming the server and credentials.
* @param logs The log manager.
* @param apiVersion The `api_version` to request.
* @param headers Extra HTTP headers to send.
* @return The exit code and the output to print.
*/
std::pair<int, json::Value>
rpcClient(
std::vector<std::string> const& args,
@@ -2068,7 +2131,9 @@ rpcClient(
if (jvOutput.isMember(jss::error))
{
jvOutput["rpc"] = jvRpc; // How the command was seen as method + params.
jvOutput["request_sent"] = jvRequest; // How the command was translated.
// Carries the config's `admin_password` and prints to stdout, so it is masked.
// `jvRpc` above is positional, so masking by member name does not reach it.
jvOutput["request_sent"] = rpc::maskSecrets(jvRequest);
}
}
+2 -1
View File
@@ -6,6 +6,7 @@
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/detail/Handler.h>
#include <xrpld/rpc/detail/MaskSecrets.h>
#include <xrpld/rpc/detail/Tuning.h>
#include <xrpl/basics/Log.h>
@@ -136,7 +137,7 @@ fillHandler(JsonContext& context, Handler const*& result)
: context.params[jss::method].asString();
JLOG(context.j.trace()) << "COMMAND:" << strCommand;
JLOG(context.j.trace()) << "REQUEST:" << context.params;
JLOG(context.j.trace()) << "REQUEST:" << loggable(context.params);
auto handler = getHandler(context.apiVersion, context.app.config().betaRpcApi, strCommand);
if (handler == nullptr)
+2 -1
View File
@@ -1,6 +1,7 @@
#include <xrpld/rpc/RPCSub.h>
#include <xrpld/rpc/RPCCall.h>
#include <xrpld/rpc/detail/MaskSecrets.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/StringUtilities.h>
@@ -87,7 +88,7 @@ public:
std::scoped_lock const sl(lock_);
auto const jm = broadcast ? j_.debug() : j_.info();
JLOG(jm) << "rpc_call::fromNetwork push: " << jvObj;
JLOG(jm) << "rpc_call::fromNetwork push: " << rpc::loggable(jvObj);
deque_.emplace_back(seq_++, jvObj);
+402 -247
View File
@@ -4,6 +4,7 @@
#include <xrpld/overlay/Overlay.h>
#include <xrpld/rpc/RPCHandler.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/detail/MaskSecrets.h>
#include <xrpld/rpc/detail/Tuning.h>
#include <xrpld/rpc/detail/WSInfoSub.h>
@@ -28,6 +29,7 @@
#include <xrpl/protocol/ApiVersion.h>
#include <xrpl/protocol/BuildInfo.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/JsonRpc.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/jss.h>
@@ -60,10 +62,13 @@
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cstddef>
#include <exception>
#include <limits>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <ostream>
#include <string>
#include <string_view>
@@ -102,6 +107,38 @@ statusRequestResponse(HttpRequestType const& request, boost::beast::http::status
return handoff;
}
/**
* The object a request presents its parameters in.
*
* `params` is an array holding one object for the by-position form and is that
* object itself for the by-name form. Anything else reads as an empty object,
* since a caller reads the version and the credentials out of it before the
* shape is validated.
*
* Answers an Object or a Null and nothing else: Role.cpp reads `admin_password`
* out of the result through the const `operator[]`, which asserts on any other
* type, so widening this makes that line a remotely reachable assert.
*
* @param request The request, which need not be an object.
* @return The parameters, or an empty object when the request presents none.
* Borrowed from @p request, so it lives as long as the request does.
*/
static json::Value const&
requestParams(json::Value const& request)
{
static json::Value const kNoParams{json::ValueType::Object};
if (!request.isObject())
return kNoParams;
auto const& params = request[jss::params];
if (params.isArray() && params.size() > 0 && params[json::UInt(0)].isObjectOrNull())
return params[json::UInt(0)];
if (params.isObject())
return params;
return kNoParams;
}
// VFALCO TODO Rewrite to use boost::beast::http::fields
static bool
authorized(Port const& port, std::map<std::string, std::string> const& h)
@@ -340,10 +377,23 @@ ServerHandler::onWSMessage(
auto const size = boost::asio::buffer_size(buffers);
if (size > rpc::tuning::kMaxRequestSize || !json::Reader{}.parse(jv, buffers) || !jv.isObject())
{
// The only place that can account for an unreadable frame: it never reaches
// processSession, where every other request is charged.
auto const is = std::static_pointer_cast<WSInfoSub>(session->appDefined);
is->getConsumer().charge(resource::kFeeMalformedRpc);
if (is->getConsumer().disconnect(journal_))
{
session->close({boost::beast::websocket::policy_error, "threshold exceeded"});
return;
}
// An unparsed body cannot be masked field-wise, so its size goes instead of its content.
// Clamped rather than narrowed: json has no integer wider than 32 bits.
json::Value jvResult(json::ValueType::Object);
jvResult[jss::type] = jss::error;
jvResult[jss::error] = "jsonInvalid";
jvResult[jss::value] = buffersToString(buffers);
jvResult[jss::size] =
json::UInt(std::min<std::size_t>(size, std::numeric_limits<json::UInt>::max()));
boost::beast::multi_buffer sb;
json::stream(jvResult, [&sb](auto const p, auto const n) {
sb.commit(boost::asio::buffer_copy(sb.prepare(n), boost::asio::buffer(p, n)));
@@ -354,7 +404,7 @@ ServerHandler::onWSMessage(
return;
}
JLOG(journal_.trace()) << "Websocket received '" << jv << "'";
JLOG(journal_.trace()) << "Websocket received '" << rpc::loggable(jv) << "'";
auto const postResult = jobQueue_.postCoro(
JtClientWebsocket,
@@ -392,22 +442,35 @@ ServerHandler::onStopped(Server&)
//------------------------------------------------------------------------------
/**
* Logs how long a request took.
*
* A slow request is reported at warn from one second and at error from ten,
* with the duration only. The request is logged at debug, since it is client
* text.
*
* @param request The request the duration belongs to.
* @param duration How long processing it took.
* @param journal Where the line is written.
*/
template <class T>
void
logDuration(json::Value const& request, T const& duration, beast::Journal& journal)
{
using namespace std::chrono_literals;
auto const level = [&] {
if (duration >= 10s)
return journal.error();
if (duration >= 1s)
return journal.warn();
return journal.debug();
}();
auto const micros = std::chrono::duration_cast<std::chrono::microseconds>(duration).count();
JLOG(level) << "RPC request processing duration = "
<< std::chrono::duration_cast<std::chrono::microseconds>(duration).count()
<< " microseconds. request = " << request;
if (duration >= 1s)
{
auto const slow = duration >= 10s ? journal.error() : journal.warn();
JLOG(slow) << "RPC request processing duration = " << micros << " microseconds.";
}
if (auto const stream = journal.debug())
{
stream << "RPC request processing duration = " << micros
<< " microseconds. request = " << rpc::loggable(request);
}
}
json::Value
@@ -442,7 +505,7 @@ ServerHandler::processSession(
jr[jss::status] = jss::error;
jr[jss::error] = apiVersion == rpc::kApiInvalidVersion ? jss::invalid_API_version
: jss::missingCommand;
jr[jss::request] = jv;
jr[jss::request] = rpc::maskSecrets(jv);
if (jv.isMember(jss::id))
jr[jss::id] = jv[jss::id];
if (jv.isMember(jss::jsonrpc))
@@ -498,11 +561,9 @@ ServerHandler::processSession(
}
catch (std::exception const& ex)
{
// LCOV_EXCL_START
jr[jss::result] = rpc::makeError(RpcInternal);
JLOG(journal_.error()) << "Exception while processing WS: " << ex.what() << "\n"
<< "Input JSON: " << json::Compact{json::Value{jv}};
// LCOV_EXCL_STOP
<< "Input JSON: " << rpc::loggable(jv);
}
is->getConsumer().charge(loadType);
@@ -519,21 +580,7 @@ ServerHandler::processSession(
jr = jr[jss::result];
jr[jss::status] = jss::error;
auto rq = jv;
if (rq.isObject())
{
if (rq.isMember(jss::passphrase.cStr()))
rq[jss::passphrase.cStr()] = "<masked>";
if (rq.isMember(jss::secret.cStr()))
rq[jss::secret.cStr()] = "<masked>";
if (rq.isMember(jss::seed.cStr()))
rq[jss::seed.cStr()] = "<masked>";
if (rq.isMember(jss::seed_hex.cStr()))
rq[jss::seed_hex.cStr()] = "<masked>";
}
jr[jss::request] = rq;
jr[jss::request] = rpc::maskSecrets(jv);
}
else
{
@@ -594,10 +641,143 @@ makeJsonError(json::Int code, json::Value&& message)
return r;
}
constexpr json::Int kMethodNotFound = -32601;
constexpr json::Int kServerOverloaded = -32604;
constexpr json::Int kForbidden = -32605;
constexpr json::Int kWrongVersion = -32606;
// Version of the JSON-RPC reply envelope, selected by the `ripplerpc` request parameter. It shapes
// a completed result into a reply; no handler observes it.
enum class RpcVersion {
// Errors are returned under `result`, keyed by `error_message`, and echo the (secret-masked)
// request. HTTP status is always 200.
V1,
// Errors are returned under `error`, keyed by `message`, and the request is not echoed. HTTP
// status is always 200.
V2,
// As V2, but the error code selects a 4xx/5xx HTTP status.
V3,
};
constexpr RpcVersion kRpcVersionIfUnspecified = RpcVersion::V1;
/**
* Maps a `ripplerpc` value onto the envelope version it selects.
*
* The value is unauthenticated, so anything but an exact match is refused.
*
* @param value The `ripplerpc` member, as sent.
* @return The envelope version, or nullopt for the caller to reject.
*/
static std::optional<RpcVersion>
rpcVersion(std::string_view value)
{
if (value == rpc::kRippleRpcVersion1)
return RpcVersion::V1;
if (value == rpc::kRippleRpcVersion2)
return RpcVersion::V2;
if (value == rpc::kRippleRpcVersion3)
return RpcVersion::V3;
return std::nullopt;
}
/**
* The HTTP status the `ripplerpc: "3.0"` envelope reports for @p code.
*
* The codes below answer 200, which is what a client calling `account_info`
* this way already reads. The list is closed: a code belongs on it only if it
* already answered a coded reply at this status.
*
* @param code The error code the reply reports.
* @return The HTTP status the reply is sent with.
*/
static int
legacyHttpStatus(ErrorCodeI code)
{
switch (code)
{
case RpcActMalformed:
case RpcActNotFound:
case RpcAlreadyMultisig:
case RpcAlreadySingleSig:
return 200;
default:
return rpc::errorCodeHttpStatus(code);
}
}
/**
* Shapes a handler @p result into the reply envelope for @p version, appending
* it to @p reply.
*
* @param version The envelope the reply takes.
* @param result A handler result, consumed.
* @param request The request's parameters, echoed on a version 1 error only,
* and where the `jsonrpc`, `ripplerpc` and `id` members are read.
* @param batch Whether to append to @p reply rather than assign it.
* @param reply The reply this writes.
* @param journal Where an error is logged.
* @return The HTTP status the reply is sent with. Only version 3 derives it
* from the error code; the others report 200. A batch discards it.
*/
static int
shapeReply(
RpcVersion version,
json::Value result,
json::Value const& request,
bool batch,
json::Value& reply,
beast::Journal journal)
{
json::Value r(json::ValueType::Object);
int status = 200;
if (!result.isMember(jss::error))
{
result[jss::status] = jss::success;
r[jss::result] = std::move(result);
}
else
{
// Read through a const reference: the non-const `operator[]` inserts a null member for an
// absent name, and the reply below is built out of `result`.
json::Value const& reported = result;
JLOG(journal.debug()) << "rpcError: " << reported[jss::error] << ": "
<< reported[jss::error_message];
if (version == RpcVersion::V3 && reported[jss::error_code].isInt())
status = legacyHttpStatus(static_cast<ErrorCodeI>(reported[jss::error_code].asInt()));
result[jss::status] = jss::error;
if (version == RpcVersion::V1)
{
result[jss::request] = rpc::maskSecrets(request);
r[jss::result] = std::move(result);
}
else
{
result[jss::code] = result[jss::error_code];
result[jss::message] = result[jss::error_message];
result.removeMember(jss::error_message);
r[jss::error] = std::move(result);
}
}
if (request.isMember(jss::jsonrpc))
r[jss::jsonrpc] = request[jss::jsonrpc];
if (request.isMember(jss::ripplerpc))
r[jss::ripplerpc] = request[jss::ripplerpc];
if (request.isMember(jss::id))
r[jss::id] = request[jss::id];
if (batch)
{
reply.append(std::move(r));
}
else
{
reply = std::move(r);
}
return status;
}
void
ServerHandler::processRequest(
@@ -611,12 +791,55 @@ ServerHandler::processRequest(
{
auto const rpcJ = app_.getJournal("RPC");
// What a request presents when the server cannot read what it presents: an unparsable body or a
// malformed entry carries no credentials to act on. A connection privileged by its address is
// still privileged, `requestRole` reading the port rather than the request for that.
static json::Value const kUnreadable(json::ValueType::Object);
// The resource entry a request is charged against before its own role is read. `Role::GUEST` is
// the requirement asked for, no method having been read that could ask for more.
auto const usageFor = [&](json::Value const& presented) {
return requestInboundEndpoint(
resourceManager_,
remoteIPAddress,
requestRole(Role::GUEST, port, presented, remoteIPAddress, user),
user,
forwardedFor);
};
// Whether a charge also asks if the connection is over the drop threshold.
enum class Threshold { Ignore, Ask };
// Charges a request rejected before its own role is read, and reports whether the connection is
// over the drop threshold. Only a caller that can act on the answer passes `Threshold::Ask`,
// since asking is itself a charge: see chargeUnreadBody below.
auto const chargeWithoutRole = [&](json::Value const& presented, Threshold threshold) {
auto usage = usageFor(presented);
usage.charge(resource::kFeeMalformedRpc);
return threshold == Threshold::Ask && usage.disconnect(journal_);
};
// Charges a body the server rejects before it reads a request out of it. The drop threshold is
// not asked about: `disconnect` is a mutator, charging the drop fee and counting a drop on
// every call made while the balance is at or above the drop threshold, and this rejection
// answers the whole body.
auto const chargeUnreadBody = [&] { usageFor(kUnreadable).charge(resource::kFeeMalformedRpc); };
json::Value jsonOrig;
{
json::Reader reader;
if ((request.size() > rpc::tuning::kMaxRequestSize) || !reader.parse(request, jsonOrig) ||
!jsonOrig || !jsonOrig.isObject())
// Only a parse failure can report the reader's reason, getFormattedErrorMessages being
// built from what the reader recorded. The other three name their own cause.
if (request.size() > rpc::tuning::kMaxRequestSize)
{
chargeUnreadBody();
httpReply(400, "Request is too large", output, rpcJ);
return;
}
json::Reader reader;
if (!reader.parse(request, jsonOrig))
{
chargeUnreadBody();
httpReply(
400,
"Unable to parse request: " + reader.getFormattedErrorMessages(),
@@ -624,15 +847,35 @@ ServerHandler::processRequest(
rpcJ);
return;
}
if (!jsonOrig)
{
// A well-formed document that carries nothing: `{}`, `[]` or `null`.
chargeUnreadBody();
httpReply(400, "Request is empty", output, rpcJ);
return;
}
if (!jsonOrig.isObject())
{
// A non-empty array, the only value the reader accepts that is neither null nor an
// object. A number, string or boolean is a parse failure, answered above.
chargeUnreadBody();
httpReply(400, "Request is not a JSON object", output, rpcJ);
return;
}
}
bool batch = false;
unsigned size = 1;
if (jsonOrig.isMember(jss::method) && jsonOrig[jss::method] == "batch")
// Spelled as a view so the name is compared in place. A bare literal would reach the
// Value-to-Value comparison instead, which builds a Value from it, allocating per request.
if (jsonOrig.isMember(jss::method) && jsonOrig[jss::method] == std::string_view{"batch"})
{
batch = true;
if (!jsonOrig.isMember(jss::params) || !jsonOrig[jss::params].isArray())
{
chargeUnreadBody();
httpReply(400, "Malformed batch request", output, rpcJ);
return;
}
@@ -640,45 +883,83 @@ ServerHandler::processRequest(
}
json::Value reply(batch ? json::ValueType::Array : json::ValueType::Object);
// Only a lone request selects the HTTP status: a batch may mix versions and reports each
// entry's outcome in its own reply, so the batch itself always succeeds.
int httpStatus = 200;
auto const start(std::chrono::high_resolution_clock::now());
for (unsigned i = 0; i < size; ++i)
{
json::Value const& jsonRPC = batch ? jsonOrig[jss::params][i] : jsonOrig;
// Only an entry of a batch can be a non-object; a lone request was checked before the loop.
// Inline rather than through `reject` below: an entry with no members carries the copy
// under `request` whatever a caller asks for.
if (!jsonRPC.isObject())
{
// Only a batch reaches here, so the loop can act on the threshold. The entry presents
// no credentials, so the connection pays.
bool const overloaded = chargeWithoutRole(kUnreadable, Threshold::Ask);
json::Value r(json::ValueType::Object);
r[jss::request] = jsonRPC;
r[jss::error] = makeJsonError(kMethodNotFound, "Method not found");
r[jss::request] = rpc::maskSecrets(jsonRPC);
r[jss::error] = makeJsonError(rpc::kJsonRpcMethodNotFound, "Method not found");
reply.append(r);
if (overloaded)
break;
continue;
}
unsigned apiVersion = rpc::kApiVersionIfUnspecified;
if (jsonRPC.isMember(jss::params) && jsonRPC[jss::params].isArray() &&
jsonRPC[jss::params].size() > 0 && jsonRPC[jss::params][0u].isObject())
{
apiVersion = rpc::getAPIVersionNumber(
jsonRPC[jss::params][json::UInt(0)], app_.config().betaRpcApi);
}
// The version lives with the request's parameters, in whichever form the request presents
// them. A `method: "batch"` entry that names none there carries it beside its method.
unsigned apiVersion =
rpc::getAPIVersionNumber(requestParams(jsonRPC), app_.config().betaRpcApi);
if (apiVersion == rpc::kApiVersionIfUnspecified && batch)
{
// for batch request, api_version may be at a different level
apiVersion = rpc::getAPIVersionNumber(jsonRPC, app_.config().betaRpcApi);
}
// Answers a request rejected before it reaches a handler. A lone request receives the
// HTTP status and `message` as the whole body; a batch entry receives an error object
// beside a copy of the entry, one reply array having no other place to name the entry that
// failed. `wrapRequest` puts that copy under `request` instead.
//
// Returns whether the loop continues, which it does only for a batch.
auto const reject =
[&](int status, json::Int code, char const* message, bool wrapRequest = false) {
if (!batch)
{
httpReply(status, message, output, rpcJ);
return false;
}
json::Value r(json::ValueType::Object);
if (!wrapRequest)
{
r = rpc::maskSecrets(jsonRPC);
}
else
{
r[jss::request] = rpc::maskSecrets(jsonRPC);
}
r[jss::error] = makeJsonError(code, message);
reply.append(std::move(r));
return true;
};
if (apiVersion == rpc::kApiInvalidVersion)
{
if (!batch)
bool const overloaded = chargeWithoutRole(
requestParams(jsonRPC), batch ? Threshold::Ask : Threshold::Ignore);
// An object-shaped rejection returns this entry under `request`, where a client
// correlating by `reply[i].request` finds it.
if (!reject(
400,
rpc::kJsonRpcWrongVersion,
jss::invalid_API_version.cStr(),
/*wrapRequest=*/true))
{
httpReply(400, jss::invalid_API_version.cStr(), output, rpcJ);
return;
}
json::Value r(json::ValueType::Object);
r[jss::request] = jsonRPC;
r[jss::error] = makeJsonError(kWrongVersion, jss::invalid_API_version.cStr());
reply.append(r);
if (overloaded)
break;
continue;
}
@@ -691,65 +972,37 @@ ServerHandler::processRequest(
apiVersion, app_.config().betaRpcApi, jsonRPC[jss::method].asString());
}
if (jsonRPC.isMember(jss::params) && jsonRPC[jss::params].isArray() &&
jsonRPC[jss::params].size() > 0 && jsonRPC[jss::params][json::UInt(0)].isObjectOrNull())
{
role = requestRole(
required, port, jsonRPC[jss::params][json::UInt(0)], remoteIPAddress, user);
}
else
{
role = requestRole(required, port, json::ValueType::Object, remoteIPAddress, user);
}
// The role comes from the credentials the request presents, which sit with its parameters
// whichever form they take.
role = requestRole(required, port, requestParams(jsonRPC), remoteIPAddress, user);
resource::Consumer usage;
if (isUnlimited(role))
auto usage =
requestInboundEndpoint(resourceManager_, remoteIPAddress, role, user, forwardedFor);
// An overloaded server sheds the request without charging for it; disconnect() has already
// accounted for the load that got it here. The loop then stops: every later entry of the
// same body would be shed too, and answering each one is itself work an overloaded server
// must not do.
if (!isUnlimited(role) && usage.disconnect(journal_))
{
usage = resourceManager_.newUnlimitedEndpoint(remoteIPAddress);
}
else
{
usage = resourceManager_.newInboundEndpoint(
remoteIPAddress, role == Role::PROXY, forwardedFor);
if (usage.disconnect(journal_))
{
if (!batch)
{
httpReply(503, "Server is overloaded", output, rpcJ);
return;
}
json::Value r = jsonRPC;
r[jss::error] = makeJsonError(kServerOverloaded, "Server is overloaded");
reply.append(r);
continue;
}
if (!reject(503, rpc::kJsonRpcServerOverloaded, "Server is overloaded"))
return;
break;
}
if (role == Role::FORBID)
{
usage.charge(resource::kFeeMalformedRpc);
if (!batch)
{
httpReply(403, "Forbidden", output, rpcJ);
if (!reject(403, rpc::kJsonRpcForbidden, "Forbidden"))
return;
}
json::Value r = jsonRPC;
r[jss::error] = makeJsonError(kForbidden, "Forbidden");
reply.append(r);
continue;
}
if (!jsonRPC.isMember(jss::method) || jsonRPC[jss::method].isNull())
{
usage.charge(resource::kFeeMalformedRpc);
if (!batch)
{
httpReply(400, "Null method", output, rpcJ);
if (!reject(400, rpc::kJsonRpcMethodNotFound, "Null method"))
return;
}
json::Value r = jsonRPC;
r[jss::error] = makeJsonError(kMethodNotFound, "Null method");
reply.append(r);
continue;
}
@@ -757,14 +1010,8 @@ ServerHandler::processRequest(
if (!method.isString())
{
usage.charge(resource::kFeeMalformedRpc);
if (!batch)
{
httpReply(400, "method is not string", output, rpcJ);
if (!reject(400, rpc::kJsonRpcMethodNotFound, "method is not string"))
return;
}
json::Value r = jsonRPC;
r[jss::error] = makeJsonError(kMethodNotFound, "method is not string");
reply.append(r);
continue;
}
@@ -772,23 +1019,13 @@ ServerHandler::processRequest(
if (strMethod.empty())
{
usage.charge(resource::kFeeMalformedRpc);
if (!batch)
{
httpReply(400, "method is empty", output, rpcJ);
if (!reject(400, rpc::kJsonRpcMethodNotFound, "method is empty"))
return;
}
json::Value r = jsonRPC;
r[jss::error] = makeJsonError(kMethodNotFound, "method is empty");
reply.append(r);
continue;
}
// Extract request parameters from the request Json as `params`.
//
// If the field "params" is empty, `params` is an empty object.
//
// Otherwise, that field must be an array of length 1 (why?)
// and we take that first entry and validate that it's an object.
// The `params` field carries the one object a handler reads, in either of the two forms the
// specification defines: the object itself, or an array holding it.
json::Value params;
if (!batch)
{
@@ -797,14 +1034,15 @@ ServerHandler::processRequest(
{
params = json::Value(json::ValueType::Object);
}
else if (!params.isArray() || params.size() != 1)
{
usage.charge(resource::kFeeMalformedRpc);
httpReply(400, "params unparsable", output, rpcJ);
return;
}
else
else if (!params.isObject())
{
if (!params.isArray() || params.size() != 1)
{
usage.charge(resource::kFeeMalformedRpc);
httpReply(400, "params unparsable", output, rpcJ);
return;
}
params = std::move(params[0u]);
if (!params.isObjectOrNull())
{
@@ -819,41 +1057,42 @@ ServerHandler::processRequest(
params = jsonRPC;
}
std::string ripplerpc = "1.0";
RpcVersion envelope = kRpcVersionIfUnspecified;
if (params.isMember(jss::ripplerpc))
{
// A `ripplerpc` the server cannot honor is a bad parameter, so the second check reports
// that code. The first keeps the method-not-found code shipped versions report.
if (!params[jss::ripplerpc].isString())
{
usage.charge(resource::kFeeMalformedRpc);
if (!batch)
{
httpReply(400, "ripplerpc is not a string", output, rpcJ);
if (!reject(400, rpc::kJsonRpcMethodNotFound, "ripplerpc is not a string"))
return;
}
json::Value r = jsonRPC;
r[jss::error] = makeJsonError(kMethodNotFound, "ripplerpc is not a string");
reply.append(r);
continue;
}
ripplerpc = params[jss::ripplerpc].asString();
auto const parsed = rpcVersion(params[jss::ripplerpc].asString());
if (!parsed)
{
usage.charge(resource::kFeeMalformedRpc);
if (!reject(
400, rpc::kJsonRpcInvalidParams, "ripplerpc is not a supported version"))
return;
continue;
}
envelope = *parsed;
}
/**
* Clear header-assigned values if not positively identified from a
* secureGateway.
*/
if (role != Role::IDENTIFIED && role != Role::PROXY)
{
forwardedFor.remove_suffix(forwardedFor.size());
user.remove_suffix(user.size());
}
// Header-assigned values belong to the connection, so an entry not identified from a
// secureGateway drops them for itself only rather than clearing them in place.
bool const identified = role == Role::IDENTIFIED || role == Role::PROXY;
std::string_view const entryForwardedFor = identified ? forwardedFor : std::string_view{};
std::string_view const entryUser = identified ? user : std::string_view{};
JLOG(journal_.debug()) << "Query: " << strMethod << params;
JLOG(journal_.debug()) << "Query: " << strMethod << rpc::loggable(params);
// Provide the JSON-RPC method as the field "command" in the request.
params[jss::command] = strMethod;
JLOG(journal_.trace()) << "doRpcCommand:" << strMethod << ":" << params;
JLOG(journal_.trace()) << "doRpcCommand:" << strMethod << ":" << rpc::loggable(params);
resource::Charge loadType = resource::kFeeReferenceRpc;
@@ -871,7 +1110,7 @@ ServerHandler::processRequest(
.apiVersion = apiVersion,
},
params,
{.user = user, .forwardedFor = forwardedFor},
{.user = entryUser, .forwardedFor = entryForwardedFor},
};
json::Value result;
@@ -883,12 +1122,9 @@ ServerHandler::processRequest(
}
catch (std::exception const& ex)
{
// LCOV_EXCL_START
result = rpc::makeError(RpcInternal);
JLOG(journal_.error())
<< "Internal error : " << ex.what()
<< " when processing request: " << json::Compact{json::Value{params}};
// LCOV_EXCL_STOP
JLOG(journal_.error()) << "Internal error : " << ex.what()
<< " when processing request: " << rpc::loggable(params);
}
auto const end = std::chrono::system_clock::now();
@@ -899,72 +1135,9 @@ ServerHandler::processRequest(
if (usage.warn())
result[jss::warning] = jss::load;
json::Value r(json::ValueType::Object);
if (ripplerpc >= "2.0")
{
if (result.isMember(jss::error))
{
result[jss::status] = jss::error;
result["code"] = result[jss::error_code];
result["message"] = result[jss::error_message];
result.removeMember(jss::error_message);
JLOG(journal_.debug())
<< "rpcError: " << result[jss::error] << ": " << result[jss::error_message];
r[jss::error] = std::move(result);
}
else
{
result[jss::status] = jss::success;
r[jss::result] = std::move(result);
}
}
else
{
// Always report "status". On an error report the request as
// received.
if (result.isMember(jss::error))
{
auto rq = params;
if (rq.isObject())
{ // But mask potentially sensitive information.
if (rq.isMember(jss::passphrase.cStr()))
rq[jss::passphrase.cStr()] = "<masked>";
if (rq.isMember(jss::secret.cStr()))
rq[jss::secret.cStr()] = "<masked>";
if (rq.isMember(jss::seed.cStr()))
rq[jss::seed.cStr()] = "<masked>";
if (rq.isMember(jss::seed_hex.cStr()))
rq[jss::seed_hex.cStr()] = "<masked>";
}
result[jss::status] = jss::error;
result[jss::request] = rq;
JLOG(journal_.debug())
<< "rpcError: " << result[jss::error] << ": " << result[jss::error_message];
}
else
{
result[jss::status] = jss::success;
}
r[jss::result] = std::move(result);
}
if (params.isMember(jss::jsonrpc))
r[jss::jsonrpc] = params[jss::jsonrpc];
if (params.isMember(jss::ripplerpc))
r[jss::ripplerpc] = params[jss::ripplerpc];
if (params.isMember(jss::id))
r[jss::id] = params[jss::id];
if (batch)
{
reply.append(std::move(r));
}
else
{
reply = std::move(r);
}
int const status = shapeReply(envelope, std::move(result), params, batch, reply, journal_);
if (!batch)
httpStatus = status;
if (reply.isMember(jss::result) && reply[jss::result].isMember(jss::result))
{
@@ -977,25 +1150,6 @@ ServerHandler::processRequest(
}
}
// If we're returning an error_code, use that to determine the HTTP status.
int const httpStatus = [&reply] {
// This feature is enabled with ripplerpc version 3.0 and above.
// Before ripplerpc version 3.0 always return 200.
if (reply.isMember(jss::ripplerpc) && reply[jss::ripplerpc].isString() &&
reply[jss::ripplerpc].asString() >= "3.0")
{
// If there's an error_code, use that to determine the HTTP Status.
if (reply.isMember(jss::error) && reply[jss::error].isMember(jss::error_code) &&
reply[jss::error][jss::error_code].isInt())
{
int const errCode = reply[jss::error][jss::error_code].asInt();
return rpc::errorCodeHttpStatus(static_cast<ErrorCodeI>(errCode));
}
}
// Return OK.
return 200;
}();
auto response = to_string(reply);
rpcTime_.notify(
@@ -1004,21 +1158,22 @@ ServerHandler::processRequest(
++rpcRequests_;
rpcSize_.notify(beast::insight::Event::value_type{response.size()});
response += '\n';
// The serialized reply is in hand, so it is logged as built; the masked copy and its second
// serialization are paid for only when a credential has to be replaced.
if (auto const stream = journal_.debug())
{
static int const kMaxSize = 10000;
if (response.size() <= kMaxSize)
if (rpc::hasSecret(reply))
{
stream << "Reply: " << response;
stream << "Reply: " << rpc::loggable(reply);
}
else
{
stream << "Reply: " << response.substr(0, kMaxSize);
stream << "Reply: " << std::string_view{response}.substr(0, rpc::kMaxLoggedChars);
}
}
response += '\n';
httpReply(httpStatus, response, output, rpcJ);
}
+6 -4
View File
@@ -8,6 +8,7 @@
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/detail/AssetCache.h>
#include <xrpld/rpc/detail/LegacyPathFind.h>
#include <xrpld/rpc/detail/MaskSecrets.h>
#include <xrpld/rpc/detail/Pathfinder.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpld/rpc/detail/Tuning.h>
@@ -1018,7 +1019,8 @@ transactionSign(
// could change the signing prefix of an alternate signature field.
std::shared_ptr<ReadView const> const ledger = app.getOpenLedger().current();
auto const j = app.getJournal("RPCHandler");
JLOG(j.debug()) << "transactionSign: " << jvRequest;
// The next statement reads the signing secret out of this same request.
JLOG(j.debug()) << "transactionSign: " << loggable(jvRequest);
// Add and amend fields based on the transaction type.
SigningForParams signForParams;
@@ -1055,7 +1057,7 @@ transactionSubmit(
auto const& ledger = app.getOpenLedger().current();
auto const j = app.getJournal("RPCHandler");
JLOG(j.debug()) << "transactionSubmit: " << jvRequest;
JLOG(j.debug()) << "transactionSubmit: " << loggable(jvRequest);
// Add and amend fields based on the transaction type.
SigningForParams signForParams;
@@ -1177,7 +1179,7 @@ transactionSignFor(
{
auto const& ledger = app.getOpenLedger().current();
auto const j = app.getJournal("RPCHandler");
JLOG(j.debug()) << "transactionSignFor: " << jvRequest;
JLOG(j.debug()) << "transactionSignFor: " << loggable(jvRequest);
// Verify presence of the signer's account field.
char const accountField[] = "account";
@@ -1301,7 +1303,7 @@ transactionSubmitMultiSigned(
{
auto const& ledger = app.getOpenLedger().current();
auto const j = app.getJournal("RPCHandler");
JLOG(j.debug()) << "transactionSubmitMultiSigned: " << jvRequest;
JLOG(j.debug()) << "transactionSubmitMultiSigned: " << loggable(jvRequest);
// When multi-signing, the "Sequence" and "SigningPubKey" fields must
// be passed in by the caller.
@@ -4,6 +4,7 @@
#include <xrpld/rpc/Context.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/server/NetworkOPs.h>
@@ -11,6 +12,13 @@
namespace xrpl {
/**
* Closes the open ledger on a stand-alone server.
*
* @param context The request. Its `params` are not read.
* @return `ledger_current_index` of the ledger the close opens, or
* `notStandAlone` when the server is on a network.
*/
json::Value
doLedgerAccept(rpc::JsonContext& context)
{
@@ -18,7 +26,7 @@ doLedgerAccept(rpc::JsonContext& context)
if (!context.app.config().standalone())
{
jvResult[jss::error] = "notStandAlone";
rpc::injectError(RpcNotStandAlone, jvResult);
}
else
{
@@ -1042,7 +1042,7 @@ doLedgerEntry(rpc::JsonContext& context)
{
if (context.apiVersion < 2u)
{
jvResult[jss::error] = "unknownOption";
rpc::injectError(RpcUnknownOption, jvResult);
return jvResult;
}
return rpc::makeParamError("No ledger_entry params provided.");
@@ -24,6 +24,9 @@
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.
inline std::unexpected<json::Value>
missingFieldError(json::StaticString const field, std::optional<std::string> err = std::nullopt)
{
+45 -14
View File
@@ -42,6 +42,17 @@
namespace xrpl {
/**
* Picks the `Sequence` a simulated transaction runs with.
*
* @param txJson The transaction, which carries an `Account` member.
* @param context The request, read for the open ledger and the queue.
* @return 0 when the transaction names a `TicketSequence`, otherwise the
* account's next queueable sequence. On either path, the error to
* report when `Account` is not a string or does not parse as an
* account; without a `TicketSequence`, also when the account is not
* in the open ledger.
*/
static std::expected<std::uint32_t, json::Value>
getAutofillSequence(json::Value const& txJson, rpc::JsonContext& context)
{
@@ -50,10 +61,10 @@ getAutofillSequence(json::Value const& txJson, rpc::JsonContext& context)
auto const& accountStr = txJson[jss::Account];
if (!accountStr.isString())
{
// sanity check, should fail earlier
// LCOV_EXCL_START
// The earlier check requires `Account` to be present, not to be a string, so this is the
// type check. Without it `asString` below throws on a numeric value, which degrades a clean
// field error into an internal one.
return std::unexpected(rpc::invalidFieldError("tx.Account"));
// LCOV_EXCL_STOP
}
auto const srcAddressID = parseBase58<AccountID>(accountStr.asString());
if (!srcAddressID.has_value())
@@ -240,6 +251,19 @@ getTxJsonFromParams(json::Value const& params)
return txJson;
}
/**
* Applies @p transaction to a copy of the open ledger as a dry run.
*
* @param context The request, read for `binary`.
* @param transaction The transaction to apply.
* @return The engine result with `applied` and `ledger_index`, and the
* transaction as `tx_blob` when `binary` is true, otherwise as
* `tx_json`. Its metadata follows as `meta_blob` or `meta` only
* when the engine produced any, which it does when the result is
* `tesSUCCESS` or a `tec` code, since both apply the transaction
* to the view. A `tel`, `tem`, `tef` or `ter` result applies
* nothing and carries no metadata.
*/
static json::Value
simulateTxn(rpc::JsonContext& context, std::shared_ptr<Transaction> transaction)
{
@@ -266,12 +290,11 @@ simulateTxn(rpc::JsonContext& context, std::shared_ptr<Transaction> transaction)
}
else
{
// shouldn't be hit
// LCOV_EXCL_START
// Every TER this can hold names a token, so this arm states the fallback rather than a
// result any transaction reaches.
jvResult[jss::engine_result] = "unknown";
jvResult[jss::engine_result_code] = result.ter;
jvResult[jss::engine_result_message] = "unknown";
// LCOV_EXCL_STOP
}
if (token == "tesSUCCESS")
@@ -307,10 +330,20 @@ simulateTxn(rpc::JsonContext& context, std::shared_ptr<Transaction> transaction)
return jvResult;
}
// {
// tx_blob: <string> XOR tx_json: <object>,
// binary: <bool>
// }
/**
* Dry-runs a transaction against the open ledger without submitting it.
*
* `params` carry `tx_blob` or `tx_json`, one of the two, and optionally
* `binary`. A credential in `params` is refused. A `tx_json` missing
* `Sequence`, `Fee` or `SigningPubKey` has it filled in, and one missing
* `NetworkID` has it filled in where the network's ID is above 1024.
*
* @param context The request.
* @return The result `simulateTxn` builds, or an error object:
* `invalidTransaction` with `error_exception` when the transaction
* does not build, `internalSimulate` with `error_exception` when
* applying it throws.
*/
json::Value
doSimulate(rpc::JsonContext& context)
{
@@ -353,7 +386,7 @@ doSimulate(rpc::JsonContext& context)
catch (std::exception& e)
{
json::Value jvResult = json::ValueType::Object;
jvResult[jss::error] = "invalidTransaction";
rpc::injectError(RpcInvalidTransaction, jvResult);
jvResult[jss::error_exception] = e.what();
return jvResult;
}
@@ -377,15 +410,13 @@ doSimulate(rpc::JsonContext& context)
{
return simulateTxn(context, transaction);
}
// LCOV_EXCL_START this is just in case, so xrpld doesn't crash
catch (std::exception const& e)
{
json::Value jvResult = json::ValueType::Object;
jvResult[jss::error] = "internalSimulate";
rpc::injectError(RpcInternalSimulate, jvResult);
jvResult[jss::error_exception] = e.what();
return jvResult;
}
// LCOV_EXCL_STOP
}
} // namespace xrpl
+20 -9
View File
@@ -37,10 +37,21 @@ getFailHard(rpc::JsonContext const& context)
context.params.isMember(jss::fail_hard) && context.params[jss::fail_hard].asBool());
}
// {
// tx_blob: <string> XOR tx_json: <object>,
// secret: <secret>
// }
/**
* Submits a transaction to the network.
*
* `params` carry `tx_blob`, a signed transaction, or `tx_json` with a
* signing credential, which the server signs first when it allows signing;
* that path is deprecated. `fail_hard` keeps a transaction the local checks
* fail from being relayed.
*
* @param context The request.
* @return The engine result and the transaction, or an error object:
* `invalidTransaction` with `error_exception` when the blob does not
* decode or fails the local checks, `internalSubmit` with
* `error_exception` when processing throws, `internalJson` when
* building the reply throws.
*/
json::Value
doSubmit(rpc::JsonContext& context)
{
@@ -90,7 +101,7 @@ doSubmit(rpc::JsonContext& context)
}
catch (std::exception& e)
{
jvResult[jss::error] = "invalidTransaction";
rpc::injectError(RpcInvalidTransaction, jvResult);
jvResult[jss::error_exception] = e.what();
return jvResult;
@@ -106,7 +117,7 @@ doSubmit(rpc::JsonContext& context)
context.app.getHashRouter(), *stTx, context.ledgerMaster.getCurrentLedger()->rules());
if (validity != Validity::Valid)
{
jvResult[jss::error] = "invalidTransaction";
rpc::injectError(RpcInvalidTransaction, jvResult);
jvResult[jss::error_exception] = "fails local checks: " + reason;
return jvResult;
@@ -117,7 +128,7 @@ doSubmit(rpc::JsonContext& context)
auto transaction = std::make_shared<Transaction>(stTx, reason, context.app);
if (transaction->getStatus() != TransStatus::NEW)
{
jvResult[jss::error] = "invalidTransaction";
rpc::injectError(RpcInvalidTransaction, jvResult);
jvResult[jss::error_exception] = "fails local checks: " + reason;
return jvResult;
@@ -133,7 +144,7 @@ doSubmit(rpc::JsonContext& context)
}
catch (std::exception& e)
{
jvResult[jss::error] = "internalSubmit";
rpc::injectError(RpcInternalSubmit, jvResult);
jvResult[jss::error_exception] = e.what();
return jvResult;
@@ -179,7 +190,7 @@ doSubmit(rpc::JsonContext& context)
}
catch (std::exception& e)
{
jvResult[jss::error] = "internalJson";
rpc::injectError(RpcInternalJson, jvResult);
jvResult[jss::error_exception] = e.what();
return jvResult;
@@ -9,6 +9,7 @@
#include <xrpl/basics/chrono.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/jss.h>
#include <rpcspec/Errors.hpp>
@@ -37,22 +38,23 @@ TransactionEntryHandler::process(Input const& input) const
if (!input.txHash &&
input.txHash.error() == ::rpc::spec::handlers::transaction_entry::TxHashError::Missing)
{
output.error = "fieldNotFoundTransaction";
output.error = ::rpc::Status{
RpcFieldNotFoundTransaction, missingFieldMessage(std::string{jss::tx_hash.cStr()})};
}
else if (output.ledger->open())
{
// We don't work on ledger current.
output.error = "notYetImplemented";
output.error = ::rpc::Status{RpcNotYetImplemented};
}
else if (!input.txHash)
{
output.error = "malformedRequest";
output.error = ::rpc::Status{RpcMalformedRequest};
}
else
{
std::tie(output.tx, output.meta) = output.ledger->txRead(*input.txHash);
if (!output.tx)
output.error = "transactionNotFound";
output.error = ::rpc::Status{RpcTransactionNotFound};
}
return output;
@@ -68,7 +70,9 @@ TransactionEntryHandler::writeResult(json::Value& value, Output const& output) c
if (output.error)
{
value[jss::error] = std::string{*output.error};
// The error travels in the Output, not as a failed process(), so the reply keeps the
// ledger fields written above beside the token, code and message.
injectSpecError(value, *output.error);
return;
}
@@ -15,7 +15,6 @@
#include <functional>
#include <memory>
#include <optional>
#include <string_view>
namespace xrpl::rpc {
@@ -25,16 +24,38 @@ public:
struct Output
{
std::shared_ptr<ReadView const> ledger;
std::optional<std::string_view> error;
/**
* The error reported beside the ledger fields when the lookup fails.
*/
std::optional<::rpc::Status> error;
std::shared_ptr<STTx const> tx;
std::shared_ptr<STObject const> meta;
};
explicit TransactionEntryHandler(JsonContext&);
/**
* Looks the transaction up in the selected ledger.
*
* @param input The parsed request: the ledger selected, and the `tx_hash`
* read or the reason there is none.
* @return The ledger with the transaction and its metadata, or the ledger
* with an error carrying its code: `fieldNotFoundTransaction`
* without a `tx_hash`, `notYetImplemented` when the ledger is the
* open one, `malformedRequest` when `tx_hash` is not a hex string,
* `transactionNotFound` when the ledger does not hold it. A ledger
* that cannot be selected is returned as the unexpected Status.
*/
[[nodiscard]] std::expected<Output, ::rpc::Status>
process(Input const& input) const;
/**
* Writes the reply: the ledger fields, then either the error with its
* code and message, or the transaction and its metadata.
*
* @param value The reply object written into.
* @param output The result of `process`.
*/
void
writeResult(json::Value& value, Output const& output) const;