mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-21 19:18:22 +00:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad1c98a6f9 | ||
|
|
a10066d634 | ||
|
|
bb4a62cd72 | ||
|
|
2a0c8ba982 | ||
|
|
4d30ee7ec3 | ||
|
|
d25df82c14 | ||
|
|
0fe9167bed | ||
|
|
2c5cee2925 | ||
|
|
450d00ad65 | ||
|
|
b515af4d4e | ||
|
|
cf495fde86 | ||
|
|
00bf88afad | ||
|
|
f45dab46d1 | ||
|
|
b87ad0f08e | ||
|
|
50c9e6335e | ||
|
|
78709c1d09 | ||
|
|
b67266aad4 | ||
|
|
79a8b4849e | ||
|
|
49410ad867 | ||
|
|
3689dca688 | ||
|
|
671a00b1ac | ||
|
|
9096ec03a7 | ||
|
|
5fec493960 | ||
|
|
a6a4fff2aa |
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
|
||||
|
||||
+39
-2
@@ -6,13 +6,19 @@ For info about how [API versioning](https://xrpl.org/request-formatting.html#api
|
||||
|
||||
The API version controls the API behavior you see. This includes what properties you see in responses, what parameters you're permitted to send in requests, and so on. You specify the API version in each of your requests. When a breaking change is introduced to the `xrpld` API, a new version is released. To avoid breaking your code, you should set (or increase) your version when you're ready to upgrade.
|
||||
|
||||
The [commandline](https://xrpl.org/docs/references/http-websocket-apis/api-conventions/request-formatting/#commandline-format) always uses the latest API version. The command line is intended for ad-hoc usage by humans, not programs or automated scripts. The command line is not meant for use in production code.
|
||||
The [commandline](https://xrpl.org/docs/references/http-websocket-apis/api-conventions/request-formatting/#commandline-format) sends `api_version` 1 unless the request names one. `kApiCommandLineVersion` is stamped on a request naming none, and a static assert holds it at or below the highest non-beta version, so the default cannot reach 3; the `json` and `json2` commands pass a caller-supplied `api_version` through as given. The command line is intended for ad-hoc usage by humans, not programs or automated scripts. The command line is not meant for use in production code.
|
||||
|
||||
For a log of breaking changes, see the **API Version [number]** headings. In general, breaking changes are associated with a particular API Version number. For non-breaking changes, scroll to the **XRP Ledger version [x.y.z]** headings. Non-breaking changes are associated with a particular XRP Ledger (`xrpld`) release.
|
||||
|
||||
## API Version 3 (Beta)
|
||||
|
||||
API version 3 is currently a beta API. It requires enabling `[beta_rpc_api]` in the xrpld configuration to use. See [API-VERSION-3.md](API-VERSION-3.md) for the full list of changes in API version 3.
|
||||
API version 3 is currently a beta API. It requires `[beta_rpc_api]` set to `1` in the xrpld configuration to use; the section takes one value line, and a bare header leaves the flag off. See [API-VERSION-3.md](API-VERSION-3.md) for the full list of changes in API version 3.
|
||||
|
||||
From this version, replies take the JSON-RPC 2.0 envelope on both transports, with the deviations [API-VERSION-3.md](API-VERSION-3.md) lists, a top-level array is accepted as a batch, and subscription messages are sent as notifications. The `ripplerpc` field no longer selects the reply envelope and is ignored.
|
||||
|
||||
A request that is rejected before it reaches a method is answered with a JSON-RPC error object carrying `jsonrpc`, `id` and `error`, whether it was sent alone or as an entry in a batch; the `id` is the request's own, or `null` where the `id` itself was the unusable member. The HTTP status is the one the matching rejection reports at earlier versions; the two rejections version 3 adds, an unusable `id` and a `method` disagreeing with the one inside `params`, answer 400. On the JSON-RPC transport three classes of rejection stay a plain text body, because the server cannot tell which envelope the client speaks: a body it could not parse or that exceeds the size limit, a body that parses to `{}` or `null` and so carries no request, and a request whose `api_version`, where the server reads it, names a version it does not serve, unless it is an entry of a batch. That place is inside `params` for a lone JSON-RPC request, a top-level value naming such a version being ignored there and the request served as version 1. The WebSocket transport never answers plain text: a message naming such a version at its top level, where that transport reads it, is answered in the legacy frame those versions have always used, and a frame the server cannot read is answered with the `jsonInvalid` frame, at every version. An empty array `[]` is not in the second class: only a version 3 client sends an array, so it is answered with a `-32600` error object and `Request is empty`. A batch entry is not in the third: an entry of a top-level array naming such a version is answered in place with a `-32606` error object in the array's envelope, and an entry of a `"method": "batch"` request is answered with an error object in the body's envelope, so neither takes the body down with it. A version 3 client must therefore tolerate a non-JSON body for the three classes.
|
||||
|
||||
A JSON-RPC (HTTP) request may also name `api_version` at its top level, beside `method` and `id`, and may send `params` as the object itself rather than an array holding it. Neither reaches the WebSocket transport, whose message is one flat object that has always carried `api_version` at its top level and whose members are the parameters themselves, so a `params` member there is not unpacked. The `params` object is the specification's own spelling; `api_version` is an XRPL extension, which the specification does not define, placed beside the members it does. Both are additive: for a lone request a top-level `api_version` is honored only when it names version 3 or above, since a lower one has been answered as version 1 for years. An entry of either batch form honors any version it names at its top level: a `"method": "batch"` entry carries its parameters at its own top level, so that is where its version sits, and one that carries a by-name `params` object is read for `api_version` and credentials from that object instead; an array entry is read the same way so that the two forms agree, and an entry naming a served version other than the body's refuses the body, while one naming a version the server does not serve is answered on its own.
|
||||
|
||||
## API Version 2
|
||||
|
||||
@@ -22,6 +28,37 @@ 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. Two things it does not reach: a WebSocket session, which reads the field only to echo it back, and API version 3, where the field selects nothing and so is neither read nor checked, `ripplerpc: "banana"` and no `ripplerpc` at all being served alike. A request sending `"2"`, `" 2.0"`, `"2.00"`, `"02.0"`, `"2.0.0"`, `"10.0"` or any other text, such as `"abc"` or `"x2"`, gets a working reply today. After this ships, such a request sent alone gets HTTP 400 with `ripplerpc is not a supported version`, charged as a malformed request, and such an entry of a `"method": "batch"` body gets that error in its own reply while the batch answers 200. A client that spells the version loosely must therefore be corrected to one of the three exact values, or omit the field. Previously the value was compared as a string, which both accepted values that name no version and ordered multi-digit versions incorrectly: `"abc"` and `"x2"` sorted above `"3.0"` and so selected version 3, and `"10.0"` sorted below `"2.0"` and so selected version 1. Requests that send one of the three supported values, or omit the field, are unaffected.
|
||||
- `subscribe`: every subscription a connection holds must name the same `api_version`. The first `subscribe` establishes it, and a later one naming a different version is refused with `apiVersionConflict`, registering nothing; the message names the version the connection's subscriptions are served at. The HTTP status is 400 where the envelope derives it from the error, with `ripplerpc: "3.0"` or at API version 3, and 200 with `ripplerpc` `"1.0"` or `"2.0"`, as for every error; a WebSocket frame carries no status. A `subscribe` refused for another reason, a stream name the server does not have for one, still fixes that version, since the registrations it makes are spread through the handler. A connection that subscribed one stream at `api_version` 2 and another at 1, in two calls, was previously served both at version 1, the later call's, so the first stream's shape changed under it. The version is fixed for the connection's life: a client that wants another version opens another connection. On the webhook path the subscriber is keyed on its `url` alone, so the refusal reaches a second admin because of a first admin's version, and the message names the version the url's subscriptions are served at, and no remedy: releasing a url means unsubscribing its streams, which would remove that first admin's subscription, and the server cannot tell the two callers apart. `API-VERSION-3.md` describes the procedure and that risk. Naming the url alone does not release it, a subscriber a stream map still holds not being evicted. This reaches every API version, since version 1 and version 2 content is not interchangeable either: version 2 renames `transaction` to `tx_json` and hoists `hash`, so a version 1 client handed version 2 content finds no `transaction` member.
|
||||
- `batch`: An entry of a `"method": "batch"` request that names `api_version` both inside `params` and at its own top level is now served at the version inside `params`. It was previously served at the top-level version whenever the one inside `params` resolved to version 1, so an entry asking for version 1 explicitly and something else at the top level changes which version answers it. A lone request can name both values too, and for it the version inside `params` has always won; reversing that precedence was confined to a batch entry.
|
||||
- `batch`: every entry of one batch request is now served at the same API version. An entry may name the version the body names, or name none and inherit it, and a body whose entries name two different versions is refused whole with `Batch entries name different versions` at HTTP 400, charged as a malformed request. **This is visible to a version 1 or 2 client:** a `"method": "batch"` body could mix versions across its entries, each entry being dispatched at its own, and the difference is observable because the reply shape changes with the version. Three bodies change. One whose entries name two different versions is now refused instead of answered. One where some entries name a version and others name none now serves all of them at the named version, where an entry naming none was served at version 1; the same applies to a version named on the request's own top level, which the entries now inherit. One whose top level names a version and whose entries name a different one is now refused, where the top-level value was ignored and each entry was served at the version it named. A body whose top level names no version or the same version as its entries, and whose entries agree with one another or name none at all, is unaffected. A `"method": "batch"` body naming at its own top level a version the server does not serve is refused with `invalid_API_version` at HTTP 400, where it was previously served at version 1 as if it had named none. No test pinned the mixing behavior and the line that produced it was a fallback about where the field sits rather than a decision that entries may differ, so this is a correction rather than a removal of a feature: a per-entry version made the entry cap above unanswerable, since a cap can only ask about a body that has one version.
|
||||
|
||||
### Additions
|
||||
|
||||
- A JSON-RPC (HTTP) request may send its parameters as an object, `"params": {"account": "r..."}`, as well as the array of one object that was already accepted. This reaches every API version: such a request was previously rejected with HTTP 400 and `params unparsable`, and is now served. A request that already sends the array form is unaffected. An entry of a `"method": "batch"` request that carries a by-name `params` object is read for `api_version` and credentials from that object. Previously such an entry had its `api_version` read from its top level and its credentials ignored, since credentials were read only from an array-form `params`.
|
||||
- A lone JSON-RPC (HTTP) request naming `api_version` 3 or above at its own top level, beside `method` and `id`, is now answered at that version. The member is an XRPL extension, which the JSON-RPC 2.0 specification does not define, placed beside the members it does. It was previously answered at version 1, the version being read from inside `params` alone; a WebSocket request, which is one flat object, has always been read there. This is deliberate: reading the version beside the specification's own members is what lets a version 3 client send a specification-shaped request. A top-level value below version 3, or one the server does not serve, is still ignored for a lone request, since `"api_version": 2` there has been answered as version 1 for years and honoring it now would change a shipped reply shape. Only API version 3 is reachable this way, and it is gated behind `[beta_rpc_api]`.
|
||||
|
||||
### 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. **Only API version 3 reports these four.** A request sending `ripplerpc: "3.0"` at API version 1 or 2 still receives 200 for all four, as it always has, so `account_info` on a malformed account or one the ledger does not hold answers 200 for those clients exactly as before.
|
||||
- `submit`, `simulate`, `transaction_entry`, `ledger_entry` and `ledger_accept`: Errors from these methods now include `error_code` and `error_message` alongside the `error` token, as every other method already did. Each error now answers the status its code names: 400 for a malformed request, 404 for `transactionNotFound`, 500 for an internal failure, and 501 for `notYetImplemented` and `notStandAlone`. That status change reaches only a request sending `ripplerpc: "3.0"`, or API version 3, which are the envelopes that derive the status from the error. With `ripplerpc` `"1.0"` the status stays 200 and the two new members appear beside `error`; with `"2.0"` the status stays 200, `error_code` appears, and the `code` and `message` members carry the code and the message rather than null, since that envelope copies them from `error_code` and `error_message` and drops `error_message`. From API version 3 the twenty-one `malformed*` tokens `ledger_entry` names each report the code they own instead of 31. See [API-VERSION-3.md](API-VERSION-3.md).
|
||||
- A reply reporting HTTP 402 or 502 now carries a status line. Those two statuses named no case in the switch that writes one, so such a reply began with a header instead and did not parse as an HTTP response at all. Both are reachable at any API version with `ripplerpc: "3.0"`, which derives the status from the error code: 402 through `highFee` from `sign`, `sign_for` or `submit` with a low `fee_mult_max`, and 502 through `dbDeserialization` from `tx`. The eleven statuses that already named a case report the same phrase they always have.
|
||||
- An error reply to a request sending `ripplerpc: "2.0"` or `"3.0"` no longer carries a stray `"error_message": null` beside the error it reports. The member appeared only when the `Server` log partition was set to debug or lower, because the log statement read `error_message` after the reply had renamed it to `message`, and reading it put it back as null. So the reply a client received depended on the server's log level, and the log line itself printed an empty message. Both are fixed.
|
||||
- A body the server rejects before it reads a request out of it now says what was wrong. A body over the size limit answers `Request is too large`, and a body that parses to `{}` or `null` answers `Request is empty`; both previously answered `Unable to parse request: ` with nothing after the colon, the parser having recorded no error for them. Any other body that is not an array does not parse, which includes one that is only whitespace and one whose top-level value is a string, number or boolean; it answers `Unable to parse request: ` followed by the parser's own reason, as it did before. The status is 400 for all three, as before, and this reaches every API version. A top-level array, the one other body nothing can read a request from, is answered with a JSON error object instead, which the entry on top-level arrays below describes.
|
||||
- `batch`: An entry that is not identified through a secure gateway no longer clears the connection's `X-User` and forwarded-for values for the entries after it, so every entry of one body reports the role and username it would have reported on its own.
|
||||
- `batch`: Every entry of a `"method": "batch"` request is now charged against the sender's resource allowance, including one rejected before it reaches a handler, and the request stops at the first entry the connection is too loaded to serve. A reply array can therefore be shorter than the request array, and its last element depends on where the batch stopped. An entry that is not a JSON object, or names an API version the server does not serve, is charged before its role is known and answered with its own rejection; if that charge took the connection over the drop threshold, the batch stops there and that rejection is the last answer. Every other entry met over the threshold is answered `Server is overloaded` and nothing follows. A client should read any reply array shorter than its request as a connection over the drop threshold, whatever the last element says. A well-behaved client is unaffected; one that sends thousands of entries in a single body no longer gets every one of them answered.
|
||||
- A body the server rejects before it reads a request out of it is now charged against the sender's resource allowance, as a malformed request already was. Ten conditions were free: a body over the size limit, one that does not parse, one carrying no document, one that is neither a JSON object nor an array, a `"method": "batch"` naming no entry array, and six ways a top-level array is not a batch this server can serve. The answer to the first five is unchanged; an array's answer changes as the entry on top-level arrays below describes, the one generic rejection it received having become six. A well-behaved client is unaffected; one that repeats such a body exhausts its allowance and is refused the next request a handler would have served. A WebSocket frame that does not parse, or exceeds the request size limit, is charged the same way, and a connection that sends only such frames is closed once it crosses the drop threshold.
|
||||
- `subscribe`, `path_find`: A subscription is now served at the API version its own `subscribe` call named, rather than at the version of the most recent `subscribe` or `path_find` on the same connection. A `path_find` no longer changes the version anything that connection has subscribed is served at. An ordinary request is unaffected: it names its own version and is answered at it, so a connection subscribed at `api_version` 3 still calls `account_info` at version 1 and reads the version 1 shape.
|
||||
- `batch`: A batch request served at API version 3 or above is now refused when it holds more than 100 entries, before any entry is dispatched. The cap reads the one version the body is served at, so no ordering of the entries evades it. A body served below version 3 is unaffected and stays bounded only by the request size limit.
|
||||
- A top-level JSON array, which is the JSON-RPC 2.0 batch form accepted from API version 3, is now rejected with a JSON-RPC error object rather than a plain text body when it cannot be served: when it is empty, holds more than 100 entries, holds nothing that could be a request, names no API version of 3 or above that the server serves, or names two different versions the server serves. An array naming no served version of 3 or above is refused for requiring version 3, or, where every version it names is one the server does not serve, with `invalid_API_version`. One entry naming a version the server does not serve does not reject an array in which another entry names a served version: it is answered on its own, inside the reply array, with the same `-32606` code. This reaches every API version, since no earlier version accepts an array at all: a client that sent one received HTTP 400 with `Unable to parse request: ` and now receives the same status with a JSON body. The message also names the version rather than the shape where the version is what is wrong, which is what a client with `[beta_rpc_api]` disabled receives.
|
||||
|
||||
## XRP Ledger server version 3.5.0
|
||||
|
||||
Version 3.5.0 is not yet released.
|
||||
|
||||
+244
-1
@@ -1,11 +1,220 @@
|
||||
# API Version 3
|
||||
|
||||
API version 3 is currently a **beta API**. It requires enabling `[beta_rpc_api]` in the xrpld configuration to use. To use this API, clients specify `"api_version" : 3` in each request.
|
||||
API version 3 is currently a **beta API**. It requires `[beta_rpc_api]` set to `1` in the xrpld configuration to use; the section takes one value line, and a bare header leaves the flag off. To use this API, clients specify `"api_version" : 3` in each request.
|
||||
|
||||
For info about how [API versioning](https://xrpl.org/request-formatting.html#api-versioning) works, including examples, please view the [XLS-22d spec](https://github.com/XRPLF/XRPL-Standards/discussions/54). For details about the implementation of API versioning, view the [implementation PR](https://github.com/XRPLF/rippled/pull/3155). API versioning ensures existing integrations and users continue to receive existing behavior, while those that request a higher API version will experience new behavior.
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
### Replies take the JSON-RPC 2.0 envelope
|
||||
|
||||
Every reply the server can attribute to API version 3 takes the envelope the [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification) defines, on both the JSON-RPC and WebSocket transports. The Deviations section below lists where the content departs from the specification, and the rejections that stay a plain text body. This replaces the three envelope shapes selected by the `ripplerpc` request field, none of which conformed to the specification that the field is named after.
|
||||
|
||||
A successful reply carries the payload under `result`:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 7,
|
||||
"result": { "ledger_hash": "F742...", "ledger_index": 2 }
|
||||
}
|
||||
```
|
||||
|
||||
A failure carries it under `error` instead:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 7,
|
||||
"error": {
|
||||
"code": -32000,
|
||||
"message": "Account malformed.",
|
||||
"data": { "error": "actMalformed", "error_code": 35 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Compared with earlier versions:
|
||||
|
||||
- **`jsonrpc` is always present** and names the protocol version.
|
||||
- **`id` is echoed from the top level of the request**, where the specification puts it. This changes the JSON-RPC transport only. Earlier versions of that transport echoed `id` only when it was nested inside `params`, so a client that placed it correctly received no `id` and could not correlate the reply with its request. A WebSocket session has always echoed a top-level `id`, and is unaffected. A request that names no `id` receives `"id": null`, so that a reply is always distinguishable from a notification; the specification would send nothing, which the Deviations section below records. The specification allows a string, a number or null there, so a request naming anything else there, an object, an array or a boolean, is rejected as an invalid request, on both transports; that reply names `"id": null` too, the id being the thing that was wrong. A number is read as this server reads every JSON number: an integer from -2147483648 to 4294967295, or a fraction. An integer outside that range fails to parse, and the body is refused as unreadable before any envelope is chosen, so a client using a millisecond timestamp as an `id` should send it as a string; the Deviations section below records the bound. Earlier versions echo whatever they are given, whatever its shape, and continue to.
|
||||
- **`status` is gone from the reply envelope**, at both levels. Whether the call succeeded is already carried by which of `result` or `error` is present. Earlier versions also let `path_find`'s `status` subcommand report one inside `result`; that one is dropped too. A subscription notification is not a reply and keeps whatever the event carries: the `transactions` stream reports `"closed"` or `"proposed"` in a `status` member of the event itself, so a version 3 subscriber still receives it under `params`.
|
||||
- **`error.code` is `-32000`** for any request that reaches a command, the specification's reserved code for an implementation-defined server error. The XRPL error token and numeric code move into `error.data`, so the two code spaces stay separate and renumbering an XRPL error can never change a JSON-RPC code. Everything else the handler reported about the failure also travels in `data`. A request naming a method this server does not have reaches no command, and the specification gives that one condition a code of its own, so it reports `-32601` instead, with `unknownCmd` and its code still under `data`.
|
||||
- **A notice stays at the top level.** Three member names are notices: `warning`, `warnings` and `deprecated`. The rule is positional, not a judgment about what each one says: any of the three that a handler puts at the **top level of its result** is moved beside the specification's members, and one nested deeper is left where the handler put it. So `ledger`'s `warnings` is moved, while `server_info`'s, nested under `info`, and `server_state`'s, nested under `state`, are not. Two of the three moved members do describe one outcome rather than the server: `wallet_propose`'s `warning` describes the wallet the call just produced, and `subscribe`'s describes an experimental feature. Both are moved, because they sit at that top level.
|
||||
- **`error_exception` is gone; its text is `error.message`.** `submit` and `simulate` reported the specific detail of a failure in an `error_exception` member of their own, leaving a generic `error_message` beside it: a `tx_blob` that fails to parse reported both `"Transaction is invalid."` and `"Transaction length invalid"`. The specific text is now the `message`, which is where every other handler puts it, and the token and code in `data` still say what kind of failure it was. A client that read `error_exception` must read `error.message`.
|
||||
- **The request is not echoed back.** Earlier versions echoed it on version 1 errors. A request rejected before it reaches a command is not echoed either, and the shape its reply takes is described below.
|
||||
- **`ripplerpc` is ignored.** It no longer selects the envelope. The field is ignored rather than rejected, as any other field the server does not honor is.
|
||||
|
||||
The envelope above describes a request that reaches a command. A request the server rejects before then (one naming no method, or a method that is not a string, or one refused for permissions or load) is answered differently, since there is no handler result to shape. One case has no reply at all: a WebSocket sender over the resource drop threshold is disconnected, whatever its message carried, as it has been at every version, while the JSON-RPC transport answers that sender with 503 and `Server is overloaded`. A request whose `api_version`, where the server reads it, names a version it does not serve is rejected before then too, but is answered outside the envelope, as plain text on the JSON-RPC transport and in the legacy frame on WebSocket, unless it is an entry of a batch, as described below: the server cannot tell which envelope that client speaks. That place is inside `params` for a lone JSON-RPC request, a top-level value naming such a version being ignored there and the request served as version 1, as the request section below describes, and the top level of a WebSocket message.
|
||||
|
||||
Such a rejection is a specification error object, whether the request was sent alone or as an entry in a batch, correlated by the request's own `id`, or naming `"id": null` where the `id` itself was the unusable member:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"error": { "code": -32602, "message": "params unparsable" }
|
||||
}
|
||||
```
|
||||
|
||||
The HTTP status is the one the rejection has always reported: 400 for a malformed request, 403 for a forbidden one and 503 for an overloaded server. Earlier versions receive that status with the message as a bare text body (`Null method`, `params unparsable`), and continue to. A request sent as an entry of a batch reports no status of its own, the body carrying several outcomes; the batch section below says where its outcome is stated instead.
|
||||
|
||||
The rejected request is **not** echoed back. Earlier versions echo a rejected batch entry with its credentials masked and continue to do so; the specification makes `error.data` optional, a client already knows what it sent, and copying a whole entry per rejection is what an overloaded server must not do.
|
||||
|
||||
The codes reported here are the specification's own: `-32600` for a request that names no usable `method`, or is not a JSON object at all, and `-32602` for invalid parameters. Earlier versions report `-32601`, method not found, for such an entry of a `"method": "batch"` request, where version 3 reports `-32600`; a lone request there is answered as plain text, with no code at all. `-32601` is what they have always sent for the entry and moving it would change `error.code` for their clients. Version 3 keeps `-32601` for the one condition the specification defines it for, a request naming a method this server does not have, which is answered from the reply path above rather than as a rejection: a request naming no usable method names nothing that could be found or not found. A request naming one method at its top level and another inside `params` names two, which is not that condition either, so it reports `-32600` from version 3 up on the JSON-RPC transport, the WebSocket transport having no `params` to disagree with, its `command` and `method` disagreement being described below; earlier versions of the JSON-RPC transport answer it as `unknownCmd` with `error_code` 32 at HTTP 200 in the `ripplerpc` `"1.0"` and `"2.0"` envelopes, and at 405 in the `"3.0"` envelope, and continue to. They also report `-32604` for an overloaded server, `-32605` for a forbidden request and `-32606` for an unsupported API version, which lie in the range the specification reserves for the protocol rather than the `-32000..-32099` range it leaves to implementations. Those are unchanged at every version, for the same reason.
|
||||
|
||||
Those three codes belong to a rejection the server makes before it dispatches a request. Load and permission can also be reported once a request has been read, and then they carry an XRPL token and so report `-32000` like any other: a request refused because the job queue is full reports `-32000` with `tooBusy` under `data` and HTTP 503, and an admin-only command called without the role reports `-32000` with `noPermission` and HTTP 401. The rule is not which subject the failure is about: **every error carrying an XRPL token reports `-32000`, with the one exception of `unknownCmd`, which reports `-32601`.**
|
||||
|
||||
On the JSON-RPC transport three classes of rejection are answered as plain text at every version, including this one, because the server cannot tell which envelope the client speaks:
|
||||
|
||||
- a body it could not parse, and one larger than the request size limit;
|
||||
- a body that parses but carries no request: `{}` or `null`;
|
||||
- a request whose `api_version`, read from inside `params` for a lone request, names a version the server does not serve, unless it is an entry of a batch.
|
||||
|
||||
The first two name no version anywhere. The third names one, but not one this server has, so it cannot know what that client expects. A lone request naming such a version at its top level only is not in the third: that value is ignored, as the request section below describes, and the request is served as version 1. A version 3 client must therefore tolerate a non-JSON body for these. A top-level value that is neither an object nor an array, such as `42` or `"text"`, does not parse at all and falls in the first class.
|
||||
|
||||
The WebSocket transport never answers plain text. A frame the server cannot read is answered with the `jsonInvalid` frame described below, and a frame naming an `api_version` the server does not serve is answered in the legacy envelope those versions have always used: `{"type": "response", "status": "error", "error": "invalid_API_version", "request": { ... }}`, with the request echoed and its credentials masked. That is the reply a version 3 WebSocket client receives from a server where `[beta_rpc_api]` is not enabled.
|
||||
|
||||
An entry of a top-level array is the exception to the third, because the array's own version is known: only a version 3 client sends an array at all. Such an entry is answered with a `-32606` error object in the array's envelope, described in the batch section below. An entry of a `"method": "batch"` request is answered the same way when the body is served at version 3, which it is when the body or any of its entries names it; a body served below version 3 keeps the shape earlier versions send.
|
||||
|
||||
One rejection is worth naming separately. On the WebSocket transport there are four ways for a request to name no one command to dispatch on: it names neither `command` nor `method`, one of those two is not a string, or the two disagree. From version 3 each is answered in the specification envelope, reporting `-32600` and a message naming which of the four it was. No such reply carries `data`: a rejection is not a failure a command reported, so it has no XRPL token to put there. Earlier versions answer all four with the single `missingCommand` token, and continue to.
|
||||
|
||||
The HTTP status still derives from the XRPL error code, now read from `error.data.error_code`. Four codes named no status in the error table and fell back to 200, which said the call succeeded on a reply that carries an error: `account_info` answered both a malformed account and a missing one with 200. They now name the status the table gives every comparable code: `actMalformed`, `alreadyMultisig` and `alreadySingleSig` report 400, and `actNotFound` reports 404.
|
||||
|
||||
Only version 3 reports those four. The legacy `ripplerpc: "3.0"` envelope derives the status from the same code and still answers 200 for them, since that is the status it has answered since it shipped and `account_info` on an account the ledger does not hold is a routine call rather than a failure. Every other code reports the status it always has, in both envelopes.
|
||||
|
||||
On the WebSocket transport the reply carries one extra member, `"type": "response"`. The specification has no place for it, but a WebSocket session also receives server-initiated messages on the same connection, and `type` is how a client tells the two apart. It is retained for that reason.
|
||||
|
||||
A WebSocket frame that does not parse as a JSON object, or exceeds the request size limit, is answered `{"type": "error", "error": "jsonInvalid", "size": <bytes>}` at every API version, including this one, unless the sender is already over the resource drop threshold, in which case the frame is charged and the connection is closed without a reply, as it is for a readable message from such a sender. There are no fields yet to shape into an envelope, and none to mask either, so the body is reported by its size rather than echoed: a frame with a stray comma would otherwise return whatever it carried, credentials included.
|
||||
|
||||
### Requests may take the specification's shape
|
||||
|
||||
Two request spellings are now accepted on the JSON-RPC transport, in addition to the ones XRPL clients already send: one the specification defines, `params` as an object, and one XRPL extension placed beside the members the specification defines, `api_version` at the top level. Both are additive, so every request that worked before still works; one shape is read differently, and it is named below. A WebSocket message is one flat object whose members are the parameters themselves: it has always carried `api_version` at its top level, and a `params` member there is not unpacked, so neither spelling applies to it.
|
||||
|
||||
- **`api_version` at the top level**, beside `method` and `id`, and not only inside `params`. For a lone JSON-RPC request a top-level value is honored only when it names version 3 or above: `"api_version": 2` there has been answered as version 1 for years, as has a version the server does not serve, and both continue to be. An entry of either batch form honors any version it names at its top level, including one the server does not serve, which is answered as an unsupported version: a `"method": "batch"` entry carries its parameters at its own top level, so that is where its version sits, and one that carries a by-name `params` object is read for `api_version` and credentials from that object instead; an array entry is read the same way so that the two forms agree, and an entry naming a served version other than the body's refuses the body.
|
||||
|
||||
A value inside `params` decides where a request carries both, which **reverses the earlier precedence**: a request that named a version inside `params` and a different one at its top level was previously answered at the top-level one whenever the value inside `params` resolved to version 1. Only a `"method": "batch"` entry could carry both, so that is the only shape the reversal reaches, and it reaches it at every API version. `API-CHANGELOG.md` records it.
|
||||
|
||||
- **`params` as the object itself**, the specification's by-name form, rather than an array holding that object. The array form is unchanged: it holds exactly one element, which is the object or `null`, as the Deviations section below records.
|
||||
|
||||
#### One API version per connection is the shape to write a client to
|
||||
|
||||
The server reads `api_version` from each request and answers that request at that version, so a connection may carry requests at several versions and each reply is shaped for the request that asked for it. Nothing refuses that. But there is no reason for a client to rely on it: a client that names one version for the life of a connection reads one reply shape throughout, and has one thing to change when it migrates. Write a client that way.
|
||||
|
||||
One case is not advice but a rule the server enforces: **every subscription a connection holds must name the same `api_version`**. A `subscribe` naming a version different from the one the connection's subscriptions are served at is refused with `apiVersionConflict` and registers nothing, because one event can match several of a connection's subscriptions and only one message goes out, so a second version would leave no shape that message could take. The version is fixed by the first `subscribe` for the connection's life, whether or not that call went on to register anything: a `subscribe` refused for a stream name the server does not have has still fixed it. A client that wants another version opens another connection.
|
||||
|
||||
A webhook subscriber is keyed on its `url` alone, so the same rule reaches every caller that names that url. To subscribe a url at a different version, send `unsubscribe` naming that `url` **and its streams**: naming the url alone does not release it, since the subscriber is not evicted while any stream map still holds it. Be aware that doing so removes the subscription of whoever subscribed that url first, whether or not that was you - the server keeps no record of which caller named it. Where the url may be shared, naming a different url is the safer answer. The refusal message names the version the url's subscriptions are served at and no remedy, for that reason. Ordinary requests are untouched by this: a connection subscribed at version 3 still calls `account_info` at version 1 and reads the version 1 shape.
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 7,
|
||||
"method": "ledger_closed",
|
||||
"api_version": 3,
|
||||
"params": { "ledger_index": "validated" }
|
||||
}
|
||||
```
|
||||
|
||||
### Batch requests
|
||||
|
||||
A top-level JSON array is accepted as a [batch](https://www.jsonrpc.org/specification#batch), and is answered with an array of replies, one per entry, each correlated by its own `id`:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "ledger_closed",
|
||||
"params": [{ "api_version": 3 }]
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "account_info",
|
||||
"params": [{ "api_version": 3 }]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
In earlier versions a top-level array is rejected with HTTP 400, and that is unchanged; an array is treated as a batch only when an entry names an `api_version` of 3 or above that the server serves, inside its own `params` or at its top level, wherever that entry sits; an array holding at least one object in which no entry does is refused whole: as below version 3 where an entry names a served version below 3 or no entry names any, and for the version named where every version named is one the server does not serve. An array holding no object at all is answered per entry instead, as below. That entry also sets the version for the array: an entry naming none of its own inherits it, whether it comes before or after, so one batch is answered in one envelope even where an entry is rejected before its parameters can be read. An entry that names a served version of its own must name that one; a body whose entries name two different served versions is refused whole, as the table below shows. An entry naming a version the server does not serve is answered on its own, as below.
|
||||
|
||||
An entry's parameters are nested exactly as a lone request's are, so an entry reports the same error it would have reported on its own.
|
||||
|
||||
An entry that is not a JSON object is not a request. It is answered in place, among the replies, with `-32600` and `Request is not a JSON object`, and the entries that are requests are still dispatched. Earlier versions answer such an entry with `-32601` and `Method not found`, and echo it back under `request`; both are unchanged for them.
|
||||
|
||||
An entry naming an `api_version` the server does not serve is answered the same way: in place, with `-32606` and `invalid_API_version`, correlated by the entry's own `id`, or naming `"id": null` where the `id` itself was unusable, and echoing nothing. It takes the array's envelope rather than the one it asked for, since the version it asked for does not exist, and unlike a lone request in that position it is not answered as plain text: the array told the server which envelope this client reads. An entry naming a version the server does have must name the array's, or the whole array is refused.
|
||||
|
||||
A rejection of the whole array is answered with one error object, since only version 3 sends an array at all. The array names no `id`, its entries do, so `id` is null. The version is set by the first entry naming a served version of 3 or above. The version rows below are read in order: the first applies where every version named is one the server does not serve, the second where no entry names a served version of 3 or above, and the third where an entry names a served version other than the one set. An entry naming a version the server does not serve beside one naming a served version is answered on its own, wherever it sits, as above:
|
||||
|
||||
| Body | HTTP | `error.code` | `error.message` |
|
||||
| --------------------------------------------------------------------------------------- | ---- | ------------ | --------------------------------------- |
|
||||
| `[]` | 400 | `-32600` | `Request is empty` |
|
||||
| More than 100 entries | 400 | `-32600` | `Batch has too many entries` |
|
||||
| Every version named is one the server does not serve | 400 | `-32606` | `invalid_API_version` |
|
||||
| Otherwise, no entry names a served version of 3 or above | 400 | `-32600` | `Batch requires API version 3 or above` |
|
||||
| An entry names a served version of 3 or above, and another names a different served one | 400 | `-32600` | `Batch entries name different versions` |
|
||||
|
||||
A non-empty array of 100 entries or fewer holding nothing that could be a request, such as `[1,2,3]` from the specification's own example, is answered with one `-32600` object per entry, since no entry can name a version or an `id`. The table's first two rows are checked before any entry is read, so an empty or oversized array is answered once, whatever it holds.
|
||||
|
||||
A batch is limited to 100 entries, and the limit is applied before the entries are read, so an oversized array is answered once rather than once per entry. The body size limit alone is not a bound on the work a batch buys, since it dispatches one command per entry.
|
||||
|
||||
The `"method": "batch"` form, which nests its entries under `params`, is unrelated to the specification and continues to work on every API version. From version 3 up it takes the same 100-entry limit, read from the one version the body is served at, so no ordering of the entries evades it. Below version 3 it stays uncapped, which is what keeps every body clients send today acceptable.
|
||||
|
||||
Every entry is charged against the sender's resource allowance, whether it reaches a handler or is rejected first, except the one shed as overloaded, whose load the drop check has already accounted for. Once a connection is over the drop threshold the remaining entries are not answered, so a reply array can be shorter than the request array. The 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 error object, and the batch stops after it when that charge crossed the threshold; every other entry met over the threshold reports `-32604` and `Server is overloaded` in its own error object. Nothing follows in either case, so a client that correlates by `id` sees answers missing for the entries at the end, and should read any short reply array as an overloaded connection whatever its last element says. This applies to both batch forms.
|
||||
|
||||
A batch whose entries are answered reports HTTP 200, whatever those entries report, including the overloaded one above. An array holding nothing that could be a request is the exception: its entries are answered one by one, but the body is a rejection rather than a batch the server served, and it reports 400. One status cannot describe several outcomes, so an entry's own reply is where its outcome is stated: whether it carries `result` or `error`, and, for an error, the specification code in `error.code` for a rejection and the XRPL token and code under `error.data` for a handler failure, every one of which reports `-32000` in `error.code`. No entry has an HTTP status of its own. A rejection of the whole array is one answer rather than many and reports its own status, as the table above shows, and so does a request sent alone.
|
||||
|
||||
### Subscription messages are notifications
|
||||
|
||||
A message the server sends without being asked, such as a `subscribe` stream event, is shaped as a specification [notification](https://www.jsonrpc.org/specification#notification): a request-shaped object naming the protocol and the method, with the event's content under `params` and no `id`.
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "ledgerClosed",
|
||||
"params": { "ledger_index": 3, "...": "..." }
|
||||
}
|
||||
```
|
||||
|
||||
The `type` member that named the event in earlier versions becomes `method`.
|
||||
|
||||
A `path_find` update is a notification too, though the server directs it at the one client that asked rather than publishing it to a stream. The specification allows one response per request, and `path_find create` already consumed it, so no later update can be one. The `id` that correlates the update with the request travels inside `params`, which is where the update has always carried it:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "path_find",
|
||||
"params": { "id": 7, "alternatives": ["..."], "full_reply": true }
|
||||
}
|
||||
```
|
||||
|
||||
An `account_history` subscription that fails reports the failure the same way, naming `account_history_tx_stream` as the method with the error under `params`. Earlier versions receive a bare error object with no `type`, which is what they have always received.
|
||||
|
||||
A subscriber named by a `url` is the exception, and receives the legacy `event` call at every version, including version 3. Such a subscriber is not sent the message directly: the server posts each message to the URL as the `params` of an `event` call, with a `seq` member counting the messages beside it. A notification sent that way would arrive nested inside a request and next to a member the specification does not define, so it would not be a notification. Which shape a subscriber receives therefore depends on how the message reaches it, not only on the version it asked for.
|
||||
|
||||
### Deviations from the specification
|
||||
|
||||
The reply follows the specification as closely as the XRPL API allows. These are the places it does not, all of them deliberate:
|
||||
|
||||
- **A request that names no `id` still receives a reply.** The specification calls such a request a notification and gives it no reply at all. XRPL clients routinely omit `id`, so a reply is always sent, with `"id": null`.
|
||||
- **A batch of such requests is answered with an array**, for the same reason, where the specification would send no body.
|
||||
- **An integer `id` must fit this server's JSON integer range**, -2147483648 to 4294967295. The specification allows any Number. One outside that range fails to parse, and the body is refused as unreadable before any envelope is chosen, so a client using a larger value, such as a millisecond timestamp, should send it as a string.
|
||||
- **By-position `params` is an array of exactly one element, an object or `null`.** The specification allows an array of any length. This server's methods take one object, so an array of any other length, or one whose element is neither an object nor `null`, is refused with `-32602` and `params unparsable`. A `null` element is served as an empty parameter object, as it has been at every version.
|
||||
- **`error.code` is `-32000`** for every failure a command reports, rather than a code per failure. The XRPL token and code carry that detail in `error.data`. A method the server does not have is the one exception, reporting `-32601`, because no command runs for it.
|
||||
- **`-32700` (parse error) and `-32603` (internal error) are never reported.** A body that does not parse is answered as plain text on the JSON-RPC transport and as a `jsonInvalid` frame on WebSocket, and neither is an envelope, so there is no `error.code` to put a parse code in. An internal failure of a command is reported the way every other failure a command reports is, with `-32000` and the `internal` token under `error.data`, so that a client reads one code space for all of them.
|
||||
- **`-32604`, `-32605` and `-32606`** lie in the band the specification reserves for the protocol. They are the codes earlier versions report for an overloaded server, a forbidden request and an unsupported API version, and moving one would change `error.code` for their clients.
|
||||
- **A rejection the server cannot attribute to a version is plain text**, not an error object. The three classes are listed above.
|
||||
- **The batch is accepted on the JSON-RPC transport only.** A WebSocket frame must be a single object, as it always has been.
|
||||
- **`ripplerpc` is ignored** rather than rejected, as any other field the server does not honor is. When support for versions 1 and 2 ends, an ignored field stays ignored, where a rejection would silently turn into acceptance.
|
||||
- **A WebSocket reply carries `"type": "response"`**, which the specification has no place for. A WebSocket session receives server-initiated messages on the same connection, and `type` is how a client tells the two apart.
|
||||
- **A batch is served at one API version.** The specification says nothing about versions. An entry may name the version the body names, or name none and inherit it, and a body whose entries name two different versions the server serves is refused whole with `-32600` and `Batch entries name different versions`; one naming a version the server does not serve is answered on its own, as below. For a top-level array it is the one the first entry naming a served version of 3 or above does, wherever that entry sits. For a `"method": "batch"` body it is the one the first entry naming a served version does; such a body may also name it at its own top level, which then decides, and one naming a version the server does not serve there is refused. An entry names a version wherever dispatch would read one from it, inside its own `params` or at its top level. An entry naming a version the server does not serve is answered on its own, as it always has been, rather than taking the body down with it, except that a top-level array holding at least one object in which no entry names a served version of 3 or above is refused whole, with `-32606` where every version named is one the server does not serve and with `-32600` otherwise, as the table above shows, while one holding no object at all is answered per entry once it has passed the empty and size checks of the table's first two rows.
|
||||
- **A reply array can be shorter than the request array.** The specification asks for one response per non-notification request; a batch stops at the entry the connection is too loaded to serve. `API-CHANGELOG.md` records this for the `"method": "batch"` form, which earlier versions serve; the array form is version 3 only, and this document is its record.
|
||||
- **`warning`, `warnings` and `deprecated` sit beside `jsonrpc`, `result` and `id`** at the top level of the reply, where the specification defines no member of its own. The rule is positional: any of the three that a handler puts at the top level of its result is moved there, whatever it describes, and one nested deeper stays where the handler put it. A client therefore reads a top-level notice from one place whether the call succeeded or failed.
|
||||
- **A subscriber named by a `url` receives the legacy `event` call**, not a notification, at every version. The transport that carries the message to the URL wraps it in a request of its own, so a notification could not survive the trip. See above.
|
||||
- **A batch is limited to 100 entries.** The specification places no bound on an array's length. One command is dispatched per entry, so the request size limit bounds the bytes rather than the work, and an array of small entries buys far more of it than its size suggests. The same limit applies to the `"method": "batch"` form from version 3 up, for the reason given above.
|
||||
- **A `subscribe` naming an `api_version` different from the one this connection's subscriptions are served at is refused.** The specification describes no such condition. Every subscription a connection holds is served at one version, for the reason given above, which is also why the remedy is a second connection rather than a second version. The refusal reports `apiVersionConflict`, registers nothing, and names the version already established. On the JSON-RPC transport it answers HTTP 400, as every version 3 error reporting a malformed request does. A WebSocket frame carries no status. `API-CHANGELOG.md` records it as breaking. This constrains `subscribe` alone: an ordinary request names its own `api_version` and is answered at it, whatever the connection has subscribed.
|
||||
- **The request's own `jsonrpc` member is neither required nor read.** The specification asks a client to send `"jsonrpc": "2.0"`. A request is served whether it names that member, names some other value, or names none at all: `api_version` is what selects the shape of the reply, and refusing an otherwise well formed call for a missing member would reject requests every XRPL client sends today. The reply always names the member.
|
||||
|
||||
### Modifications to `amm_info`
|
||||
|
||||
The order of error checks has been changed to provide more specific error messages. ([#4924](https://github.com/XRPLF/rippled/pull/4924))
|
||||
@@ -25,3 +234,37 @@ In API version 3, the following string values can be used with the `"index"` par
|
||||
- `"index": "hashes"` - Returns the "short" `LedgerHashes` ledger entry (recent ledger hashes)
|
||||
|
||||
These shortcuts are only available in API version 3 and later. In API versions 1 and 2, these string values would result in an error.
|
||||
|
||||
#### `error_code` is the code that belongs to the reported token
|
||||
|
||||
`ledger_entry` validates its request fields through helpers that name the malformed field in the `error` token, such as `malformedOwner` or `malformedDirRoot`. Below API version 3 all of them report `error_code` 31 (`invalidParams`) regardless of the token, because changing the number would have broken clients matching on it. From version 3 each token reports the code it owns.
|
||||
|
||||
A client that matches on `error_code` 31 for these methods must match on the token instead, or on the code listed below. The `error` token itself is unchanged at every version, so a client that already matches on the token needs no change.
|
||||
|
||||
| Token | `error_code` below v3 | `error_code` from v3 |
|
||||
| ------------------------------------------ | --------------------- | -------------------- |
|
||||
| `malformedRequest` | 31 | 107 |
|
||||
| `malformedAccount` | 31 | 110 |
|
||||
| `malformedAddress` | 31 | 111 |
|
||||
| `malformedAuthorized` | 31 | 112 |
|
||||
| `malformedAuthorizedCredentials` | 31 | 113 |
|
||||
| `malformedBridgeAccount` | 31 | 114 |
|
||||
| `malformedBroker` | 31 | 115 |
|
||||
| `malformedCurrency` | 31 | 116 |
|
||||
| `malformedDirRoot` | 31 | 117 |
|
||||
| `malformedDocumentID` | 31 | 118 |
|
||||
| `malformedIssue` | 31 | 119 |
|
||||
| `malformedIssuingChainDoor` | 31 | 120 |
|
||||
| `malformedLockingChainDoor` | 31 | 121 |
|
||||
| `malformedMPTIssuanceID` | 31 | 122 |
|
||||
| `malformedMPTokenIssuance` | 31 | 123 |
|
||||
| `malformedOwner` | 31 | 124 |
|
||||
| `malformedSeq` | 31 | 125 |
|
||||
| `malformedSponsee` | 31 | 126 |
|
||||
| `malformedSponsor` | 31 | 127 |
|
||||
| `malformedXChainOwnedClaimID` | 31 | 128 |
|
||||
| `malformedXChainOwnedCreateAccountClaimID` | 31 | 129 |
|
||||
|
||||
`transaction_entry` reports `malformedRequest` too, and the two handlers agree on it from version 3 on. It set that token with no `error_code` at all before this release, and now reports 107 at every API version, where `ledger_entry` reports 31 below version 3 as the table above shows.
|
||||
|
||||
The HTTP status is unchanged: every code in the table above carries 400, as `invalidParams` does. `entryNotFound`, `lgrNotFound` and `unexpectedLedgerType` are unaffected by all of this, having always carried their own code. `unknownOption` is a separate change and is not a version 3 one: `ledger_entry` reports that token at API version 1 only, it carried no `error_code` at all there, and it now carries code 109.
|
||||
@@ -1461,13 +1461,17 @@
|
||||
#
|
||||
# [beta_rpc_api]
|
||||
#
|
||||
# 0 or 1.
|
||||
# 0 or 1. The section must hold exactly one line carrying that value: a bare
|
||||
# [beta_rpc_api] header with nothing under it leaves the flag off, and the
|
||||
# server logs one line saying so.
|
||||
#
|
||||
# 0: Disable the beta API version for JSON-RPC and WebSocket [default]
|
||||
# 1: Enable the beta API version for testing. The beta API version
|
||||
# contains breaking changes that require a new API version number.
|
||||
# They are not ready for public consumption.
|
||||
#
|
||||
# The beta API version is 3; see API-VERSION-3.md.
|
||||
#
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# 10. Example Settings
|
||||
|
||||
@@ -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&);
|
||||
|
||||
|
||||
@@ -6,31 +6,25 @@
|
||||
#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. A WebSocket message is
|
||||
* one flat object and names it at its top level at every version. A JSON-RPC
|
||||
* request names it in its parameters, or at its own top level: an entry of a
|
||||
* `"method": "batch"` body at every version, a lone request from
|
||||
* kApiMinimumSpecVersion up.
|
||||
*/
|
||||
|
||||
namespace rpc {
|
||||
@@ -46,6 +40,31 @@ static constexpr auto kApiCommandLineVersion = kApiVersion<1>; // TODO Bump to
|
||||
static constexpr auto kApiBetaVersion = kApiVersion<3>;
|
||||
static constexpr auto kApiMaximumValidVersion = kApiBetaVersion;
|
||||
|
||||
/**
|
||||
* First API version that follows the JSON-RPC 2.0 specification.
|
||||
*
|
||||
* From this version up a lone JSON-RPC request may state its `api_version` at
|
||||
* its own top level, beside the members the specification defines there, as a
|
||||
* WebSocket message always has. The member is an XRPL extension: the
|
||||
* specification defines no version member.
|
||||
*
|
||||
* Distinct from kApiBetaVersion, the same number today, which says instead that
|
||||
* the version needs [beta_rpc_api] to be reachable.
|
||||
*/
|
||||
static constexpr auto kApiMinimumSpecVersion = kApiVersion<3>;
|
||||
|
||||
/**
|
||||
* Whether @p apiVersion follows the JSON-RPC 2.0 specification.
|
||||
*
|
||||
* @param apiVersion The version a request asked for.
|
||||
* @return Whether that version follows the specification.
|
||||
*/
|
||||
[[nodiscard]] constexpr bool
|
||||
isSpecVersion(unsigned apiVersion)
|
||||
{
|
||||
return apiVersion >= kApiMinimumSpecVersion;
|
||||
}
|
||||
|
||||
static_assert(kApiInvalidVersion < kApiMinimumSupportedVersion);
|
||||
static_assert(
|
||||
kApiVersionIfUnspecified >= kApiMinimumSupportedVersion &&
|
||||
@@ -57,6 +76,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)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -144,7 +145,58 @@ 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 + ledger_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,
|
||||
|
||||
// subscribe
|
||||
RpcApiVersionConflict = 130,
|
||||
|
||||
// RpcLast should always equal the last code.
|
||||
RpcLast = RpcApiVersionConflict,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -180,11 +232,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 +249,18 @@ struct ErrorInfo
|
||||
ErrorInfo const&
|
||||
getErrorInfo(ErrorCodeI code);
|
||||
|
||||
/**
|
||||
* Returns the error code that owns @p token.
|
||||
*
|
||||
* A linear scan over views measured at compile time, run once per error reply.
|
||||
* A duplicate token is a build error, so the answer is never ambiguous.
|
||||
*
|
||||
* @param token The error token to resolve.
|
||||
* @return The code the table gives @p token, or RpcUnknown if no row names it.
|
||||
*/
|
||||
ErrorCodeI
|
||||
codeForToken(std::string_view token);
|
||||
|
||||
/**
|
||||
* Add or update the json update to reflect the error code.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#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
|
||||
* XRPL token and code travel in the error's `data` member, so renumbering an
|
||||
* XRPL error cannot change a JSON-RPC code.
|
||||
*
|
||||
* The codes from kJsonRpcServerOverloaded on lie outside the
|
||||
* implementation-defined sub-range, which the specification does not allow.
|
||||
* They are the codes every shipped version reports, so moving one breaks the
|
||||
* clients matching on it.
|
||||
*/
|
||||
inline constexpr json::Int kJsonRpcServerError{-32000};
|
||||
inline constexpr json::Int kJsonRpcInvalidRequest{-32600};
|
||||
inline constexpr json::Int kJsonRpcMethodNotFound{-32601};
|
||||
inline constexpr json::Int kJsonRpcInvalidParams{-32602};
|
||||
inline constexpr json::Int kJsonRpcServerOverloaded{-32604};
|
||||
inline constexpr json::Int kJsonRpcForbidden{-32605};
|
||||
inline constexpr json::Int kJsonRpcWrongVersion{-32606};
|
||||
|
||||
} // namespace xrpl::rpc
|
||||
@@ -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
|
||||
|
||||
+176
-23
@@ -17,6 +17,7 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -57,6 +58,88 @@ exceedsSubscriptionCap(
|
||||
return additional > cap || current > cap - additional;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns @p jvObj reshaped as a JSON-RPC 2.0 notification, or nullopt when the
|
||||
* message already has the shape @p apiVersion expects.
|
||||
*
|
||||
* From version 3 a message a subscription pushes is shaped as the
|
||||
* specification's notification: an object naming the protocol and the method,
|
||||
* the message's content under `params`, and no `id`. The `type` naming the
|
||||
* event becomes the `method`.
|
||||
*
|
||||
* A path finding update is one of these, though directed at the one subscriber
|
||||
* that asked: the specification allows one response per request and
|
||||
* `path_find create` consumed it. The `id` the client correlates by travels
|
||||
* inside `params`, where the update carries it.
|
||||
*
|
||||
* A message naming no `type`, or one whose `type` is not a string, names no
|
||||
* method, so it is passed through unshaped. Shaping reports that `type` as the
|
||||
* `method` and leaves none behind, so shaping a message a second time changes
|
||||
* nothing.
|
||||
*
|
||||
* @param jvObj The message to shape.
|
||||
* @param apiVersion The subscriber's API version.
|
||||
* @return The notification, or nullopt if no reshaping is needed.
|
||||
*/
|
||||
[[nodiscard]] std::optional<json::Value>
|
||||
shapeAsNotification(json::Value const& jvObj, unsigned int apiVersion);
|
||||
|
||||
/**
|
||||
* Returns a copy of the stream error @p error naming @p stream as its `type`,
|
||||
* or nullopt when the error is sent as it stands.
|
||||
*
|
||||
* An error object names no `type`, so shapeAsNotification would pass it
|
||||
* through and a version 3 client could not classify it. The name is added only
|
||||
* where a notification is built from it: for a version below 3, or a subscriber
|
||||
* that cannot receive a notification, the object goes out unshaped and the
|
||||
* member would reach the wire undefined. Nothing is copied in that case.
|
||||
*
|
||||
* @param error The error a stream reports, as rpcError builds it.
|
||||
* @param apiVersion The subscriber's API version.
|
||||
* @param wantsNotifications Whether the subscriber can receive a notification.
|
||||
* See InfoSub::wantsNotifications.
|
||||
* @param stream The stream the error concerns, which becomes the `method`.
|
||||
* @return The stamped copy, or nullopt if the error needs no name.
|
||||
*/
|
||||
[[nodiscard]] std::optional<json::Value>
|
||||
stampStreamError(
|
||||
json::Value const& error,
|
||||
unsigned int apiVersion,
|
||||
bool wantsNotifications,
|
||||
json::StaticString const& stream);
|
||||
|
||||
class InfoSub;
|
||||
|
||||
/**
|
||||
* Sends @p jvObj to @p subscriber in the shape @p apiVersion expects.
|
||||
*
|
||||
* The version belongs to the subscription the message answers, so the caller
|
||||
* passes it from the entry it took the subscriber from. A `path_find` update
|
||||
* is served at the version its own request named, which can differ from the
|
||||
* version the connection's subscriptions are served at, and one webhook may
|
||||
* be shared by several callers.
|
||||
*
|
||||
* A shape is not transport-independent either, so a subscriber whose transport
|
||||
* cannot carry a notification is sent the message as the publisher built it.
|
||||
* See InfoSub::wantsNotifications.
|
||||
*
|
||||
* This is for a message built for the one subscriber it is going to. A stream
|
||||
* event reaching many subscribers is shaped once per version by the publisher
|
||||
* instead, which is what NetworkOPsImp's StreamBroadcast does.
|
||||
*
|
||||
* @param subscriber Where the message is going.
|
||||
* @param apiVersion The version the subscription was registered at.
|
||||
* @param jvObj The message.
|
||||
* @param broadcast Whether the message was published to a stream rather than
|
||||
* directed at this subscriber. Only a diagnostic reads it.
|
||||
*/
|
||||
void
|
||||
sendShaped(
|
||||
InfoSub& subscriber,
|
||||
unsigned int apiVersion,
|
||||
json::Value const& jvObj,
|
||||
bool broadcast = true);
|
||||
|
||||
class InfoSubRequest : public CountedObject<InfoSubRequest>
|
||||
{
|
||||
public:
|
||||
@@ -113,7 +196,11 @@ public:
|
||||
// you get transactions as they occur or once their
|
||||
// results are confirmed
|
||||
virtual void
|
||||
subAccount(Ref ispListener, HashSet<AccountID> const& vnaAccountIDs, bool realTime) = 0;
|
||||
subAccount(
|
||||
Ref ispListener,
|
||||
HashSet<AccountID> const& vnaAccountIDs,
|
||||
bool realTime,
|
||||
unsigned int apiVersion) = 0;
|
||||
|
||||
// for normal use, removes from InfoSub and server
|
||||
virtual void
|
||||
@@ -130,10 +217,12 @@ public:
|
||||
/**
|
||||
* subscribe an account's new transactions and retrieve the account's
|
||||
* historical transactions
|
||||
* @param apiVersion The version the subscription is registered at,
|
||||
* which is the version its messages are shaped for.
|
||||
* @return rpcSUCCESS if successful, otherwise an error code
|
||||
*/
|
||||
virtual ErrorCodeI
|
||||
subAccountHistory(Ref ispListener, AccountID const& account) = 0;
|
||||
subAccountHistory(Ref ispListener, AccountID const& account, unsigned int apiVersion) = 0;
|
||||
|
||||
/**
|
||||
* unsubscribe an account's transactions
|
||||
@@ -196,30 +285,34 @@ public:
|
||||
HashSet<AccountID> historyAccounts) = 0;
|
||||
|
||||
// VFALCO TODO Document the bool return value
|
||||
//
|
||||
// Every sub* below takes the version the subscription is registered at, which is the
|
||||
// version its messages are shaped for. Registering the same subscription again replaces
|
||||
// the entry, so the newest registration decides that subscription's version.
|
||||
virtual bool
|
||||
subLedger(Ref ispListener, json::Value& jvResult) = 0;
|
||||
subLedger(Ref ispListener, json::Value& jvResult, unsigned int apiVersion) = 0;
|
||||
virtual bool
|
||||
unsubLedger(std::uint64_t uListener) = 0;
|
||||
|
||||
virtual bool
|
||||
subBookChanges(Ref ispListener) = 0;
|
||||
subBookChanges(Ref ispListener, unsigned int apiVersion) = 0;
|
||||
virtual bool
|
||||
unsubBookChanges(std::uint64_t uListener) = 0;
|
||||
|
||||
virtual bool
|
||||
subManifests(Ref ispListener) = 0;
|
||||
subManifests(Ref ispListener, unsigned int apiVersion) = 0;
|
||||
virtual bool
|
||||
unsubManifests(std::uint64_t uListener) = 0;
|
||||
virtual void
|
||||
pubManifest(Manifest const&) = 0;
|
||||
|
||||
virtual bool
|
||||
subServer(Ref ispListener, json::Value& jvResult, bool admin) = 0;
|
||||
subServer(Ref ispListener, json::Value& jvResult, bool admin, unsigned int apiVersion) = 0;
|
||||
virtual bool
|
||||
unsubServer(std::uint64_t uListener) = 0;
|
||||
|
||||
virtual bool
|
||||
subBook(Ref ispListener, Book const&) = 0;
|
||||
subBook(Ref ispListener, Book const&, unsigned int apiVersion) = 0;
|
||||
|
||||
/**
|
||||
* Remove a book subscription for a live subscriber.
|
||||
@@ -248,7 +341,8 @@ public:
|
||||
* InfoSub::bookSubscriptions_ because the InfoSub is being destroyed.
|
||||
* Called by ~InfoSub() for each book in bookSubscriptions_.
|
||||
*
|
||||
* @param uListener The sequence number of the subscriber being torn down.
|
||||
* @param uListener The sequence number of the subscriber being torn
|
||||
* down.
|
||||
* @param book The order book entry to remove.
|
||||
* @return true if the entry was present and removed, false otherwise
|
||||
* (e.g., already removed by a concurrent RPC unsubscribe).
|
||||
@@ -259,22 +353,22 @@ public:
|
||||
unsubBookInternal(std::uint64_t uListener, Book const&) = 0;
|
||||
|
||||
virtual bool
|
||||
subTransactions(Ref ispListener) = 0;
|
||||
subTransactions(Ref ispListener, unsigned int apiVersion) = 0;
|
||||
virtual bool
|
||||
unsubTransactions(std::uint64_t uListener) = 0;
|
||||
|
||||
virtual bool
|
||||
subRTTransactions(Ref ispListener) = 0;
|
||||
subRTTransactions(Ref ispListener, unsigned int apiVersion) = 0;
|
||||
virtual bool
|
||||
unsubRTTransactions(std::uint64_t uListener) = 0;
|
||||
|
||||
virtual bool
|
||||
subValidations(Ref ispListener) = 0;
|
||||
subValidations(Ref ispListener, unsigned int apiVersion) = 0;
|
||||
virtual bool
|
||||
unsubValidations(std::uint64_t uListener) = 0;
|
||||
|
||||
virtual bool
|
||||
subPeerStatus(Ref ispListener) = 0;
|
||||
subPeerStatus(Ref ispListener, unsigned int apiVersion) = 0;
|
||||
|
||||
virtual bool
|
||||
unsubPeerStatus(std::uint64_t uListener) = 0;
|
||||
@@ -282,12 +376,12 @@ public:
|
||||
pubPeerStatus(std::function<json::Value()> const&) = 0;
|
||||
|
||||
virtual bool
|
||||
subConsensus(Ref ispListener) = 0;
|
||||
subConsensus(Ref ispListener, unsigned int apiVersion) = 0;
|
||||
virtual bool
|
||||
unsubConsensus(std::uint64_t uListener) = 0;
|
||||
|
||||
virtual void
|
||||
subMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) = 0;
|
||||
subMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs, unsigned int apiVersion) = 0;
|
||||
virtual void
|
||||
unsubMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) = 0;
|
||||
|
||||
@@ -320,9 +414,41 @@ public:
|
||||
Consumer&
|
||||
getConsumer();
|
||||
|
||||
/**
|
||||
* Deliver a message to this subscriber.
|
||||
*
|
||||
* @param jvObj The message.
|
||||
* @param broadcast True for a stream event published to every subscriber of
|
||||
* that stream, false for one directed at this subscriber alone
|
||||
* because it asked (a path finding update). Both are shaped the same
|
||||
* way, since neither is a response. This reports only how far the
|
||||
* message traveled, which is what a diagnostic reads.
|
||||
*/
|
||||
virtual void
|
||||
send(json::Value const& jvObj, bool broadcast) = 0;
|
||||
|
||||
/**
|
||||
* Whether a message sent to this subscriber may be shaped as a JSON-RPC
|
||||
* 2.0 notification.
|
||||
*
|
||||
* True for a subscriber the server writes a message to as it is, which
|
||||
* every WebSocket session is. False for one whose transport puts what it
|
||||
* is given inside another request, which a subscriber named by a url does:
|
||||
* a notification would reach it nested in a request and beside a member
|
||||
* the specification does not define, so it would not be a notification at
|
||||
* all. Such a subscriber receives the message as the publisher built it, at
|
||||
* every API version.
|
||||
*
|
||||
* A publisher that shapes a message asks this first.
|
||||
*
|
||||
* @return true if a notification is a shape this subscriber can receive.
|
||||
*/
|
||||
[[nodiscard]] virtual bool
|
||||
wantsNotifications() const noexcept
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint64_t
|
||||
getSeq() const;
|
||||
|
||||
@@ -385,8 +511,9 @@ public:
|
||||
/**
|
||||
* Whether this connection already tracks an account-history for @p account.
|
||||
*
|
||||
* `doSubscribe` reads this to charge the cap for an account_history_tx_stream
|
||||
* only when it is net-new, matching the account branches.
|
||||
* `doSubscribe` reads this to charge the cap for an
|
||||
* account_history_tx_stream only when it is net-new, matching the account
|
||||
* branches.
|
||||
*
|
||||
* @param account The account an account_history_tx_stream would add.
|
||||
* @return true if @p account is already in the account-history set.
|
||||
@@ -447,11 +574,26 @@ public:
|
||||
std::shared_ptr<InfoSubRequest> const&
|
||||
getRequest();
|
||||
|
||||
void
|
||||
setApiVersion(unsigned int apiVersion);
|
||||
|
||||
[[nodiscard]] unsigned int
|
||||
getApiVersion() const noexcept;
|
||||
/**
|
||||
* Establishes @p apiVersion as the version every subscription on this
|
||||
* connection is served at.
|
||||
*
|
||||
* A connection serves one version: one message has one shape, and several
|
||||
* of its subscriptions may match it. The first `subscribe` decides it and
|
||||
* it is never cleared. A `subscribe` refused for another reason still
|
||||
* establishes it, the registrations it makes being spread through the
|
||||
* handler.
|
||||
*
|
||||
* For the subscribe path alone. Nothing reads this value back: a publisher
|
||||
* reads the version recorded on the subscription it took the sink from.
|
||||
*
|
||||
* @param apiVersion The version the `subscribe` named.
|
||||
* @return nullopt when @p apiVersion is this connection's version;
|
||||
* otherwise the version already established.
|
||||
* @note Thread-safe: takes `lock_`.
|
||||
*/
|
||||
[[nodiscard]] std::optional<unsigned int>
|
||||
establishSubscriptionVersion(unsigned int apiVersion);
|
||||
|
||||
void
|
||||
insertSubMPTInfo(MPTID const& mptID);
|
||||
@@ -485,9 +627,20 @@ private:
|
||||
HashSet<AccountID> accountHistorySubscriptions_;
|
||||
HashSet<Book> bookSubscriptions_;
|
||||
HashSet<MPTID> mptSubscriptions_;
|
||||
unsigned int apiVersion_ = 0;
|
||||
// The API version every subscription on this connection was registered at, set by the first
|
||||
// `subscribe` and never cleared. Guarded by lock_. Read only by establishSubscriptionVersion,
|
||||
// so no publisher can reach it.
|
||||
std::optional<unsigned int> subscriptionApiVersion_;
|
||||
|
||||
static int
|
||||
/**
|
||||
* The next sequence number, which is what identifies a connection.
|
||||
*
|
||||
* 64 bits wide, as the counter and `seq_` are, so no two live connections
|
||||
* share an identity.
|
||||
*
|
||||
* @return A sequence number no live connection holds.
|
||||
*/
|
||||
static std::uint64_t
|
||||
assignId()
|
||||
{
|
||||
static std::atomic<std::uint64_t> kID(0);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -1034,7 +1039,8 @@ Value::removeMember(char const* key)
|
||||
if (it == value_.mapVal->end())
|
||||
return kNull;
|
||||
|
||||
Value old(it->second);
|
||||
// The entry is erased on the next line, so nothing observes what the move leaves behind.
|
||||
Value old(std::move(it->second));
|
||||
value_.mapVal->erase(it);
|
||||
return old;
|
||||
}
|
||||
|
||||
@@ -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,123 @@ 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},
|
||||
{RpcApiVersionConflict, "apiVersionConflict", "Subscriptions on one connection must all name the same api_version.", 400},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
@@ -117,7 +154,7 @@ sortErrorInfos(ErrorInfo const (&unordered)[N]) -> std::array<ErrorInfo, M>
|
||||
if (info.code <= RpcSuccess || info.code > RpcLast)
|
||||
throw(std::out_of_range("Invalid error_code_i"));
|
||||
|
||||
// The first valid code follows rpcSUCCESS immediately.
|
||||
// The first valid code follows RpcSuccess immediately.
|
||||
static_assert(RpcSuccess == 0, "Unexpected error_code_i layout.");
|
||||
int const index{info.code - 1};
|
||||
|
||||
@@ -128,10 +165,10 @@ sortErrorInfos(ErrorInfo const (&unordered)[N]) -> std::array<ErrorInfo, M>
|
||||
}
|
||||
|
||||
// Verify that all entries are filled in starting with 1 and proceeding
|
||||
// to rpcLAST.
|
||||
// to RpcLast.
|
||||
//
|
||||
// It's okay for there to be missing entries; they will contain the code
|
||||
// rpcUNKNOWN. But other than that all entries should match their index.
|
||||
// RpcUnknown. But other than that all entries should match their index.
|
||||
int codeCount{0};
|
||||
int expect{RpcBadSyntax - 1};
|
||||
for (ErrorInfo const& info : ret)
|
||||
@@ -156,6 +193,68 @@ constexpr auto kSortedErrorInfos{sortErrorInfos<RpcLast>(kUnorderedErrorInfos)};
|
||||
|
||||
constexpr ErrorInfo kUnknownError;
|
||||
|
||||
/**
|
||||
* Asserts that no two rows of @p sorted name the same token.
|
||||
*
|
||||
* A duplicate is then a build error rather than a reply reporting the wrong
|
||||
* `error_code`. `consteval` admits no run-time call, so the two `throw`
|
||||
* statements are how a constant evaluation fails and are otherwise unreachable.
|
||||
*
|
||||
* @param sorted The table, ordered by code, with a gap for every unused code.
|
||||
* @return True. It reports failure by failing to be a constant expression.
|
||||
*/
|
||||
template <std::size_t N>
|
||||
consteval bool
|
||||
tokensAreUnique(std::array<ErrorInfo, N> const& sorted)
|
||||
{
|
||||
for (std::size_t i{0}; i < N; ++i)
|
||||
{
|
||||
// Gaps in the table name no error, so they name no token either.
|
||||
if (sorted[i].code == RpcUnknown)
|
||||
continue;
|
||||
|
||||
std::string_view const token{sorted[i].token.cStr()};
|
||||
if (token.empty())
|
||||
throw(std::invalid_argument("Empty token in list"));
|
||||
|
||||
for (std::size_t j{i + 1}; j < N; ++j)
|
||||
{
|
||||
if (sorted[j].code != RpcUnknown && std::string_view{sorted[j].token.cStr()} == token)
|
||||
throw(std::invalid_argument("Duplicate token in list"));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static_assert(tokensAreUnique(kSortedErrorInfos), "Duplicate token in list");
|
||||
|
||||
/**
|
||||
* The tokens of @p sorted, measured at compile time.
|
||||
*
|
||||
* A lookup compares a caller's token against these rather than against the
|
||||
* table's own `char const*`, which it would have to measure first: comparing a
|
||||
* view with a pointer calls strlen on every row the scan passes. Built from the
|
||||
* same table, so the two cannot name different tokens.
|
||||
*
|
||||
* @param sorted The table, ordered by code, with a gap for every unused code.
|
||||
* @return One view per row, the empty view for a gap. No token a caller can
|
||||
* pass is equal to the empty view.
|
||||
*/
|
||||
template <std::size_t N>
|
||||
consteval std::array<std::string_view, N>
|
||||
tokenViews(std::array<ErrorInfo, N> const& sorted)
|
||||
{
|
||||
std::array<std::string_view, N> ret{};
|
||||
for (std::size_t i{0}; i < N; ++i)
|
||||
{
|
||||
if (sorted[i].code != RpcUnknown)
|
||||
ret[i] = std::string_view{sorted[i].token.cStr()};
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
constexpr auto kSortedTokens{tokenViews(kSortedErrorInfos)};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -186,6 +285,21 @@ getErrorInfo(ErrorCodeI code)
|
||||
return detail::kSortedErrorInfos[code - 1];
|
||||
}
|
||||
|
||||
ErrorCodeI
|
||||
codeForToken(std::string_view token)
|
||||
{
|
||||
if (token.empty())
|
||||
return RpcUnknown;
|
||||
|
||||
for (std::size_t i{0}; i < detail::kSortedTokens.size(); ++i)
|
||||
{
|
||||
// A gap holds the empty view, which the token is not, so a gap names no code here either.
|
||||
if (detail::kSortedTokens[i] == token)
|
||||
return detail::kSortedErrorInfos[i].code;
|
||||
}
|
||||
return RpcUnknown;
|
||||
}
|
||||
|
||||
json::Value
|
||||
makeError(ErrorCodeI code)
|
||||
{
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/protocol/JsonRpc.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/resource/Consumer.h>
|
||||
|
||||
#include <cstddef>
|
||||
@@ -14,6 +17,7 @@
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -306,17 +310,18 @@ InfoSub::getRequest()
|
||||
return request_;
|
||||
}
|
||||
|
||||
void
|
||||
InfoSub::setApiVersion(unsigned int apiVersion)
|
||||
std::optional<unsigned int>
|
||||
InfoSub::establishSubscriptionVersion(unsigned int apiVersion)
|
||||
{
|
||||
apiVersion_ = apiVersion;
|
||||
}
|
||||
std::scoped_lock const sl(lock_);
|
||||
|
||||
unsigned int
|
||||
InfoSub::getApiVersion() const noexcept
|
||||
{
|
||||
XRPL_ASSERT(apiVersion_ > 0, "xrpl::InfoSub::getApiVersion : valid API version");
|
||||
return apiVersion_;
|
||||
if (!subscriptionApiVersion_)
|
||||
subscriptionApiVersion_ = apiVersion;
|
||||
|
||||
if (*subscriptionApiVersion_ == apiVersion)
|
||||
return std::nullopt;
|
||||
|
||||
return subscriptionApiVersion_;
|
||||
}
|
||||
|
||||
void
|
||||
@@ -335,4 +340,57 @@ InfoSub::deleteSubMPTInfo(MPTID const& mptID)
|
||||
mptSubscriptions_.erase(mptID);
|
||||
}
|
||||
|
||||
std::optional<json::Value>
|
||||
shapeAsNotification(json::Value const& jvObj, unsigned int apiVersion)
|
||||
{
|
||||
// A subscription with no recorded version, and every version below 3, keeps the legacy shape.
|
||||
// A `type` that is not a string cannot be a `method`, so it names no method either; the const
|
||||
// operator[] answers null for a missing member, so one test covers both.
|
||||
if (!rpc::isSpecVersion(apiVersion) || !jvObj.isObject() || !jvObj[jss::type].isString())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
json::Value notification(json::ValueType::Object);
|
||||
notification[jss::jsonrpc] = rpc::kJsonRpcVersion;
|
||||
notification[jss::method] = jvObj[jss::type];
|
||||
|
||||
auto params = jvObj;
|
||||
params.removeMember(jss::type);
|
||||
notification[jss::params] = std::move(params);
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
std::optional<json::Value>
|
||||
stampStreamError(
|
||||
json::Value const& error,
|
||||
unsigned int apiVersion,
|
||||
bool wantsNotifications,
|
||||
json::StaticString const& stream)
|
||||
{
|
||||
if (!rpc::isSpecVersion(apiVersion) || !wantsNotifications)
|
||||
return std::nullopt;
|
||||
|
||||
json::Value stamped = error;
|
||||
stamped[jss::type] = stream;
|
||||
return stamped;
|
||||
}
|
||||
|
||||
void
|
||||
sendShaped(InfoSub& subscriber, unsigned int apiVersion, json::Value const& jvObj, bool broadcast)
|
||||
{
|
||||
if (subscriber.wantsNotifications())
|
||||
{
|
||||
if (auto notification = shapeAsNotification(jvObj, apiVersion))
|
||||
{
|
||||
subscriber.send(*notification, broadcast);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Neither case copies: what the publisher built is sent as it stands.
|
||||
subscriber.send(jvObj, broadcast);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1248,7 +1248,7 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
testcase("PayChan Channel_Auth RPC Api " + std::to_string(apiVersion));
|
||||
args[jss::api_version] = apiVersion;
|
||||
auto const rs =
|
||||
env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
|
||||
jtx::rpcPayload(env.rpc("json", "channel_authorize", args.toStyledString()));
|
||||
auto const error = apiVersion < 2u ? "invalidParams" : "badKeyType";
|
||||
BEAST_EXPECT(rs[jss::error] == error);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/core/tcp_stream.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
/**
|
||||
* A minimal HTTP listener a test can subscribe a webhook `url` to, so it can
|
||||
* observe what `RPCSub` actually posts.
|
||||
*
|
||||
* Accepts asynchronously on an `io_context` of its own, so it needs no
|
||||
* cooperation from whichever thread services the server under test. Each
|
||||
* connection is read with a deadline: a peer that connects and sends nothing
|
||||
* must not hold the listener open, since the destructor waits for the thread
|
||||
* to finish.
|
||||
*/
|
||||
class NotificationServer
|
||||
{
|
||||
public:
|
||||
NotificationServer()
|
||||
: acceptor_(
|
||||
ioc_,
|
||||
boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), 0))
|
||||
, port_(acceptor_.local_endpoint().port())
|
||||
{
|
||||
accept();
|
||||
thread_ = std::thread([this] { ioc_.run(); });
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the acceptor on the thread that runs the io_context, since one
|
||||
* acceptor touched from two threads while an accept is outstanding is not a
|
||||
* use asio documents, then joins that thread. run() returns once the
|
||||
* aborted accept and any connection still inside its deadline are done.
|
||||
*/
|
||||
~NotificationServer()
|
||||
{
|
||||
boost::asio::post(ioc_, [this] {
|
||||
boost::system::error_code ec;
|
||||
acceptor_.close(ec);
|
||||
});
|
||||
if (thread_.joinable())
|
||||
thread_.join();
|
||||
}
|
||||
|
||||
NotificationServer(NotificationServer const&) = delete;
|
||||
NotificationServer&
|
||||
operator=(NotificationServer const&) = delete;
|
||||
|
||||
/**
|
||||
* @return The port this listener bound to, for building a webhook url.
|
||||
*
|
||||
* Read in the constructor rather than here: `local_endpoint()` on an
|
||||
* acceptor with an accept outstanding is a use asio does not document, and
|
||||
* the throwing overload would throw once the acceptor closes. The port
|
||||
* cannot change, so reading it once is enough.
|
||||
*/
|
||||
[[nodiscard]] std::uint16_t
|
||||
port() const
|
||||
{
|
||||
return port_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the next POST body this listener has received, parsed as
|
||||
* JSON.
|
||||
*
|
||||
* A body that does not parse, or that is not a JSON object, is not queued,
|
||||
* so a peer that posts one is reported here as no body at all rather than
|
||||
* as something the caller has to check the shape of.
|
||||
*
|
||||
* @param timeout How long to wait before giving up.
|
||||
* @return The parsed body, or `std::nullopt` if none arrived in time.
|
||||
*/
|
||||
std::optional<json::Value>
|
||||
waitForNotification(std::chrono::milliseconds timeout = std::chrono::seconds(10))
|
||||
{
|
||||
std::unique_lock lock(mutex_);
|
||||
if (!cv_.wait_for(lock, timeout, [this] { return !queue_.empty(); }))
|
||||
return std::nullopt;
|
||||
auto value = std::move(queue_.front());
|
||||
queue_.pop_front();
|
||||
return value;
|
||||
}
|
||||
|
||||
private:
|
||||
using Tcp = boost::asio::ip::tcp;
|
||||
using ErrorCode = boost::system::error_code;
|
||||
|
||||
/**
|
||||
* One accepted connection: its stream, buffer, request and response. One
|
||||
* request is read and answered per connection, which is what RPCSub sends.
|
||||
*/
|
||||
struct Connection
|
||||
{
|
||||
explicit Connection(Tcp::socket socket) : stream(std::move(socket))
|
||||
{
|
||||
}
|
||||
|
||||
boost::beast::tcp_stream stream;
|
||||
boost::beast::flat_buffer buffer;
|
||||
boost::beast::http::request<boost::beast::http::string_body> request;
|
||||
boost::beast::http::response<boost::beast::http::string_body> response;
|
||||
};
|
||||
|
||||
/**
|
||||
* Arms one asynchronous accept, whose completion reads the connection and
|
||||
* arms the next.
|
||||
*/
|
||||
void
|
||||
accept()
|
||||
{
|
||||
acceptor_.async_accept([this](ErrorCode ec, Tcp::socket socket) {
|
||||
// The close the destructor posts ends the chain, either by aborting the outstanding
|
||||
// accept or by leaving the acceptor closed when a completion was already queued. Any
|
||||
// other error (ECONNABORTED for a peer gone between SYN and accept, EMFILE for a
|
||||
// process out of descriptors) is one connection's, and the listener keeps accepting.
|
||||
if (ec == boost::asio::error::operation_aborted || !acceptor_.is_open())
|
||||
return;
|
||||
|
||||
if (!ec)
|
||||
read(std::make_shared<Connection>(std::move(socket)));
|
||||
|
||||
accept();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads one request from @p connection under the deadline, queues its body
|
||||
* and answers it.
|
||||
*
|
||||
* @param connection The accepted connection, kept alive by the handler.
|
||||
*/
|
||||
void
|
||||
read(std::shared_ptr<Connection> connection)
|
||||
{
|
||||
auto& c = *connection;
|
||||
c.stream.expires_after(kTimeout);
|
||||
boost::beast::http::async_read(
|
||||
c.stream,
|
||||
c.buffer,
|
||||
c.request,
|
||||
[this, connection = std::move(connection)](ErrorCode ec, std::size_t) {
|
||||
if (ec)
|
||||
return;
|
||||
|
||||
queue(connection->request.body());
|
||||
write(connection);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses @p body and queues it for waitForNotification.
|
||||
*
|
||||
* A body that does not parse, or that is not a JSON object, is not queued.
|
||||
* `null` is what the guard is for: the reader accepts it at the root, and
|
||||
* it would read as a notification that arrived carrying nothing. A root
|
||||
* scalar cannot reach here, the reader accepting only null, an array or an
|
||||
* object there.
|
||||
*
|
||||
* @param body The POST body as received.
|
||||
*/
|
||||
void
|
||||
queue(std::string const& body)
|
||||
{
|
||||
json::Value parsed;
|
||||
if (!json::Reader{}.parse(body, parsed) || !parsed.isObject())
|
||||
return;
|
||||
|
||||
{
|
||||
std::scoped_lock const lock(mutex_);
|
||||
queue_.push_back(std::move(parsed));
|
||||
}
|
||||
cv_.notify_all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Answers @p connection with an empty 200 under the deadline; the
|
||||
* connection closes with the stream the handler holds the last reference
|
||||
* to.
|
||||
*
|
||||
* @param connection The connection whose request was read.
|
||||
*/
|
||||
static void
|
||||
write(std::shared_ptr<Connection> connection)
|
||||
{
|
||||
auto& c = *connection;
|
||||
c.response.result(boost::beast::http::status::ok);
|
||||
c.response.version(c.request.version());
|
||||
c.response.prepare_payload();
|
||||
c.stream.expires_after(kTimeout);
|
||||
boost::beast::http::async_write(
|
||||
c.stream, c.response, [connection = std::move(connection)](ErrorCode, std::size_t) {
|
||||
// The connection closes with the stream this holds the last reference to.
|
||||
});
|
||||
}
|
||||
|
||||
static constexpr std::chrono::seconds kTimeout{10};
|
||||
|
||||
boost::asio::io_context ioc_;
|
||||
Tcp::acceptor acceptor_;
|
||||
std::uint16_t const port_;
|
||||
std::thread thread_;
|
||||
|
||||
std::mutex mutex_;
|
||||
std::condition_variable cv_;
|
||||
std::deque<json::Value> queue_;
|
||||
};
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -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>
|
||||
@@ -315,6 +317,133 @@ auto const kData = JTxFieldWrapper<BlobField>(sfData);
|
||||
|
||||
auto const kAmount = JTxFieldWrapper<StAmountField>(sfAmount);
|
||||
|
||||
/**
|
||||
* Reports whether an RPC reply arrived in the JSON-RPC 2.0 envelope.
|
||||
*
|
||||
* No single member identifies it. `jsonrpc` does not, since a legacy WebSocket
|
||||
* reply echoes whatever `jsonrpc` the request sent. An object under `error`
|
||||
* does not either: the ripplerpc version 2 and 3 envelopes move the whole
|
||||
* result there, so they carry an object too.
|
||||
*
|
||||
* What is unique to an error is `code` beside `message` with neither of the two
|
||||
* members a legacy envelope always writes, `error_code` and `status`. `data` is
|
||||
* no part of the test: the specification makes it optional, and a pre-dispatch
|
||||
* rejection carries none, so testing for it would read every such rejection as
|
||||
* a legacy reply and hand a caller a null payload.
|
||||
*
|
||||
* For a success it is a `result` with no `status` at either level.
|
||||
*
|
||||
* @param reply A whole reply, as received.
|
||||
* @return Whether the specification envelope carried it.
|
||||
*/
|
||||
inline bool
|
||||
isSpecEnvelope(json::Value const& reply)
|
||||
{
|
||||
if (reply.isMember(jss::error))
|
||||
{
|
||||
auto const& err = reply[jss::error];
|
||||
return err.isObject() && err.isMember(jss::code) && err.isMember(jss::message) &&
|
||||
!err.isMember(jss::error_code) && !err.isMember(jss::status);
|
||||
}
|
||||
|
||||
return reply.isMember(jss::jsonrpc) && reply.isMember(jss::result) &&
|
||||
!reply.isMember(jss::status) && !reply[jss::result].isMember(jss::status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether a reply carries a failure under a top-level `error` object
|
||||
* in the legacy shape, which is how the `ripplerpc` 2.0 and 3.0 envelopes
|
||||
* answer a failed request: the payload sits under `error` instead of `result`,
|
||||
* `code` and `message` are added beside `error_code`, and `error_message` is
|
||||
* removed.
|
||||
*
|
||||
* @param reply A whole reply, as received.
|
||||
* @return Whether `error` is an object and the reply is not a specification
|
||||
* envelope.
|
||||
*/
|
||||
inline bool
|
||||
isLegacyErrorObject(json::Value const& reply)
|
||||
{
|
||||
return reply.isMember(jss::error) && reply[jss::error].isObject() && !isSpecEnvelope(reply);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the payload of an RPC reply in the legacy shape, whichever envelope
|
||||
* carried it.
|
||||
*
|
||||
* From API version 3 a reply follows the JSON-RPC 2.0 specification: a failure
|
||||
* arrives under `error` as {code, message, data}, with the XRPL token and code
|
||||
* inside `data`. Version 1 puts everything under `result`, and the `ripplerpc`
|
||||
* 2.0 and 3.0 envelopes put a failure under `error` with `message` in place of
|
||||
* `error_message`. A test that only cares which error a request produced can
|
||||
* call this and assert against one shape across every version.
|
||||
*
|
||||
* A test that exists to pin the envelope itself must not use this, the envelope
|
||||
* difference being what it is checking.
|
||||
*
|
||||
* @param reply A whole reply, as received.
|
||||
* @return The payload, in the shape a version 1 reply carries.
|
||||
*/
|
||||
inline json::Value
|
||||
rpcPayload(json::Value const& reply)
|
||||
{
|
||||
bool const spec = isSpecEnvelope(reply);
|
||||
|
||||
if (spec && reply.isMember(jss::error))
|
||||
{
|
||||
auto const& err = reply[jss::error];
|
||||
auto payload =
|
||||
err.isMember(jss::data) ? err[jss::data] : json::Value(json::ValueType::Object);
|
||||
payload[jss::error_message] = err[jss::message];
|
||||
payload[jss::status] = jss::error;
|
||||
return payload;
|
||||
}
|
||||
|
||||
if (spec)
|
||||
{
|
||||
// The specification has no `status`, since `result` versus `error` already says which it
|
||||
// was. Restore it so a caller can assert the same way on either envelope.
|
||||
auto payload = reply[jss::result];
|
||||
payload[jss::status] = jss::success;
|
||||
return payload;
|
||||
}
|
||||
|
||||
if (isLegacyErrorObject(reply))
|
||||
{
|
||||
// The `ripplerpc` 2.0 and 3.0 envelopes keep the payload whole under `error`, with
|
||||
// `message` in place of `error_message`. Restore the name a version 1 payload carries.
|
||||
auto payload = reply[jss::error];
|
||||
payload[jss::error_message] = payload[jss::message];
|
||||
return payload;
|
||||
}
|
||||
|
||||
return reply[jss::result];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a whole RPC reply rewritten into the legacy envelope, whichever one
|
||||
* carried it.
|
||||
*
|
||||
* Where rpcPayload returns just the payload, this keeps the reply's outer
|
||||
* shape, so an assertion that indexes `[jss::result]` and reads a top-level
|
||||
* `status` holds across every version. A `ripplerpc` 2.0 or 3.0 failure, which
|
||||
* sits under `error`, is moved under `result` the same way.
|
||||
*
|
||||
* @param reply A whole reply, as received.
|
||||
* @return The reply, in the shape a version 1 reply carries.
|
||||
*/
|
||||
inline json::Value
|
||||
rpcLegacyReply(json::Value const& reply)
|
||||
{
|
||||
if (!isSpecEnvelope(reply) && !isLegacyErrorObject(reply))
|
||||
return reply;
|
||||
|
||||
json::Value legacy(json::ValueType::Object);
|
||||
legacy[jss::result] = rpcPayload(reply);
|
||||
legacy[jss::status] = legacy[jss::result][jss::status];
|
||||
return legacy;
|
||||
}
|
||||
|
||||
// Functions used in debugging
|
||||
json::Value
|
||||
getAccountOffers(Env& env, AccountID const& acct, bool current = false);
|
||||
@@ -363,6 +492,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);
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/JsonRpc.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
/**
|
||||
* Pins the rule by which a reply's envelope is recognized.
|
||||
*
|
||||
* `isSpecEnvelope` is read by `rpcPayload` and `rpcLegacyReply`, which dozens
|
||||
* of assertions across the suites index through. A misreading there does not
|
||||
* fail loudly: it hands every one of those assertions a payload from the wrong
|
||||
* place, so they compare a null against a null and pass. The rule is also the
|
||||
* opposite of obvious, no single member identifying the envelope, so it is
|
||||
* pinned here against replies written out rather than obtained from a server.
|
||||
*
|
||||
* A server-driven test cannot do this job. It can only show the helper agreeing
|
||||
* with the envelopes the server happens to send, where what needs holding is
|
||||
* that the near misses are told apart: a legacy reply that echoes `jsonrpc`,
|
||||
* and a ripplerpc version 2 error, which is an object under `error` as well.
|
||||
*/
|
||||
class TestHelpers_test : public beast::unit_test::Suite
|
||||
{
|
||||
/**
|
||||
* A version 3 failure: {code, message} under `error`, and no legacy member.
|
||||
*
|
||||
* @return The reply, as a client receives it.
|
||||
*/
|
||||
static json::Value
|
||||
specError()
|
||||
{
|
||||
json::Value reply(json::ValueType::Object);
|
||||
reply[jss::jsonrpc] = rpc::kJsonRpcVersion;
|
||||
reply[jss::id] = 1;
|
||||
reply[jss::error] = json::ValueType::Object;
|
||||
reply[jss::error][jss::code] = rpc::kJsonRpcInvalidRequest;
|
||||
reply[jss::error][jss::message] = "Request is not a JSON object";
|
||||
return reply;
|
||||
}
|
||||
|
||||
/**
|
||||
* A version 3 success: a `result` with no `status` at either level.
|
||||
*
|
||||
* @return The reply, as a client receives it.
|
||||
*/
|
||||
static json::Value
|
||||
specSuccess()
|
||||
{
|
||||
json::Value reply(json::ValueType::Object);
|
||||
reply[jss::jsonrpc] = rpc::kJsonRpcVersion;
|
||||
reply[jss::id] = 2;
|
||||
reply[jss::result] = json::ValueType::Object;
|
||||
reply[jss::result][jss::ledger_index] = 3;
|
||||
return reply;
|
||||
}
|
||||
|
||||
/**
|
||||
* A `ripplerpc: "2.0"` failure as `shapeReply` writes it: the legacy payload
|
||||
* under `error`, with `code` and `message` beside the legacy members and no
|
||||
* `error_message`.
|
||||
*
|
||||
* @return The reply, as a client receives it.
|
||||
*/
|
||||
static json::Value
|
||||
ripplerpcError()
|
||||
{
|
||||
json::Value reply(json::ValueType::Object);
|
||||
reply[jss::ripplerpc] = "2.0";
|
||||
reply[jss::error] = json::ValueType::Object;
|
||||
reply[jss::error][jss::status] = jss::error;
|
||||
reply[jss::error][jss::error] = "invalidParams";
|
||||
reply[jss::error][jss::error_code] = RpcInvalidParams;
|
||||
reply[jss::error][jss::code] = RpcInvalidParams;
|
||||
reply[jss::error][jss::message] = "Invalid parameters.";
|
||||
return reply;
|
||||
}
|
||||
|
||||
public:
|
||||
/**
|
||||
* A version 3 failure and a version 3 success are both recognized.
|
||||
*/
|
||||
void
|
||||
testSpecEnvelopeIsRecognized()
|
||||
{
|
||||
testcase("The specification envelope is recognized");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
BEAST_EXPECT(isSpecEnvelope(specError()));
|
||||
BEAST_EXPECT(isSpecEnvelope(specSuccess()));
|
||||
}
|
||||
|
||||
/**
|
||||
* The three near misses read as legacy: a `status` at either level, an
|
||||
* echoed `jsonrpc`, and the ripplerpc version 2 error object.
|
||||
*/
|
||||
void
|
||||
testLegacyEnvelopeIsNotMistakenForIt()
|
||||
{
|
||||
testcase("A legacy envelope is not mistaken for the specification");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// The `status` a legacy reply always writes, at whichever level.
|
||||
{
|
||||
json::Value reply(json::ValueType::Object);
|
||||
reply[jss::result] = json::ValueType::Object;
|
||||
reply[jss::result][jss::status] = jss::success;
|
||||
BEAST_EXPECTS(!isSpecEnvelope(reply), to_string(reply));
|
||||
|
||||
json::Value outer(json::ValueType::Object);
|
||||
outer[jss::status] = jss::success;
|
||||
outer[jss::result] = json::ValueType::Object;
|
||||
BEAST_EXPECTS(!isSpecEnvelope(outer), to_string(outer));
|
||||
}
|
||||
|
||||
// A legacy WebSocket reply echoes the `jsonrpc` its request sent, so that member alone
|
||||
// decides nothing. This is the near miss the docstring names first.
|
||||
{
|
||||
json::Value reply(json::ValueType::Object);
|
||||
reply[jss::jsonrpc] = rpc::kJsonRpcVersion;
|
||||
reply[jss::status] = jss::success;
|
||||
reply[jss::type] = jss::response;
|
||||
reply[jss::result] = json::ValueType::Object;
|
||||
BEAST_EXPECTS(!isSpecEnvelope(reply), to_string(reply));
|
||||
}
|
||||
|
||||
// The ripplerpc version 2 error envelope moves the whole result under `error`, so an object
|
||||
// there decides nothing either. It is told apart by the two members it keeps.
|
||||
{
|
||||
json::Value reply(json::ValueType::Object);
|
||||
reply[jss::error] = json::ValueType::Object;
|
||||
reply[jss::error][jss::error] = "invalidParams";
|
||||
reply[jss::error][jss::error_code] = RpcInvalidParams;
|
||||
reply[jss::error][jss::code] = RpcInvalidParams;
|
||||
reply[jss::error][jss::message] = "Invalid parameters.";
|
||||
reply[jss::error][jss::status] = jss::error;
|
||||
BEAST_EXPECTS(!isSpecEnvelope(reply), to_string(reply));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `rpcPayload` hands a caller the legacy payload shape from either
|
||||
* envelope, with and without `data`, restores `error_message` to a
|
||||
* `ripplerpc` failure, and leaves a version 1 reply as it is.
|
||||
*/
|
||||
void
|
||||
testPayloadIsOneShapeAcrossEnvelopes()
|
||||
{
|
||||
testcase("A payload reads the same whichever envelope carried it");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// A specification failure carries the token and code inside `data`, which is what a caller
|
||||
// asserting on the legacy shape expects to find at the top level of the payload.
|
||||
{
|
||||
auto reply = specError();
|
||||
reply[jss::error][jss::data] = json::ValueType::Object;
|
||||
reply[jss::error][jss::data][jss::error] = "invalidParams";
|
||||
reply[jss::error][jss::data][jss::error_code] = RpcInvalidParams;
|
||||
|
||||
auto const payload = rpcPayload(reply);
|
||||
BEAST_EXPECTS(payload[jss::error] == "invalidParams", to_string(payload));
|
||||
BEAST_EXPECTS(payload[jss::error_code] == RpcInvalidParams, to_string(payload));
|
||||
BEAST_EXPECTS(
|
||||
payload[jss::error_message] == "Request is not a JSON object", to_string(payload));
|
||||
BEAST_EXPECTS(payload[jss::status] == jss::error, to_string(payload));
|
||||
}
|
||||
|
||||
// A pre-dispatch rejection carries no `data`, the specification making it optional. The
|
||||
// payload is then the message and the status alone rather than a null.
|
||||
{
|
||||
auto const payload = rpcPayload(specError());
|
||||
BEAST_EXPECTS(!payload.isMember(jss::error), to_string(payload));
|
||||
BEAST_EXPECTS(
|
||||
payload[jss::error_message] == "Request is not a JSON object", to_string(payload));
|
||||
BEAST_EXPECTS(payload[jss::status] == jss::error, to_string(payload));
|
||||
}
|
||||
|
||||
// A success regains the `status` the specification has no place for.
|
||||
{
|
||||
auto const payload = rpcPayload(specSuccess());
|
||||
BEAST_EXPECTS(payload[jss::ledger_index] == 3, to_string(payload));
|
||||
BEAST_EXPECTS(payload[jss::status] == jss::success, to_string(payload));
|
||||
}
|
||||
|
||||
// A legacy reply is returned as it stands, so a caller reads one shape either way.
|
||||
{
|
||||
json::Value reply(json::ValueType::Object);
|
||||
reply[jss::result] = json::ValueType::Object;
|
||||
reply[jss::result][jss::status] = jss::success;
|
||||
reply[jss::result][jss::ledger_index] = 3;
|
||||
BEAST_EXPECT(rpcPayload(reply) == reply[jss::result]);
|
||||
}
|
||||
|
||||
// A `ripplerpc` 2.0 or 3.0 failure sits under `error` with `message` in place of
|
||||
// `error_message`. The payload regains that name, so it reads as a version 1 failure.
|
||||
{
|
||||
auto const payload = rpcPayload(ripplerpcError());
|
||||
BEAST_EXPECTS(payload[jss::error] == "invalidParams", to_string(payload));
|
||||
BEAST_EXPECTS(payload[jss::error_code] == RpcInvalidParams, to_string(payload));
|
||||
BEAST_EXPECTS(payload[jss::error_message] == "Invalid parameters.", to_string(payload));
|
||||
BEAST_EXPECTS(payload[jss::status] == jss::error, to_string(payload));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `rpcLegacyReply` keeps the outer legacy shape, `status` beside `result`,
|
||||
* moves a `ripplerpc` failure under `result`, and returns a version 1
|
||||
* reply unchanged.
|
||||
*/
|
||||
void
|
||||
testLegacyReplyKeepsTheOuterShape()
|
||||
{
|
||||
testcase("A rewritten reply keeps the outer legacy shape");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
auto const legacy = rpcLegacyReply(specSuccess());
|
||||
BEAST_EXPECTS(legacy[jss::status] == jss::success, to_string(legacy));
|
||||
BEAST_EXPECTS(legacy[jss::result][jss::ledger_index] == 3, to_string(legacy));
|
||||
|
||||
auto const failed = rpcLegacyReply(specError());
|
||||
BEAST_EXPECTS(failed[jss::status] == jss::error, to_string(failed));
|
||||
BEAST_EXPECTS(
|
||||
failed[jss::result][jss::error_message] == "Request is not a JSON object",
|
||||
to_string(failed));
|
||||
|
||||
// A reply already in the legacy envelope is returned unchanged, not rewritten twice.
|
||||
json::Value reply(json::ValueType::Object);
|
||||
reply[jss::status] = jss::success;
|
||||
reply[jss::result] = json::ValueType::Object;
|
||||
reply[jss::result][jss::status] = jss::success;
|
||||
BEAST_EXPECT(rpcLegacyReply(reply) == reply);
|
||||
|
||||
// A `ripplerpc` failure moves from `error` to `result`, with `status` beside it.
|
||||
auto const moved = rpcLegacyReply(ripplerpcError());
|
||||
BEAST_EXPECTS(moved[jss::status] == jss::error, to_string(moved));
|
||||
BEAST_EXPECTS(moved[jss::result][jss::error] == "invalidParams", to_string(moved));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testSpecEnvelopeIsRecognized();
|
||||
testLegacyEnvelopeIsNotMistakenForIt();
|
||||
testPayloadIsOneShapeAcrossEnvelopes();
|
||||
testLegacyReplyKeepsTheOuterShape();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(TestHelpers, jtx, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
+29
-1
@@ -31,16 +31,44 @@ public:
|
||||
findMsg(
|
||||
std::chrono::milliseconds const& timeout,
|
||||
std::function<bool(json::Value const&)> pred) = 0;
|
||||
|
||||
/**
|
||||
* Send a command and return its reply exactly as it arrived.
|
||||
*
|
||||
* `invoke` reshapes a reply into the legacy envelope so that a caller can
|
||||
* assert one shape across API versions. A test checking the envelope itself
|
||||
* needs what the server actually sent.
|
||||
*
|
||||
* At version 3 `invoke` keeps `id` and `jsonrpc` beside the legacy members,
|
||||
* as a legacy success reply does. A legacy error reply nests the whole
|
||||
* reply under `result`, `id` included, as it always has.
|
||||
*
|
||||
* @param cmd The method to call.
|
||||
* @param params The parameters to send with it.
|
||||
* @return The reply, or nullopt if none arrived.
|
||||
*/
|
||||
virtual std::optional<json::Value>
|
||||
invokeRaw(std::string const& cmd, json::Value const& params) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a client operating through WebSockets/S.
|
||||
*
|
||||
* @param cfg The server configuration to read the port from.
|
||||
* @param v2 Whether to connect to the `ws2` port rather than `ws`.
|
||||
* @param rpcVersion The legacy `ripplerpc` envelope to select.
|
||||
* @param headers Extra headers to send with the upgrade request.
|
||||
* @param apiVersion Sent as `api_version` on every request. From version 3 it
|
||||
* selects the JSON-RPC 2.0 envelope and turns asynchronous messages into
|
||||
* notifications.
|
||||
* @return The client.
|
||||
*/
|
||||
std::unique_ptr<WSClient>
|
||||
makeWSClient(
|
||||
Config const& cfg,
|
||||
bool v2 = true,
|
||||
unsigned rpcVersion = 2,
|
||||
std::unordered_map<std::string, std::string> const& headers = {});
|
||||
std::unordered_map<std::string, std::string> const& headers = {},
|
||||
std::optional<unsigned> apiVersion = std::nullopt);
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/multisign.h>
|
||||
#include <test/jtx/seq.h>
|
||||
#include <test/jtx/ter.h>
|
||||
@@ -233,12 +234,18 @@ AMM::ammRpcInfo(
|
||||
if (ammAccount)
|
||||
jv[jss::amm_account] = *ammAccount;
|
||||
}
|
||||
auto jr =
|
||||
auto const jr =
|
||||
(apiVersion == rpc::kApiInvalidVersion
|
||||
? env_.rpc("json", "amm_info", to_string(jv))
|
||||
: env_.rpc(apiVersion, "json", "amm_info", to_string(jv)));
|
||||
if (jr.isObject() && jr.isMember(jss::result) && jr[jss::result].isMember(jss::status))
|
||||
return jr[jss::result];
|
||||
if (!jr.isObject())
|
||||
return json::ValueType::Null;
|
||||
|
||||
// rpcPayload normalizes the specification envelope that API version 3 returns, which has no
|
||||
// `status` member, back to the shape callers assert against.
|
||||
auto payload = rpcPayload(jr);
|
||||
if (payload.isMember(jss::status))
|
||||
return payload;
|
||||
return json::ValueType::Null;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include <test/jtx/WSClient.h>
|
||||
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
|
||||
#include <xrpld/core/Config.h>
|
||||
|
||||
#include <xrpl/basics/Mutex.hpp>
|
||||
@@ -9,6 +11,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>
|
||||
|
||||
@@ -125,6 +129,7 @@ class WSClientImpl : public WSClient
|
||||
std::list<std::shared_ptr<Msg>> msgs_;
|
||||
|
||||
unsigned rpcVersion_;
|
||||
std::optional<unsigned> apiVersion_;
|
||||
|
||||
void
|
||||
cleanup()
|
||||
@@ -154,17 +159,28 @@ class WSClientImpl : public WSClient
|
||||
}
|
||||
|
||||
public:
|
||||
/**
|
||||
* Connects to the server's WebSocket port and starts reading messages.
|
||||
*
|
||||
* @param cfg The server configuration to read the port from.
|
||||
* @param v2 Whether to connect to the `ws2` port rather than `ws`.
|
||||
* @param rpcVersion The legacy `ripplerpc` envelope to select.
|
||||
* @param headers Extra headers to send with the upgrade request.
|
||||
* @param apiVersion Sent as `api_version` on every request, when given.
|
||||
*/
|
||||
WSClientImpl(
|
||||
Config const& cfg,
|
||||
bool v2,
|
||||
unsigned rpcVersion,
|
||||
std::unordered_map<std::string, std::string> const& headers = {})
|
||||
std::unordered_map<std::string, std::string> const& headers = {},
|
||||
std::optional<unsigned> apiVersion = std::nullopt)
|
||||
: work_(std::in_place, boost::asio::make_work_guard(ios_))
|
||||
, strand_(boost::asio::make_strand(ios_))
|
||||
, thread_([&] { ios_.run(); })
|
||||
, stream_(ios_)
|
||||
, ws_(stream_)
|
||||
, rpcVersion_(rpcVersion)
|
||||
, apiVersion_(apiVersion)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -194,8 +210,8 @@ public:
|
||||
cleanup();
|
||||
}
|
||||
|
||||
json::Value
|
||||
invoke(std::string const& cmd, json::Value const& params) override
|
||||
std::optional<json::Value>
|
||||
invokeRaw(std::string const& cmd, json::Value const& params) override
|
||||
{
|
||||
using boost::asio::buffer;
|
||||
using namespace std::chrono_literals;
|
||||
@@ -207,14 +223,16 @@ 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
|
||||
{
|
||||
jp[jss::command] = cmd;
|
||||
}
|
||||
if (apiVersion_)
|
||||
jp[jss::api_version] = *apiVersion_;
|
||||
auto const s = to_string(jp);
|
||||
|
||||
// Use the error_code overload to avoid an unhandled exception
|
||||
@@ -226,12 +244,44 @@ public:
|
||||
return {};
|
||||
}
|
||||
|
||||
auto jv =
|
||||
findMsg(5s, [&](json::Value const& jval) { return jval[jss::type] == jss::response; });
|
||||
return findMsg(
|
||||
5s, [&](json::Value const& jval) { return jval[jss::type] == jss::response; });
|
||||
}
|
||||
|
||||
json::Value
|
||||
invoke(std::string const& cmd, json::Value const& params) override
|
||||
{
|
||||
auto jv = invokeRaw(cmd, params);
|
||||
if (jv)
|
||||
{
|
||||
// Normalize JSON output
|
||||
jv->removeMember(jss::type);
|
||||
|
||||
// Restore the legacy shape so a caller can assert the same way on either envelope,
|
||||
// whatever `api_version` the client was built with.
|
||||
if (jtx::isSpecEnvelope(*jv))
|
||||
{
|
||||
// rpcLegacyReply does the same rewrite, minus the hoist below: this client's legacy
|
||||
// branch puts `error` at the top level as well as under `result`, and callers read
|
||||
// it there. Kept apart, since hoisting it in rpcLegacyReply would change what the
|
||||
// HTTP callers of that helper see. A rejection without `data` has no token to
|
||||
// hoist.
|
||||
auto ret = jtx::rpcLegacyReply(*jv);
|
||||
if (ret[jss::result].isMember(jss::error))
|
||||
ret[jss::error] = ret[jss::result][jss::error];
|
||||
// The notices the server hoists beside `result` stay beside it, which is where a
|
||||
// legacy WebSocket reply carries them too, and so do `id` and `jsonrpc`, which a
|
||||
// legacy success reply keeps at the top level. `ripplerpc` and `api_version` are
|
||||
// not in a specification reply at all, so there is nothing to carry for them.
|
||||
for (auto const& name :
|
||||
{jss::warning, jss::warnings, jss::deprecated, jss::id, jss::jsonrpc})
|
||||
{
|
||||
if (jv->isMember(name))
|
||||
ret[name] = (*jv)[name];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
if ((*jv).isMember(jss::status) && (*jv)[jss::status] == jss::error)
|
||||
{
|
||||
json::Value ret;
|
||||
@@ -372,9 +422,10 @@ makeWSClient(
|
||||
Config const& cfg,
|
||||
bool v2,
|
||||
unsigned rpcVersion,
|
||||
std::unordered_map<std::string, std::string> const& headers)
|
||||
std::unordered_map<std::string, std::string> const& headers,
|
||||
std::optional<unsigned> apiVersion)
|
||||
{
|
||||
return std::make_unique<WSClientImpl>(cfg, v2, rpcVersion, headers);
|
||||
return std::make_unique<WSClientImpl>(cfg, v2, rpcVersion, headers, apiVersion);
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -145,7 +145,8 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
// Ledger 3 has the two txs associated with funding the account
|
||||
// All other ledgers have no txs
|
||||
|
||||
auto const hasTxs = [apiVersion](json::Value const& j) {
|
||||
auto const hasTxs = [apiVersion](json::Value const& reply) {
|
||||
auto const j = jtx::rpcLegacyReply(reply);
|
||||
switch (apiVersion)
|
||||
{
|
||||
case 1:
|
||||
@@ -190,12 +191,14 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
}
|
||||
};
|
||||
|
||||
auto const noTxs = [](json::Value const& j) {
|
||||
auto const noTxs = [](json::Value const& reply) {
|
||||
auto const j = jtx::rpcLegacyReply(reply);
|
||||
return j.isMember(jss::result) && (j[jss::result][jss::status] == "success") &&
|
||||
(j[jss::result][jss::transactions].size() == 0);
|
||||
};
|
||||
|
||||
auto const isErr = [](json::Value const& j, ErrorCodeI code) {
|
||||
auto const isErr = [](json::Value const& reply, ErrorCodeI code) {
|
||||
auto const j = jtx::rpcLegacyReply(reply);
|
||||
return j.isMember(jss::result) && j[jss::result].isMember(jss::error) &&
|
||||
j[jss::result][jss::error] == rpc::getErrorInfo(code).token;
|
||||
};
|
||||
@@ -410,7 +413,7 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
}
|
||||
|
||||
p[jss::binary] = true;
|
||||
json::Value result{env.rpc("json", "account_tx", to_string(p))};
|
||||
json::Value result{jtx::rpcLegacyReply(env.rpc("json", "account_tx", to_string(p)))};
|
||||
BEAST_EXPECT(result[jss::result][jss::status] == "success");
|
||||
|
||||
p[jss::forward] = "true";
|
||||
@@ -424,7 +427,7 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
}
|
||||
|
||||
p[jss::forward] = false;
|
||||
result = env.rpc("json", "account_tx", to_string(p));
|
||||
result = jtx::rpcLegacyReply(env.rpc("json", "account_tx", to_string(p)));
|
||||
BEAST_EXPECT(result[jss::result][jss::status] == "success");
|
||||
}
|
||||
// test limit with malformed values
|
||||
@@ -438,56 +441,56 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
// Test case: limit = 1.2 should fail (not an integer)
|
||||
p[jss::limit] = 1.2;
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = "10" should fail (string instead of integer)
|
||||
p[jss::limit] = "10";
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = true should fail (boolean instead of integer)
|
||||
p[jss::limit] = true;
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = false should fail (boolean instead of integer)
|
||||
p[jss::limit] = false;
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = -1 should fail (negative number)
|
||||
p[jss::limit] = -1;
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = [] should fail (array instead of integer)
|
||||
p[jss::limit] = json::Value(json::ValueType::Array);
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = {} should fail (object instead of integer)
|
||||
p[jss::limit] = json::Value(json::ValueType::Object);
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = "malformed" should fail (malformed string)
|
||||
p[jss::limit] = "malformed";
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = ["limit"] should fail (array with string)
|
||||
p[jss::limit] = json::Value(json::ValueType::Array);
|
||||
p[jss::limit].append("limit");
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = {"limit": 10} should fail (object with
|
||||
@@ -495,13 +498,14 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
p[jss::limit] = json::Value(json::ValueType::Object);
|
||||
p[jss::limit][jss::limit] = 10;
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] ==
|
||||
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::error_message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
|
||||
// Test case: limit = 10 should succeed (valid integer)
|
||||
p[jss::limit] = 10;
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "account_tx", to_string(p))[jss::result][jss::status] == "success");
|
||||
jtx::rpcPayload(env.rpc("json", "account_tx", to_string(p)))[jss::status] ==
|
||||
"success");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+21
-40
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+337
-142
@@ -56,6 +56,7 @@
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <source_location>
|
||||
@@ -149,12 +150,66 @@ getTypeName(FieldType typeID)
|
||||
|
||||
class LedgerEntry_test : public beast::unit_test::Suite
|
||||
{
|
||||
/**
|
||||
* Calls `ledger_entry` with @p params and returns the payload in the legacy
|
||||
* shape, whichever envelope the version selects.
|
||||
*
|
||||
* @param env The environment to call.
|
||||
* @param params The request parameters.
|
||||
* @param apiVersion The API version to name, or none to let the client
|
||||
* choose.
|
||||
* @return The reply payload, as rpcPayload shapes it.
|
||||
*/
|
||||
static json::Value
|
||||
ledgerEntry(
|
||||
jtx::Env& env,
|
||||
json::Value const& params,
|
||||
std::optional<unsigned> apiVersion = std::nullopt)
|
||||
{
|
||||
if (apiVersion)
|
||||
return jtx::rpcPayload(env.rpc(*apiVersion, "json", "ledger_entry", to_string(params)));
|
||||
return jtx::rpcPayload(env.rpc("json", "ledger_entry", to_string(params)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that @p jv reports the error @p err with the message @p msg and
|
||||
* the code the token carries below API version 3, which is what a request
|
||||
* naming no `api_version` gets.
|
||||
*
|
||||
* @param jv The reply payload, in the legacy shape.
|
||||
* @param err The expected error token.
|
||||
* @param msg The expected error message.
|
||||
* @param location The caller, named in a failure.
|
||||
*/
|
||||
void
|
||||
checkErrorValue(
|
||||
json::Value const& jv,
|
||||
std::string const& err,
|
||||
std::string const& msg,
|
||||
std::source_location const location = std::source_location::current())
|
||||
{
|
||||
checkErrorValue(1, jv, err, msg, location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that @p jv reports the error @p err with the message @p msg and
|
||||
* the code that token carries at @p apiVersion. The version leads the
|
||||
* parameter list so that @p location stays last and still captures the
|
||||
* caller.
|
||||
*
|
||||
* @param apiVersion The API version the request named.
|
||||
* @param jv The reply payload, in the legacy shape.
|
||||
* @param err The expected error token.
|
||||
* @param msg The expected error message.
|
||||
* @param location The caller, named in a failure.
|
||||
*/
|
||||
void
|
||||
checkErrorValue(
|
||||
unsigned apiVersion,
|
||||
json::Value const& jv,
|
||||
std::string const& err,
|
||||
std::string const& msg,
|
||||
std::source_location const location = std::source_location::current())
|
||||
{
|
||||
if (BEAST_EXPECT(jv.isMember(jss::status)))
|
||||
BEAST_EXPECTS(jv[jss::status] == "error", std::to_string(location.line()));
|
||||
@@ -164,6 +219,72 @@ 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 at every version.
|
||||
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. Below version 3 every one of them reports
|
||||
// the generic invalidParams (31); from version 3 each reports the code it owns.
|
||||
static std::map<std::string, int> const kMalformedCodes{
|
||||
{"malformedAccount", 110},
|
||||
{"malformedAddress", 111},
|
||||
{"malformedAuthorized", 112},
|
||||
{"malformedAuthorizedCredentials", 113},
|
||||
{"malformedBridgeAccount", 114},
|
||||
{"malformedBroker", 115},
|
||||
{"malformedCurrency", 116},
|
||||
{"malformedDirRoot", 117},
|
||||
{"malformedDocumentID", 118},
|
||||
{"malformedIssue", 119},
|
||||
{"malformedIssuingChainDoor", 120},
|
||||
{"malformedLockingChainDoor", 121},
|
||||
{"malformedMPTIssuanceID", 122},
|
||||
{"malformedMPTokenIssuance", 123},
|
||||
{"malformedOwner", 124},
|
||||
{"malformedRequest", 107},
|
||||
{"malformedSeq", 125},
|
||||
{"malformedSponsee", 126},
|
||||
{"malformedSponsor", 127},
|
||||
{"malformedXChainOwnedClaimID", 128},
|
||||
{"malformedXChainOwnedCreateAccountClaimID", 129},
|
||||
};
|
||||
|
||||
auto const expectCode = [&](int expected) {
|
||||
BEAST_EXPECTS(
|
||||
jv[jss::error_code] == expected,
|
||||
"Expected error_code " + std::to_string(expected) + " for " + err +
|
||||
" at version " + std::to_string(apiVersion) + ", 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 (auto const mit = kMalformedCodes.find(err); mit != kMalformedCodes.end())
|
||||
{
|
||||
expectCode(apiVersion < 3 ? int{RpcInvalidParams} : mit->second);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Every token either map names is checked above. A token in neither is one no
|
||||
// error table row names, which reports its neighbors' generic code at version 3
|
||||
// and so would pass an assertion derived from it. Fail instead: the missing row is
|
||||
// the defect.
|
||||
BEAST_EXPECTS(
|
||||
false,
|
||||
"Token " + err + " names no error code, at line " +
|
||||
std::to_string(location.line()));
|
||||
}
|
||||
}
|
||||
if (msg.empty())
|
||||
{
|
||||
@@ -336,25 +457,27 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
if (required)
|
||||
{
|
||||
correctRequest.removeMember(fieldName);
|
||||
json::Value const jrr = env.rpc(
|
||||
apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, correctRequest, apiVersion);
|
||||
if (apiVersion < 2u)
|
||||
{
|
||||
checkErrorValue(jrr, "unknownOption", "", location);
|
||||
checkErrorValue(apiVersion, jrr, "unknownOption", "Unknown option.", location);
|
||||
}
|
||||
else
|
||||
{
|
||||
checkErrorValue(
|
||||
jrr, "invalidParams", "No ledger_entry params provided.", location);
|
||||
apiVersion,
|
||||
jrr,
|
||||
"invalidParams",
|
||||
"No ledger_entry params provided.",
|
||||
location);
|
||||
}
|
||||
}
|
||||
auto const tryField = [&](json::Value fieldValue) -> void {
|
||||
correctRequest[fieldName] = fieldValue;
|
||||
json::Value const jrr = env.rpc(
|
||||
apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, correctRequest, apiVersion);
|
||||
auto const expectedErrMsg = rpc::expectedFieldMessage(
|
||||
fieldName, typeNameOverride.value_or(getTypeName(typeID)));
|
||||
checkErrorValue(jrr, expectedError, expectedErrMsg, location);
|
||||
checkErrorValue(apiVersion, jrr, expectedError, expectedErrMsg, location);
|
||||
};
|
||||
|
||||
auto const& badValues = getBadValues(typeID);
|
||||
@@ -384,23 +507,29 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
if (required)
|
||||
{
|
||||
correctRequest[parentFieldName].removeMember(fieldName);
|
||||
json::Value const jrr = env.rpc(
|
||||
apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, correctRequest, apiVersion);
|
||||
checkErrorValue(
|
||||
jrr, "malformedRequest", rpc::missingFieldMessage(fieldName.cStr()), location);
|
||||
apiVersion,
|
||||
jrr,
|
||||
"malformedRequest",
|
||||
rpc::missingFieldMessage(fieldName.cStr()),
|
||||
location);
|
||||
|
||||
correctRequest[parentFieldName][fieldName] = json::ValueType::Null;
|
||||
json::Value const jrr2 = env.rpc(
|
||||
apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result];
|
||||
json::Value const jrr2 = ledgerEntry(env, correctRequest, apiVersion);
|
||||
checkErrorValue(
|
||||
jrr2, "malformedRequest", rpc::missingFieldMessage(fieldName.cStr()), location);
|
||||
apiVersion,
|
||||
jrr2,
|
||||
"malformedRequest",
|
||||
rpc::missingFieldMessage(fieldName.cStr()),
|
||||
location);
|
||||
}
|
||||
auto const tryField = [&](json::Value fieldValue) -> void {
|
||||
correctRequest[parentFieldName][fieldName] = fieldValue;
|
||||
|
||||
json::Value const jrr = env.rpc(
|
||||
apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, correctRequest, apiVersion);
|
||||
checkErrorValue(
|
||||
apiVersion,
|
||||
jrr,
|
||||
expectedError,
|
||||
rpc::expectedFieldMessage(fieldName, getTypeName(typeID)),
|
||||
@@ -496,7 +625,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::ledger_hash] =
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AA";
|
||||
auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
auto const jrr = ledgerEntry(env, jvParams);
|
||||
checkErrorValue(jrr, "lgrNotFound", "ledgerNotFound");
|
||||
}
|
||||
{
|
||||
@@ -508,10 +637,12 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
forAllApiVersions([&, this](unsigned apiVersion) {
|
||||
auto const tryField = [&](json::Value fieldValue) -> void {
|
||||
jvParams[jss::ledger_hash] = fieldValue;
|
||||
json::Value const jrr = env.rpc(
|
||||
apiVersion, "json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams, apiVersion);
|
||||
checkErrorValue(
|
||||
jrr, "invalidParams", "Invalid field 'ledger_hash', not hex string.");
|
||||
apiVersion,
|
||||
jrr,
|
||||
"invalidParams",
|
||||
"Invalid field 'ledger_hash', not hex string.");
|
||||
};
|
||||
|
||||
auto const& badValues = getBadValues(typeId);
|
||||
@@ -529,7 +660,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::index] =
|
||||
"00000000000000000000000000000000000000000000000000000000000000"
|
||||
"00";
|
||||
auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
auto const jrr = ledgerEntry(env, jvParams);
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
|
||||
@@ -541,16 +672,16 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAA";
|
||||
jvParams[jss::api_version] = apiVersion;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
|
||||
if (apiVersion < 2u)
|
||||
{
|
||||
checkErrorValue(jrr, "unknownOption", "");
|
||||
checkErrorValue(apiVersion, jrr, "unknownOption", "Unknown option.");
|
||||
}
|
||||
else
|
||||
{
|
||||
checkErrorValue(jrr, "invalidParams", "No ledger_entry params provided.");
|
||||
checkErrorValue(
|
||||
apiVersion, jrr, "invalidParams", "No ledger_entry params provided.");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -573,7 +704,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
std::string const ledgerHash{to_string(env.closed()->header().hash)};
|
||||
{
|
||||
// Exercise ledger_closed along the way.
|
||||
json::Value const jrr = env.rpc("ledger_closed")[jss::result];
|
||||
json::Value const jrr = jtx::rpcPayload(env.rpc("ledger_closed"));
|
||||
BEAST_EXPECT(jrr[jss::ledger_hash] == ledgerHash);
|
||||
BEAST_EXPECT(jrr[jss::ledger_index] == 3);
|
||||
}
|
||||
@@ -584,8 +715,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value jvParams;
|
||||
jvParams[jss::account_root] = alice.human();
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr.isMember(jss::node));
|
||||
BEAST_EXPECT(jrr[jss::node][jss::Account] == alice.human());
|
||||
BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName] == "10000000000");
|
||||
@@ -603,8 +733,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::account_root] = alice.human();
|
||||
jvParams[jss::binary] = 1;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr.isMember(jss::node_binary));
|
||||
BEAST_EXPECT(jrr[jss::node_binary] == kAliceAcctRootBinary);
|
||||
}
|
||||
@@ -612,8 +741,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
// Request alice's account root using the index.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::index] = accountRootIndex;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(!jrr.isMember(jss::node_binary));
|
||||
BEAST_EXPECT(jrr.isMember(jss::node));
|
||||
BEAST_EXPECT(jrr[jss::node][jss::Account] == alice.human());
|
||||
@@ -624,8 +752,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value jvParams;
|
||||
jvParams[jss::index] = accountRootIndex;
|
||||
jvParams[jss::binary] = 0;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr.isMember(jss::node));
|
||||
BEAST_EXPECT(jrr[jss::node][jss::Account] == alice.human());
|
||||
BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName] == "10000000000");
|
||||
@@ -635,8 +762,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value jvParams;
|
||||
jvParams[jss::account] = alice.human();
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr.isMember(jss::node));
|
||||
BEAST_EXPECT(jrr[jss::node][jss::Account] == alice.human());
|
||||
BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName] == "10000000000");
|
||||
@@ -653,8 +779,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value jvParams;
|
||||
jvParams[jss::account_root] = Account("bob").human();
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
}
|
||||
@@ -727,8 +852,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
|
||||
json::Value jvParams;
|
||||
jvParams[jss::amendments] = to_string(keylet.key);
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Amendments);
|
||||
}
|
||||
|
||||
@@ -827,8 +951,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value jvParams;
|
||||
jvParams[jss::check] = to_string(checkId.key);
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Check);
|
||||
BEAST_EXPECT(jrr[jss::node][sfSendMax.jsonName] == "100000000");
|
||||
}
|
||||
@@ -863,15 +986,13 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
{
|
||||
json::Value jvParams;
|
||||
jvParams[jss::account_root] = alice.human();
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
accountRootIndex = jrr[jss::index].asString();
|
||||
}
|
||||
json::Value jvParams;
|
||||
jvParams[jss::check] = accountRootIndex;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
checkErrorValue(jrr, "unexpectedLedgerType", "Unexpected ledger type.");
|
||||
}
|
||||
{
|
||||
@@ -969,8 +1090,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::delegate][jss::account] = alice.human();
|
||||
jvParams[jss::delegate][jss::authorize] = bob.human();
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Delegate);
|
||||
BEAST_EXPECT(jrr[jss::node][sfAccount.jsonName] == alice.human());
|
||||
BEAST_EXPECT(jrr[jss::node][sfAuthorize.jsonName] == bob.human());
|
||||
@@ -981,8 +1101,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value jvParams;
|
||||
jvParams[jss::delegate] = delegateIndex;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Delegate);
|
||||
BEAST_EXPECT(jrr[jss::node][sfAccount.jsonName] == alice.human());
|
||||
BEAST_EXPECT(jrr[jss::node][sfAuthorize.jsonName] == bob.human());
|
||||
@@ -1025,8 +1144,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::deposit_preauth][jss::owner] = alice.human();
|
||||
jvParams[jss::deposit_preauth][jss::authorized] = becky.human();
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
|
||||
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::DepositPreauth);
|
||||
BEAST_EXPECT(jrr[jss::node][sfAccount.jsonName] == alice.human());
|
||||
@@ -1038,8 +1156,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value jvParams;
|
||||
jvParams[jss::deposit_preauth] = depositPreauthIndex;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
|
||||
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::DepositPreauth);
|
||||
BEAST_EXPECT(jrr[jss::node][sfAccount.jsonName] == alice.human());
|
||||
@@ -1121,8 +1238,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
arr.append(jo);
|
||||
jvParams[jss::deposit_preauth][jss::authorized_credentials] = arr;
|
||||
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
auto const expectedErrMsg = fieldValue.isNull()
|
||||
? rpc::missingFieldMessage(jss::issuer.cStr())
|
||||
: rpc::expectedFieldMessage(jss::issuer, "AccountID");
|
||||
@@ -1172,8 +1288,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
arr.append(jo);
|
||||
jvParams[jss::deposit_preauth][jss::authorized_credentials] = arr;
|
||||
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
auto const expectedErrMsg = fieldValue.isNull()
|
||||
? rpc::missingFieldMessage(jss::credential_type.cStr())
|
||||
: rpc::expectedFieldMessage(jss::credential_type, "hex string");
|
||||
@@ -1338,7 +1453,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
std::string const ledgerHash{to_string(env.closed()->header().hash)};
|
||||
{
|
||||
// Exercise ledger_closed along the way.
|
||||
json::Value const jrr = env.rpc("ledger_closed")[jss::result];
|
||||
json::Value const jrr = jtx::rpcPayload(env.rpc("ledger_closed"));
|
||||
BEAST_EXPECT(jrr[jss::ledger_hash] == ledgerHash);
|
||||
BEAST_EXPECT(jrr[jss::ledger_index] == 5);
|
||||
}
|
||||
@@ -1350,8 +1465,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value jvParams;
|
||||
jvParams[jss::directory] = dirRootIndex;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][sfIndexes.jsonName].size() == 32);
|
||||
}
|
||||
{
|
||||
@@ -1359,8 +1473,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value jvParams;
|
||||
jvParams[jss::directory] = json::ValueType::Object;
|
||||
jvParams[jss::directory][jss::dir_root] = dirRootIndex;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::index] == dirRootIndex);
|
||||
}
|
||||
{
|
||||
@@ -1369,8 +1482,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::directory] = json::ValueType::Object;
|
||||
jvParams[jss::directory][jss::owner] = alice.human();
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::index] == dirRootIndex);
|
||||
}
|
||||
{
|
||||
@@ -1379,8 +1491,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::directory] = json::ValueType::Object;
|
||||
jvParams[jss::directory][jss::dir_root] = dirRootIndex;
|
||||
jvParams[jss::directory][jss::sub_index] = 1;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::index] != dirRootIndex);
|
||||
BEAST_EXPECT(jrr[jss::node][sfIndexes.jsonName].size() == 2);
|
||||
}
|
||||
@@ -1391,8 +1502,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::directory][jss::owner] = alice.human();
|
||||
jvParams[jss::directory][jss::sub_index] = 1;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::index] != dirRootIndex);
|
||||
BEAST_EXPECT(jrr[jss::node][sfIndexes.jsonName].size() == 2);
|
||||
}
|
||||
@@ -1440,8 +1550,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::directory][jss::owner] = alice.human();
|
||||
jvParams[jss::directory][jss::dir_root] = dirRootIndex;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
checkErrorValue(
|
||||
jrr, "malformedRequest", "Must have exactly one of `owner` and `dir_root` fields.");
|
||||
}
|
||||
@@ -1451,8 +1560,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::directory] = json::ValueType::Object;
|
||||
jvParams[jss::directory][jss::sub_index] = 1;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
checkErrorValue(
|
||||
jrr, "malformedRequest", "Must have exactly one of `owner` and `dir_root` fields.");
|
||||
}
|
||||
@@ -1494,8 +1602,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::escrow] = json::ValueType::Object;
|
||||
jvParams[jss::escrow][jss::owner] = alice.human();
|
||||
jvParams[jss::escrow][jss::seq] = env.seq(alice) - 1;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][jss::Amount] == XRP(333).value().getText());
|
||||
escrowIndex = jrr[jss::index].asString();
|
||||
}
|
||||
@@ -1504,8 +1611,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value jvParams;
|
||||
jvParams[jss::escrow] = escrowIndex;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][jss::Amount] == XRP(333).value().getText());
|
||||
}
|
||||
{
|
||||
@@ -1532,8 +1638,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
Keylet const keylet = keylet::feeSettings();
|
||||
json::Value jvParams;
|
||||
jvParams[jss::fee] = to_string(keylet.key);
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::FeeSettings);
|
||||
}
|
||||
|
||||
@@ -1554,8 +1659,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
Keylet const keylet = keylet::skip();
|
||||
json::Value jvParams;
|
||||
jvParams[jss::hashes] = to_string(keylet.key);
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::LedgerHashes);
|
||||
}
|
||||
|
||||
@@ -1588,8 +1692,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
{
|
||||
json::Value jvParams;
|
||||
jvParams[jss::nft_offer] = to_string(offerID);
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::NFTokenOffer);
|
||||
BEAST_EXPECT(jrr[jss::node][sfOwner.jsonName] == issuer.human());
|
||||
BEAST_EXPECT(jrr[jss::node][sfNFTokenID.jsonName] == to_string(nftokenID0));
|
||||
@@ -1650,8 +1753,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
{
|
||||
json::Value jvParams;
|
||||
jvParams[jss::nft_page] = to_string(nftpage.key);
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::NFTokenPage);
|
||||
}
|
||||
|
||||
@@ -1703,8 +1805,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
|
||||
json::Value jvParams;
|
||||
jvParams[jss::nunl] = to_string(keylet.key);
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::NegativeUNL);
|
||||
}
|
||||
|
||||
@@ -1737,8 +1838,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::offer][jss::account] = alice.human();
|
||||
jvParams[jss::offer][jss::seq] = env.seq(alice) - 1;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][jss::TakerGets] == "322000000");
|
||||
offerIndex = jrr[jss::index].asString();
|
||||
}
|
||||
@@ -1746,8 +1846,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
// Request the offer using its index.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::offer] = offerIndex;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][jss::TakerGets] == "322000000");
|
||||
}
|
||||
|
||||
@@ -1804,8 +1903,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value jvParams;
|
||||
jvParams[jss::payment_channel] = to_string(payChanIndex);
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][sfAmount.jsonName] == "57000000");
|
||||
BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName] == "0");
|
||||
BEAST_EXPECT(jrr[jss::node][sfSettleDelay.jsonName] == 18);
|
||||
@@ -1842,8 +1940,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value jvParams;
|
||||
jvParams[jss::payment_channel] = ledgerHash;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
|
||||
@@ -1891,8 +1988,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[fieldName][jss::accounts][1u] = gw.human();
|
||||
jvParams[fieldName][jss::currency] = "USD";
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName][jss::value] == "-97");
|
||||
BEAST_EXPECT(jrr[jss::node][sfHighLimit.jsonName][jss::value] == "999");
|
||||
}
|
||||
@@ -1914,8 +2010,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[fieldName][jss::accounts][0u] = alice.human();
|
||||
jvParams[fieldName][jss::currency] = "USD";
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
checkErrorValue(
|
||||
jrr,
|
||||
"malformedRequest",
|
||||
@@ -1932,8 +2027,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[fieldName][jss::accounts][2u] = alice.human();
|
||||
jvParams[fieldName][jss::currency] = "USD";
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
checkErrorValue(
|
||||
jrr,
|
||||
"malformedRequest",
|
||||
@@ -1952,8 +2046,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[fieldName][jss::accounts][1u] = gw.human();
|
||||
jvParams[fieldName][jss::currency] = "USD";
|
||||
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
checkErrorValue(
|
||||
jrr,
|
||||
"malformedAddress",
|
||||
@@ -1967,8 +2060,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[fieldName][jss::accounts][1u] = badAccount;
|
||||
jvParams[fieldName][jss::currency] = "USD";
|
||||
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
checkErrorValue(
|
||||
jrr,
|
||||
"malformedAddress",
|
||||
@@ -1992,8 +2084,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[fieldName][jss::accounts][1u] = alice.human();
|
||||
jvParams[fieldName][jss::currency] = "USD";
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
checkErrorValue(jrr, "malformedRequest", "Cannot have a trustline to self.");
|
||||
}
|
||||
}
|
||||
@@ -2094,7 +2185,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::sponsorship][jss::sponsor] = alice.human();
|
||||
jvParams[jss::sponsorship][jss::sponsee] = bob.human();
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
auto const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Sponsorship);
|
||||
BEAST_EXPECT(jrr[jss::node][sfOwner.jsonName] == alice.human());
|
||||
BEAST_EXPECT(jrr[jss::node][sfSponsee.jsonName] == bob.human());
|
||||
@@ -2105,8 +2196,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value jvParams;
|
||||
jvParams[jss::sponsorship] = sponsorshipIndex;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Sponsorship);
|
||||
BEAST_EXPECT(jrr[jss::node][sfOwner.jsonName] == alice.human());
|
||||
BEAST_EXPECT(jrr[jss::node][sfSponsee.jsonName] == bob.human());
|
||||
@@ -2146,8 +2236,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value jvParams;
|
||||
jvParams[jss::ticket] = to_string(keylet::ticket(env.master, tkt1).key);
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
{
|
||||
@@ -2156,8 +2245,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
tkt1.advanceBy(1);
|
||||
jvParams[jss::ticket] = to_string(keylet::ticket(env.master, tkt1).key);
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Ticket);
|
||||
BEAST_EXPECT(jrr[jss::node][sfTicketSequence.jsonName] == tkt1.value());
|
||||
}
|
||||
@@ -2169,8 +2257,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::ticket][jss::account] = env.master.human();
|
||||
jvParams[jss::ticket][jss::ticket_seq] = tkt1.value();
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::node][jss::index] == to_string(keylet::ticket(env.master, tkt1).key));
|
||||
}
|
||||
@@ -2182,8 +2269,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::ticket][jss::account] = env.master.human();
|
||||
jvParams[jss::ticket][jss::ticket_seq] = tkt1.value();
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
{
|
||||
@@ -2191,8 +2277,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value jvParams;
|
||||
jvParams[jss::ticket] = to_string(keylet::account(env.master).key);
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
checkErrorValue(jrr, "unexpectedLedgerType", "Unexpected ledger type.");
|
||||
}
|
||||
|
||||
@@ -2321,8 +2406,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value jvParams;
|
||||
jvParams[jss::did] = alice.human();
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][sfDIDDocument.jsonName] == strHex(std::string{"data"}));
|
||||
BEAST_EXPECT(jrr[jss::node][sfURI.jsonName] == strHex(std::string{"uri"}));
|
||||
}
|
||||
@@ -2331,8 +2415,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value jvParams;
|
||||
jvParams[jss::did] = env.master.human();
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
{
|
||||
@@ -2443,8 +2526,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value jvParams;
|
||||
jvParams[jss::mpt_issuance] = strHex(mptAlice.issuanceID());
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(jrr[jss::node][sfMPTokenMetadata.jsonName] == strHex(std::string{"123"}));
|
||||
BEAST_EXPECT(jrr[jss::node][jss::mpt_issuance_id] == strHex(mptAlice.issuanceID()));
|
||||
}
|
||||
@@ -2453,8 +2535,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value jvParams;
|
||||
jvParams[jss::mpt_issuance] = badMptID;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
{
|
||||
@@ -2464,8 +2545,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::mptoken][jss::account] = bob.human();
|
||||
jvParams[jss::mptoken][jss::mpt_issuance_id] = strHex(mptAlice.issuanceID());
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::node][sfMPTokenIssuanceID.jsonName] == strHex(mptAlice.issuanceID()));
|
||||
}
|
||||
@@ -2476,8 +2556,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jvParams[jss::mptoken][jss::account] = bob.human();
|
||||
jvParams[jss::mptoken][jss::mpt_issuance_id] = badMptID;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
json::Value const jrr = ledgerEntry(env, jvParams);
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
{
|
||||
@@ -2593,23 +2672,20 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
json::Value const& jv,
|
||||
json::StaticString const& expectedType,
|
||||
std::optional<std::string> const& expectedError = {}) {
|
||||
auto const payload = jtx::rpcPayload(jv);
|
||||
if (good)
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
jv.isObject() && jv.isMember(jss::result) &&
|
||||
!jv[jss::result].isMember(jss::error) &&
|
||||
jv[jss::result].isMember(jss::node) &&
|
||||
jv[jss::result][jss::node].isMember(sfLedgerEntryType.jsonName) &&
|
||||
jv[jss::result][jss::node][sfLedgerEntryType.jsonName] == expectedType,
|
||||
jv.isObject() && !payload.isMember(jss::error) && payload.isMember(jss::node) &&
|
||||
payload[jss::node].isMember(sfLedgerEntryType.jsonName) &&
|
||||
payload[jss::node][sfLedgerEntryType.jsonName] == expectedType,
|
||||
to_string(jv));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
jv.isObject() && jv.isMember(jss::result) &&
|
||||
jv[jss::result].isMember(jss::error) &&
|
||||
!jv[jss::result].isMember(jss::node) &&
|
||||
jv[jss::result][jss::error] == expectedError.value_or("entryNotFound"),
|
||||
jv.isObject() && payload.isMember(jss::error) && !payload.isMember(jss::node) &&
|
||||
payload[jss::error] == expectedError.value_or("entryNotFound"),
|
||||
to_string(jv));
|
||||
}
|
||||
};
|
||||
@@ -2734,7 +2810,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
params[jss::api_version] = 3;
|
||||
auto const jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
// Index is correct either way
|
||||
BEAST_EXPECT(jv[jss::result][jss::index].asString() == hexKey);
|
||||
BEAST_EXPECT(jtx::rpcPayload(jv)[jss::index].asString() == hexKey);
|
||||
checkResult(good, jv, expectedType);
|
||||
}
|
||||
|
||||
@@ -2981,12 +3057,93 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
env.close();
|
||||
|
||||
// Request a check.
|
||||
json::Value const jrr = env.rpc("ledger_entry", to_string(checkId.key))[jss::result];
|
||||
json::Value const jrr = jtx::rpcPayload(env.rpc("ledger_entry", to_string(checkId.key)));
|
||||
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Check);
|
||||
BEAST_EXPECT(jrr[jss::node][sfSendMax.jsonName] == "100000000");
|
||||
}
|
||||
|
||||
public:
|
||||
/**
|
||||
* The malformed arms of the object-shaped entry types that nothing else
|
||||
* feeds.
|
||||
*
|
||||
* Each arm names its own token, and the tokens and messages asserted here
|
||||
* are literals, so a helper handed the wrong code fails the case that
|
||||
* reaches it.
|
||||
*/
|
||||
void
|
||||
testMalformedTypedFields()
|
||||
{
|
||||
testcase("Malformed typed fields");
|
||||
using namespace test::jtx;
|
||||
Env env{*this};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
|
||||
// A 64-hex string parses as a UInt256, so the `loan` cases below can reach the field
|
||||
// after it.
|
||||
static constexpr char const* kHash =
|
||||
"1111111111111111111111111111111111111111111111111111111111111111";
|
||||
|
||||
{
|
||||
// `dir_root` present but not a hash. The arm above requires exactly one of `owner`
|
||||
// and `dir_root`, so this is reached only with `dir_root` named.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::directory] = json::ValueType::Object;
|
||||
jvParams[jss::directory][jss::dir_root] = "notahash";
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "malformedDirRoot", "Invalid field 'dir_root', not hash.");
|
||||
}
|
||||
{
|
||||
// `loan_broker` as an object naming no `owner`.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::loan_broker] = json::ValueType::Object;
|
||||
jvParams[jss::loan_broker][jss::seq] = 1;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "malformedRequest", "Missing field 'owner'.");
|
||||
}
|
||||
{
|
||||
// `loan_broker` naming an `owner` that parses, so the `seq` below it is reached.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::loan_broker] = json::ValueType::Object;
|
||||
jvParams[jss::loan_broker][jss::owner] = alice.human();
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "malformedRequest", "Missing field 'seq'.");
|
||||
}
|
||||
{
|
||||
// `loan` as an object naming no `loan_broker_id`.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::loan] = json::ValueType::Object;
|
||||
jvParams[jss::loan][jss::loan_seq] = 1;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "malformedRequest", "Missing field 'loan_broker_id'.");
|
||||
}
|
||||
{
|
||||
// `loan` naming a `loan_broker_id` that parses, so the `loan_seq` below it is reached.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::loan] = json::ValueType::Object;
|
||||
jvParams[jss::loan][jss::loan_broker_id] = kHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "malformedRequest", "Missing field 'loan_seq'.");
|
||||
}
|
||||
{
|
||||
// `mpt_issuance` that is not a Hash192. A hash of the wrong width reaches the same
|
||||
// arm as text does.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::mpt_issuance] = kHash;
|
||||
json::Value const jrr =
|
||||
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(
|
||||
jrr, "malformedMPTokenIssuance", "Invalid field 'mpt_issuance', not Hash192.");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
@@ -3021,6 +3178,7 @@ public:
|
||||
testFixed();
|
||||
testHashes();
|
||||
testCLI();
|
||||
testMalformedTypedFields();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3112,6 +3270,43 @@ class LedgerEntry_XChain_test : public beast::unit_test::Suite,
|
||||
|
||||
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
|
||||
}
|
||||
{
|
||||
// A `bridge_account` that is neither door. The case above names the issuing chain's
|
||||
// door, which passes this check and fails the lookup instead.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::bridge_account] = mcAlice.human();
|
||||
jvParams[jss::bridge] = jvb;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
mcEnv.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
|
||||
checkErrorValue(jrr, "malformedRequest", "");
|
||||
}
|
||||
{
|
||||
// A `LockingChainIssue` that `issueFromJson` refuses. It is read before the issuing
|
||||
// chain's issue, so a malformed pair reports this arm first.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::bridge_account] = mcDoor.human();
|
||||
jvParams[jss::bridge] = jvb;
|
||||
jvParams[jss::bridge][sfLockingChainIssue.jsonName] = "notanissue";
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
mcEnv.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
|
||||
checkErrorValue(jrr, "malformedIssue", "Invalid field 'LockingChainIssue', not Issue.");
|
||||
}
|
||||
{
|
||||
// The same for `IssuingChainIssue`, reached only once the locking chain issue reads.
|
||||
json::Value jvParams;
|
||||
jvParams[jss::bridge_account] = mcDoor.human();
|
||||
jvParams[jss::bridge] = jvb;
|
||||
jvParams[jss::bridge][sfIssuingChainIssue.jsonName] = "notanissue";
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
json::Value const jrr =
|
||||
mcEnv.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
|
||||
checkErrorValue(jrr, "malformedIssue", "Invalid field 'IssuingChainIssue', not Issue.");
|
||||
}
|
||||
{
|
||||
// create two claim ids and verify that the bridge counter was
|
||||
// incremented
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
|
||||
@@ -280,7 +281,7 @@ public:
|
||||
// the purpose in this test is to force the ledger expiration/out of
|
||||
// date check to trigger
|
||||
env.timeKeeper().adjustCloseTime(Weeks{3});
|
||||
auto const result = env.rpc(apiVersion, "ledger_request", "1")[jss::result];
|
||||
auto const result = test::jtx::rpcPayload(env.rpc(apiVersion, "ledger_request", "1"));
|
||||
BEAST_EXPECT(result[jss::status] == "error");
|
||||
if (apiVersion == 1)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -248,14 +248,15 @@ public:
|
||||
params[jss::role] = "gateway";
|
||||
params[jss::transactions] = "asdf";
|
||||
|
||||
auto lines = env.rpc("json", "noripple_check", to_string(params));
|
||||
auto const lines =
|
||||
test::jtx::rpcPayload(env.rpc("json", "noripple_check", to_string(params)));
|
||||
if (apiVersion < 2u)
|
||||
{
|
||||
BEAST_EXPECT(lines[jss::result][jss::status] == "success");
|
||||
BEAST_EXPECT(lines[jss::status] == "success");
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(lines[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(lines[jss::error] == "invalidParams");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/config/Constants.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/Seed.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
@@ -73,11 +75,95 @@ public:
|
||||
BEAST_EXPECT(warned && booted);
|
||||
}
|
||||
|
||||
/**
|
||||
* The load warning reaches the client on both transports at every
|
||||
* version, at the place that version puts it. From API version 3 that is
|
||||
* the top level of the specification envelope, which reserves no place
|
||||
* for a notice and so carries it beside its own members. Below version 3
|
||||
* it is beside `result` on the WebSocket transport and inside `result`
|
||||
* on the JSON-RPC one.
|
||||
*
|
||||
* @param useWS True to test the WebSocket transport, false for JSON-RPC.
|
||||
* @param apiVersion The API version to request the warning under.
|
||||
*/
|
||||
void
|
||||
testWarningLocation(bool useWS, unsigned apiVersion)
|
||||
{
|
||||
testcase << "Load warning on " << (useWS ? "WS" : "HTTP") << " at API version "
|
||||
<< apiVersion;
|
||||
using namespace jtx;
|
||||
Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
|
||||
cfg->loadFromString(std::string("[") + Sections::kSigningSupport + "]\ntrue");
|
||||
return noAdmin(std::move(cfg));
|
||||
})};
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
|
||||
// invokeRaw is needed on the WebSocket transport: invoke rewrites a version 3 reply into
|
||||
// the legacy envelope, which is the very difference under test.
|
||||
auto const ws = useWS ? makeWSClient(env.app().config(), true, 2, {}, apiVersion) : nullptr;
|
||||
auto const http = useWS ? nullptr : makeJSONRPCClient(env.app().config());
|
||||
|
||||
json::Value tx = json::ValueType::Object;
|
||||
tx[jss::tx_json] = pay(alice, bob, XRP(1));
|
||||
tx[jss::secret] = toBase58(generateSeed("alice"));
|
||||
// A WebSocket request carries api_version in the same flat object, which makeWSClient adds.
|
||||
if (!useWS)
|
||||
tx[jss::api_version] = apiVersion;
|
||||
|
||||
auto const send = [&] -> std::optional<json::Value> {
|
||||
if (useWS)
|
||||
return ws->invokeRaw("sign", tx);
|
||||
auto jv = http->invoke("sign", tx);
|
||||
if (jv.isNull())
|
||||
return std::nullopt;
|
||||
return jv;
|
||||
};
|
||||
|
||||
// Signing is charged heavily enough that a limited consumer is warned within a few calls.
|
||||
// Stop at the first reply that carries a warning anywhere: that reply is the one under
|
||||
// test, and continuing would eventually get the client booted instead.
|
||||
std::optional<json::Value> warned;
|
||||
for (int i = 0; i < 500 && !warned; ++i)
|
||||
{
|
||||
auto const reply = send();
|
||||
// A booted client gets nothing back, which means no reply ever carried the warning.
|
||||
if (!reply)
|
||||
break;
|
||||
if (reply->isMember(jss::warning) || (*reply)[jss::result].isMember(jss::warning))
|
||||
warned = reply;
|
||||
}
|
||||
|
||||
if (!BEAST_EXPECT(warned.has_value()))
|
||||
return;
|
||||
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access) checked above
|
||||
json::Value const& reply = *warned;
|
||||
if (apiVersion >= rpc::kApiMinimumSpecVersion || useWS)
|
||||
{
|
||||
BEAST_EXPECT(reply[jss::warning] == jss::load);
|
||||
BEAST_EXPECT(!reply[jss::result].isMember(jss::warning));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(reply[jss::result][jss::warning] == jss::load);
|
||||
BEAST_EXPECT(!reply.isMember(jss::warning));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testOverload(false /* http */);
|
||||
testOverload(true /* ws */);
|
||||
|
||||
for (auto const useWS : {false, true})
|
||||
{
|
||||
for (auto const apiVersion : {1u, 2u, 3u})
|
||||
testWarningLocation(useWS, apiVersion);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1070
-28
File diff suppressed because it is too large.
Load diff
@@ -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); });
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <test/jtx/multisign.h>
|
||||
@@ -785,7 +786,7 @@ class Transaction_test : public beast::unit_test::Suite
|
||||
params[jss::transaction] = to_string(txn->getTransactionID());
|
||||
params[jss::binary] = false;
|
||||
params[jss::api_version] = apiVersion;
|
||||
return env.client().invoke("tx", params);
|
||||
return test::jtx::rpcLegacyReply(env.client().invoke("tx", params));
|
||||
}(),
|
||||
};
|
||||
|
||||
@@ -853,7 +854,7 @@ class Transaction_test : public beast::unit_test::Suite
|
||||
params[jss::transaction] = to_string(txn->getTransactionID());
|
||||
params[jss::binary] = true;
|
||||
params[jss::api_version] = apiVersion;
|
||||
return env.client().invoke("tx", params);
|
||||
return test::jtx::rpcLegacyReply(env.client().invoke("tx", params));
|
||||
}();
|
||||
|
||||
if (BEAST_EXPECT(result[jss::status] == "success"))
|
||||
|
||||
@@ -128,21 +128,22 @@ class Version_test : public beast::unit_test::Suite
|
||||
using namespace test::jtx;
|
||||
Env env{*this};
|
||||
|
||||
auto const withoutApiVerion = std::string("{ ") +
|
||||
"\"jsonrpc\": \"2.0\", "
|
||||
"\"ripplerpc\": \"2.0\", "
|
||||
"\"id\": 5, "
|
||||
"\"method\": \"version\", "
|
||||
"\"params\": {}}";
|
||||
auto const withApiVerion = std::string("{ ") +
|
||||
"\"jsonrpc\": \"2.0\", "
|
||||
"\"ripplerpc\": \"2.0\", "
|
||||
"\"id\": 6, "
|
||||
"\"method\": \"version\", "
|
||||
"\"params\": { "
|
||||
"\"api_version\": " +
|
||||
std::to_string(rpc::kApiMaximumSupportedVersion) + "}}";
|
||||
auto re = env.rpc("json2", '[' + withoutApiVerion + ", " + withApiVerion + ']');
|
||||
// Both entries name the same version: the command-line client fills kApiCommandLineVersion
|
||||
// into an entry naming none, and one batch is served at one version, so an entry naming
|
||||
// none beside one naming another version would be a mixed body and be refused whole.
|
||||
auto const entry = [](int id) {
|
||||
return std::string("{ ") +
|
||||
"\"jsonrpc\": \"2.0\", "
|
||||
"\"ripplerpc\": \"2.0\", "
|
||||
"\"id\": " +
|
||||
std::to_string(id) +
|
||||
", "
|
||||
"\"method\": \"version\", "
|
||||
"\"params\": { "
|
||||
"\"api_version\": " +
|
||||
std::to_string(rpc::kApiMaximumSupportedVersion) + "}}";
|
||||
};
|
||||
auto re = env.rpc("json2", '[' + entry(5) + ", " + entry(6) + ']');
|
||||
|
||||
if (!BEAST_EXPECT(re.isArray()))
|
||||
return;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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());
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#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. The static
|
||||
// assert is what keeps the second one the last entry as codes are appended.
|
||||
EXPECT_EQ(rpc::codeForToken("badSyntax"), RpcBadSyntax);
|
||||
EXPECT_EQ(rpc::codeForToken("apiVersionConflict"), RpcApiVersionConflict);
|
||||
static_assert(RpcApiVersionConflict == RpcLast);
|
||||
}
|
||||
|
||||
TEST(ErrorCodes, code_for_token_rejects_unnamed_tokens)
|
||||
{
|
||||
EXPECT_EQ(rpc::codeForToken("notATokenAnyEntryNames"), RpcUnknown);
|
||||
EXPECT_EQ(rpc::codeForToken(""), RpcUnknown);
|
||||
|
||||
// Tokens are matched whole, not by prefix, and the comparison is case-sensitive.
|
||||
EXPECT_EQ(rpc::codeForToken("invalidParam"), RpcUnknown);
|
||||
EXPECT_EQ(rpc::codeForToken("invalidParamsX"), RpcUnknown);
|
||||
EXPECT_EQ(rpc::codeForToken("invalidparams"), RpcUnknown);
|
||||
|
||||
// The placeholder that gaps in the table report names no error, so it resolves to nothing.
|
||||
EXPECT_EQ(rpc::codeForToken("unknown"), RpcUnknown);
|
||||
}
|
||||
|
||||
TEST(ErrorCodes, every_token_round_trips_to_its_own_code)
|
||||
{
|
||||
// The views the lookup scans and the rows it answers from are two arrays built from one table.
|
||||
// Every row's token resolving to that row's code is what proves the two are index-aligned,
|
||||
// which no static assertion checks.
|
||||
for (int i = RpcBadSyntax; i <= RpcLast; ++i)
|
||||
{
|
||||
auto const code = static_cast<ErrorCodeI>(i);
|
||||
auto const& info = rpc::getErrorInfo(code);
|
||||
|
||||
// Gaps report the placeholder, which is covered above.
|
||||
if (info.code == RpcUnknown)
|
||||
continue;
|
||||
|
||||
EXPECT_EQ(rpc::codeForToken(info.token.cStr()), code) << "token: " << info.token.cStr();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
#include <xrpl/server/InfoSub.h>
|
||||
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/RPCErr.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstddef>
|
||||
@@ -58,3 +64,126 @@ TEST(InfoSubSubscriptionCap, explicit_cap)
|
||||
// The overflow guard still holds with a small explicit cap.
|
||||
EXPECT_TRUE(exceedsSubscriptionCap(0, std::numeric_limits<std::size_t>::max(), cap));
|
||||
}
|
||||
|
||||
// A message sent to an api-3 subscriber becomes a notification: `type` names the method and the
|
||||
// rest of the message becomes `params`.
|
||||
TEST(InfoSubNotification, shapes_message_from_spec_version)
|
||||
{
|
||||
json::Value event(json::ValueType::Object);
|
||||
event[jss::type] = "ledgerClosed";
|
||||
event[jss::ledger_index] = 3;
|
||||
|
||||
auto const shaped = shapeAsNotification(event, rpc::kApiMinimumSpecVersion);
|
||||
ASSERT_TRUE(shaped.has_value());
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access) asserted above
|
||||
json::Value const& notification = *shaped;
|
||||
// The literal the document shows, not the constant production writes.
|
||||
EXPECT_EQ(notification[jss::jsonrpc], "2.0");
|
||||
EXPECT_EQ(notification[jss::method], "ledgerClosed");
|
||||
EXPECT_EQ(notification[jss::params][jss::ledger_index], 3);
|
||||
// Exactly the three members: no id, no `type`, and nothing of a reply's envelope.
|
||||
EXPECT_EQ(notification.size(), 3u);
|
||||
EXPECT_FALSE(notification.isMember(jss::id));
|
||||
EXPECT_FALSE(notification[jss::params].isMember(jss::type));
|
||||
}
|
||||
|
||||
// A path finding update is a notification too, for the reason shapeAsNotification's docstring
|
||||
// gives, so the `id` the client correlates by has to survive inside `params`.
|
||||
TEST(InfoSubNotification, shapes_a_directed_update)
|
||||
{
|
||||
json::Value update(json::ValueType::Object);
|
||||
update[jss::type] = "path_find";
|
||||
update[jss::id] = 7;
|
||||
update[jss::full_reply] = true;
|
||||
|
||||
auto const shaped = shapeAsNotification(update, rpc::kApiMinimumSpecVersion);
|
||||
ASSERT_TRUE(shaped.has_value());
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access) asserted above
|
||||
json::Value const& notification = *shaped;
|
||||
EXPECT_EQ(notification[jss::method], "path_find");
|
||||
EXPECT_FALSE(notification.isMember(jss::id));
|
||||
EXPECT_EQ(notification[jss::params][jss::id], 7);
|
||||
EXPECT_EQ(notification[jss::params][jss::full_reply], true);
|
||||
}
|
||||
|
||||
TEST(InfoSubNotification, leaves_everything_else_alone)
|
||||
{
|
||||
json::Value event(json::ValueType::Object);
|
||||
event[jss::type] = "ledgerClosed";
|
||||
|
||||
// Below the specification version the shape is the legacy one.
|
||||
EXPECT_FALSE(shapeAsNotification(event, 1).has_value());
|
||||
EXPECT_FALSE(shapeAsNotification(event, 2).has_value());
|
||||
// A version is recorded at subscribe time; 0 means it was never recorded.
|
||||
EXPECT_FALSE(shapeAsNotification(event, 0).has_value());
|
||||
// Every version from 3 up shapes, not version 3 alone.
|
||||
EXPECT_TRUE(shapeAsNotification(event, 4).has_value());
|
||||
|
||||
// A message naming no `type` names no method to dispatch on. rpcError builds one: the caller
|
||||
// that reports a failure this way names the stream it concerns before sending it.
|
||||
EXPECT_FALSE(
|
||||
shapeAsNotification(rpcError(RpcInternal), rpc::kApiMinimumSpecVersion).has_value());
|
||||
|
||||
// A `type` that is not a string cannot be a `method`, so it names no method either.
|
||||
json::Value numbered(json::ValueType::Object);
|
||||
numbered[jss::type] = 1;
|
||||
EXPECT_FALSE(shapeAsNotification(numbered, rpc::kApiMinimumSpecVersion).has_value());
|
||||
|
||||
// A non-object has no members to move under `params`.
|
||||
EXPECT_FALSE(shapeAsNotification(json::Value{"text"}, rpc::kApiMinimumSpecVersion).has_value());
|
||||
}
|
||||
|
||||
// A shaped message names no `type`, so shaping it again returns nullopt. Pinned so a message can
|
||||
// never be wrapped twice.
|
||||
TEST(InfoSubNotification, shapes_a_message_only_once)
|
||||
{
|
||||
json::Value event(json::ValueType::Object);
|
||||
event[jss::type] = "ledgerClosed";
|
||||
event[jss::ledger_index] = 3;
|
||||
|
||||
auto const shaped = shapeAsNotification(event, rpc::kApiMinimumSpecVersion);
|
||||
ASSERT_TRUE(shaped.has_value());
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access) asserted above
|
||||
EXPECT_FALSE(shapeAsNotification(*shaped, rpc::kApiMinimumSpecVersion).has_value());
|
||||
}
|
||||
|
||||
// An account_history subscription reports a stream failure as an error object, which
|
||||
// stampStreamError names after the stream for a subscriber a notification is built for, so the
|
||||
// failure is a notification like any other event on that stream. The three sites that send one
|
||||
// need a failing relational database to reach, so the stamping and the shaping are asserted here.
|
||||
TEST(InfoSubNotification, stamps_a_stream_failure_for_a_notification_subscriber)
|
||||
{
|
||||
json::Value const failure = rpcError(RpcInternal);
|
||||
|
||||
auto const stamped = stampStreamError(
|
||||
failure, rpc::kApiMinimumSpecVersion, true, jss::account_history_tx_stream);
|
||||
ASSERT_TRUE(stamped.has_value());
|
||||
// NOLINTBEGIN(bugprone-unchecked-optional-access) asserted above
|
||||
EXPECT_EQ((*stamped)[jss::type], jss::account_history_tx_stream);
|
||||
EXPECT_EQ((*stamped)[jss::error], "internal");
|
||||
// The input is left as it was: the copy is the stamped one.
|
||||
EXPECT_FALSE(failure.isMember(jss::type));
|
||||
|
||||
auto const shaped = shapeAsNotification(*stamped, rpc::kApiMinimumSpecVersion);
|
||||
// NOLINTEND(bugprone-unchecked-optional-access)
|
||||
ASSERT_TRUE(shaped.has_value());
|
||||
// NOLINTBEGIN(bugprone-unchecked-optional-access) asserted above
|
||||
EXPECT_EQ((*shaped)[jss::method], jss::account_history_tx_stream);
|
||||
EXPECT_EQ((*shaped)[jss::params][jss::error], "internal");
|
||||
EXPECT_EQ((*shaped)[jss::params][jss::error_code], RpcInternal);
|
||||
EXPECT_FALSE(shaped->isMember(jss::id));
|
||||
// NOLINTEND(bugprone-unchecked-optional-access)
|
||||
}
|
||||
|
||||
// Below version 3, and for a subscriber that cannot receive a notification, the error goes out as
|
||||
// it stands: no copy and no `type`, which an unshaped object would carry onto the wire.
|
||||
TEST(InfoSubNotification, leaves_a_stream_failure_alone_otherwise)
|
||||
{
|
||||
json::Value const failure = rpcError(RpcInternal);
|
||||
|
||||
EXPECT_FALSE(stampStreamError(failure, 1, true, jss::account_history_tx_stream).has_value());
|
||||
EXPECT_FALSE(stampStreamError(failure, 2, true, jss::account_history_tx_stream).has_value());
|
||||
EXPECT_FALSE(stampStreamError(
|
||||
failure, rpc::kApiMinimumSpecVersion, false, jss::account_history_tx_stream)
|
||||
.has_value());
|
||||
}
|
||||
@@ -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>");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+352
-147
@@ -123,6 +123,7 @@
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/host_name.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/container/small_vector.hpp>
|
||||
#include <boost/system/detail/errc.hpp>
|
||||
#include <boost/system/detail/error_code.hpp>
|
||||
#include <boost/system/system_error.hpp>
|
||||
@@ -562,7 +563,11 @@ public:
|
||||
// InfoSub::Source.
|
||||
//
|
||||
void
|
||||
subAccount(InfoSub::Ref ispListener, HashSet<AccountID> const& vnaAccountIDs, bool rt) override;
|
||||
subAccount(
|
||||
InfoSub::Ref ispListener,
|
||||
HashSet<AccountID> const& vnaAccountIDs,
|
||||
bool rt,
|
||||
unsigned int apiVersion) override;
|
||||
void
|
||||
unsubAccount(InfoSub::Ref ispListener, HashSet<AccountID> const& vnaAccountIDs, bool rt)
|
||||
override;
|
||||
@@ -573,15 +578,17 @@ public:
|
||||
unsubAccountInternal(std::uint64_t seq, HashSet<AccountID> const& vnaAccountIDs, bool rt)
|
||||
override;
|
||||
|
||||
ErrorCodeI
|
||||
subAccountHistory(InfoSub::Ref ispListener, AccountID const& account, unsigned int apiVersion)
|
||||
override;
|
||||
void
|
||||
subMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) override;
|
||||
subMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs, unsigned int apiVersion)
|
||||
override;
|
||||
void
|
||||
unsubMPT(InfoSub::Ref ispListener, HashSet<MPTID> const& mptIDs) override;
|
||||
void
|
||||
unsubMPTInternal(std::uint64_t seq, MPTID const& mptID) override;
|
||||
|
||||
ErrorCodeI
|
||||
subAccountHistory(InfoSub::Ref ispListener, AccountID const& account) override;
|
||||
void
|
||||
unsubAccountHistory(InfoSub::Ref ispListener, AccountID const& account, bool historyOnly)
|
||||
override;
|
||||
@@ -598,58 +605,59 @@ public:
|
||||
HashSet<AccountID> historyAccounts) override;
|
||||
|
||||
bool
|
||||
subLedger(InfoSub::Ref ispListener, json::Value& jvResult) override;
|
||||
subLedger(InfoSub::Ref ispListener, json::Value& jvResult, unsigned int apiVersion) override;
|
||||
bool
|
||||
unsubLedger(std::uint64_t uListener) override;
|
||||
|
||||
bool
|
||||
subBookChanges(InfoSub::Ref ispListener) override;
|
||||
subBookChanges(InfoSub::Ref ispListener, unsigned int apiVersion) override;
|
||||
bool
|
||||
unsubBookChanges(std::uint64_t uListener) override;
|
||||
|
||||
bool
|
||||
subServer(InfoSub::Ref ispListener, json::Value& jvResult, bool admin) override;
|
||||
subServer(InfoSub::Ref ispListener, json::Value& jvResult, bool admin, unsigned int apiVersion)
|
||||
override;
|
||||
bool
|
||||
unsubServer(std::uint64_t uListener) override;
|
||||
|
||||
bool
|
||||
subBook(InfoSub::Ref ispListener, Book const&) override;
|
||||
subBook(InfoSub::Ref ispListener, Book const&, unsigned int apiVersion) override;
|
||||
bool
|
||||
unsubBook(InfoSub::Ref ispListener, Book const&) override;
|
||||
bool
|
||||
unsubBookInternal(std::uint64_t uListener, Book const&) override;
|
||||
|
||||
bool
|
||||
subManifests(InfoSub::Ref ispListener) override;
|
||||
subManifests(InfoSub::Ref ispListener, unsigned int apiVersion) override;
|
||||
bool
|
||||
unsubManifests(std::uint64_t uListener) override;
|
||||
void
|
||||
pubManifest(Manifest const&) override;
|
||||
|
||||
bool
|
||||
subTransactions(InfoSub::Ref ispListener) override;
|
||||
subTransactions(InfoSub::Ref ispListener, unsigned int apiVersion) override;
|
||||
bool
|
||||
unsubTransactions(std::uint64_t uListener) override;
|
||||
|
||||
bool
|
||||
subRTTransactions(InfoSub::Ref ispListener) override;
|
||||
subRTTransactions(InfoSub::Ref ispListener, unsigned int apiVersion) override;
|
||||
bool
|
||||
unsubRTTransactions(std::uint64_t uListener) override;
|
||||
|
||||
bool
|
||||
subValidations(InfoSub::Ref ispListener) override;
|
||||
subValidations(InfoSub::Ref ispListener, unsigned int apiVersion) override;
|
||||
bool
|
||||
unsubValidations(std::uint64_t uListener) override;
|
||||
|
||||
bool
|
||||
subPeerStatus(InfoSub::Ref ispListener) override;
|
||||
subPeerStatus(InfoSub::Ref ispListener, unsigned int apiVersion) override;
|
||||
bool
|
||||
unsubPeerStatus(std::uint64_t uListener) override;
|
||||
void
|
||||
pubPeerStatus(std::function<json::Value()> const&) override;
|
||||
|
||||
bool
|
||||
subConsensus(InfoSub::Ref ispListener) override;
|
||||
subConsensus(InfoSub::Ref ispListener, unsigned int apiVersion) override;
|
||||
bool
|
||||
unsubConsensus(std::uint64_t uListener) override;
|
||||
|
||||
@@ -816,7 +824,30 @@ private:
|
||||
getHostId(bool forAdmin);
|
||||
|
||||
private:
|
||||
using SubMapType = HashMap<std::uint64_t, InfoSub::Wptr>;
|
||||
/**
|
||||
* One subscription in a seq-keyed subscription map.
|
||||
*
|
||||
* Carries the API version the subscription was registered at, so a
|
||||
* publisher reads the version from the entry it took the sink from rather
|
||||
* than off the sink itself. The sink is shared: one connection holds every
|
||||
* subscription made on it, and one webhook (keyed on its url alone) is
|
||||
* shared by every admin that named it. Every entry a sink appears in names
|
||||
* the same version, `doSubscribe` refusing a `subscribe` that would make it
|
||||
* otherwise.
|
||||
*
|
||||
* Registering the same subscription again replaces the entry, so the
|
||||
* newest registration decides that subscription's version. An unrelated
|
||||
* subscription on the same connection is a separate entry and keeps the
|
||||
* version it named.
|
||||
*/
|
||||
struct Subscriber
|
||||
{
|
||||
InfoSub::Wptr sink;
|
||||
// Every registration site names it; the zero satisfies pro-type-member-init.
|
||||
unsigned int apiVersion{};
|
||||
};
|
||||
|
||||
using SubMapType = HashMap<std::uint64_t, Subscriber>;
|
||||
using SubInfoMapType = HashMap<AccountID, SubMapType>;
|
||||
using SubRpcMapType = HashMap<std::string, InfoSub::pointer>;
|
||||
using SubMPTInfoMapType = HashMap<MPTID, SubMapType>;
|
||||
@@ -850,11 +881,13 @@ private:
|
||||
{
|
||||
InfoSub::pointer sink;
|
||||
std::shared_ptr<SubAccountHistoryIndex> index;
|
||||
unsigned int apiVersion{};
|
||||
};
|
||||
struct SubAccountHistoryInfoWeak
|
||||
{
|
||||
InfoSub::Wptr sinkWptr;
|
||||
std::shared_ptr<SubAccountHistoryIndex> index;
|
||||
unsigned int apiVersion{};
|
||||
};
|
||||
using SubAccountHistoryMapType =
|
||||
HashMap<AccountID, HashMap<std::uint64_t, SubAccountHistoryInfoWeak>>;
|
||||
@@ -1103,6 +1136,127 @@ std::array<json::StaticString const, 5> const NetworkOPsImp::StateAccounting::kS
|
||||
static auto const kGenesisAccountId =
|
||||
calcAccountID(generateKeyPair(KeyType::Secp256k1, generateSeed("masterpassphrase")).first);
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* One connection that is to receive one message, with the API version that
|
||||
* message is selected for.
|
||||
*
|
||||
* Keyed on the connection's `getSeq()`, so a connection matched by several
|
||||
* subscriptions at once is named once. One transaction can match the same
|
||||
* connection through both the accepted and the proposed account map, or
|
||||
* through two of the accounts it affects, and one message goes out, as every
|
||||
* version has sent.
|
||||
*/
|
||||
struct Notified
|
||||
{
|
||||
InfoSub::pointer sink;
|
||||
unsigned int apiVersion{};
|
||||
};
|
||||
|
||||
using NotifyMap = HashMap<std::uint64_t, Notified>;
|
||||
|
||||
/**
|
||||
* Notes @p subscriber as needing one message, selected for @p apiVersion.
|
||||
*
|
||||
* The first entry a walk reaches decides, and every later one for the same
|
||||
* connection is dropped. That is unambiguous because a connection holds
|
||||
* subscriptions at one API version only, which `doSubscribe` enforces, so every
|
||||
* entry this could reach for one connection names the same version.
|
||||
*
|
||||
* @param notify Where the connection is noted.
|
||||
* @param subscriber The connection.
|
||||
* @param apiVersion The version the matched subscription was registered at.
|
||||
*/
|
||||
void
|
||||
noteSubscriber(NotifyMap& notify, InfoSub::pointer const& subscriber, unsigned int apiVersion)
|
||||
{
|
||||
notify.try_emplace(subscriber->getSeq(), subscriber, apiVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the shape a stream event takes for each API version, across one publish
|
||||
* loop, so one copy per version serves every subscriber on that version rather
|
||||
* than the one copy per subscriber sendShaped would make.
|
||||
*
|
||||
* Reuse is safe because nothing on the way out alters the copy: sendTo writes
|
||||
* it as it stands. A message tailored to one subscriber must not travel this
|
||||
* way: an account history message carries that subscriber's own transaction
|
||||
* index, so it goes through sendShaped.
|
||||
*/
|
||||
class StreamBroadcast
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Sends the content @p message holds for @p apiVersion, in the shape that
|
||||
* version expects.
|
||||
*
|
||||
* @param subscriber Where the message is going.
|
||||
* @param apiVersion The version the subscription was registered at, read
|
||||
* by the caller from the entry it took the subscriber from.
|
||||
* @param message The event, one content per version.
|
||||
*/
|
||||
void
|
||||
sendTo(InfoSub& subscriber, unsigned int apiVersion, MultiApiJson const& message)
|
||||
{
|
||||
message.visit(
|
||||
apiVersion, [&](json::Value const& jv) { sendTo(subscriber, apiVersion, jv); });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends @p message to @p subscriber in the shape its API version expects.
|
||||
*
|
||||
* @param subscriber Where the message is going. One that cannot receive a
|
||||
* notification is sent the message as the publisher built it,
|
||||
* whatever its version, and its shape is not held.
|
||||
* @param apiVersion The API version that subscriber is on, read once by the
|
||||
* caller, which reads it already to choose the content.
|
||||
* @param message The event as the publisher built it for that version.
|
||||
*/
|
||||
void
|
||||
sendTo(InfoSub& subscriber, unsigned int apiVersion, json::Value const& message)
|
||||
{
|
||||
if (subscriber.wantsNotifications())
|
||||
{
|
||||
auto held = std::ranges::find(shaped_, apiVersion, &Shape::version);
|
||||
if (held == shaped_.end())
|
||||
{
|
||||
if (auto notification = shapeAsNotification(message, apiVersion))
|
||||
{
|
||||
shaped_.emplace_back(apiVersion, std::move(*notification));
|
||||
held = std::prev(shaped_.end());
|
||||
}
|
||||
}
|
||||
|
||||
if (held != shaped_.end())
|
||||
{
|
||||
subscriber.send(held->message, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Neither case copies: what the publisher built is sent as it stands.
|
||||
subscriber.send(message, true);
|
||||
}
|
||||
|
||||
private:
|
||||
struct Shape
|
||||
{
|
||||
unsigned int version{};
|
||||
json::Value message;
|
||||
};
|
||||
|
||||
// Derived so that a new API version cannot make a publish allocate inside streamLock_.
|
||||
static constexpr std::size_t kMaxShapes =
|
||||
rpc::kApiMaximumValidVersion - rpc::kApiMinimumSpecVersion + 1;
|
||||
|
||||
// One entry per version that shapes, found by a linear scan, where a map would allocate a
|
||||
// bucket array and a node to hold one of them.
|
||||
boost::container::small_vector<Shape, kMaxShapes> shaped_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
inline OperatingMode
|
||||
NetworkOPsImp::getOperatingMode() const
|
||||
@@ -2437,11 +2591,12 @@ NetworkOPsImp::pubManifest(Manifest const& mo)
|
||||
jvObj[jss::domain] = mo.domain;
|
||||
jvObj[jss::manifest] = strHex(mo.serialized);
|
||||
|
||||
StreamBroadcast broadcast;
|
||||
for (auto i = streamMaps_[SManifests].begin(); i != streamMaps_[SManifests].end();)
|
||||
{
|
||||
if (auto p = i->second.lock())
|
||||
if (auto p = i->second.sink.lock())
|
||||
{
|
||||
p->send(jvObj, true);
|
||||
broadcast.sendTo(*p, i->second.apiVersion, jvObj);
|
||||
toRelease.push_back(std::move(p));
|
||||
++i;
|
||||
}
|
||||
@@ -2539,16 +2694,17 @@ NetworkOPsImp::pubServer()
|
||||
|
||||
lastFeeSummary_ = f;
|
||||
|
||||
StreamBroadcast broadcast;
|
||||
for (auto i = streamMaps_[SServer].begin(); i != streamMaps_[SServer].end();)
|
||||
{
|
||||
InfoSub::pointer p = i->second.lock();
|
||||
InfoSub::pointer p = i->second.sink.lock();
|
||||
|
||||
// VFALCO TODO research the possibility of using thread queues and
|
||||
// linearizing the deletion of subscribers with the
|
||||
// sending of JSON data.
|
||||
if (p)
|
||||
{
|
||||
p->send(jvObj, true);
|
||||
broadcast.sendTo(*p, i->second.apiVersion, jvObj);
|
||||
toRelease.push_back(std::move(p));
|
||||
++i;
|
||||
}
|
||||
@@ -2577,11 +2733,12 @@ NetworkOPsImp::pubConsensus(ConsensusPhase phase)
|
||||
jvObj[jss::type] = "consensusPhase";
|
||||
jvObj[jss::consensus] = to_string(phase);
|
||||
|
||||
StreamBroadcast broadcast;
|
||||
for (auto i = streamMap.begin(); i != streamMap.end();)
|
||||
{
|
||||
if (auto p = i->second.lock())
|
||||
if (auto p = i->second.sink.lock())
|
||||
{
|
||||
p->send(jvObj, true);
|
||||
broadcast.sendTo(*p, i->second.apiVersion, jvObj);
|
||||
toRelease.push_back(std::move(p));
|
||||
++i;
|
||||
}
|
||||
@@ -2687,13 +2844,12 @@ NetworkOPsImp::pubValidation(std::shared_ptr<STValidation> const& val)
|
||||
}
|
||||
});
|
||||
|
||||
StreamBroadcast broadcast;
|
||||
for (auto i = streamMaps_[SValidations].begin(); i != streamMaps_[SValidations].end();)
|
||||
{
|
||||
if (auto p = i->second.lock())
|
||||
if (auto p = i->second.sink.lock())
|
||||
{
|
||||
multiObj.visit(
|
||||
p->getApiVersion(), //
|
||||
[&](json::Value const& jv) { p->send(jv, true); });
|
||||
broadcast.sendTo(*p, i->second.apiVersion, multiObj);
|
||||
toRelease.push_back(std::move(p));
|
||||
++i;
|
||||
}
|
||||
@@ -2721,13 +2877,14 @@ NetworkOPsImp::pubPeerStatus(std::function<json::Value()> const& func)
|
||||
|
||||
jvObj[jss::type] = "peerStatusChange";
|
||||
|
||||
StreamBroadcast broadcast;
|
||||
for (auto i = streamMaps_[SPeerStatus].begin(); i != streamMaps_[SPeerStatus].end();)
|
||||
{
|
||||
InfoSub::pointer p = i->second.lock();
|
||||
InfoSub::pointer p = i->second.sink.lock();
|
||||
|
||||
if (p)
|
||||
{
|
||||
p->send(jvObj, true);
|
||||
broadcast.sendTo(*p, i->second.apiVersion, jvObj);
|
||||
toRelease.push_back(std::move(p));
|
||||
++i;
|
||||
}
|
||||
@@ -3287,16 +3444,15 @@ NetworkOPsImp::pubProposedTransaction(
|
||||
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
|
||||
StreamBroadcast broadcast;
|
||||
auto it = streamMaps_[SRtTransactions].begin();
|
||||
while (it != streamMaps_[SRtTransactions].end())
|
||||
{
|
||||
InfoSub::pointer p = it->second.lock();
|
||||
InfoSub::pointer p = it->second.sink.lock();
|
||||
|
||||
if (p)
|
||||
{
|
||||
jvObj.visit(
|
||||
p->getApiVersion(), //
|
||||
[&](json::Value const& jv) { p->send(jv, true); });
|
||||
broadcast.sendTo(*p, it->second.apiVersion, jvObj);
|
||||
toRelease.push_back(std::move(p));
|
||||
++it;
|
||||
}
|
||||
@@ -3384,13 +3540,17 @@ NetworkOPsImp::publishLedgerStreams(
|
||||
{
|
||||
jvObj[jss::validated_ledgers] = registry_.get().getLedgerMaster().getCompleteLedgers();
|
||||
}
|
||||
// One instance serves one event. The ledger event and the book-changes event below are two,
|
||||
// so each loop declares its own: hoisting one above both would hand the second event's
|
||||
// subscribers the shape held for the first.
|
||||
StreamBroadcast broadcast;
|
||||
auto it = streamMaps_[SLedger].begin();
|
||||
while (it != streamMaps_[SLedger].end())
|
||||
{
|
||||
InfoSub::pointer p = it->second.lock();
|
||||
InfoSub::pointer p = it->second.sink.lock();
|
||||
if (p)
|
||||
{
|
||||
p->send(jvObj, true);
|
||||
broadcast.sendTo(*p, it->second.apiVersion, jvObj);
|
||||
toRelease.push_back(std::move(p));
|
||||
++it;
|
||||
}
|
||||
@@ -3405,13 +3565,14 @@ NetworkOPsImp::publishLedgerStreams(
|
||||
{
|
||||
json::Value const jvObj = xrpl::rpc::computeBookChanges(lpAccepted);
|
||||
|
||||
StreamBroadcast broadcast;
|
||||
auto it = streamMaps_[SBookChanges].begin();
|
||||
while (it != streamMaps_[SBookChanges].end())
|
||||
{
|
||||
InfoSub::pointer p = it->second.lock();
|
||||
InfoSub::pointer p = it->second.sink.lock();
|
||||
if (p)
|
||||
{
|
||||
p->send(jvObj, true);
|
||||
broadcast.sendTo(*p, it->second.apiVersion, jvObj);
|
||||
toRelease.push_back(std::move(p));
|
||||
++it;
|
||||
}
|
||||
@@ -3615,16 +3776,18 @@ NetworkOPsImp::pubValidatedTransaction(
|
||||
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
|
||||
// One event, so both loops send through the same shapes: a subscriber of both streams is
|
||||
// sent the same message twice.
|
||||
StreamBroadcast broadcast;
|
||||
|
||||
auto it = streamMaps_[STransactions].begin();
|
||||
while (it != streamMaps_[STransactions].end())
|
||||
{
|
||||
InfoSub::pointer p = it->second.lock();
|
||||
InfoSub::pointer p = it->second.sink.lock();
|
||||
|
||||
if (p)
|
||||
{
|
||||
jvObj.visit(
|
||||
p->getApiVersion(), //
|
||||
[&](json::Value const& jv) { p->send(jv, true); });
|
||||
broadcast.sendTo(*p, it->second.apiVersion, jvObj);
|
||||
toRelease.push_back(std::move(p));
|
||||
++it;
|
||||
}
|
||||
@@ -3638,13 +3801,11 @@ NetworkOPsImp::pubValidatedTransaction(
|
||||
|
||||
while (it != streamMaps_[SRtTransactions].end())
|
||||
{
|
||||
InfoSub::pointer p = it->second.lock();
|
||||
InfoSub::pointer p = it->second.sink.lock();
|
||||
|
||||
if (p)
|
||||
{
|
||||
jvObj.visit(
|
||||
p->getApiVersion(), //
|
||||
[&](json::Value const& jv) { p->send(jv, true); });
|
||||
broadcast.sendTo(*p, it->second.apiVersion, jvObj);
|
||||
toRelease.push_back(std::move(p));
|
||||
++it;
|
||||
}
|
||||
@@ -3687,7 +3848,9 @@ NetworkOPsImp::pubBookTransaction(AcceptedLedgerTx const& alTx, MultiApiJson con
|
||||
// ~InfoSub() reacquires bookLock_ via unsubBook() on its own and serializes
|
||||
// safely with concurrent traffic.
|
||||
|
||||
std::vector<InfoSub::pointer> listeners;
|
||||
// `seen` names the connections already noted, so a connection subscribed to two of the
|
||||
// affected books receives one message, for the reason noteSubscriber gives.
|
||||
std::vector<Notified> listeners;
|
||||
HashSet<std::uint64_t> seen;
|
||||
|
||||
// Sized for the common case where every affected book has at most
|
||||
@@ -3708,7 +3871,7 @@ NetworkOPsImp::pubBookTransaction(AcceptedLedgerTx const& alTx, MultiApiJson con
|
||||
|
||||
for (auto sit = it->second.begin(); sit != it->second.end();)
|
||||
{
|
||||
if (auto p = sit->second.lock())
|
||||
if (auto p = sit->second.sink.lock())
|
||||
{
|
||||
// Defensive: subBook_ entries are normally cleared by
|
||||
// ~InfoSub() -> unsubBook(), so we rarely see expired
|
||||
@@ -3716,7 +3879,9 @@ NetworkOPsImp::pubBookTransaction(AcceptedLedgerTx const& alTx, MultiApiJson con
|
||||
// where the last strong ref is dropped between insertion
|
||||
// and our lock() call.
|
||||
if (seen.emplace(p->getSeq()).second)
|
||||
listeners.emplace_back(std::move(p));
|
||||
{
|
||||
listeners.emplace_back(std::move(p), sit->second.apiVersion);
|
||||
}
|
||||
++sit;
|
||||
}
|
||||
else
|
||||
@@ -3732,10 +3897,9 @@ NetworkOPsImp::pubBookTransaction(AcceptedLedgerTx const& alTx, MultiApiJson con
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const& p : listeners)
|
||||
{
|
||||
jvObj.visit(p->getApiVersion(), [&](json::Value const& jv) { p->send(jv, true); });
|
||||
}
|
||||
StreamBroadcast broadcast;
|
||||
for (auto const& entry : listeners)
|
||||
broadcast.sendTo(*entry.sink, entry.apiVersion, jvObj);
|
||||
// listeners destructs here, outside bookLock_; ~InfoSub (if any fires)
|
||||
// will reacquire bookLock_ via unsubBook with no iterator hazard.
|
||||
}
|
||||
@@ -3746,7 +3910,8 @@ NetworkOPsImp::pubAccountTransaction(
|
||||
AcceptedLedgerTx const& transaction,
|
||||
bool last)
|
||||
{
|
||||
HashSet<InfoSub::pointer> notify;
|
||||
// See noteSubscriber.
|
||||
NotifyMap notify;
|
||||
int iProposed = 0;
|
||||
int iAccepted = 0;
|
||||
|
||||
@@ -3766,11 +3931,11 @@ NetworkOPsImp::pubAccountTransaction(
|
||||
|
||||
while (it != simiIt->second.end())
|
||||
{
|
||||
InfoSub::pointer const p = it->second.lock();
|
||||
InfoSub::pointer const p = it->second.sink.lock();
|
||||
|
||||
if (p)
|
||||
{
|
||||
notify.insert(p);
|
||||
noteSubscriber(notify, p, it->second.apiVersion);
|
||||
++it;
|
||||
++iProposed;
|
||||
}
|
||||
@@ -3787,11 +3952,11 @@ NetworkOPsImp::pubAccountTransaction(
|
||||
auto it = simiIt->second.begin();
|
||||
while (it != simiIt->second.end())
|
||||
{
|
||||
InfoSub::pointer const p = it->second.lock();
|
||||
InfoSub::pointer const p = it->second.sink.lock();
|
||||
|
||||
if (p)
|
||||
{
|
||||
notify.insert(p);
|
||||
noteSubscriber(notify, p, it->second.apiVersion);
|
||||
++it;
|
||||
++iAccepted;
|
||||
}
|
||||
@@ -3819,7 +3984,11 @@ NetworkOPsImp::pubAccountTransaction(
|
||||
if (auto const isSptr = info.sinkWptr.lock(); isSptr)
|
||||
{
|
||||
accountHistoryNotify.emplace_back(
|
||||
SubAccountHistoryInfo{.sink = isSptr, .index = info.index});
|
||||
SubAccountHistoryInfo{
|
||||
.sink = isSptr,
|
||||
.index = info.index,
|
||||
.apiVersion = info.apiVersion,
|
||||
});
|
||||
++it;
|
||||
}
|
||||
else
|
||||
@@ -3846,11 +4015,13 @@ NetworkOPsImp::pubAccountTransaction(
|
||||
auto const trResult = transaction.getResult();
|
||||
MultiApiJson jvObj = transJson(stTxn, trResult, true, ledger, metaRef);
|
||||
|
||||
for (InfoSub::Ref isrListener : notify)
|
||||
{
|
||||
jvObj.visit(
|
||||
isrListener->getApiVersion(), //
|
||||
[&](json::Value const& jv) { isrListener->send(jv, true); });
|
||||
// Scoped to this loop: the history loop below shares no shape, see StreamBroadcast.
|
||||
StreamBroadcast broadcast;
|
||||
for (auto const& [seq, entry] : notify)
|
||||
{
|
||||
broadcast.sendTo(*entry.sink, entry.apiVersion, jvObj);
|
||||
}
|
||||
}
|
||||
|
||||
if (last)
|
||||
@@ -3868,9 +4039,9 @@ NetworkOPsImp::pubAccountTransaction(
|
||||
|
||||
jvObj.set(jss::account_history_tx_index, index->forwardTxIndex++);
|
||||
|
||||
jvObj.visit(
|
||||
info.sink->getApiVersion(), //
|
||||
[&](json::Value const& jv) { info.sink->send(jv, true); });
|
||||
jvObj.visit(info.apiVersion, [&](json::Value const& jv) {
|
||||
sendShaped(*info.sink, info.apiVersion, jv);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3881,40 +4052,36 @@ NetworkOPsImp::pubProposedAccountTransaction(
|
||||
std::shared_ptr<STTx const> const& tx,
|
||||
TER result)
|
||||
{
|
||||
HashSet<InfoSub::pointer> notify;
|
||||
// See noteSubscriber.
|
||||
NotifyMap 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.sink.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);
|
||||
}
|
||||
noteSubscriber(notify, p, it->second.apiVersion);
|
||||
++it;
|
||||
++iProposed;
|
||||
}
|
||||
else
|
||||
{
|
||||
it = simiIt->second.erase(it);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3923,31 +4090,18 @@ 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)
|
||||
{
|
||||
jvObj.visit(
|
||||
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); });
|
||||
// Scoped to this loop, see StreamBroadcast.
|
||||
StreamBroadcast broadcast;
|
||||
for (auto const& [seq, entry] : notify)
|
||||
{
|
||||
broadcast.sendTo(*entry.sink, entry.apiVersion, jvObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3960,7 +4114,8 @@ void
|
||||
NetworkOPsImp::subAccount(
|
||||
InfoSub::Ref isrListener,
|
||||
HashSet<AccountID> const& vnaAccountIDs,
|
||||
bool rt)
|
||||
bool rt,
|
||||
unsigned int apiVersion)
|
||||
{
|
||||
SubInfoMapType& subMap = rt ? subRTAccount_ : subAccount_;
|
||||
|
||||
@@ -3973,6 +4128,8 @@ NetworkOPsImp::subAccount(
|
||||
|
||||
std::scoped_lock const sl(accountLock_);
|
||||
|
||||
Subscriber const entry{.sink = isrListener, .apiVersion = apiVersion};
|
||||
|
||||
for (auto const& naAccountID : vnaAccountIDs)
|
||||
{
|
||||
auto const simIterator = subMap.find(naAccountID);
|
||||
@@ -3980,14 +4137,14 @@ NetworkOPsImp::subAccount(
|
||||
{
|
||||
// Not found, note that account has a new single listener.
|
||||
SubMapType usisElement;
|
||||
usisElement[isrListener->getSeq()] = isrListener;
|
||||
usisElement[isrListener->getSeq()] = entry;
|
||||
// VFALCO NOTE This is making a needless copy of naAccountID
|
||||
subMap.insert(simIterator, make_pair(naAccountID, usisElement));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Found, note that the account has another listener.
|
||||
simIterator->second[isrListener->getSeq()] = isrListener;
|
||||
simIterator->second.insert_or_assign(isrListener->getSeq(), entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4084,7 +4241,7 @@ NetworkOPsImp::cleanupAccountSubscriptions(
|
||||
SubInfoMapType& subMap)
|
||||
{
|
||||
// Plain account maps need no per-entry teardown before erase.
|
||||
cleanupSubscriptionMap(seq, accounts, subMap, [](InfoSub::Wptr const&) {});
|
||||
cleanupSubscriptionMap(seq, accounts, subMap, [](Subscriber const&) {});
|
||||
}
|
||||
|
||||
void
|
||||
@@ -4170,8 +4327,8 @@ NetworkOPsImp::pubMPTTransaction(AcceptedLedgerTx const& alTx, MultiApiJson cons
|
||||
return;
|
||||
|
||||
// Declared before the lock so a last-reference ~InfoSub runs after
|
||||
// mptLock_ is released (see the deferred-destruction rule).
|
||||
HashSet<InfoSub::pointer> notify;
|
||||
// mptLock_ is released (see the deferred-destruction rule). See noteSubscriber.
|
||||
NotifyMap notify;
|
||||
|
||||
{
|
||||
std::scoped_lock const sl(mptLock_);
|
||||
@@ -4183,11 +4340,11 @@ NetworkOPsImp::pubMPTTransaction(AcceptedLedgerTx const& alTx, MultiApiJson cons
|
||||
auto it = simiIt->second.begin();
|
||||
while (it != simiIt->second.end())
|
||||
{
|
||||
InfoSub::pointer const p = it->second.lock();
|
||||
InfoSub::pointer const p = it->second.sink.lock();
|
||||
|
||||
if (p)
|
||||
{
|
||||
notify.insert(p);
|
||||
noteSubscriber(notify, p, it->second.apiVersion);
|
||||
++it;
|
||||
}
|
||||
else
|
||||
@@ -4202,16 +4359,18 @@ NetworkOPsImp::pubMPTTransaction(AcceptedLedgerTx const& alTx, MultiApiJson cons
|
||||
if (notify.empty())
|
||||
return;
|
||||
|
||||
for (InfoSub::Ref isrListener : notify)
|
||||
StreamBroadcast broadcast;
|
||||
for (auto const& [seq, entry] : notify)
|
||||
{
|
||||
jvObj.visit(isrListener->getApiVersion(), [&](json::Value const& jv) {
|
||||
isrListener->send(jv, true);
|
||||
});
|
||||
broadcast.sendTo(*entry.sink, entry.apiVersion, jvObj);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
NetworkOPsImp::subMPT(InfoSub::Ref isrListener, HashSet<MPTID> const& mptIDs)
|
||||
NetworkOPsImp::subMPT(
|
||||
InfoSub::Ref isrListener,
|
||||
HashSet<MPTID> const& mptIDs,
|
||||
unsigned int apiVersion)
|
||||
{
|
||||
// Insert into subMPT_ before the InfoSub (unsubMPT removes in reverse), as
|
||||
// subBook does, so a racing unsubMPT cannot leave a subMPT_ entry that the
|
||||
@@ -4219,6 +4378,8 @@ NetworkOPsImp::subMPT(InfoSub::Ref isrListener, HashSet<MPTID> const& mptIDs)
|
||||
{
|
||||
std::scoped_lock const sl(mptLock_);
|
||||
|
||||
Subscriber const entry{.sink = isrListener, .apiVersion = apiVersion};
|
||||
|
||||
for (auto const& mptID : mptIDs)
|
||||
{
|
||||
JLOG(journal_.trace()) << "subMPT: MPT: " << to_string(mptID);
|
||||
@@ -4228,13 +4389,13 @@ NetworkOPsImp::subMPT(InfoSub::Ref isrListener, HashSet<MPTID> const& mptIDs)
|
||||
{
|
||||
// Not found, note that the MPT issuance has a new single listener.
|
||||
SubMapType usisElement;
|
||||
usisElement[isrListener->getSeq()] = isrListener;
|
||||
usisElement[isrListener->getSeq()] = entry;
|
||||
subMPT_.insert(simIterator, make_pair(mptID, usisElement));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Found, note that the MPT issuance has another listener.
|
||||
simIterator->second[isrListener->getSeq()] = isrListener;
|
||||
simIterator->second.insert_or_assign(isrListener->getSeq(), entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4318,10 +4479,18 @@ NetworkOPsImp::addAccountHistoryJob(SubAccountHistoryInfoWeak subInfo)
|
||||
});
|
||||
};
|
||||
|
||||
// Every message this sends reports a failure of the history job, an error object naming no
|
||||
// `type`, so the stream it concerns is named for a subscriber a notification is built for.
|
||||
auto const send = [&](json::Value const& jvObj, bool unsubscribe) -> bool {
|
||||
if (auto const sptr = subInfo.sinkWptr.lock())
|
||||
{
|
||||
sptr->send(jvObj, true);
|
||||
auto const stamped = stampStreamError(
|
||||
jvObj,
|
||||
subInfo.apiVersion,
|
||||
sptr->wantsNotifications(),
|
||||
jss::account_history_tx_stream);
|
||||
sendShaped(*sptr, subInfo.apiVersion, stamped ? *stamped : jvObj);
|
||||
|
||||
if (unsubscribe)
|
||||
unsubAccountHistory(sptr, accountId, false);
|
||||
return true;
|
||||
@@ -4333,9 +4502,9 @@ NetworkOPsImp::addAccountHistoryJob(SubAccountHistoryInfoWeak subInfo)
|
||||
auto const sendMultiApiJson = [&](MultiApiJson const& jvObj, bool unsubscribe) -> bool {
|
||||
if (auto sptr = subInfo.sinkWptr.lock())
|
||||
{
|
||||
jvObj.visit(
|
||||
sptr->getApiVersion(), //
|
||||
[&](json::Value const& jv) { sptr->send(jv, true); });
|
||||
jvObj.visit(subInfo.apiVersion, [&](json::Value const& jv) {
|
||||
sendShaped(*sptr, subInfo.apiVersion, jv);
|
||||
});
|
||||
|
||||
if (unsubscribe)
|
||||
unsubAccountHistory(sptr, accountId, false);
|
||||
@@ -4548,7 +4717,10 @@ NetworkOPsImp::subAccountHistoryStart(
|
||||
}
|
||||
|
||||
ErrorCodeI
|
||||
NetworkOPsImp::subAccountHistory(InfoSub::Ref isrListener, AccountID const& accountId)
|
||||
NetworkOPsImp::subAccountHistory(
|
||||
InfoSub::Ref isrListener,
|
||||
AccountID const& accountId,
|
||||
unsigned int apiVersion)
|
||||
{
|
||||
if (!isrListener->insertSubAccountHistory(accountId))
|
||||
{
|
||||
@@ -4561,17 +4733,18 @@ NetworkOPsImp::subAccountHistory(InfoSub::Ref isrListener, AccountID const& acco
|
||||
SubAccountHistoryInfoWeak ahi{
|
||||
.sinkWptr = isrListener,
|
||||
.index = std::make_shared<SubAccountHistoryIndex>(accountId),
|
||||
.apiVersion = apiVersion,
|
||||
};
|
||||
auto const simIterator = subAccountHistory_.find(accountId);
|
||||
if (simIterator == subAccountHistory_.end())
|
||||
{
|
||||
HashMap<std::uint64_t, SubAccountHistoryInfoWeak> inner;
|
||||
inner.emplace(isrListener->getSeq(), ahi);
|
||||
inner.insert_or_assign(isrListener->getSeq(), ahi);
|
||||
subAccountHistory_.insert(simIterator, std::make_pair(accountId, inner));
|
||||
}
|
||||
else
|
||||
{
|
||||
simIterator->second.emplace(isrListener->getSeq(), ahi);
|
||||
simIterator->second.insert_or_assign(isrListener->getSeq(), ahi);
|
||||
}
|
||||
|
||||
auto const ledger = registry_.get().getLedgerMaster().getValidatedLedger();
|
||||
@@ -4632,7 +4805,7 @@ NetworkOPsImp::unsubAccountHistoryInternal(
|
||||
}
|
||||
|
||||
bool
|
||||
NetworkOPsImp::subBook(InfoSub::Ref isrListener, Book const& book)
|
||||
NetworkOPsImp::subBook(InfoSub::Ref isrListener, Book const& book, unsigned int apiVersion)
|
||||
{
|
||||
// Server-side insert first, then InfoSub bookkeeping. If the InfoSub-side
|
||||
// insert throws, the orphan in subBook_ is cleared by the expired-weak_ptr
|
||||
@@ -4640,7 +4813,8 @@ NetworkOPsImp::subBook(InfoSub::Ref isrListener, Book const& book)
|
||||
// call unsubBookInternal for a key that was never inserted server-side.
|
||||
{
|
||||
std::scoped_lock const sl(bookLock_);
|
||||
subBook_[book].try_emplace(isrListener->getSeq(), isrListener);
|
||||
subBook_[book].insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion});
|
||||
}
|
||||
isrListener->insertBookSubscription(book);
|
||||
return true;
|
||||
@@ -4688,7 +4862,7 @@ NetworkOPsImp::acceptLedger(std::optional<std::chrono::milliseconds> consensusDe
|
||||
|
||||
// <-- bool: true=added, false=already there
|
||||
bool
|
||||
NetworkOPsImp::subLedger(InfoSub::Ref isrListener, json::Value& jvResult)
|
||||
NetworkOPsImp::subLedger(InfoSub::Ref isrListener, json::Value& jvResult, unsigned int apiVersion)
|
||||
{
|
||||
if (auto const lpClosed = ledgerMaster_.getValidatedLedger())
|
||||
{
|
||||
@@ -4710,15 +4884,21 @@ NetworkOPsImp::subLedger(InfoSub::Ref isrListener, json::Value& jvResult)
|
||||
}
|
||||
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
return streamMaps_[SLedger].emplace(isrListener->getSeq(), isrListener).second;
|
||||
return streamMaps_[SLedger]
|
||||
.insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion})
|
||||
.second;
|
||||
}
|
||||
|
||||
// <-- bool: true=added, false=already there
|
||||
bool
|
||||
NetworkOPsImp::subBookChanges(InfoSub::Ref isrListener)
|
||||
NetworkOPsImp::subBookChanges(InfoSub::Ref isrListener, unsigned int apiVersion)
|
||||
{
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
return streamMaps_[SBookChanges].emplace(isrListener->getSeq(), isrListener).second;
|
||||
return streamMaps_[SBookChanges]
|
||||
.insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion})
|
||||
.second;
|
||||
}
|
||||
|
||||
// <-- bool: true=erased, false=was not there
|
||||
@@ -4739,10 +4919,13 @@ NetworkOPsImp::unsubBookChanges(std::uint64_t uSeq)
|
||||
|
||||
// <-- bool: true=added, false=already there
|
||||
bool
|
||||
NetworkOPsImp::subManifests(InfoSub::Ref isrListener)
|
||||
NetworkOPsImp::subManifests(InfoSub::Ref isrListener, unsigned int apiVersion)
|
||||
{
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
return streamMaps_[SManifests].emplace(isrListener->getSeq(), isrListener).second;
|
||||
return streamMaps_[SManifests]
|
||||
.insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion})
|
||||
.second;
|
||||
}
|
||||
|
||||
// <-- bool: true=erased, false=was not there
|
||||
@@ -4755,7 +4938,11 @@ NetworkOPsImp::unsubManifests(std::uint64_t uSeq)
|
||||
|
||||
// <-- bool: true=added, false=already there
|
||||
bool
|
||||
NetworkOPsImp::subServer(InfoSub::Ref isrListener, json::Value& jvResult, bool admin)
|
||||
NetworkOPsImp::subServer(
|
||||
InfoSub::Ref isrListener,
|
||||
json::Value& jvResult,
|
||||
bool admin,
|
||||
unsigned int apiVersion)
|
||||
{
|
||||
UInt256 uRandom;
|
||||
|
||||
@@ -4775,7 +4962,10 @@ NetworkOPsImp::subServer(InfoSub::Ref isrListener, json::Value& jvResult, bool a
|
||||
toBase58(TokenType::NodePublic, registry_.get().getApp().nodeIdentity().first);
|
||||
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
return streamMaps_[SServer].emplace(isrListener->getSeq(), isrListener).second;
|
||||
return streamMaps_[SServer]
|
||||
.insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion})
|
||||
.second;
|
||||
}
|
||||
|
||||
// <-- bool: true=erased, false=was not there
|
||||
@@ -4788,10 +4978,13 @@ NetworkOPsImp::unsubServer(std::uint64_t uSeq)
|
||||
|
||||
// <-- bool: true=added, false=already there
|
||||
bool
|
||||
NetworkOPsImp::subTransactions(InfoSub::Ref isrListener)
|
||||
NetworkOPsImp::subTransactions(InfoSub::Ref isrListener, unsigned int apiVersion)
|
||||
{
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
return streamMaps_[STransactions].emplace(isrListener->getSeq(), isrListener).second;
|
||||
return streamMaps_[STransactions]
|
||||
.insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion})
|
||||
.second;
|
||||
}
|
||||
|
||||
// <-- bool: true=erased, false=was not there
|
||||
@@ -4804,10 +4997,13 @@ NetworkOPsImp::unsubTransactions(std::uint64_t uSeq)
|
||||
|
||||
// <-- bool: true=added, false=already there
|
||||
bool
|
||||
NetworkOPsImp::subRTTransactions(InfoSub::Ref isrListener)
|
||||
NetworkOPsImp::subRTTransactions(InfoSub::Ref isrListener, unsigned int apiVersion)
|
||||
{
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
return streamMaps_[SRtTransactions].emplace(isrListener->getSeq(), isrListener).second;
|
||||
return streamMaps_[SRtTransactions]
|
||||
.insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion})
|
||||
.second;
|
||||
}
|
||||
|
||||
// <-- bool: true=erased, false=was not there
|
||||
@@ -4820,10 +5016,13 @@ NetworkOPsImp::unsubRTTransactions(std::uint64_t uSeq)
|
||||
|
||||
// <-- bool: true=added, false=already there
|
||||
bool
|
||||
NetworkOPsImp::subValidations(InfoSub::Ref isrListener)
|
||||
NetworkOPsImp::subValidations(InfoSub::Ref isrListener, unsigned int apiVersion)
|
||||
{
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
return streamMaps_[SValidations].emplace(isrListener->getSeq(), isrListener).second;
|
||||
return streamMaps_[SValidations]
|
||||
.insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion})
|
||||
.second;
|
||||
}
|
||||
|
||||
void
|
||||
@@ -4842,10 +5041,13 @@ NetworkOPsImp::unsubValidations(std::uint64_t uSeq)
|
||||
|
||||
// <-- bool: true=added, false=already there
|
||||
bool
|
||||
NetworkOPsImp::subPeerStatus(InfoSub::Ref isrListener)
|
||||
NetworkOPsImp::subPeerStatus(InfoSub::Ref isrListener, unsigned int apiVersion)
|
||||
{
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
return streamMaps_[SPeerStatus].emplace(isrListener->getSeq(), isrListener).second;
|
||||
return streamMaps_[SPeerStatus]
|
||||
.insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion})
|
||||
.second;
|
||||
}
|
||||
|
||||
// <-- bool: true=erased, false=was not there
|
||||
@@ -4858,10 +5060,13 @@ NetworkOPsImp::unsubPeerStatus(std::uint64_t uSeq)
|
||||
|
||||
// <-- bool: true=added, false=already there
|
||||
bool
|
||||
NetworkOPsImp::subConsensus(InfoSub::Ref isrListener)
|
||||
NetworkOPsImp::subConsensus(InfoSub::Ref isrListener, unsigned int apiVersion)
|
||||
{
|
||||
std::scoped_lock const sl(streamLock_);
|
||||
return streamMaps_[SConsensusPhase].emplace(isrListener->getSeq(), isrListener).second;
|
||||
return streamMaps_[SConsensusPhase]
|
||||
.insert_or_assign(
|
||||
isrListener->getSeq(), Subscriber{.sink = isrListener, .apiVersion = apiVersion})
|
||||
.second;
|
||||
}
|
||||
|
||||
// <-- bool: true=erased, false=was not there
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -53,12 +53,14 @@ PathRequest::PathRequest(
|
||||
std::shared_ptr<InfoSub> const& subscriber,
|
||||
int id,
|
||||
PathRequestManager& owner,
|
||||
beast::Journal journal)
|
||||
beast::Journal journal,
|
||||
unsigned int apiVersion)
|
||||
: app_(app)
|
||||
, journal_(journal)
|
||||
, owner_(owner)
|
||||
, wpSubscriber_(subscriber)
|
||||
, consumer_(subscriber->getConsumer())
|
||||
, apiVersion_(apiVersion)
|
||||
, jvStatus_(json::ValueType::Object)
|
||||
, lastIndex_(0)
|
||||
, inProgress_(false)
|
||||
@@ -82,6 +84,9 @@ PathRequest::PathRequest(
|
||||
, owner_(owner)
|
||||
, fCompletion_(std::move(completion))
|
||||
, consumer_(consumer)
|
||||
// A ripple_path_find request answers its caller once and pushes no update, so it names no
|
||||
// version.
|
||||
, apiVersion_(0)
|
||||
, jvStatus_(json::ValueType::Object)
|
||||
, lastIndex_(0)
|
||||
, inProgress_(false)
|
||||
|
||||
@@ -57,7 +57,8 @@ public:
|
||||
std::shared_ptr<InfoSub> const& subscriber,
|
||||
int id,
|
||||
PathRequestManager&,
|
||||
beast::Journal journal);
|
||||
beast::Journal journal,
|
||||
unsigned int apiVersion);
|
||||
|
||||
// ripple_path_find semantics
|
||||
// Completion function is called after path update is complete
|
||||
@@ -98,6 +99,19 @@ public:
|
||||
std::function<bool()> const& continueCallback = {});
|
||||
InfoSub::pointer
|
||||
getSubscriber() const;
|
||||
|
||||
/**
|
||||
* The API version the `path_find create` request named, recorded for the
|
||||
* publisher of its updates.
|
||||
*
|
||||
* @return The version, or zero for a `ripple_path_find` request, which
|
||||
* produces no update to shape.
|
||||
*/
|
||||
[[nodiscard]] unsigned int
|
||||
apiVersion() const noexcept
|
||||
{
|
||||
return apiVersion_;
|
||||
}
|
||||
bool
|
||||
hasCompletion();
|
||||
|
||||
@@ -139,6 +153,8 @@ private:
|
||||
std::function<void()> fCompletion_;
|
||||
resource::Consumer& consumer_; // Charge according to source currencies
|
||||
|
||||
unsigned int const apiVersion_;
|
||||
|
||||
json::Value jvId_;
|
||||
json::Value jvStatus_; // Last result
|
||||
|
||||
|
||||
@@ -125,11 +125,17 @@ PathRequestManager::updateAll(std::shared_ptr<ReadView const> const& inLedger)
|
||||
ipSub.reset();
|
||||
json::Value update = request->doUpdate(cache, false, continueCallback);
|
||||
request->updateComplete();
|
||||
// From API version 3 this `type` names the method of a notification:
|
||||
// the request that started the path finding has had its one response.
|
||||
// The `id` doUpdate copied in travels inside `params`. False says the
|
||||
// update was directed at this subscriber, which only a diagnostic
|
||||
// reads.
|
||||
update[jss::type] = "path_find";
|
||||
ipSub = getSubscriber(request);
|
||||
if (ipSub)
|
||||
{
|
||||
ipSub->send(update, false);
|
||||
sendShaped(
|
||||
*ipSub, request->apiVersion(), update, /*broadcast=*/false);
|
||||
remove = false;
|
||||
++processed;
|
||||
}
|
||||
@@ -235,10 +241,11 @@ json::Value
|
||||
PathRequestManager::makePathRequest(
|
||||
std::shared_ptr<InfoSub> const& subscriber,
|
||||
std::shared_ptr<ReadView const> const& inLedger,
|
||||
json::Value const& requestJson)
|
||||
json::Value const& requestJson,
|
||||
unsigned int apiVersion)
|
||||
{
|
||||
auto const req =
|
||||
std::make_shared<PathRequest>(app_, subscriber, ++lastIdentifier_, *this, journal_);
|
||||
auto const req = std::make_shared<PathRequest>(
|
||||
app_, subscriber, ++lastIdentifier_, *this, journal_, apiVersion);
|
||||
|
||||
auto [valid, jvRes] = req->doCreate(getAssetCache(inLedger, false), requestJson);
|
||||
|
||||
|
||||
@@ -37,7 +37,8 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all of the contained PathRequest instances.
|
||||
* Updates every contained PathRequest against @p ledger and sends each
|
||||
* update to its subscriber, shaped for the API version the request named.
|
||||
*
|
||||
* @param ledger Ledger we are pathfinding in.
|
||||
*/
|
||||
@@ -50,13 +51,22 @@ public:
|
||||
std::shared_ptr<AssetCache>
|
||||
getAssetCache(std::shared_ptr<ReadView const> const& ledger, bool authoritative);
|
||||
|
||||
// Create a new-style path request that pushes
|
||||
// updates to a subscriber
|
||||
/**
|
||||
* Creates a path request that pushes updates to @p subscriber.
|
||||
*
|
||||
* @param subscriber The connection the updates go to.
|
||||
* @param ledger The ledger the first search runs against.
|
||||
* @param request The `path_find create` parameters.
|
||||
* @param apiVersion The API version the request named, recorded on the
|
||||
* request.
|
||||
* @return The first result, or an error when the request is malformed.
|
||||
*/
|
||||
json::Value
|
||||
makePathRequest(
|
||||
std::shared_ptr<InfoSub> const& subscriber,
|
||||
std::shared_ptr<ReadView const> const& ledger,
|
||||
json::Value const& request);
|
||||
json::Value const& request,
|
||||
unsigned int apiVersion);
|
||||
|
||||
// Create an old-style path request that is
|
||||
// managed by a coroutine and updated by
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2083,6 +2148,13 @@ rpcClient(
|
||||
{
|
||||
nRet = std::stoi(jvOutput[jss::error][jss::error_code].asString());
|
||||
}
|
||||
else if (
|
||||
jvOutput[jss::error].isMember(jss::data) &&
|
||||
jvOutput[jss::error][jss::data].isMember(jss::error_code))
|
||||
{
|
||||
// The specification envelope, from API version 3, carries the XRPL code in `data`.
|
||||
nRet = std::stoi(jvOutput[jss::error][jss::data][jss::error_code].asString());
|
||||
}
|
||||
else
|
||||
{
|
||||
nRet = RpcBadSyntax;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -81,13 +82,22 @@ public:
|
||||
|
||||
~RPCSubImp() override = default;
|
||||
|
||||
/**
|
||||
* Queues @p jvObj for the sending thread, which posts it to the url as
|
||||
* the parameters of an `event` call.
|
||||
*
|
||||
* @param jvObj The message, queued as it arrived at every API version.
|
||||
* See wantsNotifications below.
|
||||
* @param broadcast Whether the message was published to a stream; only
|
||||
* the log level reads it.
|
||||
*/
|
||||
void
|
||||
send(json::Value const& jvObj, bool broadcast) override
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -101,6 +111,19 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* sendThread nests what `send` queued inside an `event` request, beside
|
||||
* `seq`, so a notification would arrive nested and is never built for this
|
||||
* subscriber.
|
||||
*
|
||||
* @return false, always.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
wantsNotifications() const noexcept override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
setUsername(std::string const& strUsername) override
|
||||
{
|
||||
|
||||
+1156
-273
File diff suppressed because it is too large.
Load diff
@@ -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>
|
||||
@@ -24,7 +25,6 @@
|
||||
#include <xrpl/core/NetworkIDService.h>
|
||||
#include <xrpl/json/json_writer.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -454,9 +454,25 @@ struct TransactionPreProcessResult
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks a sign or submit request and builds the transaction it describes.
|
||||
*
|
||||
* @param params The request's parameters, which `tx_json` is read out of and
|
||||
* autofilled fields are written into.
|
||||
* @param apiVersion The API version the request is served at, which selects
|
||||
* the error reported for a ledger that is not current.
|
||||
* @param role The caller's role.
|
||||
* @param signingArgs Who signs, and for whom, filled in as the request is
|
||||
* read.
|
||||
* @param validatedLedgerAge How old the last validated ledger is.
|
||||
* @param app The application.
|
||||
* @param rules The rules of the ledger the transaction is built against.
|
||||
* @return The transaction, or the error to report.
|
||||
*/
|
||||
static TransactionPreProcessResult
|
||||
transactionPreProcessImpl(
|
||||
json::Value& params,
|
||||
unsigned apiVersion,
|
||||
Role role,
|
||||
SigningForParams& signingArgs,
|
||||
std::chrono::seconds validatedLedgerAge,
|
||||
@@ -505,13 +521,7 @@ transactionPreProcessImpl(
|
||||
|
||||
// Check tx_json fields, but don't add any.
|
||||
auto [txJsonResult, srcAddressID] = checkTxJsonFields(
|
||||
txJson,
|
||||
role,
|
||||
verify,
|
||||
validatedLedgerAge,
|
||||
app.config(),
|
||||
app.getFeeTrack(),
|
||||
getAPIVersionNumber(params, app.config().betaRpcApi));
|
||||
txJson, role, verify, validatedLedgerAge, app.config(), app.getFeeTrack(), apiVersion);
|
||||
|
||||
if (rpc::containsError(txJsonResult))
|
||||
return std::move(txJsonResult);
|
||||
@@ -1018,12 +1028,13 @@ 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;
|
||||
TransactionPreProcessResult const preprocResult = transactionPreProcessImpl(
|
||||
jvRequest, role, signForParams, validatedLedgerAge, app, ledger->rules());
|
||||
jvRequest, apiVersion, role, signForParams, validatedLedgerAge, app, ledger->rules());
|
||||
|
||||
if (!preprocResult.second)
|
||||
return preprocResult.first;
|
||||
@@ -1055,12 +1066,12 @@ 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;
|
||||
TransactionPreProcessResult const preprocResult = transactionPreProcessImpl(
|
||||
jvRequest, role, signForParams, validatedLedgerAge, app, ledger->rules());
|
||||
jvRequest, apiVersion, role, signForParams, validatedLedgerAge, app, ledger->rules());
|
||||
|
||||
if (!preprocResult.second)
|
||||
return preprocResult.first;
|
||||
@@ -1177,7 +1188,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";
|
||||
@@ -1228,7 +1239,7 @@ transactionSignFor(
|
||||
SigningForParams signForParams(*signerAccountID);
|
||||
|
||||
TransactionPreProcessResult const preprocResult = transactionPreProcessImpl(
|
||||
jvRequest, role, signForParams, validatedLedgerAge, app, ledger->rules());
|
||||
jvRequest, apiVersion, role, signForParams, validatedLedgerAge, app, ledger->rules());
|
||||
|
||||
if (!preprocResult.second)
|
||||
return preprocResult.first;
|
||||
@@ -1301,7 +1312,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.
|
||||
@@ -1315,13 +1326,7 @@ transactionSubmitMultiSigned(
|
||||
json::Value& txJson(jvRequest["tx_json"]);
|
||||
|
||||
auto [txJsonResult, srcAddressID] = checkTxJsonFields(
|
||||
txJson,
|
||||
role,
|
||||
true,
|
||||
validatedLedgerAge,
|
||||
app.config(),
|
||||
app.getFeeTrack(),
|
||||
getAPIVersionNumber(jvRequest, app.config().betaRpcApi));
|
||||
txJson, role, true, validatedLedgerAge, app.config(), app.getFeeTrack(), apiVersion);
|
||||
|
||||
if (rpc::containsError(txJsonResult))
|
||||
return std::move(txJsonResult);
|
||||
|
||||
@@ -69,6 +69,18 @@ static constexpr int kMaxJobQueueClients = 500;
|
||||
constexpr auto kMaxValidatedLedgerAge = std::chrono::minutes{2};
|
||||
static constexpr int kMaxRequestSize = 1000000;
|
||||
|
||||
/**
|
||||
* Maximum number of entries in a batch, in either of its two forms, from API
|
||||
* version 3 up.
|
||||
*
|
||||
* A batch dispatches one command per entry, and an entry can be small, so the
|
||||
* body size limit bounds the bytes rather than the work.
|
||||
*
|
||||
* Below version 3 the `method: "batch"` form is bounded by the body size limit
|
||||
* and the resource drop threshold only.
|
||||
*/
|
||||
static constexpr unsigned kMaxBatchEntries = 100;
|
||||
|
||||
/**
|
||||
* Maximum number of pages in one response from a binary LedgerData request.
|
||||
*/
|
||||
|
||||
@@ -49,6 +49,13 @@ public:
|
||||
return fwdfor_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes @p jv to the session as built: the shape belongs to the
|
||||
* subscription the message answers, not to the connection, so the publisher
|
||||
* shapes it. See sendShaped and NetworkOPsImp's StreamBroadcast.
|
||||
*
|
||||
* @param jv The message, in the shape the subscriber receives.
|
||||
*/
|
||||
void
|
||||
send(json::Value const& jv, bool) override
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -71,7 +71,7 @@ parseObjectID(
|
||||
{
|
||||
return *uNodeIndex;
|
||||
}
|
||||
return ledger_entry_helpers::invalidFieldError("malformedRequest", fieldName, expectedType);
|
||||
return ledger_entry_helpers::invalidFieldError(RpcMalformedRequest, fieldName, expectedType);
|
||||
}
|
||||
|
||||
static std::expected<UInt256, json::Value>
|
||||
@@ -107,7 +107,7 @@ parseAccountRoot(
|
||||
return keylet::account(*account).key;
|
||||
}
|
||||
|
||||
return ledger_entry_helpers::invalidFieldError("malformedAddress", fieldName, "AccountID");
|
||||
return ledger_entry_helpers::invalidFieldError(RpcMalformedAddress, fieldName, "AccountID");
|
||||
}
|
||||
|
||||
auto const parseAmendments = fixed(keylet::amendments());
|
||||
@@ -129,12 +129,12 @@ parseAMM(
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
|
||||
auto const asset = ledger_entry_helpers::requiredAsset(params, jss::asset, "malformedRequest");
|
||||
auto const asset = ledger_entry_helpers::requiredAsset(params, jss::asset, RpcMalformedRequest);
|
||||
if (!asset)
|
||||
return std::unexpected(asset.error());
|
||||
|
||||
auto const asset2 =
|
||||
ledger_entry_helpers::requiredAsset(params, jss::asset2, "malformedRequest");
|
||||
ledger_entry_helpers::requiredAsset(params, jss::asset2, RpcMalformedRequest);
|
||||
if (!asset2)
|
||||
return std::unexpected(asset2.error());
|
||||
|
||||
@@ -162,14 +162,14 @@ parseBridge(
|
||||
return std::unexpected(bridge.error());
|
||||
|
||||
auto const account = ledger_entry_helpers::requiredAccountID(
|
||||
params, jss::bridge_account, "malformedBridgeAccount");
|
||||
params, jss::bridge_account, RpcMalformedBridgeAccount);
|
||||
if (!account)
|
||||
return std::unexpected(account.error());
|
||||
|
||||
STXChainBridge::ChainType const chainType =
|
||||
STXChainBridge::srcChain(account.value() == bridge->lockingChainDoor());
|
||||
if (account.value() != bridge->door(chainType))
|
||||
return ledger_entry_helpers::malformedError("malformedRequest", "");
|
||||
return ledger_entry_helpers::malformedError(RpcMalformedRequest, "");
|
||||
|
||||
return keylet::bridge(*bridge, chainType).key;
|
||||
}
|
||||
@@ -186,12 +186,12 @@ parseCheck(
|
||||
}
|
||||
|
||||
auto const account =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, RpcMalformedAddress);
|
||||
if (!account)
|
||||
return std::unexpected(account.error());
|
||||
|
||||
auto const sequence =
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest");
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::seq, RpcMalformedRequest);
|
||||
if (!sequence)
|
||||
return std::unexpected(sequence.error());
|
||||
|
||||
@@ -211,17 +211,17 @@ parseCredential(
|
||||
}
|
||||
|
||||
auto const subject =
|
||||
ledger_entry_helpers::requiredAccountID(cred, jss::subject, "malformedRequest");
|
||||
ledger_entry_helpers::requiredAccountID(cred, jss::subject, RpcMalformedRequest);
|
||||
if (!subject)
|
||||
return std::unexpected(subject.error());
|
||||
|
||||
auto const issuer =
|
||||
ledger_entry_helpers::requiredAccountID(cred, jss::issuer, "malformedRequest");
|
||||
ledger_entry_helpers::requiredAccountID(cred, jss::issuer, RpcMalformedRequest);
|
||||
if (!issuer)
|
||||
return std::unexpected(issuer.error());
|
||||
|
||||
auto const credType = ledger_entry_helpers::requiredHexBlob(
|
||||
cred, jss::credential_type, kMaxCredentialTypeLength, "malformedRequest");
|
||||
cred, jss::credential_type, kMaxCredentialTypeLength, RpcMalformedRequest);
|
||||
if (!credType)
|
||||
return std::unexpected(credType.error());
|
||||
|
||||
@@ -240,12 +240,12 @@ parseDelegate(
|
||||
}
|
||||
|
||||
auto const account =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, RpcMalformedAddress);
|
||||
if (!account)
|
||||
return std::unexpected(account.error());
|
||||
|
||||
auto const authorize =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::authorize, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::authorize, RpcMalformedAddress);
|
||||
if (!authorize)
|
||||
return std::unexpected(authorize.error());
|
||||
|
||||
@@ -258,7 +258,7 @@ parseAuthorizeCredentials(json::Value const& jv)
|
||||
if (!jv.isArray())
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedAuthorizedCredentials", jss::authorized_credentials, "array");
|
||||
RpcMalformedAuthorizedCredentials, jss::authorized_credentials, "array");
|
||||
}
|
||||
|
||||
std::uint32_t const n = jv.size();
|
||||
@@ -266,7 +266,7 @@ parseAuthorizeCredentials(json::Value const& jv)
|
||||
{
|
||||
return std::unexpected(
|
||||
ledger_entry_helpers::malformedError(
|
||||
"malformedAuthorizedCredentials",
|
||||
RpcMalformedAuthorizedCredentials,
|
||||
"Invalid field '" + std::string(jss::authorized_credentials) +
|
||||
"', array too long."));
|
||||
}
|
||||
@@ -275,7 +275,7 @@ parseAuthorizeCredentials(json::Value const& jv)
|
||||
{
|
||||
return std::unexpected(
|
||||
ledger_entry_helpers::malformedError(
|
||||
"malformedAuthorizedCredentials",
|
||||
RpcMalformedAuthorizedCredentials,
|
||||
"Invalid field '" + std::string(jss::authorized_credentials) + "', array empty."));
|
||||
}
|
||||
|
||||
@@ -285,23 +285,23 @@ parseAuthorizeCredentials(json::Value const& jv)
|
||||
if (!jo.isObject())
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedAuthorizedCredentials", jss::authorized_credentials, "array of objects");
|
||||
RpcMalformedAuthorizedCredentials, jss::authorized_credentials, "array of objects");
|
||||
}
|
||||
|
||||
if (auto const value = ledger_entry_helpers::hasRequired(
|
||||
jo, {jss::issuer, jss::credential_type}, "malformedAuthorizedCredentials");
|
||||
jo, {jss::issuer, jss::credential_type}, RpcMalformedAuthorizedCredentials);
|
||||
!value)
|
||||
{
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
|
||||
auto const issuer = ledger_entry_helpers::requiredAccountID(
|
||||
jo, jss::issuer, "malformedAuthorizedCredentials");
|
||||
jo, jss::issuer, RpcMalformedAuthorizedCredentials);
|
||||
if (!issuer)
|
||||
return std::unexpected(issuer.error());
|
||||
|
||||
auto const credentialType = ledger_entry_helpers::requiredHexBlob(
|
||||
jo, jss::credential_type, kMaxCredentialTypeLength, "malformedAuthorizedCredentials");
|
||||
jo, jss::credential_type, kMaxCredentialTypeLength, RpcMalformedAuthorizedCredentials);
|
||||
if (!credentialType)
|
||||
return std::unexpected(credentialType.error());
|
||||
|
||||
@@ -328,12 +328,12 @@ parseDepositPreauth(
|
||||
if ((dp.isMember(jss::authorized) == dp.isMember(jss::authorized_credentials)))
|
||||
{
|
||||
return ledger_entry_helpers::malformedError(
|
||||
"malformedRequest",
|
||||
RpcMalformedRequest,
|
||||
"Must have exactly one of `authorized` and "
|
||||
"`authorized_credentials`.");
|
||||
}
|
||||
|
||||
auto const owner = ledger_entry_helpers::requiredAccountID(dp, jss::owner, "malformedOwner");
|
||||
auto const owner = ledger_entry_helpers::requiredAccountID(dp, jss::owner, RpcMalformedOwner);
|
||||
if (!owner)
|
||||
{
|
||||
return std::unexpected(owner.error());
|
||||
@@ -346,7 +346,7 @@ parseDepositPreauth(
|
||||
return keylet::depositPreauth(*owner, *authorized).key;
|
||||
}
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedAuthorized", jss::authorized, "AccountID");
|
||||
RpcMalformedAuthorized, jss::authorized, "AccountID");
|
||||
}
|
||||
|
||||
auto const& ac(dp[jss::authorized_credentials]);
|
||||
@@ -359,7 +359,7 @@ parseDepositPreauth(
|
||||
{
|
||||
// TODO: this error message is bad/inaccurate
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedAuthorizedCredentials", jss::authorized_credentials, "array");
|
||||
RpcMalformedAuthorizedCredentials, jss::authorized_credentials, "array");
|
||||
}
|
||||
|
||||
return keylet::depositPreauth(*owner, sorted).key;
|
||||
@@ -374,7 +374,7 @@ parseDID(
|
||||
auto const account = ledger_entry_helpers::parse<AccountID>(params);
|
||||
if (!account)
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError("malformedAddress", fieldName, "AccountID");
|
||||
return ledger_entry_helpers::invalidFieldError(RpcMalformedAddress, fieldName, "AccountID");
|
||||
}
|
||||
|
||||
return keylet::did(*account).key;
|
||||
@@ -396,13 +396,13 @@ parseDirectoryNode(
|
||||
params[jss::sub_index].isBool()))
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedRequest", jss::sub_index, "number");
|
||||
RpcMalformedRequest, jss::sub_index, "number");
|
||||
}
|
||||
|
||||
if (params.isMember(jss::owner) == params.isMember(jss::dir_root))
|
||||
{
|
||||
return ledger_entry_helpers::malformedError(
|
||||
"malformedRequest", "Must have exactly one of `owner` and `dir_root` fields.");
|
||||
RpcMalformedRequest, "Must have exactly one of `owner` and `dir_root` fields.");
|
||||
}
|
||||
|
||||
std::uint64_t const uSubIndex = params.get(jss::sub_index, 0).asUInt();
|
||||
@@ -414,22 +414,19 @@ parseDirectoryNode(
|
||||
return keylet::page(*uDirRoot, uSubIndex).key;
|
||||
}
|
||||
|
||||
return ledger_entry_helpers::invalidFieldError("malformedDirRoot", jss::dir_root, "hash");
|
||||
return ledger_entry_helpers::invalidFieldError(RpcMalformedDirRoot, jss::dir_root, "hash");
|
||||
}
|
||||
|
||||
if (params.isMember(jss::owner))
|
||||
// Exactly one of `owner` and `dir_root` is present, checked above, and the `dir_root` arm
|
||||
// returned, so this is the `owner` arm.
|
||||
auto const ownerID = ledger_entry_helpers::parse<AccountID>(params[jss::owner]);
|
||||
if (!ownerID)
|
||||
{
|
||||
auto const ownerID = ledger_entry_helpers::parse<AccountID>(params[jss::owner]);
|
||||
if (!ownerID)
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedAddress", jss::owner, "AccountID");
|
||||
}
|
||||
|
||||
return keylet::page(keylet::ownerDir(*ownerID), uSubIndex).key;
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
RpcMalformedAddress, jss::owner, "AccountID");
|
||||
}
|
||||
|
||||
return ledger_entry_helpers::malformedError("malformedRequest", "");
|
||||
return keylet::page(keylet::ownerDir(*ownerID), uSubIndex).key;
|
||||
}
|
||||
|
||||
static std::expected<UInt256, json::Value>
|
||||
@@ -443,10 +440,10 @@ parseEscrow(
|
||||
return parseObjectID(params, fieldName);
|
||||
}
|
||||
|
||||
auto const id = ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner");
|
||||
auto const id = ledger_entry_helpers::requiredAccountID(params, jss::owner, RpcMalformedOwner);
|
||||
if (!id)
|
||||
return std::unexpected(id.error());
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedSeq");
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, RpcMalformedSeq);
|
||||
if (!seq)
|
||||
return std::unexpected(seq.error());
|
||||
|
||||
@@ -469,7 +466,7 @@ parseFixed(
|
||||
}
|
||||
if (!params.asBool())
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError("invalidParams", fieldName, "true");
|
||||
return ledger_entry_helpers::invalidFieldError(RpcInvalidParams, fieldName, "true");
|
||||
}
|
||||
|
||||
return keylet.key;
|
||||
@@ -506,10 +503,10 @@ parseLoanBroker(
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
|
||||
auto const id = ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner");
|
||||
auto const id = ledger_entry_helpers::requiredAccountID(params, jss::owner, RpcMalformedOwner);
|
||||
if (!id)
|
||||
return std::unexpected(id.error());
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedSeq");
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, RpcMalformedSeq);
|
||||
if (!seq)
|
||||
return std::unexpected(seq.error());
|
||||
|
||||
@@ -529,10 +526,10 @@ parseLoan(
|
||||
}
|
||||
|
||||
auto const id =
|
||||
ledger_entry_helpers::requiredUInt256(params, jss::loan_broker_id, "malformedBroker");
|
||||
ledger_entry_helpers::requiredUInt256(params, jss::loan_broker_id, RpcMalformedBroker);
|
||||
if (!id)
|
||||
return std::unexpected(id.error());
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::loan_seq, "malformedSeq");
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::loan_seq, RpcMalformedSeq);
|
||||
if (!seq)
|
||||
return std::unexpected(seq.error());
|
||||
|
||||
@@ -552,12 +549,12 @@ parseMPToken(
|
||||
}
|
||||
|
||||
auto const mptIssuanceID = ledger_entry_helpers::requiredUInt192(
|
||||
params, jss::mpt_issuance_id, "malformedMPTIssuanceID");
|
||||
params, jss::mpt_issuance_id, RpcMalformedMPTIssuanceID);
|
||||
if (!mptIssuanceID)
|
||||
return std::unexpected(mptIssuanceID.error());
|
||||
|
||||
auto const account =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAccount");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, RpcMalformedAccount);
|
||||
if (!account)
|
||||
return std::unexpected(account.error());
|
||||
|
||||
@@ -574,7 +571,7 @@ parseMPTokenIssuance(
|
||||
if (!mptIssuanceID)
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedMPTokenIssuance", fieldName, "Hash192");
|
||||
RpcMalformedMPTokenIssuance, fieldName, "Hash192");
|
||||
}
|
||||
|
||||
return keylet::mptokenIssuance(*mptIssuanceID).key;
|
||||
@@ -592,12 +589,12 @@ parseNFTokenOffer(
|
||||
}
|
||||
|
||||
auto const owner =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::owner, RpcMalformedOwner);
|
||||
if (!owner)
|
||||
return std::unexpected(owner.error());
|
||||
|
||||
auto const sequence =
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest");
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::seq, RpcMalformedRequest);
|
||||
if (!sequence)
|
||||
return std::unexpected(sequence.error());
|
||||
|
||||
@@ -628,11 +625,11 @@ parseOffer(
|
||||
}
|
||||
|
||||
auto const id =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, RpcMalformedAddress);
|
||||
if (!id)
|
||||
return std::unexpected(id.error());
|
||||
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest");
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, RpcMalformedRequest);
|
||||
if (!seq)
|
||||
return std::unexpected(seq.error());
|
||||
|
||||
@@ -652,12 +649,12 @@ parseOracle(
|
||||
}
|
||||
|
||||
auto const id =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAccount");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, RpcMalformedAccount);
|
||||
if (!id)
|
||||
return std::unexpected(id.error());
|
||||
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(
|
||||
params, jss::oracle_document_id, "malformedDocumentID");
|
||||
params, jss::oracle_document_id, RpcMalformedDocumentID);
|
||||
if (!seq)
|
||||
return std::unexpected(seq.error());
|
||||
|
||||
@@ -676,17 +673,17 @@ parsePayChannel(
|
||||
}
|
||||
|
||||
auto const account =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, RpcMalformedAddress);
|
||||
if (!account)
|
||||
return std::unexpected(account.error());
|
||||
|
||||
auto const destination =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::destination, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::destination, RpcMalformedAddress);
|
||||
if (!destination)
|
||||
return std::unexpected(destination.error());
|
||||
|
||||
auto const sequence =
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest");
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::seq, RpcMalformedRequest);
|
||||
if (!sequence)
|
||||
return std::unexpected(sequence.error());
|
||||
|
||||
@@ -708,15 +705,15 @@ parsePermissionedDomain(
|
||||
if (!pd.isObject())
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedRequest", fieldName, "hex string or object");
|
||||
RpcMalformedRequest, fieldName, "hex string or object");
|
||||
}
|
||||
|
||||
auto const account =
|
||||
ledger_entry_helpers::requiredAccountID(pd, jss::account, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(pd, jss::account, RpcMalformedAddress);
|
||||
if (!account)
|
||||
return std::unexpected(account.error());
|
||||
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(pd, jss::seq, "malformedRequest");
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(pd, jss::seq, RpcMalformedRequest);
|
||||
if (!seq)
|
||||
return std::unexpected(seq.error());
|
||||
|
||||
@@ -747,7 +744,7 @@ parseRippleState(
|
||||
if (!jvRippleState[jss::accounts].isArray() || jvRippleState[jss::accounts].size() != 2)
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedRequest", jss::accounts, "length-2 array of Accounts");
|
||||
RpcMalformedRequest, jss::accounts, "length-2 array of Accounts");
|
||||
}
|
||||
|
||||
auto const id1 = ledger_entry_helpers::parse<AccountID>(jvRippleState[jss::accounts][0u]);
|
||||
@@ -755,19 +752,19 @@ parseRippleState(
|
||||
if (!id1 || !id2)
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedAddress", jss::accounts, "array of Accounts");
|
||||
RpcMalformedAddress, jss::accounts, "array of Accounts");
|
||||
}
|
||||
if (id1 == id2)
|
||||
{
|
||||
return ledger_entry_helpers::malformedError(
|
||||
"malformedRequest", "Cannot have a trustline to self.");
|
||||
RpcMalformedRequest, "Cannot have a trustline to self.");
|
||||
}
|
||||
|
||||
if (!jvRippleState[jss::currency].isString() || jvRippleState[jss::currency] == "" ||
|
||||
!toCurrency(uCurrency, jvRippleState[jss::currency].asString()))
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedCurrency", jss::currency, "Currency");
|
||||
RpcMalformedCurrency, jss::currency, "Currency");
|
||||
}
|
||||
|
||||
return keylet::trustLine(*id1, *id2, uCurrency).key;
|
||||
@@ -785,7 +782,7 @@ parseSignerList(
|
||||
}
|
||||
|
||||
auto const id =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, RpcMalformedAddress);
|
||||
if (!id)
|
||||
return std::unexpected(id.error());
|
||||
|
||||
@@ -802,12 +799,12 @@ parseSponsorship(
|
||||
return parseObjectID(params, fieldName);
|
||||
|
||||
auto const sponsorID =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::sponsor, "malformedSponsor");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::sponsor, RpcMalformedSponsor);
|
||||
if (!sponsorID)
|
||||
return std::unexpected(sponsorID.error());
|
||||
|
||||
auto const sponseeID =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::sponsee, "malformedSponsee");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::sponsee, RpcMalformedSponsee);
|
||||
if (!sponseeID)
|
||||
return std::unexpected(sponseeID.error());
|
||||
|
||||
@@ -829,14 +826,14 @@ parseTransactionProposal(
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
|
||||
auto const targetID =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, RpcMalformedAddress);
|
||||
if (!targetID)
|
||||
return std::unexpected(targetID.error());
|
||||
|
||||
// The proposed transaction's TicketSequence (a proposed transaction is
|
||||
// ticket-only), mirroring how parseTicket looks up a Ticket object.
|
||||
auto const ticketSequence =
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::ticket_seq, "malformedRequest");
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::ticket_seq, RpcMalformedRequest);
|
||||
if (!ticketSequence)
|
||||
return std::unexpected(ticketSequence.error());
|
||||
|
||||
@@ -855,12 +852,12 @@ parseTicket(
|
||||
}
|
||||
|
||||
auto const id =
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
|
||||
ledger_entry_helpers::requiredAccountID(params, jss::account, RpcMalformedAddress);
|
||||
if (!id)
|
||||
return std::unexpected(id.error());
|
||||
|
||||
auto const seq =
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::ticket_seq, "malformedRequest");
|
||||
ledger_entry_helpers::requiredUInt32(params, jss::ticket_seq, RpcMalformedRequest);
|
||||
if (!seq)
|
||||
return std::unexpected(seq.error());
|
||||
|
||||
@@ -879,11 +876,11 @@ parseVault(
|
||||
return parseObjectID(params, fieldName);
|
||||
}
|
||||
|
||||
auto const id = ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner");
|
||||
auto const id = ledger_entry_helpers::requiredAccountID(params, jss::owner, RpcMalformedOwner);
|
||||
if (!id)
|
||||
return std::unexpected(id.error());
|
||||
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest");
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, RpcMalformedRequest);
|
||||
if (!seq)
|
||||
return std::unexpected(seq.error());
|
||||
|
||||
@@ -907,7 +904,7 @@ parseXChainOwnedClaimID(
|
||||
return std::unexpected(bridgeSpec.error());
|
||||
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(
|
||||
claimId, jss::xchain_owned_claim_id, "malformedXChainOwnedClaimID");
|
||||
claimId, jss::xchain_owned_claim_id, RpcMalformedXChainOwnedClaimID);
|
||||
if (!seq)
|
||||
{
|
||||
return std::unexpected(seq.error());
|
||||
@@ -935,7 +932,7 @@ parseXChainOwnedCreateAccountClaimID(
|
||||
auto const seq = ledger_entry_helpers::requiredUInt32(
|
||||
claimId,
|
||||
jss::xchain_owned_create_account_claim_id,
|
||||
"malformedXChainOwnedCreateAccountClaimID");
|
||||
RpcMalformedXChainOwnedCreateAccountClaimID);
|
||||
if (!seq)
|
||||
{
|
||||
return std::unexpected(seq.error());
|
||||
@@ -1042,7 +1039,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,47 +24,90 @@
|
||||
|
||||
namespace xrpl::ledger_entry_helpers {
|
||||
|
||||
/**
|
||||
* Reports @p field as absent, under the token @p code names.
|
||||
*
|
||||
* `error_code` is `invalidParams` (31) whatever @p code is: clients match on 31
|
||||
* for these tokens, so a token whose own code differs disagrees with the code
|
||||
* beside it, and reconciling the two is the reply envelope's concern.
|
||||
*
|
||||
* @param field The field the request did not name.
|
||||
* @param code The code whose token names the field.
|
||||
* @return The error, as an unexpected value.
|
||||
*/
|
||||
inline std::unexpected<json::Value>
|
||||
missingFieldError(json::StaticString const field, std::optional<std::string> err = std::nullopt)
|
||||
missingFieldError(json::StaticString const field, ErrorCodeI code = RpcMalformedRequest)
|
||||
{
|
||||
json::Value json = json::ValueType::Object;
|
||||
json[jss::error] = err.value_or("malformedRequest");
|
||||
json[jss::error] = rpc::getErrorInfo(code).token;
|
||||
json[jss::error_code] = RpcInvalidParams;
|
||||
json[jss::error_message] = rpc::missingFieldMessage(std::string(field.cStr()));
|
||||
return std::unexpected(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports @p field as holding something that is not @p type, under the token
|
||||
* @p code names.
|
||||
*
|
||||
* Reports `invalidParams` as the code, for the reason missingFieldError gives.
|
||||
*
|
||||
* @param code The code whose token names the field.
|
||||
* @param field The field whose value is wrong.
|
||||
* @param type The type the field was expected to hold, named in the message.
|
||||
* @return The error, as an unexpected value.
|
||||
*/
|
||||
inline std::unexpected<json::Value>
|
||||
invalidFieldError(std::string const& err, json::StaticString const field, std::string const& type)
|
||||
invalidFieldError(ErrorCodeI code, json::StaticString const field, std::string const& type)
|
||||
{
|
||||
json::Value json = json::ValueType::Object;
|
||||
json[jss::error] = err;
|
||||
json[jss::error] = rpc::getErrorInfo(code).token;
|
||||
json[jss::error_code] = RpcInvalidParams;
|
||||
json[jss::error_message] = rpc::expectedFieldMessage(field, type);
|
||||
return std::unexpected(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports @p message under the token @p code names.
|
||||
*
|
||||
* For a value of the right type that is wrong in a way only the caller can
|
||||
* state. Reports `invalidParams` as the code, for the reason
|
||||
* missingFieldError gives.
|
||||
*
|
||||
* @param code The code whose token names the error. Callers pass a field's
|
||||
* token or a condition's, such as a trust line to self.
|
||||
* @param message What is wrong.
|
||||
* @return The error, as an unexpected value.
|
||||
*/
|
||||
inline std::unexpected<json::Value>
|
||||
malformedError(std::string const& err, std::string const& message)
|
||||
malformedError(ErrorCodeI code, std::string const& message)
|
||||
{
|
||||
json::Value json = json::ValueType::Object;
|
||||
json[jss::error] = err;
|
||||
json[jss::error] = rpc::getErrorInfo(code).token;
|
||||
json[jss::error_code] = RpcInvalidParams;
|
||||
json[jss::error_message] = message;
|
||||
return std::unexpected(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the first of @p fields the request does not name, if any.
|
||||
*
|
||||
* @param params The request's parameters.
|
||||
* @param fields The fields the request must name.
|
||||
* @param code The code whose token names a missing field. Defaults to
|
||||
* `RpcMalformedRequest`, as missingFieldError's does.
|
||||
* @return true if every field is named, otherwise the error.
|
||||
*/
|
||||
inline std::expected<bool, json::Value>
|
||||
hasRequired(
|
||||
json::Value const& params,
|
||||
std::initializer_list<json::StaticString> fields,
|
||||
std::optional<std::string> err = std::nullopt)
|
||||
ErrorCodeI code = RpcMalformedRequest)
|
||||
{
|
||||
for (auto const field : fields)
|
||||
{
|
||||
if (!params.isMember(field) || params[field].isNull())
|
||||
{
|
||||
return missingFieldError(field, err);
|
||||
return missingFieldError(field, code);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -74,12 +117,25 @@ template <class T>
|
||||
std::optional<T>
|
||||
parse(json::Value const& param);
|
||||
|
||||
/**
|
||||
* Reads @p fieldName out of @p params, parsed as T.
|
||||
*
|
||||
* Absent or null is a missing field; present but unparsable is an invalid one
|
||||
* of @p expectedType.
|
||||
*
|
||||
* @param params The request parameters.
|
||||
* @param fieldName The field to read.
|
||||
* @param code The code whose token names the field in an error. `error_code`
|
||||
* stays `invalidParams` whatever it is.
|
||||
* @param expectedType The type named in an invalid-field message.
|
||||
* @return The parsed value, or the error to report.
|
||||
*/
|
||||
template <class T>
|
||||
std::expected<T, json::Value>
|
||||
required(
|
||||
json::Value const& params,
|
||||
json::StaticString const fieldName,
|
||||
std::string const& err,
|
||||
ErrorCodeI code,
|
||||
std::string const& expectedType)
|
||||
{
|
||||
if (!params.isMember(fieldName) || params[fieldName].isNull())
|
||||
@@ -90,7 +146,7 @@ required(
|
||||
{
|
||||
return *obj;
|
||||
}
|
||||
return invalidFieldError(err, fieldName, expectedType);
|
||||
return invalidFieldError(code, fieldName, expectedType);
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -109,13 +165,14 @@ parse(json::Value const& param)
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param code The code whose token names the field in an error. `error_code`
|
||||
* stays `invalidParams` whatever it is.
|
||||
*/
|
||||
inline std::expected<AccountID, json::Value>
|
||||
requiredAccountID(
|
||||
json::Value const& params,
|
||||
json::StaticString const fieldName,
|
||||
std::string const& err)
|
||||
requiredAccountID(json::Value const& params, json::StaticString const fieldName, ErrorCodeI code)
|
||||
{
|
||||
return required<AccountID>(params, fieldName, err, "AccountID");
|
||||
return required<AccountID>(params, fieldName, code, "AccountID");
|
||||
}
|
||||
|
||||
inline std::optional<Blob>
|
||||
@@ -131,12 +188,16 @@ parseHexBlob(json::Value const& param, std::size_t maxLength)
|
||||
return blob;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param code The code whose token names the field in an error. `error_code`
|
||||
* stays `invalidParams` whatever it is.
|
||||
*/
|
||||
inline std::expected<Blob, json::Value>
|
||||
requiredHexBlob(
|
||||
json::Value const& params,
|
||||
json::StaticString const fieldName,
|
||||
std::size_t maxLength,
|
||||
std::string const& err)
|
||||
ErrorCodeI code)
|
||||
{
|
||||
if (!params.isMember(fieldName) || params[fieldName].isNull())
|
||||
{
|
||||
@@ -146,7 +207,7 @@ requiredHexBlob(
|
||||
{
|
||||
return *blob;
|
||||
}
|
||||
return invalidFieldError(err, fieldName, "hex string");
|
||||
return invalidFieldError(code, fieldName, "hex string");
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -166,13 +227,14 @@ parse(json::Value const& param)
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param code The code whose token names the field in an error. `error_code`
|
||||
* stays `invalidParams` whatever it is.
|
||||
*/
|
||||
inline std::expected<std::uint32_t, json::Value>
|
||||
requiredUInt32(
|
||||
json::Value const& params,
|
||||
json::StaticString const fieldName,
|
||||
std::string const& err)
|
||||
requiredUInt32(json::Value const& params, json::StaticString const fieldName, ErrorCodeI code)
|
||||
{
|
||||
return required<std::uint32_t>(params, fieldName, err, "number");
|
||||
return required<std::uint32_t>(params, fieldName, code, "number");
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -188,13 +250,14 @@ parse(json::Value const& param)
|
||||
return uNodeIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param code The code whose token names the field in an error. `error_code`
|
||||
* stays `invalidParams` whatever it is.
|
||||
*/
|
||||
inline std::expected<UInt256, json::Value>
|
||||
requiredUInt256(
|
||||
json::Value const& params,
|
||||
json::StaticString const fieldName,
|
||||
std::string const& err)
|
||||
requiredUInt256(json::Value const& params, json::StaticString const fieldName, ErrorCodeI code)
|
||||
{
|
||||
return required<UInt256>(params, fieldName, err, "Hash256");
|
||||
return required<UInt256>(params, fieldName, code, "Hash256");
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -210,13 +273,14 @@ parse(json::Value const& param)
|
||||
return field;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param code The code whose token names the field in an error. `error_code`
|
||||
* stays `invalidParams` whatever it is.
|
||||
*/
|
||||
inline std::expected<UInt192, json::Value>
|
||||
requiredUInt192(
|
||||
json::Value const& params,
|
||||
json::StaticString const fieldName,
|
||||
std::string const& err)
|
||||
requiredUInt192(json::Value const& params, json::StaticString const fieldName, ErrorCodeI code)
|
||||
{
|
||||
return required<UInt192>(params, fieldName, err, "Hash192");
|
||||
return required<UInt192>(params, fieldName, code, "Hash192");
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -233,12 +297,24 @@ parse(json::Value const& param)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param code The code whose token names the field in an error. `error_code`
|
||||
* stays `invalidParams` whatever it is.
|
||||
*/
|
||||
inline std::expected<Asset, json::Value>
|
||||
requiredAsset(json::Value const& params, json::StaticString const fieldName, std::string const& err)
|
||||
requiredAsset(json::Value const& params, json::StaticString const fieldName, ErrorCodeI code)
|
||||
{
|
||||
return required<Asset>(params, fieldName, err, "Asset");
|
||||
return required<Asset>(params, fieldName, code, "Asset");
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the bridge the four `*ChainDoor` and `*ChainIssue` fields of @p params
|
||||
* describe.
|
||||
*
|
||||
* @param params The request object naming the four fields.
|
||||
* @return The bridge, or the error of the first field that is missing or
|
||||
* malformed.
|
||||
*/
|
||||
inline std::expected<STXChainBridge, json::Value>
|
||||
parseBridgeFields(json::Value const& params)
|
||||
{
|
||||
@@ -256,14 +332,14 @@ parseBridgeFields(json::Value const& params)
|
||||
}
|
||||
|
||||
auto const lockingChainDoor =
|
||||
requiredAccountID(params, jss::LockingChainDoor, "malformedLockingChainDoor");
|
||||
requiredAccountID(params, jss::LockingChainDoor, RpcMalformedLockingChainDoor);
|
||||
if (!lockingChainDoor)
|
||||
{
|
||||
return std::unexpected(lockingChainDoor.error());
|
||||
}
|
||||
|
||||
auto const issuingChainDoor =
|
||||
requiredAccountID(params, jss::IssuingChainDoor, "malformedIssuingChainDoor");
|
||||
requiredAccountID(params, jss::IssuingChainDoor, RpcMalformedIssuingChainDoor);
|
||||
if (!issuingChainDoor)
|
||||
{
|
||||
return std::unexpected(issuingChainDoor.error());
|
||||
@@ -276,7 +352,7 @@ parseBridgeFields(json::Value const& params)
|
||||
}
|
||||
catch (std::runtime_error const& ex)
|
||||
{
|
||||
return invalidFieldError("malformedIssue", jss::LockingChainIssue, "Issue");
|
||||
return invalidFieldError(RpcMalformedIssue, jss::LockingChainIssue, "Issue");
|
||||
}
|
||||
|
||||
Issue issuingChainIssue;
|
||||
@@ -286,7 +362,7 @@ parseBridgeFields(json::Value const& params)
|
||||
}
|
||||
catch (std::runtime_error const& ex)
|
||||
{
|
||||
return invalidFieldError("malformedIssue", jss::IssuingChainIssue, "Issue");
|
||||
return invalidFieldError(RpcMalformedIssue, jss::IssuingChainIssue, "Issue");
|
||||
}
|
||||
|
||||
return STXChainBridge(
|
||||
|
||||
@@ -12,6 +12,16 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Serves `path_find`: `create` opens a path request that pushes updates to the
|
||||
* connection, recording the API version this request named on it, `close` ends
|
||||
* the connection's open request and `status` reports it.
|
||||
*
|
||||
* @param context The request and the connection it arrived on.
|
||||
* @return The subcommand's result, or an error when pathfinding is disabled,
|
||||
* the subcommand is missing or unknown, no connection can receive
|
||||
* updates, or there is no open request to close or report.
|
||||
*/
|
||||
json::Value
|
||||
doPathFind(rpc::JsonContext& context)
|
||||
{
|
||||
@@ -28,8 +38,6 @@ doPathFind(rpc::JsonContext& context)
|
||||
if (!context.infoSub)
|
||||
return rpcError(RpcNoEvents);
|
||||
|
||||
context.infoSub->setApiVersion(context.apiVersion);
|
||||
|
||||
auto const sSubCommand = context.params[jss::subcommand].asString();
|
||||
|
||||
if (sSubCommand == "create")
|
||||
@@ -37,7 +45,7 @@ doPathFind(rpc::JsonContext& context)
|
||||
context.loadType = resource::kFeeHeavyBurdenRpc;
|
||||
context.infoSub->clearRequest();
|
||||
return context.app.getPathRequestManager().makePathRequest(
|
||||
context.infoSub, lpLedger, context.params);
|
||||
context.infoSub, lpLedger, context.params, context.apiVersion);
|
||||
}
|
||||
|
||||
if (sSubCommand == "close")
|
||||
|
||||
@@ -46,6 +46,18 @@ wouldExceedSubscriptionCap(InfoSub::Ref ispSub, std::size_t additional, std::siz
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* Registers the streams, accounts, books and MPT issuances @p context names
|
||||
* for the connection the request arrived on, or for the webhook url it names.
|
||||
*
|
||||
* Every subscription one subscriber holds is served at one API version, so a
|
||||
* request naming a version other than the one already established is refused
|
||||
* with `apiVersionConflict` before anything is registered.
|
||||
*
|
||||
* @param context The request, its connection and the server it runs on.
|
||||
* @return The registrations' first messages, for the streams that send one, or
|
||||
* an error.
|
||||
*/
|
||||
json::Value
|
||||
doSubscribe(rpc::JsonContext& context)
|
||||
{
|
||||
@@ -59,6 +71,29 @@ doSubscribe(rpc::JsonContext& context)
|
||||
return rpcError(RpcInvalidParams);
|
||||
}
|
||||
|
||||
// Every subscription on a subscriber is served at one API version; see
|
||||
// establishSubscriptionVersion. Checked before any stream is registered, so a refused call
|
||||
// still fixes the version, which is why the message says "served at". An ordinary request is
|
||||
// answered at its own version.
|
||||
auto const refuseVersionConflict = [&] -> std::optional<json::Value> {
|
||||
if (auto const established = ispSub->establishSubscriptionVersion(context.apiVersion))
|
||||
{
|
||||
// The url message states the condition and no remedy: the subscriber is keyed on its
|
||||
// url alone, so releasing it would remove whoever subscribed it first, and the server
|
||||
// cannot tell the two callers apart. The connection message does name its remedy, a
|
||||
// second connection harming nobody.
|
||||
auto const subject = context.params.isMember(jss::url)
|
||||
? "Subscriptions on this url are served at api_version " +
|
||||
std::to_string(*established) + "."
|
||||
: "Subscriptions on this connection are served at api_version " +
|
||||
std::to_string(*established) + "; use a new connection.";
|
||||
|
||||
JLOG(context.j.debug()) << "doSubscribe: " << subject;
|
||||
return rpc::makeError(RpcApiVersionConflict, subject);
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
if (context.params.isMember(jss::url))
|
||||
{
|
||||
if (context.role != Role::ADMIN)
|
||||
@@ -106,6 +141,13 @@ doSubscribe(rpc::JsonContext& context)
|
||||
{
|
||||
JLOG(context.j.trace()) << "doSubscribe: reusing: " << strUrl;
|
||||
|
||||
// Checked before the credentials are written, because this subscriber belongs to
|
||||
// whoever subscribed the url first: a refused subscribe that had already overwritten
|
||||
// their username would leave their endpoint rejecting the stream it still receives,
|
||||
// with nothing to tell them why.
|
||||
if (auto const conflict = refuseVersionConflict())
|
||||
return *conflict;
|
||||
|
||||
if (auto const rpcSub = std::dynamic_pointer_cast<RPCSub>(ispSub))
|
||||
{
|
||||
// Why do we need to check isMember against jss::username and
|
||||
@@ -123,7 +165,9 @@ doSubscribe(rpc::JsonContext& context)
|
||||
{
|
||||
ispSub = context.infoSub;
|
||||
}
|
||||
ispSub->setApiVersion(context.apiVersion);
|
||||
|
||||
if (auto const conflict = refuseVersionConflict())
|
||||
return *conflict;
|
||||
|
||||
// Effective per-connection subscription cap: a configured override if set,
|
||||
// otherwise the built-in default. Resolved once and reused by every branch.
|
||||
@@ -146,43 +190,44 @@ doSubscribe(rpc::JsonContext& context)
|
||||
std::string const streamName = it.asString();
|
||||
if (streamName == "server")
|
||||
{
|
||||
context.netOps.subServer(ispSub, jvResult, context.role == Role::ADMIN);
|
||||
context.netOps.subServer(
|
||||
ispSub, jvResult, context.role == Role::ADMIN, context.apiVersion);
|
||||
}
|
||||
else if (streamName == "ledger")
|
||||
{
|
||||
context.netOps.subLedger(ispSub, jvResult);
|
||||
context.netOps.subLedger(ispSub, jvResult, context.apiVersion);
|
||||
}
|
||||
else if (streamName == "book_changes")
|
||||
{
|
||||
context.netOps.subBookChanges(ispSub);
|
||||
context.netOps.subBookChanges(ispSub, context.apiVersion);
|
||||
}
|
||||
else if (streamName == "manifests")
|
||||
{
|
||||
context.netOps.subManifests(ispSub);
|
||||
context.netOps.subManifests(ispSub, context.apiVersion);
|
||||
}
|
||||
else if (streamName == "transactions")
|
||||
{
|
||||
context.netOps.subTransactions(ispSub);
|
||||
context.netOps.subTransactions(ispSub, context.apiVersion);
|
||||
}
|
||||
else if (
|
||||
streamName == "transactions_proposed" ||
|
||||
streamName == "rt_transactions") // DEPRECATED
|
||||
{
|
||||
context.netOps.subRTTransactions(ispSub);
|
||||
context.netOps.subRTTransactions(ispSub, context.apiVersion);
|
||||
}
|
||||
else if (streamName == "validations")
|
||||
{
|
||||
context.netOps.subValidations(ispSub);
|
||||
context.netOps.subValidations(ispSub, context.apiVersion);
|
||||
}
|
||||
else if (streamName == "peer_status")
|
||||
{
|
||||
if (context.role != Role::ADMIN)
|
||||
return rpcError(RpcNoPermission);
|
||||
context.netOps.subPeerStatus(ispSub);
|
||||
context.netOps.subPeerStatus(ispSub, context.apiVersion);
|
||||
}
|
||||
else if (streamName == "consensus")
|
||||
{
|
||||
context.netOps.subConsensus(ispSub);
|
||||
context.netOps.subConsensus(ispSub, context.apiVersion);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -238,11 +283,11 @@ doSubscribe(rpc::JsonContext& context)
|
||||
}
|
||||
|
||||
if (hasProposed)
|
||||
context.netOps.subAccount(ispSub, proposedIds, true);
|
||||
context.netOps.subAccount(ispSub, proposedIds, true, context.apiVersion);
|
||||
|
||||
if (hasAccounts)
|
||||
{
|
||||
context.netOps.subAccount(ispSub, accountIds, false);
|
||||
context.netOps.subAccount(ispSub, accountIds, false, context.apiVersion);
|
||||
JLOG(context.j.debug()) << "doSubscribe: accounts: " << accountIds.size();
|
||||
}
|
||||
|
||||
@@ -267,7 +312,8 @@ doSubscribe(rpc::JsonContext& context)
|
||||
if (wouldExceedSubscriptionCap(ispSub, historyCharge, subscriptionCap))
|
||||
return rpc::makeParamError("Too many subscriptions for this connection.");
|
||||
|
||||
if (auto const result = context.netOps.subAccountHistory(ispSub, *id); result != RpcSuccess)
|
||||
if (auto const result = context.netOps.subAccountHistory(ispSub, *id, context.apiVersion);
|
||||
result != RpcSuccess)
|
||||
{
|
||||
return rpcError(result);
|
||||
}
|
||||
@@ -337,14 +383,14 @@ doSubscribe(rpc::JsonContext& context)
|
||||
return rpcError(RpcBadMarket);
|
||||
}
|
||||
|
||||
context.netOps.subBook(ispSub, book);
|
||||
context.netOps.subBook(ispSub, book, context.apiVersion);
|
||||
|
||||
// both_sides is deprecated.
|
||||
bool const both = (j.isMember(jss::both) && j[jss::both].asBool()) ||
|
||||
(j.isMember(jss::both_sides) && j[jss::both_sides].asBool());
|
||||
|
||||
if (both)
|
||||
context.netOps.subBook(ispSub, reversed(book));
|
||||
context.netOps.subBook(ispSub, reversed(book), context.apiVersion);
|
||||
|
||||
// state_now is deprecated.
|
||||
if ((j.isMember(jss::snapshot) && j[jss::snapshot].asBool()) ||
|
||||
@@ -409,7 +455,7 @@ doSubscribe(rpc::JsonContext& context)
|
||||
if (!ispSub->tryReserveMPTSubscriptions(ids, subscriptionCap))
|
||||
return rpc::makeParamError("Too many subscriptions for this connection.");
|
||||
|
||||
context.netOps.subMPT(ispSub, ids);
|
||||
context.netOps.subMPT(ispSub, ids, context.apiVersion);
|
||||
JLOG(context.j.debug()) << "doSubscribe: mpts: " << ids.size();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user