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
@@ -103,7 +103,6 @@ words:
|
||||
- dearmor
|
||||
- decryptor
|
||||
- dedented
|
||||
- dedup
|
||||
- deleteme
|
||||
- demultiplexer
|
||||
- deserializaton
|
||||
@@ -345,7 +344,6 @@ words:
|
||||
- unambiguity
|
||||
- unauthorizes
|
||||
- unauthorizing
|
||||
- undeserializable
|
||||
- unergonomic
|
||||
- unfetched
|
||||
- unfindable
|
||||
|
||||
@@ -36,18 +36,3 @@ updates:
|
||||
rust-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: uv
|
||||
directory: /cmake/scripts/codegen
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
time: "04:00"
|
||||
timezone: Etc/GMT
|
||||
commit-message:
|
||||
prefix: "chore: [DEPENDABOT] "
|
||||
target-branch: develop
|
||||
groups:
|
||||
codegen-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
@@ -62,7 +62,6 @@ libxrpl.tx > xrpl.protocol
|
||||
libxrpl.tx > xrpl.server
|
||||
libxrpl.tx > xrpl.tx
|
||||
test.app > test.jtx
|
||||
test.app > tests.libxrpl
|
||||
test.app > test.unit_test
|
||||
test.app > xrpl.basics
|
||||
test.app > xrpl.config
|
||||
@@ -175,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
|
||||
|
||||
@@ -8,7 +8,6 @@ on:
|
||||
branches:
|
||||
- "develop"
|
||||
- "release/*"
|
||||
- "staging/*"
|
||||
paths:
|
||||
- "crates/**/Cargo.toml"
|
||||
- "crates/Cargo.lock"
|
||||
|
||||
@@ -243,7 +243,7 @@ jobs:
|
||||
- should-run
|
||||
- build-test
|
||||
# Only run when committing to a PR that targets a release branch.
|
||||
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release/') }}
|
||||
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release') }}
|
||||
uses: ./.github/workflows/reusable-upload-recipe.yml
|
||||
secrets:
|
||||
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
# This workflow runs all workflows to build and test the code on various Linux
|
||||
# flavors, as well as on MacOS and Windows, on a scheduled basis, on merge into
|
||||
# the 'develop', 'release/*' or 'staging/*' branches, or when requested
|
||||
# manually. Upon pushes to the develop branch, it also uploads the libxrpl
|
||||
# recipe to the Conan remote.
|
||||
# the 'develop' or 'release*' branches, or when requested manually. Upon pushes
|
||||
# to the develop branch it also uploads the libxrpl recipe to the Conan remote.
|
||||
name: Trigger
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "develop"
|
||||
- "release/*"
|
||||
- "staging/*"
|
||||
- "release*"
|
||||
paths:
|
||||
# These paths are unique to `on-trigger.yml`.
|
||||
- ".github/workflows/on-trigger.yml"
|
||||
@@ -64,9 +62,9 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
# When a PR is merged into the develop branch, it will be assigned a unique
|
||||
# When a PR is merged into the develop branch it will be assigned a unique
|
||||
# group identifier, so execution will continue even if another PR is merged
|
||||
# while it is still running. In all other cases, the group identifier is shared
|
||||
# while it is still running. In all other cases the group identifier is shared
|
||||
# per branch, so that any in-progress runs are cancelled when a new commit is
|
||||
# pushed.
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' && github.sha || github.ref }}
|
||||
@@ -100,7 +98,7 @@ jobs:
|
||||
# However, we do not enable ccache for events targeting a release branch,
|
||||
# to protect against the rare case that the output produced by ccache is
|
||||
# not identical to a regular compilation.
|
||||
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !startsWith(github.ref, 'refs/heads/release/') }}
|
||||
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !startsWith(github.ref, 'refs/heads/release') }}
|
||||
os: ${{ matrix.os }}
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -8,8 +8,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "develop"
|
||||
- "release/*"
|
||||
- "staging/*"
|
||||
- "release*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -31,12 +31,6 @@ on:
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
unittest_args:
|
||||
description: "Additional arguments to pass to rippled when running tests"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
cmake_target:
|
||||
description: "The CMake target to build."
|
||||
required: true
|
||||
@@ -364,7 +358,6 @@ jobs:
|
||||
working-directory: ${{ runner.os == 'Windows' && format('{0}/{1}', env.BUILD_DIR, inputs.build_type) || env.BUILD_DIR }}
|
||||
env:
|
||||
BUILD_NPROC: ${{ steps.nproc.outputs.nproc }}
|
||||
UNITTEST_ARGS: ${{ inputs.unittest_args }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
# Coverage builds are slower due to instrumentation; use fewer parallel jobs to avoid flakiness
|
||||
|
||||
@@ -45,7 +45,6 @@ jobs:
|
||||
build_type: ${{ matrix.build_type }}
|
||||
ccache_enabled: ${{ inputs.ccache_enabled }}
|
||||
cmake_args: ${{ matrix.cmake_args }}
|
||||
unittest_args: ${{ matrix.unittest_args }}
|
||||
cmake_target: ${{ matrix.cmake_target }}
|
||||
runs_on: ${{ toJSON(matrix.architecture.runner) }}
|
||||
image: ${{ matrix.image || '' }}
|
||||
|
||||
@@ -123,19 +123,13 @@ jobs:
|
||||
|
||||
- name: Fix style
|
||||
if: ${{ steps.files_changed.outcome != 'success' }}
|
||||
# pre-commit installs every hook's environment, even for hooks that match
|
||||
# no files, and node-based hooks need libatomic, which this image lacks.
|
||||
# Exit code 1 just means hooks modified files or found issues.
|
||||
env:
|
||||
SKIP: prettier,cspell
|
||||
run: |
|
||||
git add --update
|
||||
pre-commit run || [ $? -eq 1 ]
|
||||
pre-commit run --all-files || true
|
||||
|
||||
- name: Generate git diff
|
||||
if: ${{ steps.files_changed.outcome != 'success' }}
|
||||
run: |
|
||||
git diff HEAD | tee "${DIFF_FILE}"
|
||||
git diff | tee "${DIFF_FILE}"
|
||||
|
||||
- name: Upload clang-tidy diff output
|
||||
if: ${{ github.event.repository.visibility == 'public' && steps.files_changed.outcome != 'success' }}
|
||||
|
||||
@@ -95,15 +95,11 @@ jobs:
|
||||
apt-get update -qq
|
||||
apt-get install -y "./${PACKAGE}"
|
||||
|
||||
# Rocky Linux and AlmaLinux pick a mirror from a mirrorlist, and now and
|
||||
# then get one that serves the repository metadata at a few hundred kB/s,
|
||||
# which alone outlasts the job's timeout. dnf only abandons a mirror below
|
||||
# 1000 B/s for 30 s, so raise that floor to fail over to the next one fast.
|
||||
- name: Install the RPM
|
||||
if: ${{ inputs.package_type == 'rpm' }}
|
||||
env:
|
||||
PACKAGE: ${{ steps.find.outputs.package }}
|
||||
run: dnf install -y --setopt=minrate=1M --setopt=timeout=10 "./${PACKAGE}"
|
||||
run: dnf install -y "./${PACKAGE}"
|
||||
|
||||
- name: Run xrpld
|
||||
run: xrpld --version
|
||||
|
||||
+39
-5
@@ -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.
|
||||
@@ -35,9 +72,6 @@ Version 3.5.0 is not yet released.
|
||||
|
||||
- `channel_authorize`: The `channel_id` field now returns an `invalidParams` error if the value is not a string. [#7582](https://github.com/XRPLF/rippled/pull/7582)
|
||||
- `channel_verify`: The `channel_id` and `signature` fields now return an `invalidParams` error if the value is not a string. [#7582](https://github.com/XRPLF/rippled/pull/7582)
|
||||
- `account_currencies`, `account_nfts`, `account_offers`: A request with more than one problem may now report a different one of them. An invalid `ledger_hash` or `ledger_index` is reported before an invalid or missing `account`. An invalid `limit` or `marker` is reported before `lgrNotFound` or `actNotFound`, and for `account_offers` also before `actMalformed`. A request with a single problem returns the same error as before. [#8561](https://github.com/XRPLF/rippled/pull/8561)
|
||||
- `deposit_authorized`: A `credentials` entry that is all zeros (for example `"0"` or 64 `0` characters) now returns a `badCredentials` error instead of looking up a credential that can never exist.
|
||||
- `account_info`, `account_lines`, `account_channels`: A request with more than one problem may now report a different one of them. An invalid `ledger_hash` or `ledger_index` is reported before a missing or invalid `account`, or `ident` for `account_info`. For `account_lines`, a `peer` that is not a string, or an invalid `limit` or `marker`, is reported before `lgrNotFound`, `actNotFound` or a malformed `account`, and an invalid `limit` or `marker` also before a malformed `peer`. For `account_channels`, a malformed `account` is reported before `lgrNotFound`, and an invalid `destination_account`, `limit` or `marker` before `lgrNotFound` or `actNotFound`. A request with a single problem returns the same error as before. [#8573](https://github.com/XRPLF/rippled/pull/8573)
|
||||
|
||||
### Bugfixes in 3.5.0
|
||||
|
||||
|
||||
+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.
|
||||
@@ -2,12 +2,33 @@
|
||||
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| These instructions assume you have a C++ development environment ready with Git, Python, Conan, CMake, and a C++ compiler. For help setting one up on Linux, macOS, or Windows, [see this guide](./docs/build/environment.md).<br><br>These instructions also assume a basic familiarity with Conan and CMake. If you are unfamiliar with Conan, you can read our [crash course](./docs/build/conan.md) or the official [Getting Started][conan-getting-started] walkthrough. |
|
||||
|
||||
## Supported platforms
|
||||
## Minimum Requirements
|
||||
|
||||
Building and testing are supported on Linux, macOS, and (best effort) Windows;
|
||||
see [Supported development platforms](./docs/build/environment.md#supported-development-platforms).
|
||||
Running `xrpld` is officially supported on Linux only;
|
||||
see [Requirements](./docs/install.md#requirements).
|
||||
For the hardware needed to run a node, see
|
||||
[System Requirements](https://xrpl.org/system-requirements.html).
|
||||
|
||||
For the software needed to build xrpld, see the
|
||||
[environment setup guide](./docs/build/environment.md).
|
||||
|
||||
## Operating Systems
|
||||
|
||||
### Linux
|
||||
|
||||
The Ubuntu Linux distribution has received the highest level of quality
|
||||
assurance, testing, and support. We also support Red Hat and use Debian
|
||||
internally.
|
||||
Our Linux CI tooling is distro-independent and uses a Nix-based environment, so it should be possible to build on other Linux distributions as well, although we have not tested them.
|
||||
|
||||
### macOS
|
||||
|
||||
Many `xrpld` engineers use macOS for development.
|
||||
The minimum supported version is macOS 15 (Sequoia).
|
||||
CI testing is done in macOS 26 (Tahoe), but the build defaults `CMAKE_OSX_DEPLOYMENT_TARGET` to 15.
|
||||
|
||||
### Windows
|
||||
|
||||
Windows is used by some engineers for development only, and is not recommended
|
||||
for production use.
|
||||
|
||||
## Steps
|
||||
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ if(is_gcc)
|
||||
# GCC-specific fixes
|
||||
add_compile_options(-Wno-unknown-pragmas -Wno-subobject-linkage)
|
||||
# -Wno-subobject-linkage can be removed when we upgrade GCC version to at least 13.3
|
||||
elseif(is_any_clang)
|
||||
elseif(is_clang)
|
||||
# Clang-specific fixes
|
||||
add_compile_options(-Wno-unknown-warning-option) # Ignore unknown warning options
|
||||
elseif(is_msvc)
|
||||
|
||||
+1
-1
@@ -332,7 +332,7 @@ This ensures that configuration changes don't introduce new warnings across the
|
||||
|
||||
### Installing clang-tidy
|
||||
|
||||
The [Nix development shell](./docs/build/nix.md) provides `clang-tidy` at the [tested version](./docs/build/environment.md#tested-compiler-versions).
|
||||
See the [environment setup guide](./docs/build/environment.md#clang-tidy) for how to get clang-tidy.
|
||||
|
||||
### Running clang-tidy locally
|
||||
|
||||
|
||||
+10
-16
@@ -34,7 +34,7 @@ so tools can filter them and protection rules can target `release/*` and `stagin
|
||||
- **Branches only move forward.**
|
||||
`develop`, `staging/X.Y.x`, and `release/X.Y.x` are never rewritten.
|
||||
- **Cherry-picking only goes from `develop` to a staging branch**,
|
||||
for fixes that must get into a release after the [code freeze](#release-candidates)
|
||||
for fixes that must get into a release after the code freeze
|
||||
(see [Emergency Fixes From `develop`](#emergency-fixes-from-develop)).
|
||||
- **Security fixes are prepared privately and published with the release that contains them**,
|
||||
so vulnerabilities are not disclosed prematurely.
|
||||
@@ -49,7 +49,6 @@ develop staging/X.Y.x & release/X.Y.x
|
||||
│
|
||||
├── Tag: X.Y.0-b1
|
||||
├── Tag: X.Y.0-bN
|
||||
│ (2 weeks before rc1: freeze on large changes starts)
|
||||
├── Tag: X.Y.0-rc1 ───────────────┐ (branches created)
|
||||
│ │
|
||||
│ (development continues) ├── Fixes
|
||||
@@ -76,20 +75,16 @@ develop staging/X.Y.x & release/X.Y.x
|
||||
3. **Branches:** `staging/X.Y.x` and `release/X.Y.x` are created from `develop`
|
||||
at the commit tagged `X.Y.0-rc1`.
|
||||
The first RC also kicks off the QE process.
|
||||
|
||||
### Freeze on Large Changes
|
||||
|
||||
From two weeks before `X.Y.0-rc1` until `X.Y.0` is [merged back](#merging-back-into-develop),
|
||||
large changes (e.g., big refactors, moving or renaming many files, mass reformatting)
|
||||
are not merged into `develop`.
|
||||
4. **Code freeze:** No new features or unrelated changes are pulled from `develop`
|
||||
into `staging/X.Y.x` or `release/X.Y.x`.
|
||||
Only critical stabilization fixes go into the line.
|
||||
5. **No large changes on `develop`:** Until `X.Y.0` is [merged back](#merging-back-into-develop),
|
||||
large changes (e.g., big refactors, moving or renaming many files, mass reformatting)
|
||||
are not merged into `develop`,
|
||||
so that fixes on the line and the merge back don't run into conflicts.
|
||||
|
||||
### Release Candidates
|
||||
|
||||
After branching, the line is in code freeze:
|
||||
no new features or unrelated changes are pulled from `develop`
|
||||
into `staging/X.Y.x` or `release/X.Y.x`.
|
||||
Only critical stabilization fixes go into the line.
|
||||
|
||||
1. Fixes are developed against `staging/X.Y.x`.
|
||||
2. When ready, a new RC is created on `staging/X.Y.x`,
|
||||
and `release/X.Y.x` is fast-forwarded to it.
|
||||
@@ -111,8 +106,7 @@ one right after the other.
|
||||
2. `release/X.Y.x` is fast-forwarded to `staging/X.Y.x`,
|
||||
and the release is tagged on it.
|
||||
3. The release is immediately [merged back into `develop`](#merging-back-into-develop).
|
||||
For a major-minor release, this lifts the [freeze on large changes](#freeze-on-large-changes)
|
||||
in `develop`.
|
||||
For a major-minor release, this lifts the freeze on large changes in `develop`.
|
||||
|
||||
### Merging Back Into `develop`
|
||||
|
||||
@@ -133,7 +127,7 @@ After the merge, `git log develop..<tag>` must be empty.
|
||||
### Emergency Fixes From `develop`
|
||||
|
||||
If a commit was merged to `develop`
|
||||
and needs to be included in a release after the [code freeze](#release-candidates):
|
||||
and needs to be included in a release after the code freeze:
|
||||
|
||||
1. Create a PR that cherry-picks the commit onto `staging/X.Y.x`.
|
||||
2. Leave `develop` as it is, with no reverts.
|
||||
|
||||
+5
-34
@@ -1360,39 +1360,6 @@
|
||||
# Example:
|
||||
# owner_reserve = 200000 # 0.2 XRP
|
||||
#
|
||||
# gas_limit = <gas>
|
||||
#
|
||||
# The gas limit is the maximum amount of gas that can be
|
||||
# consumed by a single transaction. The gas limit is used to prevent
|
||||
# transactions from consuming too many resources.
|
||||
#
|
||||
# If this parameter is unspecified, xrpld will use an internal
|
||||
# default. Don't change this without understanding the consequences.
|
||||
#
|
||||
# Example:
|
||||
# gas_limit = 1000000 # 1 million gas
|
||||
#
|
||||
# bytecode_size_limit = <bytes>
|
||||
#
|
||||
# The bytecode size limit is the maximum size of a WASM extension in
|
||||
# bytes. The size limit is used to prevent extensions from consuming
|
||||
# too many resources.
|
||||
#
|
||||
# If this parameter is unspecified, xrpld will use an internal
|
||||
# default. Don't change this without understanding the consequences.
|
||||
#
|
||||
# Example:
|
||||
# bytecode_size_limit = 100000 # 100 kb
|
||||
#
|
||||
# gas_price = <micro-drops>
|
||||
#
|
||||
# The gas price is the conversion between WASM gas and its price in drops.
|
||||
#
|
||||
# If this parameter is unspecified, xrpld will use an internal
|
||||
# default. Don't change this without understanding the consequences.
|
||||
#
|
||||
# Example:
|
||||
# gas_price = 1000000 # 1 drop per gas
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# 9. Misc Settings
|
||||
@@ -1494,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
|
||||
|
||||
@@ -9,21 +9,13 @@ include_guard(GLOBAL)
|
||||
# --------------------------------------------------------------------
|
||||
# Compiler detection (C++)
|
||||
# --------------------------------------------------------------------
|
||||
# is_any_clang is true for both LLVM Clang and AppleClang. Their version numbers
|
||||
# and feature sets differ, so use is_llvm_clang or is_apple_clang when it matters.
|
||||
set(is_any_clang FALSE)
|
||||
set(is_llvm_clang FALSE)
|
||||
set(is_apple_clang FALSE)
|
||||
set(is_clang FALSE)
|
||||
set(is_gcc FALSE)
|
||||
set(is_msvc FALSE)
|
||||
set(is_xcode FALSE)
|
||||
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
set(is_any_clang TRUE)
|
||||
set(is_llvm_clang TRUE)
|
||||
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
|
||||
set(is_any_clang TRUE)
|
||||
set(is_apple_clang TRUE)
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") # Clang or AppleClang
|
||||
set(is_clang TRUE)
|
||||
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
set(is_gcc TRUE)
|
||||
elseif(MSVC)
|
||||
|
||||
@@ -130,7 +130,7 @@ else()
|
||||
INTERFACE
|
||||
-Wall
|
||||
-Wdeprecated
|
||||
$<$<BOOL:${is_any_clang}>:-Wno-deprecated-declarations>
|
||||
$<$<BOOL:${is_clang}>:-Wno-deprecated-declarations>
|
||||
$<$<BOOL:${wextra}>:-Wextra
|
||||
-Wno-unused-parameter>
|
||||
$<$<BOOL:${werr}>:-Werror>
|
||||
@@ -177,7 +177,7 @@ else()
|
||||
if(
|
||||
is_nix_compiler
|
||||
AND is_linux
|
||||
AND is_any_clang
|
||||
AND is_clang
|
||||
AND CMAKE_CXX_FLAGS MATCHES "stdlib=libstdc"
|
||||
)
|
||||
string(
|
||||
@@ -204,10 +204,7 @@ if(voidstar)
|
||||
"Antithesis instrumentation requires Linux, aborting..."
|
||||
)
|
||||
elseif(
|
||||
NOT (
|
||||
is_llvm_clang
|
||||
AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 16.0
|
||||
)
|
||||
NOT (is_clang AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 16.0)
|
||||
)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
|
||||
+5
-10
@@ -275,6 +275,10 @@ if(xrpld)
|
||||
patch_nix_binary(xrpld)
|
||||
if(tests)
|
||||
target_compile_definitions(xrpld PUBLIC ENABLE_TESTS)
|
||||
target_compile_definitions(
|
||||
xrpld
|
||||
PRIVATE UNIT_TEST_REFERENCE_FEE=${UNIT_TEST_REFERENCE_FEE}
|
||||
)
|
||||
endif()
|
||||
target_include_directories(
|
||||
xrpld
|
||||
@@ -292,16 +296,7 @@ if(xrpld)
|
||||
OUT_VAR rpcspec_instantiations
|
||||
VALUE_TYPE "::json::Value"
|
||||
VIEW_HEADER "xrpld/rpc/detail/JsonObjectView.hpp"
|
||||
HANDLERS
|
||||
account_channels
|
||||
account_currencies
|
||||
account_info
|
||||
account_lines
|
||||
account_nfts
|
||||
account_offers
|
||||
book_changes
|
||||
ledger
|
||||
transaction_entry
|
||||
HANDLERS book_changes ledger transaction_entry
|
||||
)
|
||||
target_sources(xrpld PRIVATE ${rpcspec_instantiations})
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ target_link_options(common INTERFACE ${sanitizers_linker_flags})
|
||||
# This module appends -fsanitize-ignorelist=<path> for Clang builds.
|
||||
# The ignorelist path contains CMAKE_SOURCE_DIR, so it must be set here, rather than in the Conan profile.
|
||||
# GCC does not support -fsanitize-ignorelist.
|
||||
if(is_any_clang)
|
||||
if(is_clang)
|
||||
set(ignorelist_path
|
||||
"${CMAKE_SOURCE_DIR}/sanitizers/suppressions/sanitizer-ignorelist.txt"
|
||||
)
|
||||
|
||||
@@ -23,8 +23,11 @@ if(NOT is_multiconfig)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(is_llvm_clang)
|
||||
if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16.0)
|
||||
if(is_clang) # both Clang and AppleClang
|
||||
if(
|
||||
"${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang"
|
||||
AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16.0
|
||||
)
|
||||
message(FATAL_ERROR "This project requires clang 16 or later")
|
||||
endif()
|
||||
elseif(is_gcc)
|
||||
|
||||
@@ -23,6 +23,12 @@ option(assert "Enables asserts, even in release builds" OFF)
|
||||
option(xrpld "Build xrpld" ON)
|
||||
|
||||
option(tests "Build tests" ON)
|
||||
if(tests)
|
||||
# This setting allows making a separate workflow to test fees other than default 10
|
||||
if(NOT UNIT_TEST_REFERENCE_FEE)
|
||||
set(UNIT_TEST_REFERENCE_FEE "10" CACHE STRING "")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
option(benchmark "Build benchmarks" ON)
|
||||
|
||||
@@ -56,11 +62,11 @@ if(unity)
|
||||
set(CMAKE_UNITY_BUILD ON CACHE BOOL "Do a unity build")
|
||||
endif()
|
||||
|
||||
if(is_llvm_clang AND is_linux)
|
||||
if(is_clang AND is_linux)
|
||||
option(voidstar "Enable Antithesis instrumentation." OFF)
|
||||
endif()
|
||||
|
||||
if(is_gcc OR is_any_clang)
|
||||
if(is_gcc OR is_clang)
|
||||
include(ProcessorCount)
|
||||
ProcessorCount(PROCESSOR_COUNT)
|
||||
|
||||
@@ -119,7 +125,7 @@ else()
|
||||
set(use_mold OFF CACHE BOOL "mold linker, linux only" FORCE)
|
||||
endif()
|
||||
|
||||
if(is_any_clang)
|
||||
if(is_clang)
|
||||
option(use_lld "enables detection of lld linker" ON)
|
||||
else()
|
||||
set(use_lld OFF CACHE BOOL "try lld linker, clang only" FORCE)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
pcpp>=1.30
|
||||
|
||||
# Parser combinator library - used to parse the macro DSL
|
||||
pyparsing>=3.3.3
|
||||
pyparsing>=3.0.0
|
||||
|
||||
# Template engine - used to generate C++ code from templates
|
||||
Mako>=1.4.2
|
||||
Mako>=1.2.2
|
||||
@@ -1,8 +1,8 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.in --generate-hashes --output-file requirements.txt
|
||||
mako==1.4.3 \
|
||||
--hash=sha256:723296007c870bfd6b3f0c3230dba7198096e5269297ebf5e4eff9e7ffa39d4f \
|
||||
--hash=sha256:cd6537fe88d5fec315c55c2f8529bc4ce7a9a352ad7db3eeaa6a66e2dd4ec37a
|
||||
mako==1.3.12 \
|
||||
--hash=sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9 \
|
||||
--hash=sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a
|
||||
# via -r requirements.in
|
||||
markupsafe==3.0.3 \
|
||||
--hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \
|
||||
@@ -99,7 +99,7 @@ pcpp==1.30 \
|
||||
--hash=sha256:05fe08292b6da57f385001c891a87f40d6aa7f46787b03e8ba326d20a3297c6e \
|
||||
--hash=sha256:5af9fbce55f136d7931ae915fae03c34030a3b36c496e72d9636cedc8e2543a1
|
||||
# via -r requirements.in
|
||||
pyparsing==3.3.3 \
|
||||
--hash=sha256:928ae7e20211f3b6f3915a72f06a0cfd29ab9d24279dd6346b6b1a7146397d36 \
|
||||
--hash=sha256:ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4
|
||||
pyparsing==3.3.2 \
|
||||
--hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \
|
||||
--hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc
|
||||
# via -r requirements.in
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"requires": [
|
||||
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
|
||||
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1782392402.420688",
|
||||
"xrpl-rpc-spec/0.1.25#2daa763e2d86ccc4ad0a0fa4d1983e9f%1791907455.579992",
|
||||
"xrpl-rpc-spec/0.1.21#d536f87a2ae7d313452746cfa3ac4404%1790869005.122975",
|
||||
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1782392403.185447",
|
||||
"soci/4.0.3#e726491a03468795453f7c83fc924a96%1782392402.679521",
|
||||
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1782307151.633168",
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ class Xrpl(ConanFile):
|
||||
"nudb/2.0.9",
|
||||
"openssl/3.6.3",
|
||||
"soci/4.0.3",
|
||||
"xrpl-rpc-spec/0.1.25",
|
||||
"xrpl-rpc-spec/0.1.21",
|
||||
"zlib/1.3.2",
|
||||
]
|
||||
|
||||
|
||||
Vendored
+77
-32
@@ -4,18 +4,6 @@ This document explains how to set one up.
|
||||
|
||||
[BUILD.md]: ../../BUILD.md
|
||||
|
||||
## Supported development platforms
|
||||
|
||||
- **Linux**: the [Nix development shell](./nix.md),
|
||||
which provides the same toolchain CI uses on any distribution.
|
||||
- **macOS** 15 (Sequoia) or newer: the [Nix development shell](./nix.md),
|
||||
or the system [Apple Clang](#macos-apple-clang).
|
||||
CI runs on macOS 26 (Tahoe), but the build defaults `CMAKE_OSX_DEPLOYMENT_TARGET` to 15.
|
||||
- **Windows**: best effort, see [Windows](#windows).
|
||||
|
||||
For the platforms supported for running `xrpld`,
|
||||
see [Requirements](../install.md#requirements).
|
||||
|
||||
## Tested compiler versions
|
||||
|
||||
`xrpld` is built in the **C++23** dialect by default, so your toolchain has to
|
||||
@@ -47,16 +35,20 @@ compiler building. Treat support for anything outside the table as best-effort.
|
||||
|
||||
## Required tools
|
||||
|
||||
Besides a compiler, building `xrpld` requires
|
||||
[Git](https://git-scm.com/downloads),
|
||||
[Python](https://www.python.org/downloads/),
|
||||
[Conan](https://conan.io/downloads.html),
|
||||
and [CMake](https://cmake.org/download/).
|
||||
Building with `-Drust=ON` additionally requires a Rust toolchain,
|
||||
see [Rust](#rust).
|
||||
Besides a compiler, building `xrpld` requires:
|
||||
|
||||
| Tool | Minimum version |
|
||||
| ------------------------------------------- | --------------- |
|
||||
| [Git](https://git-scm.com/downloads) | any recent |
|
||||
| [Python](https://www.python.org/downloads/) | 3.11 |
|
||||
| [Conan](https://conan.io/downloads.html) | 2.17 |
|
||||
| [CMake](https://cmake.org/download/) | 3.16 |
|
||||
|
||||
On Linux and macOS, the [Nix development shell](./nix.md) provides all of them
|
||||
at the versions CI uses, recorded in [`nix/check-tools/`](../../nix/check-tools).
|
||||
(see below). On Windows they have to be installed manually.
|
||||
|
||||
Building with `-Drust=ON` additionally requires a Rust toolchain, see
|
||||
[Rust](#rust). A default build does not, so it is not in the table above.
|
||||
|
||||
Once they are in place, verify that everything is installed and runnable with:
|
||||
|
||||
@@ -66,19 +58,60 @@ Once they are in place, verify that everything is installed and runnable with:
|
||||
|
||||
## Linux and macOS
|
||||
|
||||
Use the [Nix development shell](./nix.md):
|
||||
it provides the compiler and every tool CI uses,
|
||||
without installing anything system-wide.
|
||||
The **recommended way** to get a development environment on Linux and macOS is
|
||||
the Nix development shell. It provides the exact tooling used in CI — `git`,
|
||||
`python`, `conan`, `cmake`, `clang-tidy`, `clang-format`, and everything else —
|
||||
with a single command and without installing anything system-wide:
|
||||
|
||||
### macOS: Apple Clang
|
||||
```bash
|
||||
nix --experimental-features 'nix-command flakes' develop
|
||||
```
|
||||
|
||||
You can also build with the system Apple Clang,
|
||||
installing the other [required tools](#required-tools) any way you like
|
||||
(or getting them from `nix develop .#apple-clang`).
|
||||
Its version is whatever your installed Xcode (or Command Line Tools) provides,
|
||||
so check that `clang --version` is at least the [tested one](#tested-compiler-versions).
|
||||
If several Xcode versions are installed,
|
||||
select one with `sudo xcode-select -s <path>` or the `DEVELOPER_DIR` environment variable.
|
||||
On **Linux**, Nix also provides the compiler (GCC); on **macOS**, it provides
|
||||
Clang. If you instead opt to use your system-wide Apple Clang (via
|
||||
`nix develop .#apple-clang`), you need to manage its version yourself (see
|
||||
below).
|
||||
|
||||
See [Using the Nix development shell](./nix.md) for installation and usage
|
||||
details, including how to select a different compiler and why we recommend Nix
|
||||
over a hand-maintained environment.
|
||||
|
||||
### macOS: managing the Apple Clang version
|
||||
|
||||
If you use your system-wide Apple Clang on macOS (via `nix develop .#apple-clang`),
|
||||
the compiler version is whatever your installed Xcode (or Command Line Tools)
|
||||
provides. The following command should return a version greater than or equal to
|
||||
the [tested one](#tested-compiler-versions):
|
||||
|
||||
```bash
|
||||
clang --version
|
||||
```
|
||||
|
||||
If you develop other applications using Xcode, you might be consistently
|
||||
updating to the newest version of Apple Clang, which will likely cause issues
|
||||
building xrpld. You may want to install and pin a specific version of Xcode:
|
||||
|
||||
1. **Download Xcode**
|
||||
- Visit [Apple Developer Downloads](https://developer.apple.com/download/more/)
|
||||
- Sign in with your Apple Developer account
|
||||
- Search for an Xcode version that includes the expected Apple Clang version
|
||||
- Download the `.xip` file
|
||||
|
||||
2. **Install and configure Xcode**
|
||||
|
||||
```bash
|
||||
# Extract the .xip file and rename for version management
|
||||
# Example: Xcode_16.2.app
|
||||
|
||||
# Move to Applications directory
|
||||
sudo mv Xcode_16.2.app /Applications/
|
||||
|
||||
# Set as default toolchain (persistent)
|
||||
sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
|
||||
|
||||
# Set as environment variable (temporary)
|
||||
export DEVELOPER_DIR=/Applications/Xcode_16.2.app/Contents/Developer
|
||||
```
|
||||
|
||||
## Windows
|
||||
|
||||
@@ -90,7 +123,8 @@ manually:
|
||||
"x64 Native Tools Command Prompt". CI configures CMake with the
|
||||
`Visual Studio 18 2026` generator.
|
||||
- [Git for Windows](https://git-scm.com/download/win)
|
||||
- recent releases of Python, Conan, and CMake
|
||||
- Python, Conan, and CMake, at the versions listed in
|
||||
[Required tools](#required-tools).
|
||||
- a [Rust toolchain](https://rustup.rs) — only needed to build with
|
||||
`-Drust=ON`, see [Rust](#rust)
|
||||
|
||||
@@ -111,5 +145,16 @@ version.
|
||||
Everything else the Rust build needs on the CMake side comes from Conan along
|
||||
with the rest of the dependencies, so there is nothing further to install.
|
||||
|
||||
## Clang-tidy
|
||||
|
||||
`clang-tidy` is required to run static analysis checks locally (see
|
||||
[CONTRIBUTING.md](../../CONTRIBUTING.md)). It is not required to build the
|
||||
project. The version this project uses is listed in
|
||||
[Tested compiler versions](#tested-compiler-versions).
|
||||
|
||||
On Linux and macOS, the [Nix development shell](./nix.md) provides that exact
|
||||
version out of the box — run it via `run-clang-tidy`. No separate installation
|
||||
is needed.
|
||||
|
||||
[cpp23-support]: https://en.cppreference.com/w/cpp/compiler_support/23
|
||||
[packages.nix]: ../../nix/packages.nix
|
||||
Vendored
+9
-2
@@ -9,7 +9,9 @@ This guide explains how to use Nix to set up a reproducible development environm
|
||||
- **Reproducible environment**: Everyone gets the same versions of tools and compilers
|
||||
- **Matches CI**: The Linux CI runs in Docker images built from this exact Nix environment, and CI builds some macOS configurations in it as well
|
||||
- **No system pollution**: Dependencies are isolated and don't affect your system packages
|
||||
- **Consistent compilers**: The GCC and Clang shells use the same versions as CI
|
||||
- **Quick setup**: Get started with a single command
|
||||
- **Works on Linux and macOS**: Consistent experience across platforms
|
||||
|
||||
## Install Nix
|
||||
|
||||
@@ -25,8 +27,13 @@ From the root of the xrpld repository, enter the default development shell:
|
||||
nix --experimental-features 'nix-command flakes' develop
|
||||
```
|
||||
|
||||
This downloads and sets up all required development tools (CMake, Ninja, Conan, etc.)
|
||||
and the compiler for your platform (see [Platform notes](#platform-notes)).
|
||||
This will:
|
||||
|
||||
- Download and set up all required development tools (CMake, Ninja, Conan, etc.)
|
||||
- Configure the appropriate compiler for your platform:
|
||||
- **Linux**: GCC (provided by Nix)
|
||||
- **macOS**: Clang (provided by Nix)
|
||||
|
||||
The first time you run this command, it will take a few minutes to download and build the environment. Subsequent runs will be much faster.
|
||||
|
||||
### Platform notes
|
||||
|
||||
@@ -10,19 +10,6 @@ and DNF on Red Hat-based distributions such as RHEL, AlmaLinux, and Rocky Linux,
|
||||
where `yum` is a symlink to `dnf`.
|
||||
To build from source instead, see [BUILD.md](../BUILD.md).
|
||||
|
||||
## Requirements
|
||||
|
||||
Linux is the only operating system officially supported for running `xrpld`;
|
||||
macOS and Windows builds are for development only.
|
||||
|
||||
Any x86-64 Linux distribution with APT or DNF and glibc 2.31 or newer should work
|
||||
(see [How libc is handled](../nix/docker/README.md#how-libc-is-handled)).
|
||||
CI tests the packages on Debian 11–13, Ubuntu 20.04–26.04,
|
||||
RHEL (UBI) 9–10, AlmaLinux 9–10, and Rocky Linux 9–10.
|
||||
|
||||
For the hardware needed to run a node,
|
||||
see [System Requirements](https://xrpl.org/system-requirements.html).
|
||||
|
||||
## Release channels
|
||||
|
||||
Packages are published to four channels:
|
||||
|
||||
@@ -39,31 +39,6 @@ checkedSub(std::int64_t a, std::int64_t b) noexcept
|
||||
return a - b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiply two signed 64-bit integers, returning std::nullopt when the exact
|
||||
* mathematical product is not representable in std::int64_t.
|
||||
*/
|
||||
[[nodiscard]] constexpr std::optional<std::int64_t>
|
||||
checkedMul(std::int64_t a, std::int64_t b) noexcept
|
||||
{
|
||||
using L = std::numeric_limits<std::int64_t>;
|
||||
|
||||
if (a == 0 || b == 0)
|
||||
return 0;
|
||||
|
||||
if (a > 0)
|
||||
{
|
||||
if (b > 0 ? a > L::max() / b : b < L::min() / a)
|
||||
return std::nullopt;
|
||||
}
|
||||
else if (b > 0 ? a < L::min() / b : b < L::max() / a)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return a * b;
|
||||
}
|
||||
|
||||
static_assert(checkedAdd(0, 0) == 0);
|
||||
static_assert(checkedAdd(1, -1) == 0);
|
||||
static_assert(checkedAdd(-5, 2) == -3);
|
||||
@@ -106,53 +81,6 @@ static_assert(
|
||||
!checkedSub(std::numeric_limits<std::int64_t>::min(), std::numeric_limits<std::int64_t>::max())
|
||||
.has_value());
|
||||
|
||||
static_assert(checkedMul(0, std::numeric_limits<std::int64_t>::min()) == 0);
|
||||
static_assert(checkedMul(std::numeric_limits<std::int64_t>::min(), 0) == 0);
|
||||
static_assert(checkedMul(-1, -1) == 1);
|
||||
// a > 0, b > 0
|
||||
static_assert(
|
||||
checkedMul(std::numeric_limits<std::int64_t>::max(), 1) ==
|
||||
std::numeric_limits<std::int64_t>::max());
|
||||
static_assert(
|
||||
checkedMul(std::numeric_limits<std::int64_t>::max() / 2, 2) ==
|
||||
std::numeric_limits<std::int64_t>::max() - 1);
|
||||
static_assert(!checkedMul((std::numeric_limits<std::int64_t>::max() / 2) + 1, 2).has_value());
|
||||
static_assert(!checkedMul(2, (std::numeric_limits<std::int64_t>::max() / 2) + 1).has_value());
|
||||
// a > 0, b < 0
|
||||
static_assert(
|
||||
checkedMul(std::numeric_limits<std::int64_t>::max(), -1) ==
|
||||
-std::numeric_limits<std::int64_t>::max());
|
||||
static_assert(
|
||||
checkedMul((std::numeric_limits<std::int64_t>::max() / 2) + 1, -2) ==
|
||||
std::numeric_limits<std::int64_t>::min());
|
||||
static_assert(!checkedMul((std::numeric_limits<std::int64_t>::max() / 2) + 2, -2).has_value());
|
||||
static_assert(
|
||||
checkedMul(1, std::numeric_limits<std::int64_t>::min()) ==
|
||||
std::numeric_limits<std::int64_t>::min());
|
||||
static_assert(
|
||||
checkedMul(2, std::numeric_limits<std::int64_t>::min() / 2) ==
|
||||
std::numeric_limits<std::int64_t>::min());
|
||||
static_assert(!checkedMul(2, (std::numeric_limits<std::int64_t>::min() / 2) - 1).has_value());
|
||||
// a < 0, b > 0
|
||||
static_assert(
|
||||
checkedMul(std::numeric_limits<std::int64_t>::min(), 1) ==
|
||||
std::numeric_limits<std::int64_t>::min());
|
||||
static_assert(
|
||||
checkedMul(std::numeric_limits<std::int64_t>::min() / 2, 2) ==
|
||||
std::numeric_limits<std::int64_t>::min());
|
||||
static_assert(!checkedMul((std::numeric_limits<std::int64_t>::min() / 2) - 1, 2).has_value());
|
||||
static_assert(!checkedMul(std::numeric_limits<std::int64_t>::min(), 2).has_value());
|
||||
// a < 0, b < 0
|
||||
static_assert(
|
||||
checkedMul(-(std::numeric_limits<std::int64_t>::max() / 2), -2) ==
|
||||
std::numeric_limits<std::int64_t>::max() - 1);
|
||||
static_assert(!checkedMul(std::numeric_limits<std::int64_t>::min() / 2, -2).has_value());
|
||||
static_assert(
|
||||
checkedMul(-1, -std::numeric_limits<std::int64_t>::max()) ==
|
||||
std::numeric_limits<std::int64_t>::max());
|
||||
static_assert(!checkedMul(std::numeric_limits<std::int64_t>::min(), -1).has_value());
|
||||
static_assert(!checkedMul(-1, std::numeric_limits<std::int64_t>::min()).has_value());
|
||||
|
||||
/**
|
||||
* Calculate one number divided by another number in percentage.
|
||||
* The result is rounded up to the next integer, and capped in the range [0,100]
|
||||
|
||||
@@ -352,9 +352,9 @@ class Number final
|
||||
using rep = std::int64_t;
|
||||
using InternalRep = MantissaRange::rep;
|
||||
|
||||
bool negative_{false};
|
||||
InternalRep mantissa_{0};
|
||||
int exponent_{std::numeric_limits<int>::lowest()};
|
||||
bool negative_{false};
|
||||
|
||||
public:
|
||||
// The range for the exponent when normalized
|
||||
@@ -650,12 +650,8 @@ private:
|
||||
shiftExponent(int exponentDelta) const;
|
||||
};
|
||||
|
||||
// The number class currently has 3 tail padding bytes. If you are adding new
|
||||
// members, try to fit them there before expanding the size of the class.
|
||||
static_assert(sizeof(Number) == 16);
|
||||
|
||||
constexpr Number::Number(bool negative, InternalRep mantissa, int exponent, Unchecked) noexcept
|
||||
: mantissa_{mantissa}, exponent_{exponent}, negative_(negative)
|
||||
: negative_(negative), mantissa_{mantissa}, exponent_{exponent}
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -153,24 +153,6 @@ private:
|
||||
Policy policy,
|
||||
Callback&& replaceCallback = nullptr);
|
||||
|
||||
/**
|
||||
* canonicalizeImpl variant for callers that already hold `mutex_`.
|
||||
*
|
||||
* The unnamed `scoped_lock<MutexType> const&` parameter is proof of
|
||||
* ownership; this overload does not acquire the lock. Use it from other
|
||||
* methods (e.g. fetchAndModify) that need to compose canonicalize with
|
||||
* additional work under a single critical section, without relying on
|
||||
* recursive-mutex re-entry.
|
||||
*/
|
||||
template <class Policy, class Callback = std::nullptr_t>
|
||||
bool
|
||||
canonicalizeImpl(
|
||||
std::scoped_lock<MutexType> const&,
|
||||
key_type const& key,
|
||||
CanonicalizeClientPointerType<Policy> data,
|
||||
Policy policy,
|
||||
Callback&& replaceCallback = nullptr);
|
||||
|
||||
public:
|
||||
/**
|
||||
* Replace aliased objects with originals.
|
||||
@@ -257,6 +239,9 @@ public:
|
||||
bool
|
||||
retrieve(key_type const& key, T& data);
|
||||
|
||||
MutexType&
|
||||
peekMutex();
|
||||
|
||||
std::vector<key_type>
|
||||
getKeys() const;
|
||||
|
||||
@@ -278,22 +263,6 @@ public:
|
||||
fetch(key_type const& digest, Handler const& h);
|
||||
// End CachedSLEs functions.
|
||||
|
||||
/**
|
||||
* Fetch or create an entry and execute a callback while holding the lock.
|
||||
*
|
||||
* The entry for the given key is fetched from the cache or created if it
|
||||
* doesn't exist. The callback is then invoked with a reference to the
|
||||
* entry while the cache mutex is still held, allowing safe modification
|
||||
* of the cached object.
|
||||
*
|
||||
* @param key The key to fetch or create
|
||||
* @param callback Function to call with the entry under lock.
|
||||
* Signature: void(T&)
|
||||
*/
|
||||
template <class Callback>
|
||||
void
|
||||
fetchAndModify(key_type const& key, Callback&& callback);
|
||||
|
||||
private:
|
||||
SharedPointerType
|
||||
initialFetch(key_type const& key, std::scoped_lock<MutexType> const& l);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <xrpl/basics/Log.h> // IWYU pragma: keep
|
||||
#include <xrpl/basics/TaggedCache.h>
|
||||
#include <xrpl/basics/scope.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -336,29 +335,6 @@ template <class Policy, class Callback>
|
||||
inline bool
|
||||
TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash, KeyEqual, Mutex>::
|
||||
canonicalizeImpl(
|
||||
key_type const& key,
|
||||
CanonicalizeClientPointerType<Policy> data,
|
||||
Policy policy,
|
||||
Callback&& replaceCallback)
|
||||
{
|
||||
std::scoped_lock<MutexType> const lock(mutex_);
|
||||
return canonicalizeImpl(lock, key, data, policy, std::forward<Callback>(replaceCallback));
|
||||
}
|
||||
|
||||
template <
|
||||
class Key,
|
||||
class T,
|
||||
bool IsKeyCache,
|
||||
class SharedWeakUnionPointer,
|
||||
class SharedPointerType,
|
||||
class Hash,
|
||||
class KeyEqual,
|
||||
class Mutex>
|
||||
template <class Policy, class Callback>
|
||||
inline bool
|
||||
TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash, KeyEqual, Mutex>::
|
||||
canonicalizeImpl(
|
||||
std::scoped_lock<MutexType> const&,
|
||||
key_type const& key,
|
||||
CanonicalizeClientPointerType<Policy> data,
|
||||
[[maybe_unused]] Policy policy,
|
||||
@@ -378,6 +354,8 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
|
||||
// passing a const argument is a compile error.
|
||||
constexpr bool replaceCached = std::is_same_v<Policy, detail::ReplaceCached>;
|
||||
|
||||
std::scoped_lock const lock(mutex_);
|
||||
|
||||
auto const cit = cache_.find(key);
|
||||
|
||||
if (cit == cache_.end())
|
||||
@@ -594,6 +572,22 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
|
||||
return true;
|
||||
}
|
||||
|
||||
template <
|
||||
class Key,
|
||||
class T,
|
||||
bool IsKeyCache,
|
||||
class SharedWeakUnionPointer,
|
||||
class SharedPointerType,
|
||||
class Hash,
|
||||
class KeyEqual,
|
||||
class Mutex>
|
||||
inline auto
|
||||
TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash, KeyEqual, Mutex>::
|
||||
peekMutex() -> MutexType&
|
||||
{
|
||||
return mutex_;
|
||||
}
|
||||
|
||||
template <
|
||||
class Key,
|
||||
class T,
|
||||
@@ -706,49 +700,6 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
|
||||
}
|
||||
// End CachedSLEs functions.
|
||||
|
||||
template <
|
||||
class Key,
|
||||
class T,
|
||||
bool IsKeyCache,
|
||||
class SharedWeakUnionPointer,
|
||||
class SharedPointerType,
|
||||
class Hash,
|
||||
class KeyEqual,
|
||||
class Mutex>
|
||||
template <class Callback>
|
||||
inline void
|
||||
TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash, KeyEqual, Mutex>::
|
||||
fetchAndModify(key_type const& key, Callback&& callback)
|
||||
{
|
||||
static_assert(
|
||||
!IsKeyCache, "fetchAndModify is only supported for value caches, not key-only caches");
|
||||
static_assert(
|
||||
std::is_same_v<std::shared_ptr<T>, SharedPointerType> ||
|
||||
std::is_same_v<intr_ptr::SharedPtr<T>, SharedPointerType>,
|
||||
"fetchAndModify requires std::shared_ptr<T> or intr_ptr::SharedPtr<T>");
|
||||
|
||||
std::scoped_lock<MutexType> const lock(mutex_);
|
||||
|
||||
// Same pointer-type dispatch as insert(): the placeholder must have exactly
|
||||
// SharedPointerType so canonicalizeImpl can write the cached pointer back.
|
||||
SharedPointerType entry = [] {
|
||||
if constexpr (std::is_same_v<std::shared_ptr<T>, SharedPointerType>)
|
||||
{
|
||||
return std::make_shared<T>();
|
||||
}
|
||||
else
|
||||
{
|
||||
return intr_ptr::makeShared<T>();
|
||||
}
|
||||
}();
|
||||
canonicalizeImpl(lock, key, entry, detail::ReplaceClient{});
|
||||
|
||||
ALWAYS(
|
||||
entry != nullptr, "xrpl::TaggedCache::fetchAndModify : entry present after canonicalize");
|
||||
|
||||
callback(*entry);
|
||||
}
|
||||
|
||||
template <
|
||||
class Key,
|
||||
class T,
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace beast::unit_test {
|
||||
@@ -23,7 +22,6 @@ namespace beast::unit_test {
|
||||
class Runner
|
||||
{
|
||||
std::string arg_;
|
||||
std::optional<std::int64_t> referenceFee_;
|
||||
bool default_ = false;
|
||||
bool failed_ = false;
|
||||
bool cond_ = false;
|
||||
@@ -59,26 +57,6 @@ public:
|
||||
return arg_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the reference fee (in drops) for tests.
|
||||
*
|
||||
* If provided, this value is used in every suite that does not override it.
|
||||
*/
|
||||
void
|
||||
referenceFee(std::int64_t fee)
|
||||
{
|
||||
referenceFee_ = fee;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the reference fee, if any.
|
||||
*/
|
||||
[[nodiscard]] std::optional<std::int64_t> const&
|
||||
referenceFee() const
|
||||
{
|
||||
return referenceFee_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the specified suite.
|
||||
* @return `true` if any conditions failed.
|
||||
|
||||
@@ -308,18 +308,6 @@ protected:
|
||||
}
|
||||
|
||||
public:
|
||||
/**
|
||||
* Return the reference fee associated with the runner.
|
||||
*/
|
||||
std::optional<std::int64_t>
|
||||
referenceFee() const
|
||||
{
|
||||
assert(runner_);
|
||||
if (runner_ == nullptr)
|
||||
return {};
|
||||
return runner_->referenceFee();
|
||||
}
|
||||
|
||||
/**
|
||||
* DEPRECATED
|
||||
* @return `true` if the test condition indicates success(a false value)
|
||||
|
||||
@@ -94,7 +94,6 @@ struct Keys
|
||||
static constexpr auto kBbtOptions = "bbt_options";
|
||||
static constexpr auto kBgThreads = "bg_threads";
|
||||
static constexpr auto kBlockSize = "block_size";
|
||||
static constexpr auto kBytecodeSizeLimit = "bytecode_size_limit";
|
||||
static constexpr auto kCacheAge = "cache_age";
|
||||
static constexpr auto kCacheMb = "cache_mb";
|
||||
static constexpr auto kCacheSize = "cache_size";
|
||||
@@ -109,8 +108,6 @@ struct Keys
|
||||
static constexpr auto kFileSizeMult = "file_size_mult";
|
||||
static constexpr auto kFilterBits = "filter_bits";
|
||||
static constexpr auto kFilterFull = "filter_full";
|
||||
static constexpr auto kGasLimit = "gas_limit";
|
||||
static constexpr auto kGasPrice = "gas_price";
|
||||
static constexpr auto kHardSet = "hard_set";
|
||||
static constexpr auto kHighThreads = "high_threads";
|
||||
static constexpr auto kHoldTime = "hold_time";
|
||||
|
||||
@@ -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&);
|
||||
|
||||
|
||||
@@ -257,42 +257,13 @@ public:
|
||||
header_.validated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this ledger as accepted and attempt to make it immutable.
|
||||
*
|
||||
* The close-time fields are recorded first, since the ledger hash covers
|
||||
* them.
|
||||
*
|
||||
* @param closeTime The consensus-agreed close time.
|
||||
* @param closeResolution The close time resolution.
|
||||
* @param correctCloseTime Whether consensus agreed on the close time. If
|
||||
* false, kSLcfNoConsensusTime is recorded in closeFlags instead.
|
||||
* @return What setImmutable() returned.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
void
|
||||
setAccepted(
|
||||
NetClock::time_point closeTime,
|
||||
NetClock::duration closeResolution,
|
||||
bool correctCloseTime);
|
||||
|
||||
/**
|
||||
* Mark this ledger as immutable, so it can no longer be modified.
|
||||
*
|
||||
* A ledger built or loaded locally cannot have an invalid map, since only
|
||||
* a map syncing against hashes from outside can be proven impossible (see
|
||||
* SHAMap::addKnownNode), so a caller on such a path may treat a false
|
||||
* return as a broken internal invariant. A caller assembling a ledger from
|
||||
* peer data may not: for it, false is an outcome a peer can produce.
|
||||
*
|
||||
* @param rehash Whether to recompute the ledger hash from the header
|
||||
* fields. The transaction and account hashes are recomputed from the
|
||||
* maps too, but only the first time.
|
||||
* @return false if either map is Invalid, leaving the immutable flag as it
|
||||
* was and the header untouched. A map invalidated partway through can
|
||||
* still leave the other one immutable, so a false return means the
|
||||
* ledger must be discarded rather than retried.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
void
|
||||
setImmutable(bool rehash = true);
|
||||
|
||||
bool
|
||||
@@ -301,28 +272,23 @@ public:
|
||||
return immutable_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether both maps can still be the maps the header names. See
|
||||
* SHAMap::isValid().
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
mapsValid() const
|
||||
{
|
||||
return txMap_.isValid() && stateMap_.isValid();
|
||||
}
|
||||
/* Mark this ledger as "should be full".
|
||||
|
||||
/**
|
||||
* Mark this ledger as "should be full", indicating that the local server
|
||||
* wants all the corresponding nodes in durable storage.
|
||||
*
|
||||
* Const because it reflects metadata, not data this ledger shares with
|
||||
* other nodes on the network.
|
||||
*/
|
||||
"Full" is metadata property of the ledger, it indicates
|
||||
that the local server wants all the corresponding nodes
|
||||
in durable storage.
|
||||
|
||||
This is marked `const` because it reflects metadata
|
||||
and not data that is in common with other nodes on the
|
||||
network.
|
||||
*/
|
||||
void
|
||||
setFull() const
|
||||
{
|
||||
txMap_.setFull(header_.seq);
|
||||
stateMap_.setFull(header_.seq);
|
||||
txMap_.setFull();
|
||||
txMap_.setLedgerSeq(header_.seq);
|
||||
stateMap_.setFull();
|
||||
stateMap_.setLedgerSeq(header_.seq);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -452,22 +418,6 @@ private:
|
||||
static std::pair<std::shared_ptr<STTx const>, std::shared_ptr<STObject const>>
|
||||
deserializeTxPlusMeta(SHAMapItem const& item);
|
||||
|
||||
/**
|
||||
* Make both maps immutable.
|
||||
*
|
||||
* Each map is settled on its own, so neither is left mid-sync because the
|
||||
* other refused.
|
||||
*
|
||||
* @return Whether both maps are immutable.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
setMapsImmutable()
|
||||
{
|
||||
bool const txImmutable = txMap_.setImmutable();
|
||||
bool const stateImmutable = stateMap_.setImmutable();
|
||||
return txImmutable && stateImmutable;
|
||||
}
|
||||
|
||||
bool immutable_;
|
||||
|
||||
// A SHAMap containing the transactions associated with this ledger.
|
||||
|
||||
@@ -265,13 +265,6 @@ canWithdraw(
|
||||
[[nodiscard]] TER
|
||||
canWithdraw(ReadView const& view, STTx const& tx);
|
||||
|
||||
/**
|
||||
* Pays out a withdrawal from a vault or loan broker pseudo-account.
|
||||
*
|
||||
* `sourceAmount` leaves `sourceAcct` and `destinationAmount` reaches `dstAcct`.
|
||||
* The two differ only when the withdrawal carries a transfer fee; the
|
||||
* difference is the fee and is settled through the issuer.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
doWithdraw(
|
||||
ApplyViewContext ctx,
|
||||
@@ -279,8 +272,7 @@ doWithdraw(
|
||||
AccountID const& dstAcct,
|
||||
AccountID const& sourceAcct,
|
||||
XRPAmount priorBalance,
|
||||
STAmount const& destinationAmount,
|
||||
STAmount const& sourceAmount,
|
||||
STAmount const& amount,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
@@ -28,6 +29,7 @@
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
@@ -311,11 +313,11 @@ getAMMOfferStartWithTakerPays(
|
||||
* is equal to LOB quality (in this case AMM offer quality is
|
||||
* better than LOB quality) or AMM offer is equal to LOB quality
|
||||
* (in this case SPQ is better than LOB quality).
|
||||
* The economically coarser integral offer side is calculated first. The
|
||||
* result is rounded down, which makes the offer quality better. Calculating
|
||||
* takerPays first instead would round down takerGets when that is the coarser
|
||||
* side, giving worse quality than LOB quality, and the offer might fail to
|
||||
* generate.
|
||||
* Pre-amendment code calculates takerPays first. If takerGets is the
|
||||
* economically coarser integral side, it is rounded down, which results in
|
||||
* worse offer quality than LOB quality, and the offer might fail to generate.
|
||||
* Post-amendment code calculates the economically coarser integral offer side
|
||||
* first. The result is rounded down, which makes the offer quality better.
|
||||
* It might not be possible to match either SPQ or AMM offer to LOB
|
||||
* quality. This generally happens at higher fees.
|
||||
* @param pool AMM pool balances
|
||||
@@ -329,8 +331,69 @@ changeSpotPriceQuality(
|
||||
TAmounts<TIn, TOut> const& pool,
|
||||
Quality const& quality,
|
||||
std::uint16_t tfee,
|
||||
Rules const& rules,
|
||||
beast::Journal j)
|
||||
{
|
||||
if (!rules.enabled(fixAMMv1_1))
|
||||
{
|
||||
// Finds takerPays (i) and takerGets (o) such that given pool
|
||||
// composition poolGets(I) and poolPays(O): (O - o) / (I + i) = quality.
|
||||
// Where takerGets is calculated as the swapAssetIn (see below).
|
||||
// The above equation produces the quadratic equation:
|
||||
// i^2*(1-fee) + i*I*(2-fee) + I^2 - I*O/quality,
|
||||
// which is solved for i, and o is found with swapAssetIn().
|
||||
auto const f = feeMult(tfee); // 1 - fee
|
||||
auto const& a = f;
|
||||
auto const b = pool.in * (1 + f);
|
||||
Number const c = pool.in * pool.in - pool.in * pool.out * quality.rate();
|
||||
auto const res = b * b - 4 * a * c;
|
||||
if (res < 0)
|
||||
{
|
||||
return std::nullopt; // LCOV_EXCL_LINE
|
||||
}
|
||||
if (auto const nTakerPaysPropose = (-b + root2(res)) / (2 * a); nTakerPaysPropose > 0)
|
||||
{
|
||||
auto const nTakerPays = [&] {
|
||||
// The fee might make the AMM offer quality less than CLOB
|
||||
// quality. Therefore, AMM offer has to satisfy this constraint:
|
||||
// o / i >= q. Substituting o with swapAssetIn() gives: i <= O /
|
||||
// q - I / (1 - fee).
|
||||
auto const nTakerPaysConstraint = pool.out * quality.rate() - pool.in / f;
|
||||
if (nTakerPaysPropose > nTakerPaysConstraint)
|
||||
return nTakerPaysConstraint;
|
||||
return nTakerPaysPropose;
|
||||
}();
|
||||
if (nTakerPays <= 0)
|
||||
{
|
||||
JLOG(j.trace()) << "changeSpotPriceQuality calc failed: " << to_string(pool.in)
|
||||
<< " " << to_string(pool.out) << " " << quality << " " << tfee;
|
||||
return std::nullopt;
|
||||
}
|
||||
auto const takerPays =
|
||||
toAmount<TIn>(getAsset(pool.in), nTakerPays, Number::RoundingMode::Upward);
|
||||
// should not fail
|
||||
auto amounts = TAmounts<TIn, TOut>{takerPays, swapAssetIn(pool, takerPays, tfee)};
|
||||
if (Quality{amounts} < quality &&
|
||||
!withinRelativeDistance(Quality{amounts}, quality, Number(1, -7)))
|
||||
{
|
||||
JLOG(j.error()) << "changeSpotPriceQuality failed: " << to_string(pool.in) << " "
|
||||
<< to_string(pool.out) << " "
|
||||
<< " " << quality << " " << tfee << " " << to_string(amounts.in)
|
||||
<< " " << to_string(amounts.out);
|
||||
Throw<std::runtime_error>("changeSpotPriceQuality failed");
|
||||
}
|
||||
|
||||
JLOG(j.trace()) << "changeSpotPriceQuality succeeded: " << to_string(pool.in) << " "
|
||||
<< to_string(pool.out) << " "
|
||||
<< " " << quality << " " << tfee << " " << to_string(amounts.in) << " "
|
||||
<< to_string(amounts.out);
|
||||
return amounts;
|
||||
}
|
||||
JLOG(j.trace()) << "changeSpotPriceQuality calc failed: " << to_string(pool.in) << " "
|
||||
<< to_string(pool.out) << " " << quality << " " << tfee;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto amounts = [&] {
|
||||
bool const inIntegral = getAsset(pool.in).integral();
|
||||
bool const outIntegral = getAsset(pool.out).integral();
|
||||
@@ -396,49 +459,57 @@ template <typename TIn, typename TOut>
|
||||
TOut
|
||||
swapAssetIn(TAmounts<TIn, TOut> const& pool, TIn const& assetIn, std::uint16_t tfee)
|
||||
{
|
||||
// set rounding to always favor the amm. Clip to zero.
|
||||
// calculate:
|
||||
// pool.out -
|
||||
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
|
||||
// and explicitly set the rounding modes
|
||||
// Favoring the amm means we should:
|
||||
// minimize:
|
||||
// pool.out -
|
||||
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
|
||||
// maximize:
|
||||
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
|
||||
// (pool.in * pool.out)
|
||||
// minimize:
|
||||
// (pool.in + assetIn * feeMult(tfee)),
|
||||
// minimize:
|
||||
// assetIn * feeMult(tfee)
|
||||
// feeMult is: (1-fee), fee is tfee/100000
|
||||
// minimize:
|
||||
// 1-fee
|
||||
// maximize:
|
||||
// fee
|
||||
SaveNumberRoundMode const _{Number::getround()};
|
||||
if (auto const& rules = getCurrentTransactionRules(); rules && rules->enabled(fixAMMv1_1))
|
||||
{
|
||||
// set rounding to always favor the amm. Clip to zero.
|
||||
// calculate:
|
||||
// pool.out -
|
||||
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
|
||||
// and explicitly set the rounding modes
|
||||
// Favoring the amm means we should:
|
||||
// minimize:
|
||||
// pool.out -
|
||||
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
|
||||
// maximize:
|
||||
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
|
||||
// (pool.in * pool.out)
|
||||
// minimize:
|
||||
// (pool.in + assetIn * feeMult(tfee)),
|
||||
// minimize:
|
||||
// assetIn * feeMult(tfee)
|
||||
// feeMult is: (1-fee), fee is tfee/100000
|
||||
// minimize:
|
||||
// 1-fee
|
||||
// maximize:
|
||||
// fee
|
||||
SaveNumberRoundMode const _{Number::getround()};
|
||||
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const numerator = pool.in * pool.out;
|
||||
auto const fee = getFee(tfee);
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const numerator = pool.in * pool.out;
|
||||
auto const fee = getFee(tfee);
|
||||
|
||||
Number::setround(Number::RoundingMode::Downward);
|
||||
auto const denom = pool.in + assetIn * (1 - fee);
|
||||
Number::setround(Number::RoundingMode::Downward);
|
||||
auto const denom = pool.in + assetIn * (1 - fee);
|
||||
|
||||
if (denom.signum() <= 0)
|
||||
return toAmount<TOut>(getAsset(pool.out), 0);
|
||||
if (denom.signum() <= 0)
|
||||
return toAmount<TOut>(getAsset(pool.out), 0);
|
||||
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const ratio = numerator / denom;
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const ratio = numerator / denom;
|
||||
|
||||
Number::setround(Number::RoundingMode::Downward);
|
||||
auto const swapOut = pool.out - ratio;
|
||||
Number::setround(Number::RoundingMode::Downward);
|
||||
auto const swapOut = pool.out - ratio;
|
||||
|
||||
if (swapOut.signum() < 0)
|
||||
return toAmount<TOut>(getAsset(pool.out), 0);
|
||||
if (swapOut.signum() < 0)
|
||||
return toAmount<TOut>(getAsset(pool.out), 0);
|
||||
|
||||
return toAmount<TOut>(getAsset(pool.out), swapOut, Number::RoundingMode::Downward);
|
||||
return toAmount<TOut>(getAsset(pool.out), swapOut, Number::RoundingMode::Downward);
|
||||
}
|
||||
|
||||
return toAmount<TOut>(
|
||||
getAsset(pool.out),
|
||||
pool.out - (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
|
||||
Number::RoundingMode::Downward);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -455,49 +526,57 @@ template <typename TIn, typename TOut>
|
||||
TIn
|
||||
swapAssetOut(TAmounts<TIn, TOut> const& pool, TOut const& assetOut, std::uint16_t tfee)
|
||||
{
|
||||
// set rounding to always favor the amm. Clip to zero.
|
||||
// calculate:
|
||||
// ((pool.in * pool.out) / (pool.out - assetOut) - pool.in) /
|
||||
// (1-tfee/100000)
|
||||
// maximize:
|
||||
// ((pool.in * pool.out) / (pool.out - assetOut) - pool.in)
|
||||
// maximize:
|
||||
// (pool.in * pool.out) / (pool.out - assetOut)
|
||||
// maximize:
|
||||
// (pool.in * pool.out)
|
||||
// minimize
|
||||
// (pool.out - assetOut)
|
||||
// minimize:
|
||||
// (1-tfee/100000)
|
||||
// maximize:
|
||||
// tfee/100000
|
||||
|
||||
SaveNumberRoundMode const _{Number::getround()};
|
||||
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const numerator = pool.in * pool.out;
|
||||
|
||||
Number::setround(Number::RoundingMode::Downward);
|
||||
auto const denom = pool.out - assetOut;
|
||||
if (denom.signum() <= 0)
|
||||
if (auto const& rules = getCurrentTransactionRules(); rules && rules->enabled(fixAMMv1_1))
|
||||
{
|
||||
return toMaxAmount<TIn>(getAsset(pool.in));
|
||||
// set rounding to always favor the amm. Clip to zero.
|
||||
// calculate:
|
||||
// ((pool.in * pool.out) / (pool.out - assetOut) - pool.in) /
|
||||
// (1-tfee/100000)
|
||||
// maximize:
|
||||
// ((pool.in * pool.out) / (pool.out - assetOut) - pool.in)
|
||||
// maximize:
|
||||
// (pool.in * pool.out) / (pool.out - assetOut)
|
||||
// maximize:
|
||||
// (pool.in * pool.out)
|
||||
// minimize
|
||||
// (pool.out - assetOut)
|
||||
// minimize:
|
||||
// (1-tfee/100000)
|
||||
// maximize:
|
||||
// tfee/100000
|
||||
|
||||
SaveNumberRoundMode const _{Number::getround()};
|
||||
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const numerator = pool.in * pool.out;
|
||||
|
||||
Number::setround(Number::RoundingMode::Downward);
|
||||
auto const denom = pool.out - assetOut;
|
||||
if (denom.signum() <= 0)
|
||||
{
|
||||
return toMaxAmount<TIn>(getAsset(pool.in));
|
||||
}
|
||||
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const ratio = numerator / denom;
|
||||
auto const numerator2 = ratio - pool.in;
|
||||
auto const fee = getFee(tfee);
|
||||
|
||||
Number::setround(Number::RoundingMode::Downward);
|
||||
auto const feeMult = 1 - fee;
|
||||
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const swapIn = numerator2 / feeMult;
|
||||
if (swapIn.signum() < 0)
|
||||
return toAmount<TIn>(getAsset(pool.in), 0);
|
||||
|
||||
return toAmount<TIn>(getAsset(pool.in), swapIn, Number::RoundingMode::Upward);
|
||||
}
|
||||
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const ratio = numerator / denom;
|
||||
auto const numerator2 = ratio - pool.in;
|
||||
auto const fee = getFee(tfee);
|
||||
|
||||
Number::setround(Number::RoundingMode::Downward);
|
||||
auto const feeMult = 1 - fee;
|
||||
|
||||
Number::setround(Number::RoundingMode::Upward);
|
||||
auto const swapIn = numerator2 / feeMult;
|
||||
if (swapIn.signum() < 0)
|
||||
return toAmount<TIn>(getAsset(pool.in), 0);
|
||||
|
||||
return toAmount<TIn>(getAsset(pool.in), swapIn, Number::RoundingMode::Upward);
|
||||
return toAmount<TIn>(
|
||||
getAsset(pool.in),
|
||||
((pool.in * pool.out) / (pool.out - assetOut) - pool.in) / feeMult(tfee),
|
||||
Number::RoundingMode::Upward);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
@@ -28,8 +27,6 @@
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
@@ -563,54 +560,4 @@ escrowUnlockApplyHelper<MPTIssue>(
|
||||
journal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart escrow kill switches, driven by the voted FeeSettings.
|
||||
*
|
||||
* Zeroing `bytecodeSizeLimit` stops new uploads while leaving existing escrows
|
||||
* finishable without forcing holders to wait for `CancelAfter`.
|
||||
* Zeroing `gasLimit` stops both.
|
||||
*/
|
||||
/** @{ */
|
||||
inline bool
|
||||
isBytecodeUploadDisabled(Fees const& fees)
|
||||
{
|
||||
return fees.bytecodeSizeLimit == 0 || fees.gasLimit == 0;
|
||||
}
|
||||
|
||||
inline bool
|
||||
isBytecodeExecutionDisabled(Fees const& fees)
|
||||
{
|
||||
return fees.gasLimit == 0;
|
||||
}
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* Owner count an escrow costs.
|
||||
*
|
||||
* An escrow costs one increment per started 500 bytes of bytecode, and one
|
||||
* increment if it carries no bytecode.
|
||||
*/
|
||||
template <class T>
|
||||
int32_t
|
||||
calculateAdditionalReserve(T const& finishFunction)
|
||||
{
|
||||
static auto constexpr kBytecodeReserveIncrement = 500;
|
||||
|
||||
if (!finishFunction)
|
||||
return 1;
|
||||
|
||||
auto const size = finishFunction->size();
|
||||
if (size == 0)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
// Empty Bytecode is refused at create, so no escrow carries it. Ceiling
|
||||
// division would answer 0, which would subtract less than the create added.
|
||||
UNREACHABLE("xrpl::calculateAdditionalReserve : empty Bytecode");
|
||||
return 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
return static_cast<int32_t>((size + kBytecodeReserveIncrement - 1) / kBytecodeReserveIncrement);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -7,10 +7,10 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
@@ -31,9 +31,9 @@ namespace xrpl {
|
||||
/**
|
||||
* Broker cover preclaim precision guard (fixCleanup3_2_0).
|
||||
*
|
||||
* Prevents a silent sub-ULP no-op where a deposit, withdrawal, or clawback
|
||||
* amount is so small that it rounds to zero at sfCoverAvailable's scale.
|
||||
* Without this guard, both the pseudo trust-line and sfCoverAvailable would
|
||||
* Prevents a "silent sub-ULP no-op" where a deposit, withdrawal, or clawback
|
||||
* amount is so small that it rounds to zero at `sfCoverAvailable`'s scale.
|
||||
* Without this guard, both the pseudo trust-line and `sfCoverAvailable` would
|
||||
* identically absorb the rounded zero, resulting in a successful transaction
|
||||
* (tesSUCCESS) where no funds actually moved.
|
||||
*
|
||||
@@ -44,8 +44,8 @@ namespace xrpl {
|
||||
* @param j Journal for logging.
|
||||
* @param logPrefix Transactor name for log diagnostics.
|
||||
*
|
||||
* @return tecPRECISION_LOSS if the request rounds to zero at cover scale.
|
||||
* tesSUCCESS if the amendment is disabled or the request is safely supra-ULP.
|
||||
* @return `tecPRECISION_LOSS` if the request rounds to zero at cover scale.
|
||||
* `tesSUCCESS` if the amendment is disabled or the request is safely supra-ULP.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
canApplyToBrokerCover(
|
||||
@@ -56,82 +56,6 @@ canApplyToBrokerCover(
|
||||
beast::Journal j,
|
||||
std::string_view logPrefix);
|
||||
|
||||
namespace detail {
|
||||
|
||||
/**
|
||||
* Return a LoanBroker's posterior live cover exponent after applying an
|
||||
* unrounded delta.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getPosteriorBrokerCoverScale(SLE::ConstRef vault, SLE::ConstRef broker, Number const& delta);
|
||||
|
||||
/**
|
||||
* Round a cover outflow delta (cover withdraw, cover clawback, the
|
||||
* broker-side decrease of a default's cover credit) at the LoanBroker's
|
||||
* posterior live exponent. Safe to round the delta directly for an outflow:
|
||||
* amount on the posterior grid of CoverAvailable always leaves CoverAvailable
|
||||
* minus amount representable, since the posterior grid is exactly the grid
|
||||
* CoverAvailable itself will canonicalize to after the subtraction.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorBrokerCoverScale(
|
||||
SLE::ConstRef vault,
|
||||
SLE::ConstRef broker,
|
||||
STAmount const& delta,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* Round a cover debit (cover withdraw, cover clawback, the broker-side
|
||||
* decrease of a default's cover credit): amount is the non-negative
|
||||
* magnitude leaving CoverAvailable. Negates so the posterior is computed for
|
||||
* CoverAvailable minus amount, then negates the result back to a
|
||||
* non-negative magnitude, per roundToPosteriorBrokerCoverScale's outflow
|
||||
* rationale above. Magnitude in, magnitude out -- unlike the vault-side
|
||||
* outflow dispatcher (clampVaultOutflow), which takes and returns a negative
|
||||
* delta; callers on each side already hold the value in that side's native
|
||||
* form.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
debitToPosteriorBrokerCoverScale(
|
||||
SLE::ConstRef vault,
|
||||
SLE::ConstRef broker,
|
||||
STAmount const& amount,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
/**
|
||||
* Round a cover inflow (cover deposit, LoanPay's fee redirect into cover)
|
||||
* into CoverAvailable by flooring the sum rather than the delta:
|
||||
* credit = floor16(CoverAvailable + raw) - CoverAvailable, never finer
|
||||
* than the Vault's base exponent (-Scale). See
|
||||
* creditToPosteriorAvailableScale's doc for why the sum, not the delta,
|
||||
* must be floored; the same reasoning applies to CoverAvailable.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorBrokerCoverScale(
|
||||
SLE::ConstRef vault,
|
||||
SLE::ConstRef broker,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
/**
|
||||
* Check whether amount is an admissible optional cover inflow.
|
||||
*
|
||||
* Legacy and CashBasis Vaults always succeed. A LoanBroker attached to a
|
||||
* FixedPrecision Vault must remain at the Vault's base scale after applying
|
||||
* the rounded amount, and its posterior CoverAvailable must stay within the
|
||||
* Open zone.
|
||||
*
|
||||
* LoanPay's fee redirect into cover is a mandatory inflow and does not call
|
||||
* this check, so it can push CoverAvailable past the Open zone limit this
|
||||
* check enforces. Once that happens, every subsequent LoanBrokerCoverDeposit
|
||||
* returns tecLIMIT_EXCEEDED until the owner withdraws cover back under the
|
||||
* limit; LoanBrokerCoverWithdraw and LoanBrokerCoverClawback are unaffected.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkOptionalBrokerCoverInflow(SLE::ConstRef vault, SLE::ConstRef broker, STAmount const& amount);
|
||||
|
||||
// Lending protocol has dependencies, so capture them here.
|
||||
bool
|
||||
checkLendingProtocolDependencies(Rules const& rules, STTx const& tx);
|
||||
@@ -140,13 +64,13 @@ checkLendingProtocolDependencies(Rules const& rules, STTx const& tx);
|
||||
* The accounts and asset that LoanManage::defaultLoan's fixCleanup3_4_0
|
||||
* freeze/lock exemption applies to.
|
||||
*
|
||||
* defaultLoan moves funds from the LoanBroker pseudo-account to the Vault
|
||||
* pseudo-account via accountSend. Since neither is the vault asset's
|
||||
* `defaultLoan` moves funds from the LoanBroker pseudo-account to the Vault
|
||||
* pseudo-account via `accountSend`. Since neither is the vault asset's
|
||||
* issuer, this is a third-party transfer that transits through the issuer in
|
||||
* two hops (broker -> issuer, issuer -> vault; see
|
||||
* directSendNoLimitIOU/directSendNoLimitMPT), so the exemption must cover
|
||||
* `directSendNoLimitIOU`/`directSendNoLimitMPT`), so the exemption must cover
|
||||
* both the issuer/broker and issuer/vault pairs, not a direct broker/vault
|
||||
* pair. asset scopes it further to the vault's own currency/MPT issuance,
|
||||
* pair. `asset` scopes it further to the vault's own currency/MPT issuance,
|
||||
* so an unrelated one the same accounts happen to hold is still protected.
|
||||
*/
|
||||
struct LoanDefaultFreezeExemptAccounts
|
||||
@@ -164,10 +88,10 @@ struct LoanDefaultFreezeExemptAccounts
|
||||
* @param view Ledger view used to resolve the Loan -> LoanBroker -> Vault
|
||||
* chain.
|
||||
* @param tx The transaction under invariant review.
|
||||
* @return The exempt accounts and asset if tx is a ttLOAN_MANAGE
|
||||
* transaction with the tfLoanDefault flag set, fixCleanup3_4_0 is
|
||||
* @return The exempt accounts and asset if `tx` is a `ttLOAN_MANAGE`
|
||||
* transaction with the `tfLoanDefault` flag set, `fixCleanup3_4_0` is
|
||||
* enabled, and the loan/broker/vault objects it references can all be
|
||||
* resolved; std::nullopt otherwise.
|
||||
* resolved; `std::nullopt` otherwise.
|
||||
*/
|
||||
[[nodiscard]] std::optional<LoanDefaultFreezeExemptAccounts>
|
||||
getLoanDefaultFreezeExemptAccounts(ReadView const& view, STTx const& tx);
|
||||
@@ -330,38 +254,28 @@ adjustImpreciseNumber(
|
||||
value = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a signed delta to a LoanBroker's DebtTotal: on FixedPrecision Vaults,
|
||||
* add the exact delta (DebtTotal moves by exactly the same amount as the
|
||||
* Vault's AssetsDeployed, never re-rounded); on Legacy/CashBasis Vaults,
|
||||
* round through adjustImpreciseNumber at vaultScale. Shared by LoanSet
|
||||
* (origination) and LoanPay (payment, where delta is negative).
|
||||
*
|
||||
* @param brokerSle The LoanBroker whose DebtTotal is adjusted.
|
||||
* @param vaultSle The LoanBroker's Vault.
|
||||
* @param delta The signed change to DebtTotal.
|
||||
* @param vaultScale The Vault scale the caller captured before changing
|
||||
* AssetsTotal. Legacy/CashBasis round DebtTotal at that scale, not at
|
||||
* the scale after this transaction's AssetsTotal change. Unused on
|
||||
* FixedPrecision Vaults.
|
||||
*/
|
||||
void
|
||||
adjustBrokerDebtTotal(
|
||||
SLE::Ref brokerSle,
|
||||
SLE::ConstRef vaultSle,
|
||||
Number const& delta,
|
||||
int vaultScale);
|
||||
inline int
|
||||
getAssetsTotalScale(SLE::ConstRef vaultSle)
|
||||
{
|
||||
if (!vaultSle)
|
||||
return Number::kMinExponent - 1; // LCOV_EXCL_LINE
|
||||
return scale(vaultSle->at(sfAssetsTotal), vaultSle->at(sfAsset));
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum required broker cover, rounded up.
|
||||
*
|
||||
* DebtTotal is a broker-level aggregate, never rounded at an individual
|
||||
* loan's scale. Legacy and CashBasis Vaults round at the live AssetsTotal
|
||||
* exponent. FixedPrecision Vaults round at the Vault's base exponent
|
||||
* (-Scale, or 0 for integral assets).
|
||||
*/
|
||||
Number
|
||||
minimumBrokerCover(Number const& debtTotal, TenthBips32 coverRateMinimum, SLE::ConstRef vaultSle);
|
||||
// Compute the minimum required broker cover, rounded consistently.
|
||||
// DebtTotal is a broker-level aggregate maintained at vault scale, so the
|
||||
// rounding must also use vault scale — never an individual loan's scale.
|
||||
inline Number
|
||||
minimumBrokerCover(Number const& debtTotal, TenthBips32 coverRateMinimum, SLE::ConstRef vaultSle)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vaultSle && vaultSle->getType() == ltVAULT, "xrpl::minimumBrokerCover : valid Vault sle");
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
|
||||
return roundToAsset(
|
||||
vaultSle->at(sfAsset),
|
||||
tenthBipsOfValue(debtTotal, coverRateMinimum),
|
||||
getAssetsTotalScale(vaultSle));
|
||||
}
|
||||
|
||||
TER
|
||||
checkLoanGuards(
|
||||
@@ -465,10 +379,10 @@ loanPaymentDeltas(LoanPaymentParts const& parts);
|
||||
|
||||
} // namespace cash_basis
|
||||
|
||||
// Public dispatchers: pick cash_basis:: if the Vault's LEVersion
|
||||
// (VaultHelpers::getVaultVersion) is CashBasis or later (CashBasis or
|
||||
// FixedPrecision), else instant_recognition::. These are the only entry
|
||||
// points transactors call.
|
||||
// Public dispatchers: pick cash_basis:: if featureLendingProtocolV1_1 is
|
||||
// enabled AND the Vault's LEVersion (VaultHelpers::getVaultVersion) is
|
||||
// VaultVersion::CashBasis, else instant_recognition::. These are the only entry points
|
||||
// transactors call.
|
||||
AccountingDeltas
|
||||
loanOriginationDeltas(
|
||||
SLE::ConstRef vaultSle,
|
||||
|
||||
@@ -312,22 +312,12 @@ checkCreateMPT(
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* MaximumAmount doesn't exceed 2**63-1
|
||||
*/
|
||||
// MaximumAmount doesn't exceed 2**63-1
|
||||
std::int64_t
|
||||
maxMPTAmount(SLE const& sleIssuance);
|
||||
|
||||
/**
|
||||
* std::nullopt if the issuance doesn't exist.
|
||||
*/
|
||||
std::optional<std::int64_t>
|
||||
maxMPTAmount(ReadView const& view, MPTID const& mptID);
|
||||
|
||||
/**
|
||||
* OutstandingAmount may overflow and available amount might be negative.
|
||||
* But available amount is always <= |MaximumAmount - OutstandingAmount|.
|
||||
*/
|
||||
// OutstandingAmount may overflow and available amount might be negative.
|
||||
// But available amount is always <= |MaximumAmount - OutstandingAmount|.
|
||||
std::int64_t
|
||||
availableMPTAmount(SLE const& sleIssuance);
|
||||
|
||||
@@ -341,12 +331,7 @@ availableMPTAmount(ReadView const& view, MPTID const& mptID);
|
||||
* directSendNoFee transactions that bypass the payment engine.
|
||||
* 2. **accountSend & Payment Engine (Overflow: Yes):** A temporary overflow
|
||||
* check when `OutstandingAmount > UINT64_MAX`. This higher threshold is used
|
||||
* for `accountSend` and payments processed via the payment engine, whose
|
||||
* reverse pass may issue before it redeems.
|
||||
*
|
||||
* In both cases a single `sendAmount > MaximumAmount` is rejected. Such a send
|
||||
* can never settle: redemptions within the transaction are bounded by the
|
||||
* outstanding amount, so the final OutstandingAmount would exceed the cap.
|
||||
* for `accountSend` and payments processed via the payment engine.
|
||||
*/
|
||||
bool
|
||||
isMPTOverflow(
|
||||
@@ -368,12 +353,6 @@ issuerFundsToSelfIssue(ReadView const& view, MPTIssue const& issue);
|
||||
/**
|
||||
* Facilitate tracking of MPT sold by an issuer owning MPT sell offer.
|
||||
* See ApplyView::issuerSelfDebitHookMPT().
|
||||
*
|
||||
* `amount` may be 0. BookStep removes an offer whose fee-adjusted funded
|
||||
* output is 0, but other clips (for example MaximumAmount via limitStepIn)
|
||||
* can still consume a zero slice of a funded remainder. The hook must
|
||||
* accept 0: first call still snapshots origBalance; a zero debit adds
|
||||
* nothing to selfDebit.
|
||||
*/
|
||||
void
|
||||
issuerSelfDebitHookMPT(ApplyView& view, MPTIssue const& issue, std::uint64_t amount);
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::proposal {
|
||||
|
||||
@@ -38,21 +31,30 @@ proposalOwnerCount(STObject const& proposedTx)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the proposed transaction is independently submittable through the
|
||||
* ordinary multi-sign path: not a proposal transaction itself, not a
|
||||
* pseudo-transaction, not itself flagged as someone else's inner batch
|
||||
* transaction, and — if it is a Batch — every one of its own inner
|
||||
* transactions carries a TransactionType and is neither a proposal, a
|
||||
* pseudo-transaction, nor a nested Batch. A Batch inner transaction cannot
|
||||
* lack a TransactionType, be pseudo, or be a nested Batch (STTx construction
|
||||
* rejects a typeless inner and an inner Batch, and preflight0 rejects the
|
||||
* pseudo/tfInnerBatchTxn combination), but those guards live outside this
|
||||
* feature, so they are checked again here rather than relied upon.
|
||||
* Whether the proposed transaction is itself a proposal transaction, which
|
||||
* would nest one proposal inside another.
|
||||
*
|
||||
* TODO: cover ttTRANSACTION_PROPOSAL_SIGN once that transaction exists.
|
||||
* TODO: cover ttTRANSACTION_PROPOSAL_SIGN and ttTRANSACTION_PROPOSAL_CANCEL
|
||||
* once those transactions exist.
|
||||
*/
|
||||
inline bool
|
||||
isProposalTx(STObject const& proposedTx)
|
||||
{
|
||||
return proposedTx.getFieldU16(sfTransactionType) == ttTRANSACTION_PROPOSAL_CREATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the proposed transaction is independently submittable through the
|
||||
* ordinary multi-sign path: not a nested proposal, not a pseudo-transaction,
|
||||
* not itself flagged as someone else's inner batch transaction, and — if it
|
||||
* is a Batch — none of its own inner transactions is a nested proposal or a
|
||||
* pseudo-transaction either. A Batch inner transaction cannot itself be
|
||||
* pseudo (preflight0 rejects the pseudo/tfInnerBatchTxn combination
|
||||
* generically), but that guard lives outside this feature, so it is checked
|
||||
* again here rather than relied upon.
|
||||
*/
|
||||
bool
|
||||
isValidProposalTxnType(STObject const& proposedTx);
|
||||
isValidProposal(STObject const& proposedTx);
|
||||
|
||||
/**
|
||||
* Whether the proposed transaction carries any signature field.
|
||||
@@ -82,93 +84,4 @@ hasEmptySigningPubKey(STObject const& proposedTx)
|
||||
proposedTx.getFieldVL(sfSigningPubKey).empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the proposal is terminal (On-Chain Cosigner spec §4.5). A terminal proposal
|
||||
* stops accepting signatures and anyone may delete it. An already signed
|
||||
* payload can still execute after Expiration, because the Ticket reservation
|
||||
* compares payloads only; a passed LastLedgerSequence does stop it.
|
||||
*
|
||||
* A proposal is terminal when either:
|
||||
* - its Expiration has passed (the parent ledger closed at or after it), or
|
||||
* - the proposed transaction carries a LastLedgerSequence and the current
|
||||
* ledger sequence is strictly greater than it. This mirrors the ordinary
|
||||
* transactor's tefMAX_LEDGER rule: at view.seq() == LastLedgerSequence the
|
||||
* payload can still be submitted in the current ledger, so the proposal is
|
||||
* still live.
|
||||
*
|
||||
* @param view The ledger the deciding transaction is being applied to.
|
||||
* @param expiration The proposal's Expiration field.
|
||||
* @param proposedTx The proposal's ProposedTransaction field.
|
||||
*/
|
||||
bool
|
||||
isTerminal(
|
||||
ReadView const& view,
|
||||
std::optional<std::uint32_t> expiration,
|
||||
STObject const& proposedTx);
|
||||
|
||||
/**
|
||||
* Delete a TransactionProposal ledger entry.
|
||||
*
|
||||
* Removes the entry from its Owner's directory, releases the reserve the
|
||||
* proposal holds against the Owner, and erases the entry. Shared by every
|
||||
* deletion path the spec defines (On-Chain Cosigner spec §4.5): automatic cleanup when the
|
||||
* proposed transaction's TicketSequence is consumed, and the
|
||||
* TransactionProposalCancel / TransactionProposalSign cleanup paths once
|
||||
* those transactions exist.
|
||||
*
|
||||
* The release goes through decreaseOwnerCountForObject, so it follows the
|
||||
* sfSponsor an entry created with reserve sponsorship carries: the reserve is
|
||||
* returned to whoever was holding it, the Owner or that sponsor.
|
||||
*
|
||||
* @param view The apply view for making changes
|
||||
* @param sleProposal The TransactionProposal ledger entry to delete
|
||||
* @param j Journal for logging
|
||||
* @return tesSUCCESS, tecINTERNAL for an invalid entry, or tefBAD_LEDGER if
|
||||
* the ledger contradicts the entry
|
||||
*/
|
||||
TER
|
||||
deleteProposal(ApplyView& view, SLE::pointer const& sleProposal, beast::Journal j);
|
||||
|
||||
/**
|
||||
* Whether tx carries the same payload as a proposal's stored
|
||||
* ProposedTransaction (On-Chain Cosigner spec §4.2.1).
|
||||
*
|
||||
* The payload is the signing payload: the fields the collected signatures
|
||||
* commit to. Compared is every field except those flagged kNotSigning
|
||||
* (TxnSignature, Signers, BatchSigners, CounterpartySignature,
|
||||
* SponsorSignature, Signature, MasterSignature) and SigningPubKey, which is
|
||||
* stored empty and filled only if the target signs with its own key. So the
|
||||
* completed transaction matches no matter which mix of collected or
|
||||
* off-ledger signatures it carries, and any change to a non-signature field
|
||||
* results in a false-return value.
|
||||
*
|
||||
* Signature (PaymentChannelClaim and the XChain attestation transactions)
|
||||
* is deliberately not compared: it is outside the account's own TxnSignature
|
||||
* even for an ordinary submission, so a proposal cannot bind it more tightly
|
||||
* than the signatures it collects do, and those transactors still validate a
|
||||
* substituted value exactly as they would for an ordinary submission.
|
||||
* MasterSignature appears in no transaction format.
|
||||
*
|
||||
* @param proposedTx The proposal's ProposedTransaction field.
|
||||
* @param tx The transaction to compare, typically an STTx.
|
||||
*/
|
||||
bool
|
||||
payloadMatches(STObject const& proposedTx, STObject const& tx);
|
||||
|
||||
/**
|
||||
* Whether tx may consume its Ticket.
|
||||
*
|
||||
* While a proposal keyed to the Ticket exists, the target's Ticket is
|
||||
* reserved: only the proposal's own proposed transaction may spend it
|
||||
* (On-Chain Cosigner spec §4.2.1), so that unrelated target-account activity cannot
|
||||
* invalidate the proposal while signatures are being collected.
|
||||
* TransactionProposalCancel is not allowed through: it deletes the
|
||||
* reservation without consuming the ticket.
|
||||
*
|
||||
* @param view The ledger containing the Ticket and any reservation.
|
||||
* @param tx The transaction attempting to consume that ticket.
|
||||
*/
|
||||
bool
|
||||
canConsumeTicket(ReadView const& view, STTx const& tx);
|
||||
|
||||
} // namespace xrpl::proposal
|
||||
@@ -316,20 +316,6 @@ transferRate(ReadView const& view, Asset const& asset);
|
||||
[[nodiscard]] Rate
|
||||
transferRate(ReadView const& view, STAmount const& amount);
|
||||
|
||||
/**
|
||||
* Returns the amount a sender must part with for `amount` to reach the
|
||||
* receiver once the issuer takes the transfer fee. Rounds up.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
addTransferFee(STAmount const& amount, Rate const& rate);
|
||||
|
||||
/**
|
||||
* Returns the amount a receiver gets when `amount` leaves the sender and the
|
||||
* issuer takes the transfer fee. Rounds down.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
subtractTransferFee(STAmount const& amount, Rate const& rate);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// Holding operations (Asset-based dispatchers)
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
@@ -14,239 +12,11 @@
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class STTx;
|
||||
|
||||
/**
|
||||
* @brief Return the Vault's current assets total.
|
||||
*
|
||||
* FixedPrecision Vaults return AssetsAvailable plus AssetsDeployed, with no
|
||||
* rounding to the asset's 16-digit precision: the sum is exact at Number's
|
||||
* active 19-digit mantissa width (the large mantissa range, enabled by
|
||||
* featureSingleAssetVault or featureLendingProtocol), rounds Downward regardless of the
|
||||
* caller's ambient mode beyond that, and is not rounded to match the cached
|
||||
* sfAssetsTotal field. Legacy and CashBasis Vaults return the stored
|
||||
* AssetsTotal.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
* @return The current assets total.
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
getAssetsTotal(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* @brief A change to a FixedPrecision Vault's balances.
|
||||
*/
|
||||
struct VaultBalanceChange
|
||||
{
|
||||
/**
|
||||
* Signed change to AssetsAvailable. Must be the exact amount moved on the
|
||||
* Vault pseudo-account in the same transaction. Absent means no cash
|
||||
* moved; adjustVaultBalances then uses zero of the Vault's own asset.
|
||||
*/
|
||||
std::optional<STAmount> cash = std::nullopt;
|
||||
|
||||
/**
|
||||
* Signed change to AssetsDeployed: open Loan principal, exact.
|
||||
*/
|
||||
Number deployed = 0;
|
||||
|
||||
/**
|
||||
* Signed change to scheduled, unpaid interest (YieldUnrealized), exact.
|
||||
*/
|
||||
Number yield = 0;
|
||||
|
||||
/**
|
||||
* Signed change to impaired principal (LossUnrealized), exact.
|
||||
*/
|
||||
Number loss = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Apply a balance change to a FixedPrecision Vault. The only writer of
|
||||
* AssetsAvailable, AssetsDeployed, AssetsTotal, YieldUnrealized and
|
||||
* LossUnrealized on these Vaults after creation.
|
||||
*
|
||||
* @param vault The vault SLE. Must be FixedPrecision.
|
||||
* @param change The balance change to apply.
|
||||
* @param j Journal for logging the fatal (tefBAD_LEDGER) case.
|
||||
*
|
||||
* @return tesSUCCESS; tecLIMIT_EXCEEDED if LossUnrealized would exceed
|
||||
* AssetsDeployed; tefBAD_LEDGER if AssetsAvailable, AssetsDeployed,
|
||||
* LossUnrealized or YieldUnrealized would become negative.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
adjustVaultBalances(SLE::Ref vault, VaultBalanceChange const& change, beast::Journal j);
|
||||
|
||||
/**
|
||||
* @brief Apply a signed cash movement to a Vault of any version. FixedPrecision
|
||||
* Vaults go through adjustVaultBalances; Legacy and CashBasis Vaults add the
|
||||
* amount to both AssetsTotal and AssetsAvailable.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
* @param delta Signed change to AssetsAvailable; the exact amount moved on the
|
||||
* Vault pseudo-account.
|
||||
* @param j Journal passed through to adjustVaultBalances.
|
||||
*
|
||||
* @return tesSUCCESS, or the adjustVaultBalances error for FixedPrecision Vaults.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
adjustVaultCash(SLE::Ref vault, STAmount const& delta, beast::Journal j);
|
||||
|
||||
/**
|
||||
* Return the Vault's current live exponent.
|
||||
*
|
||||
* Legacy and CashBasis Vaults use the exponent of AssetsTotal. FixedPrecision
|
||||
* Vaults use max(base exponent, exponent of AssetsTotal): the grid is never
|
||||
* finer than -Scale and coarsens once AssetsTotal outgrows 16 digits there.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getVaultScale(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Return the Vault's base exponent.
|
||||
*
|
||||
* Legacy and CashBasis Vaults use their current live exponent. FixedPrecision
|
||||
* Vaults use -Scale, or 0 for integral assets.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getVaultBaseScale(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Round an amount at the Vault's posterior live exponent.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorVaultScale(
|
||||
SLE::ConstRef vault,
|
||||
STAmount const& amount,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
namespace detail {
|
||||
|
||||
/**
|
||||
* Return the Vault's posterior live exponent after applying an unrounded delta.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getPosteriorVaultScale(SLE::ConstRef vault, STAmount const& delta);
|
||||
|
||||
/**
|
||||
* Shared grid-floor core of getVaultScale / getPosteriorVaultScale /
|
||||
* getPosteriorBrokerCoverScale: baseScale when reference is zero, otherwise
|
||||
* max(baseScale, scale(reference, asset)), rounded Downward regardless of
|
||||
* the caller's ambient rounding mode.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
liveScale(Number const& reference, Asset const& asset, int baseScale);
|
||||
|
||||
/**
|
||||
* Shared core of getPosteriorVaultScale and getPosteriorBrokerCoverScale:
|
||||
* the exponent reference + delta would have under version's live-scale rule
|
||||
* (floored at baseScale for FixedPrecision, unbounded for Legacy/CashBasis).
|
||||
*
|
||||
* @param version The Vault's LEVersion.
|
||||
* @param asset The Vault's underlying asset.
|
||||
* @param baseScale The Vault's base exponent; unused outside the
|
||||
* FixedPrecision case.
|
||||
* @param reference The balance the delta is applied to (AssetsAvailable,
|
||||
* AssetsTotal, or a LoanBroker's CoverAvailable).
|
||||
* @param delta The unrounded change to reference.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
posteriorAssetScale(
|
||||
VaultVersion version,
|
||||
Asset const& asset,
|
||||
int baseScale,
|
||||
Number const& reference,
|
||||
Number const& delta);
|
||||
|
||||
/**
|
||||
* Shared core of creditToPosteriorAvailableScale and
|
||||
* creditToPosteriorBrokerCoverScale: floors reference + raw at the sum's
|
||||
* own live scale (never finer than baseScale) and returns the difference
|
||||
* from reference, so the delta applied is exactly what moves reference onto
|
||||
* the floored sum. See creditToPosteriorAvailableScale's doc for why the
|
||||
* sum, not raw alone, must be floored. raw is the exact amount; it is not
|
||||
* rounded on its own before being added to reference. The sum and its scale
|
||||
* are computed once, in roundingMode, so a Downward credit is always
|
||||
* between zero and raw.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorScale(
|
||||
Asset const& asset,
|
||||
Number const& reference,
|
||||
int baseScale,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* Round a FixedPrecision cash inflow (LoanPay's credit, LoanManage's
|
||||
* default cover credit) into AssetsAvailable, never AssetsTotal, which is
|
||||
* a derived cache, by flooring the sum rather than the delta:
|
||||
* credit equals floor16(AssetsAvailable + raw) minus AssetsAvailable, never
|
||||
* finer than the Vault's base exponent (-Scale).
|
||||
*
|
||||
* Flooring only the delta at its own posterior grid is not enough: the sum
|
||||
* can still need a 17th digit when AssetsAvailable is off the coarser grid
|
||||
* right after crossing a power of ten. Example at Scale 6: AssetsAvailable
|
||||
* equals 9999999999.999999 (16 digits already) and a credit of 0.000011
|
||||
* floors to 0.00001 on its own (finer) grid, but 9999999999.999999 plus
|
||||
* 0.00001 equals 10000000000.000009, which still needs 17 digits. Flooring
|
||||
* the sum instead, floor16(9999999999.999999 + 0.000011) minus
|
||||
* 9999999999.999999, is exact by construction, since STAmount's own
|
||||
* 16-digit canonical form of the sum is what gets subtracted from.
|
||||
*
|
||||
* raw is taken as an exact Number: rounding it to 16 digits before adding
|
||||
* it to AssetsAvailable could drop a tail that moves the floored sum.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorAvailableScale(
|
||||
SLE::ConstRef vault,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
/**
|
||||
* Open-zone capacity ceiling: 9 * 10^(15 + baseScale).
|
||||
*
|
||||
* Defined only for FixedPrecision Vaults, where this is 9 * 10^(15 - P).
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
getVaultOpenLimit(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Check whether amount is an admissible optional inflow.
|
||||
*
|
||||
* Legacy and CashBasis Vaults always succeed. FixedPrecision Vaults must
|
||||
* remain at their base scale after applying the rounded amount, and the
|
||||
* posterior capacity (AssetsTotal + YieldUnrealized + rounded amount) must
|
||||
* stay within the Open zone.
|
||||
*
|
||||
* The amount is rounded toward zero at the posterior live exponent.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkOptionalVaultInflow(SLE::ConstRef vault, STAmount const& amount);
|
||||
|
||||
/**
|
||||
* Open-zone capacity after adding roundedAmount: AssetsTotal + YieldUnrealized
|
||||
* + roundedAmount, computed TowardsZero regardless of the caller's ambient
|
||||
* rounding mode. Defined only for FixedPrecision Vaults.
|
||||
*
|
||||
* Shared by checkOptionalVaultInflow and LoanSet's loan-origination capacity
|
||||
* check, which pair this formula with different scale checks (posterior vs.
|
||||
* current), so only the capacity formula itself -- not the whole check -- is
|
||||
* shared here.
|
||||
*
|
||||
* @param vault The vault SLE. Must be FixedPrecision.
|
||||
* @param roundedAmount The amount to add, already rounded to the relevant
|
||||
* posterior or base scale by the caller.
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
vaultOpenZoneCapacity(SLE::ConstRef vault, Number const& roundedAmount);
|
||||
|
||||
/**
|
||||
* From the perspective of a vault, return the number of shares to give
|
||||
* depositor when they offer a fixed amount of assets. Note, since shares are
|
||||
@@ -276,93 +46,31 @@ assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
|
||||
sharesToAssetsDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount const& shares);
|
||||
|
||||
/**
|
||||
* Adjusts a requested asset change (delta) to match the decimal scale of the
|
||||
* updated total vault assets. This ensures sfAssetsTotal, sfAssetsAvailable,
|
||||
* Adjusts a requested asset change (`delta`) to match the decimal scale of the
|
||||
* updated total vault assets. This ensures `sfAssetsTotal`, `sfAssetsAvailable`,
|
||||
* and the actual asset transfer change by the exact same representable amount.
|
||||
*
|
||||
* Rounding strategy:
|
||||
* - Legacy/CashBasis debits (withdrawals): rounds down the magnitude of delta
|
||||
* on the new scale to prevent paying out more than requested.
|
||||
* - Legacy/CashBasis credits: floors the resulting total asset balance and
|
||||
* returns the difference from the current total.
|
||||
* - FixedPrecision credits only (VaultDeposit): rounds the delta toward zero
|
||||
* at the scale getAssetsTotal (AssetsAvailable + AssetsDeployed, derived,
|
||||
* not the stored AssetsTotal cache) would have after applying delta.
|
||||
* FixedPrecision outflows (VaultWithdraw, VaultClawback) never call this;
|
||||
* they use clampVaultOutflow instead.
|
||||
* - Debits (withdrawals): Rounds down `|delta|` on the new scale to prevent
|
||||
* paying out more than requested.
|
||||
* - Credits (deposits): Floors the resulting total asset balance and returns the
|
||||
* difference from the current total. This prevents crediting the vault with
|
||||
* more assets than the user deposited.
|
||||
*
|
||||
* Key rules:
|
||||
* - The returned magnitude never exceeds the magnitude of delta.
|
||||
* - Returns tecPRECISION_LOSS if the change is smaller than 1 ULP of the target scale
|
||||
* - The returned magnitude never exceeds `|delta|`.
|
||||
* - Returns `tecPRECISION_LOSS` if the change is smaller than 1 ULP of the target scale
|
||||
* (prevents share operations when totals cannot change).
|
||||
* - For integer assets (XRP, MPT), rounding is a no-op.
|
||||
*
|
||||
* @param vault The vault ledger entry.
|
||||
* @param delta The requested signed change to sfAssetsTotal.
|
||||
* @return The rounded, positive magnitude, or tecPRECISION_LOSS if the
|
||||
* @return The rounded, positive magnitude, or `tecPRECISION_LOSS` if the
|
||||
* change is below representable precision.
|
||||
*/
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta);
|
||||
|
||||
/**
|
||||
* Outflow clamp dispatcher for VaultWithdraw and VaultClawback: rounds a
|
||||
* negative delta to the scale appropriate for the Vault's version -- for
|
||||
* FixedPrecision, at AssetsAvailable's own posterior scale (the balance the
|
||||
* cash actually leaves); for Legacy/CashBasis, clampToAssetsTotalScale --
|
||||
* so both call sites share one dispatch point. Same tecPRECISION_LOSS and
|
||||
* integral-asset and magnitude-never-exceeds-delta rules as
|
||||
* clampToAssetsTotalScale.
|
||||
*
|
||||
* @param vault The vault ledger entry.
|
||||
* @param delta The requested signed outflow (negative).
|
||||
* @return The rounded, positive magnitude, or tecPRECISION_LOSS if the
|
||||
* change is below representable precision.
|
||||
*/
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampVaultOutflow(SLE::ConstRef vault, STAmount const& delta);
|
||||
|
||||
/**
|
||||
* Returns the Vault's base exponent for asset at scale, before a Vault
|
||||
* object exists to read it from: 0 for an integral asset, -scale otherwise.
|
||||
* Used by VaultCreate::preclaim, which only has the proposed Scale field,
|
||||
* not yet a Vault SLE; getVaultBaseScale is the post-creation equivalent.
|
||||
*
|
||||
* @param asset The (prospective) Vault's underlying asset.
|
||||
* @param scale The (prospective) Vault's sfScale value, ignored for
|
||||
* integral assets.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
vaultBaseScale(Asset const& asset, std::uint8_t scale);
|
||||
|
||||
/**
|
||||
* Returns true iff value is exactly representable both as an STAmount of
|
||||
* asset and on the base grid at baseScale (no precision lost rounding
|
||||
* TowardsZero at baseScale). Returns false, rather than throwing, for a value
|
||||
* out of range for the asset. Shared representability check for
|
||||
* VaultCreate::preclaim, VaultSet::preclaim and LoanBrokerSet::preclaim's
|
||||
* AssetsMaximum / DebtMaximum / cover-rate field checks.
|
||||
*
|
||||
* @param asset The vault's underlying asset.
|
||||
* @param value The value to check.
|
||||
* @param baseScale The vault's base exponent (see vaultBaseScale /
|
||||
* getVaultBaseScale).
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isOnVaultBaseGrid(Asset const& asset, Number const& value, int baseScale);
|
||||
|
||||
/**
|
||||
* Checks that a requested AssetsMaximum is exactly representable on the
|
||||
* Vault's base grid, otherwise associateAsset would silently round the cap
|
||||
* the owner asked for. Used by VaultSet::preclaim on an existing Vault.
|
||||
*
|
||||
* @param vault The vault ledger entry.
|
||||
* @param amount The requested AssetsMaximum.
|
||||
* @return tesSUCCESS, or tecPRECISION_LOSS if amount is not representable.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkAssetsMaximum(SLE::ConstRef vault, Number const& amount);
|
||||
|
||||
/**
|
||||
* Controls whether to truncate shares instead of rounding.
|
||||
*/
|
||||
@@ -390,24 +98,21 @@ enum class WaiveUnrealizedLoss : bool { No = false, Yes = true };
|
||||
assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive);
|
||||
|
||||
/**
|
||||
* Reject a cash outflow that is dust (see debitIsNonZeroDust) or whose check
|
||||
* overflows Number, which is easy to hit with a large Scale.
|
||||
* Returns true if debiting `amount` from `total` (the current value of a
|
||||
* vault's sfAssetsTotal or sfAssetsAvailable) would canonicalize to the
|
||||
* same STAmount value. This happens when `amount` is non-zero but too small
|
||||
* to change the stored total at STAmount's precision. Shares would still
|
||||
* move, so the ValidVault invariant would fail after apply; callers use
|
||||
* this to reject the transaction upfront instead.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
* @param amount The amount to debit.
|
||||
* @param j Journal for the debug log.
|
||||
* @param name Transaction name used as the log prefix.
|
||||
*
|
||||
* @return tesSUCCESS; tecPRECISION_LOSS if amount is dust; tecPATH_DRY if the
|
||||
* check overflows. Overflow means the transaction cannot apply, but
|
||||
* ledger state is still consistent.
|
||||
* @param asset The vault's underlying asset, used to canonicalize both
|
||||
* sides the same way the ledger will when the field is stored.
|
||||
* @param total The field's current value.
|
||||
* @param amount The amount to debit. Zero always returns false; that case
|
||||
* is rejected separately.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkDebitNotDust(
|
||||
SLE::ConstRef vault,
|
||||
Number const& amount,
|
||||
beast::Journal j,
|
||||
std::string_view name);
|
||||
[[nodiscard]] bool
|
||||
debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount);
|
||||
|
||||
/**
|
||||
* From the perspective of a vault, return the number of shares to demand from
|
||||
@@ -453,7 +158,7 @@ sharesToAssetsWithdraw(
|
||||
WaiveUnrealizedLoss waive = WaiveUnrealizedLoss::No);
|
||||
|
||||
/**
|
||||
* Returns true iff account holds all of the vault's outstanding shares,
|
||||
* Returns true iff `account` holds all of the vault's outstanding shares —
|
||||
* i.e. is the sole remaining shareholder. Returns false if the account
|
||||
* holds no shares or fewer than the total outstanding.
|
||||
*
|
||||
@@ -466,13 +171,11 @@ sharesToAssetsWithdraw(
|
||||
isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef issuance);
|
||||
|
||||
/**
|
||||
* Resolves a Vault's LEVersion.
|
||||
*
|
||||
* LEVersion is the single point every accounting and rounding helper
|
||||
* should call to decide which protocol a Vault follows. It is written
|
||||
* at VaultCreate and is not updated afterwards, so a Vault created
|
||||
* under an older amendment keeps that behaviour after later amendments
|
||||
* activate. Absent sfLEVersion resolves to VaultVersion::Legacy.
|
||||
* Resolves a Vault's LEVersion, the single point every accounting touch
|
||||
* point should call to determine which recognition model (instant interest
|
||||
* recognition vs. cash-basis) a Vault uses. Vaults created before featureLendingProtocolV1_1
|
||||
* activated never have sfLEVersion set, which resolves here to
|
||||
* VaultVersion::Legacy.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
*
|
||||
@@ -482,34 +185,6 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultVersion(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Decodes an already-extracted sfLEVersion value with the same range check as
|
||||
* getVaultVersion. Usable from contexts, such as an invariant's ledger-entry
|
||||
* snapshot, that keep the field value but not the owning SLE.
|
||||
*
|
||||
* @param leVersion The value of sfLEVersion, or nullopt if absent.
|
||||
*
|
||||
* @return The decoded LEVersion, or VaultVersion::Legacy if absent.
|
||||
*/
|
||||
[[nodiscard]] VaultVersion
|
||||
decodeVaultVersion(std::optional<std::uint8_t> leVersion);
|
||||
|
||||
/**
|
||||
* Resolves which LEVersion a newly-created Vault should get under the
|
||||
* currently active amendments. This is the only place that decides Vault
|
||||
* version policy; VaultCreate calls it instead of checking amendments
|
||||
* directly.
|
||||
*
|
||||
* @param rules The active ledger rules.
|
||||
*
|
||||
* @return VaultVersion::FixedPrecision once featureLendingProtocolV1_1,
|
||||
* featureLendingProtocolV1_2 and fixCleanup3_4_0 are all enabled;
|
||||
* VaultVersion::CashBasis
|
||||
* once featureLendingProtocolV1_1 is enabled; VaultVersion::Legacy otherwise.
|
||||
*/
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultDefaultVersion(Rules const& rules);
|
||||
|
||||
/**
|
||||
* Resolves the VaultKind of a vault SLE. Returns VaultKind::ClosedEnded when
|
||||
* sfVaultKind is present and equal to that value; anything else (including an
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -348,19 +348,16 @@ isAuditorMirrorCurrent(SLE const& issuance, SLE const& mptoken);
|
||||
areMirrorsCurrent(SLE const& issuance, SLE const& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Set the holder's issuer mirror epoch, and the issuer mirror key, to match the issuance's
|
||||
* current key.
|
||||
* @brief Set the holder's issuer mirror epoch to match the issuance's current issuer key epoch.
|
||||
*
|
||||
* Call this after writing the issuer mirror ciphertext under the issuance's
|
||||
* currently registered issuer key, so that the mirror reads as current afterwards.
|
||||
* Also records sfIssuerMirrorEncryptionKey as the issuance's current issuer key
|
||||
* (both fields stay absent at epoch 0, where the original issuer key applies).
|
||||
*
|
||||
* @param issuance The MPTokenIssuance ledger object.
|
||||
* @param mptoken The holder's MPToken ledger entry to update.
|
||||
*/
|
||||
void
|
||||
setIssuerMirrorCurrent(SLE const& issuance, SLE& mptoken);
|
||||
setIssuerMirrorEpoch(SLE const& issuance, SLE& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Set the holder's auditor mirror epoch to match the issuance's current auditor key epoch.
|
||||
@@ -375,18 +372,16 @@ void
|
||||
setAuditorMirrorEpoch(SLE const& issuance, SLE& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Set the holder's MPToken mirror epochs, and the issuer mirror key, to match the issuance's
|
||||
* current keys.
|
||||
* @brief Set the holder's MPToken mirror epochs to match the issuance's current key epochs.
|
||||
*
|
||||
* Call this after writing mirror ciphertexts under the issuance's currently
|
||||
* registered keys, so that the mirrors read as current afterwards. Also records
|
||||
* sfIssuerMirrorEncryptionKey via setIssuerMirrorCurrent.
|
||||
* registered keys, so that the mirrors read as current afterwards.
|
||||
*
|
||||
* @param issuance The MPTokenIssuance ledger object.
|
||||
* @param mptoken The holder's MPToken ledger entry to update.
|
||||
*/
|
||||
void
|
||||
setMirrorsCurrent(SLE const& issuance, SLE& mptoken);
|
||||
setMirrorEpochs(SLE const& issuance, SLE& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Verifies revealed amount encryptions for all recipients.
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -10,25 +10,6 @@ namespace xrpl {
|
||||
// This was the reference fee units used in the old fee calculation.
|
||||
inline constexpr std::uint32_t kFeeUnitsDeprecated = 10;
|
||||
|
||||
// Number of micro-drops in one drop.
|
||||
inline constexpr std::uint32_t microDropsPerDrop{1'000'000};
|
||||
|
||||
/**
|
||||
* Hard protocol bounds on the Feature Extension fee settings. A voted value
|
||||
* can never fall outside these, so `preflight`, which has no view, may bound
|
||||
* against them. Gas and bytecode limits are capped; gas price has a floor
|
||||
* (zero would make WASM execution effectively free).
|
||||
*/
|
||||
inline constexpr std::uint32_t kMaxGasLimit{2'000'000};
|
||||
inline constexpr std::uint32_t kMaxBytecodeSizeLimit{200'000};
|
||||
inline constexpr std::uint32_t kMinGasPrice{1};
|
||||
|
||||
// The following default values of fee settings will seed into FeeSettings
|
||||
// and write to the ledger on featureSmartEscrow activation.
|
||||
inline constexpr std::uint32_t kDefaultGasLimit{1'000'000};
|
||||
inline constexpr std::uint32_t kDefaultBytecodeSizeLimit{100'000};
|
||||
inline constexpr std::uint32_t kDefaultGasPrice{1'000'000};
|
||||
|
||||
/**
|
||||
* Reflects the fee settings for a particular ledger.
|
||||
*
|
||||
@@ -52,47 +33,13 @@ struct Fees
|
||||
*/
|
||||
XRPAmount increment{0};
|
||||
|
||||
/**
|
||||
* @brief Gas limit for Feature Extensions (instructions).
|
||||
*
|
||||
* This and the one below default to the protocol values rather than 0,
|
||||
* because an explicit 0 is the voted kill switch.
|
||||
*/
|
||||
std::uint32_t gasLimit{kDefaultGasLimit};
|
||||
|
||||
/**
|
||||
* @brief Bytecode size limit for Feature Extensions (bytes).
|
||||
*/
|
||||
std::uint32_t bytecodeSizeLimit{kDefaultBytecodeSizeLimit};
|
||||
|
||||
/**
|
||||
* @brief Price of WASM gas (micro-drops).
|
||||
*/
|
||||
std::uint32_t gasPrice{kDefaultGasPrice};
|
||||
|
||||
explicit Fees() = default;
|
||||
Fees(Fees const&) = default;
|
||||
Fees&
|
||||
operator=(Fees const&) = default;
|
||||
|
||||
Fees(XRPAmount base, XRPAmount reserve, XRPAmount increment)
|
||||
: base{base}, reserve{reserve}, increment{increment}
|
||||
{
|
||||
}
|
||||
|
||||
Fees(
|
||||
XRPAmount base,
|
||||
XRPAmount reserve,
|
||||
XRPAmount increment,
|
||||
std::uint32_t gasLimit,
|
||||
std::uint32_t bytecodeSizeLimit,
|
||||
std::uint32_t gasPrice)
|
||||
: base{base}
|
||||
, reserve{reserve}
|
||||
, increment{increment}
|
||||
, gasLimit{gasLimit}
|
||||
, bytecodeSizeLimit{bytecodeSizeLimit}
|
||||
, gasPrice{gasPrice}
|
||||
: base(base), reserve(reserve), increment(increment)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -311,46 +311,24 @@ constexpr std::uint8_t kVaultStrategyFirstComeFirstServe = 1;
|
||||
* Default IOU scale factor for a Vault
|
||||
*/
|
||||
constexpr std::uint8_t kVaultDefaultIouScale = 6;
|
||||
|
||||
/**
|
||||
* Maximum Scale for a Vault created before featureLendingProtocolV1_2.
|
||||
* Chosen so 1 IOU can always convert to shares:
|
||||
* 10^19 > maxMPTokenAmount (2^64-1) > 10^18.
|
||||
* Maximum scale factor for a Vault. The number is chosen to ensure that
|
||||
* 1 IOU can be always converted to shares.
|
||||
* 10^19 > maxMPTokenAmount (2^64-1) > 10^18
|
||||
*/
|
||||
constexpr std::uint8_t kLegacyVaultMaximumIouScale = 18;
|
||||
constexpr std::uint8_t kVaultMaximumIouScale = 18;
|
||||
|
||||
/**
|
||||
* Maximum Scale for a Vault created under featureLendingProtocolV1_2.
|
||||
*/
|
||||
constexpr std::uint8_t kFixedPrecisionVaultMaximumIouScale = 10;
|
||||
|
||||
/**
|
||||
* Vault ledger-entry schema versions, persisted as sfLEVersion.
|
||||
*
|
||||
* LEVersion records which protocol a Vault was created under so later
|
||||
* amendments can change the rules for new Vaults without rewriting
|
||||
* existing ones. VaultCreate writes it from the then-active lending
|
||||
* amendments; later transactions do not update it. A Vault created
|
||||
* under an older amendment keeps that amendment's behaviour for its
|
||||
* lifetime, even after a newer lending amendment activates.
|
||||
*
|
||||
* Absent sfLEVersion is implicit Legacy (version 0): instant interest
|
||||
* recognition and a dynamic AssetsTotal scale. CashBasis (V1.1) uses
|
||||
* cash-basis recognition on the same dynamic scale. FixedPrecision
|
||||
* (V1.2) keeps cash-basis recognition and adds the lifetime base grid.
|
||||
* Vault ledger-entry schema versions. Assigned to newly created
|
||||
* Vaults once featureLendingProtocolV1_1 is enabled. Vaults created before
|
||||
* activation are left without LEVersion (implicit legacy version 0,
|
||||
* instant interest recognition).
|
||||
*/
|
||||
enum class VaultVersion : uint8_t {
|
||||
Legacy = 0,
|
||||
CashBasis,
|
||||
FixedPrecision,
|
||||
};
|
||||
|
||||
// Code compares VaultVersion values with < and >= (e.g. "CashBasis or later"),
|
||||
// so each later version must stay numerically larger than the one before.
|
||||
static_assert(
|
||||
VaultVersion::Legacy < VaultVersion::CashBasis &&
|
||||
VaultVersion::CashBasis < VaultVersion::FixedPrecision);
|
||||
|
||||
/**
|
||||
* Vault kind. Distinguishes closed-ended vaults from the default open-ended
|
||||
* kind. Persisted as sfVaultKind (UINT8); absent means OpenEnded.
|
||||
@@ -398,11 +376,6 @@ constexpr std::uint32_t kMaxInvestmentPeriod = std::chrono::seconds{std::chrono:
|
||||
*/
|
||||
constexpr std::uint8_t kMaxAssetCheckDepth = 5;
|
||||
|
||||
/**
|
||||
* Maximum length of a Data field in Escrow object that can be updated by WASM code.
|
||||
*/
|
||||
constexpr std::size_t kMaxWasmDataLength = 1 * 1024; // 1KB
|
||||
|
||||
/**
|
||||
* A ledger index.
|
||||
*/
|
||||
|
||||
@@ -696,16 +696,6 @@ divRound(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundU
|
||||
STAmount
|
||||
divRoundStrict(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp);
|
||||
|
||||
/**
|
||||
* Returns amt * num / den, rounded in the requested direction.
|
||||
*
|
||||
* Dispatches to the XRPAmount, IOUAmount or MPTAmount overload. XRP and MPT
|
||||
* results are exact; an IOU result is rounded to the 16-digit mantissa in the
|
||||
* requested direction. Only Upward and Downward are supported.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
mulRatio(STAmount const& amt, std::uint32_t num, std::uint32_t den, Number::RoundingMode rounding);
|
||||
|
||||
// Someone is offering X for Y, what is the rate?
|
||||
// Rate: smaller is better, the taker wants the most out: in/out
|
||||
// VFALCO TODO Return a Quality object
|
||||
@@ -776,21 +766,6 @@ roundToAsset(
|
||||
return roundToScale(ret, scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an arbitrary precision Number can be converted to an STAmount
|
||||
* of a given Asset without exceeding the asset's range.
|
||||
*
|
||||
* Use this before `roundToAsset` or `STAmount{asset, value}` on unchecked
|
||||
* input, since those throw for out-of-range values, e.g. XRP above
|
||||
* `STAmount::kMaxNativeN`.
|
||||
*
|
||||
* @param asset The relevant asset
|
||||
* @param value The value to be checked
|
||||
* @return true if the value is in range for the asset; false otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isRepresentable(Asset const& asset, Number const& value);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
inline bool
|
||||
|
||||
@@ -133,6 +133,7 @@ enum TEMcodes : TERUnderlyingType {
|
||||
temBAD_MPT,
|
||||
temBAD_CIPHERTEXT,
|
||||
temINVALID_BYTECODE,
|
||||
temTEMP_DISABLED,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -183,8 +184,6 @@ enum TEFcodes : TERUnderlyingType {
|
||||
tefBAD_PATH_COUNT,
|
||||
tefNO_BYTECODE,
|
||||
tefBYTECODE_NOT_INCLUDED,
|
||||
tefTEMP_DISABLED,
|
||||
tefEXCEEDS_LIMIT,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -232,8 +231,6 @@ enum TERcodes : TERUnderlyingType {
|
||||
terNO_DELEGATE_PERMISSION, // Delegate does not have permission
|
||||
terLOCKED, // MPT is locked
|
||||
terNO_PERMISSION, // No permission but retry
|
||||
terTICKET_RESERVED, // Ticket is reserved and cannot be consumed
|
||||
// until the reservation is released
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/MathUtilities.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
@@ -70,13 +69,7 @@ public:
|
||||
constexpr XRPAmount
|
||||
operator*(value_type const& rhs) const
|
||||
{
|
||||
if (auto const result = checkedMul(drops_, rhs))
|
||||
return XRPAmount{*result};
|
||||
// Not reached in constant evaluation: an overflowing constant
|
||||
// expression fails to compile instead.
|
||||
XRPAmount product{*this};
|
||||
product *= rhs;
|
||||
return product;
|
||||
return XRPAmount{drops_ * rhs};
|
||||
}
|
||||
|
||||
friend constexpr XRPAmount
|
||||
@@ -89,23 +82,37 @@ public:
|
||||
XRPAmount&
|
||||
operator+=(XRPAmount const& other)
|
||||
{
|
||||
return *this += other.drops();
|
||||
drops_ += other.drops();
|
||||
return *this;
|
||||
}
|
||||
|
||||
XRPAmount&
|
||||
operator-=(XRPAmount const& other)
|
||||
{
|
||||
return *this -= other.drops();
|
||||
drops_ -= other.drops();
|
||||
return *this;
|
||||
}
|
||||
|
||||
XRPAmount&
|
||||
operator+=(value_type const& rhs);
|
||||
operator+=(value_type const& rhs)
|
||||
{
|
||||
drops_ += rhs;
|
||||
return *this;
|
||||
}
|
||||
|
||||
XRPAmount&
|
||||
operator-=(value_type const& rhs);
|
||||
operator-=(value_type const& rhs)
|
||||
{
|
||||
drops_ -= rhs;
|
||||
return *this;
|
||||
}
|
||||
|
||||
XRPAmount&
|
||||
operator*=(value_type const& rhs);
|
||||
operator*=(value_type const& rhs)
|
||||
{
|
||||
drops_ *= rhs;
|
||||
return *this;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
operator-() const
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
XRPL_FEATURE(Cosign, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(SmartEscrow, Supported::No, VoteBehavior::DefaultNo)
|
||||
// Requires LendingProtocolV1_1. New vaults take FixedPrecision plus cash-basis.
|
||||
XRPL_FEATURE(LendingProtocolV1_2, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(ConfidentialMPTKeyRotation, Supported::No, VoteBehavior::DefaultNo)
|
||||
@@ -61,6 +60,7 @@ XRPL_FIX (InnerObjTemplate2, Supported::Yes, VoteBehavior::DefaultNo
|
||||
XRPL_FIX (EnforceNFTokenTrustline, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (ReducedOffersV2, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(NFTokenMintOffer, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (AMMv1_1, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (PreviousTxnID, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (XChainRewardRounding, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (EmptyDID, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
@@ -106,7 +106,6 @@ XRPL_RETIRE_FIX(1623)
|
||||
XRPL_RETIRE_FIX(1781)
|
||||
XRPL_RETIRE_FIX(AmendmentMajorityCalc)
|
||||
XRPL_RETIRE_FIX(AMMOverflowOffer)
|
||||
XRPL_RETIRE_FIX(AMMv1_1)
|
||||
XRPL_RETIRE_FIX(CheckThreading)
|
||||
XRPL_RETIRE_FIX(DisallowIncomingV1)
|
||||
XRPL_RETIRE_FIX(InnerObjTemplate)
|
||||
|
||||
@@ -417,7 +417,6 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
|
||||
{sfAuditorEncryptionKey, SoeOptional},
|
||||
{sfIssuerKeyEpoch, SoeOptional},
|
||||
{sfAuditorKeyEpoch, SoeOptional},
|
||||
{sfInitialIssuerEncryptionKey, SoeOptional},
|
||||
{sfConfidentialOutstandingAmount, SoeDefault},
|
||||
}))
|
||||
|
||||
@@ -439,7 +438,6 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
|
||||
{sfAuditorEncryptedBalance, SoeOptional},
|
||||
{sfIssuerKeyMirrorEpoch, SoeOptional},
|
||||
{sfAuditorKeyMirrorEpoch, SoeOptional},
|
||||
{sfIssuerMirrorEncryptionKey, SoeOptional},
|
||||
{sfHolderEncryptionKey, SoeOptional},
|
||||
{sfRecoveryKey, SoeOptional},
|
||||
}))
|
||||
@@ -516,8 +514,6 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
|
||||
{sfAssetsAvailable, SoeDefault},
|
||||
{sfAssetsMaximum, SoeDefault},
|
||||
{sfLossUnrealized, SoeDefault},
|
||||
{sfYieldUnrealized, SoeDefault},
|
||||
{sfAssetsDeployed, SoeDefault},
|
||||
{sfShareMPTID, SoeRequired},
|
||||
{sfWithdrawalPolicy, SoeRequired},
|
||||
{sfScale, SoeDefault},
|
||||
|
||||
@@ -240,8 +240,6 @@ TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14)
|
||||
TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16)
|
||||
TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
TYPED_SFIELD(sfYieldUnrealized, NUMBER, 18, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
TYPED_SFIELD(sfAssetsDeployed, NUMBER, 19, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
|
||||
// 32-bit signed (common)
|
||||
TYPED_SFIELD(sfLoanScale, INT32, 1)
|
||||
@@ -333,8 +331,6 @@ TYPED_SFIELD(sfAmountCommitment, VL, 45)
|
||||
TYPED_SFIELD(sfBalanceCommitment, VL, 46)
|
||||
TYPED_SFIELD(sfBytecode, VL, 47)
|
||||
TYPED_SFIELD(sfRecoveryKey, VL, 48)
|
||||
TYPED_SFIELD(sfInitialIssuerEncryptionKey, VL, 49)
|
||||
TYPED_SFIELD(sfIssuerMirrorEncryptionKey, VL, 50)
|
||||
|
||||
// account (common)
|
||||
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
|
||||
|
||||
@@ -1175,20 +1175,6 @@ TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 95, TransactionProposalCreate,
|
||||
{sfExpiration, SoeRequired},
|
||||
}))
|
||||
|
||||
// ttTRANSACTION_PROPOSAL_SIGN arrives in a later PR and takes the next unassigned type.
|
||||
|
||||
/** This transaction deletes a TransactionProposal, releasing its owner's
|
||||
reserve. While the proposal is live only its Owner or its target account
|
||||
may cancel; once it is terminal anyone may (On-Chain Cosigner spec §7). */
|
||||
#if TRANSACTION_INCLUDE
|
||||
# include <xrpl/tx/transactors/proposal/TransactionProposalCancel.h>
|
||||
#endif
|
||||
TRANSACTION(ttTRANSACTION_PROPOSAL_CANCEL, 94, TransactionProposalCancel,
|
||||
({.amendment = featureCosign}),
|
||||
({
|
||||
{sfProposalID, SoeRequired},
|
||||
}))
|
||||
|
||||
/** This system-generated transaction type is used to update the status of the various amendments.
|
||||
|
||||
For details, see: https://xrpl.org/amendments.html
|
||||
|
||||
@@ -245,16 +245,13 @@ 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)
|
||||
JSS(expected_date_UTC); // out: any (warnings)
|
||||
JSS(expected_ledger_size); // out: TxQ
|
||||
JSS(expiration); // out: AccountOffers, AccountChannels, ValidatorList, amm_info
|
||||
JSS(gas_limit); // out: NetworkOPs
|
||||
JSS(bytecode_size_limit); // out: NetworkOPs
|
||||
JSS(gas_price); // out: NetworkOPs
|
||||
JSS(fail_hard); // in: Sign, Submit
|
||||
JSS(failed); // out: InboundLedger
|
||||
JSS(feature); // in: Feature
|
||||
@@ -562,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
|
||||
|
||||
@@ -316,30 +316,6 @@ public:
|
||||
return this->sle_->isFieldPresent(sfAuditorKeyMirrorEpoch);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfIssuerMirrorEncryptionKey (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getIssuerMirrorEncryptionKey() const
|
||||
{
|
||||
if (hasIssuerMirrorEncryptionKey())
|
||||
return this->sle_->at(sfIssuerMirrorEncryptionKey);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfIssuerMirrorEncryptionKey is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasIssuerMirrorEncryptionKey() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfIssuerMirrorEncryptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfHolderEncryptionKey (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
@@ -589,17 +565,6 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfIssuerMirrorEncryptionKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
MPTokenBuilder&
|
||||
setIssuerMirrorEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfIssuerMirrorEncryptionKey] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfHolderEncryptionKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -399,30 +399,6 @@ public:
|
||||
return this->sle_->isFieldPresent(sfAuditorKeyEpoch);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfInitialIssuerEncryptionKey (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getInitialIssuerEncryptionKey() const
|
||||
{
|
||||
if (hasInitialIssuerEncryptionKey())
|
||||
return this->sle_->at(sfInitialIssuerEncryptionKey);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfInitialIssuerEncryptionKey is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasInitialIssuerEncryptionKey() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfInitialIssuerEncryptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfConfidentialOutstandingAmount (SoeDefault)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
@@ -694,17 +670,6 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfInitialIssuerEncryptionKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
MPTokenIssuanceBuilder&
|
||||
setInitialIssuerEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfInitialIssuerEncryptionKey] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfConfidentialOutstandingAmount (SoeDefault)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -242,54 +242,6 @@ public:
|
||||
return this->sle_->isFieldPresent(sfLossUnrealized);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfYieldUnrealized (SoeDefault)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_NUMBER::type::value_type>
|
||||
getYieldUnrealized() const
|
||||
{
|
||||
if (hasYieldUnrealized())
|
||||
return this->sle_->at(sfYieldUnrealized);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfYieldUnrealized is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasYieldUnrealized() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfYieldUnrealized);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfAssetsDeployed (SoeDefault)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_NUMBER::type::value_type>
|
||||
getAssetsDeployed() const
|
||||
{
|
||||
if (hasAssetsDeployed())
|
||||
return this->sle_->at(sfAssetsDeployed);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfAssetsDeployed is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasAssetsDeployed() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfAssetsDeployed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfShareMPTID (SoeRequired)
|
||||
* @return The field value.
|
||||
@@ -619,28 +571,6 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfYieldUnrealized (SoeDefault)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
VaultBuilder&
|
||||
setYieldUnrealized(std::decay_t<typename SF_NUMBER::type::value_type> const& value)
|
||||
{
|
||||
object_[sfYieldUnrealized] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfAssetsDeployed (SoeDefault)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
VaultBuilder&
|
||||
setAssetsDeployed(std::decay_t<typename SF_NUMBER::type::value_type> const& value)
|
||||
{
|
||||
object_[sfAssetsDeployed] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfShareMPTID (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
// This file is auto-generated. Do not edit.
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBase.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::transactions {
|
||||
|
||||
class TransactionProposalCancelBuilder;
|
||||
|
||||
/**
|
||||
* @brief Transaction: TransactionProposalCancel
|
||||
*
|
||||
* Type: ttTRANSACTION_PROPOSAL_CANCEL (94)
|
||||
* Delegable: Delegation::NotDelegable
|
||||
* Amendment: featureCosign
|
||||
* Privileges: Privilege::NoPriv
|
||||
*
|
||||
* Immutable wrapper around STTx providing type-safe field access.
|
||||
* Use TransactionProposalCancelBuilder to construct new transactions.
|
||||
*/
|
||||
class TransactionProposalCancel : public TransactionBase
|
||||
{
|
||||
public:
|
||||
static constexpr xrpl::TxType txType = ttTRANSACTION_PROPOSAL_CANCEL;
|
||||
|
||||
/**
|
||||
* @brief Construct a TransactionProposalCancel transaction wrapper from an existing STTx object.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
explicit TransactionProposalCancel(std::shared_ptr<STTx const> tx)
|
||||
: TransactionBase(std::move(tx))
|
||||
{
|
||||
// Verify transaction type
|
||||
if (tx_->getTxnType() != txType)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for TransactionProposalCancel");
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction-specific field getters
|
||||
|
||||
/**
|
||||
* @brief Get sfProposalID (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_UINT256::type::value_type
|
||||
getProposalID() const
|
||||
{
|
||||
return this->tx_->at(sfProposalID);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builder for TransactionProposalCancel transactions.
|
||||
*
|
||||
* Provides a fluent interface for constructing transactions with method chaining.
|
||||
* Uses STObject internally for flexible transaction construction.
|
||||
* Inherits common field setters from TransactionBuilderBase.
|
||||
*/
|
||||
class TransactionProposalCancelBuilder : public TransactionBuilderBase<TransactionProposalCancelBuilder>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new TransactionProposalCancelBuilder with required fields.
|
||||
* @param account The account initiating the transaction.
|
||||
* @param proposalID The sfProposalID field value.
|
||||
* @param sequence Optional sequence number for the transaction.
|
||||
* @param fee Optional fee for the transaction.
|
||||
*/
|
||||
TransactionProposalCancelBuilder(SF_ACCOUNT::type::value_type account,
|
||||
std::decay_t<typename SF_UINT256::type::value_type> const& proposalID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
|
||||
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
|
||||
)
|
||||
: TransactionBuilderBase<TransactionProposalCancelBuilder>(ttTRANSACTION_PROPOSAL_CANCEL, account, sequence, fee)
|
||||
{
|
||||
setProposalID(proposalID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a TransactionProposalCancelBuilder from an existing STTx object.
|
||||
* @param tx The existing transaction to copy from.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
TransactionProposalCancelBuilder(std::shared_ptr<STTx const> tx)
|
||||
{
|
||||
if (tx->getTxnType() != ttTRANSACTION_PROPOSAL_CANCEL)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for TransactionProposalCancelBuilder");
|
||||
}
|
||||
object_ = *tx;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Transaction-specific field setters
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Set sfProposalID (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
TransactionProposalCancelBuilder&
|
||||
setProposalID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
|
||||
{
|
||||
object_[sfProposalID] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the TransactionProposalCancel wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
* @param secretKey The secret key for signing.
|
||||
* @return The constructed transaction wrapper.
|
||||
*/
|
||||
TransactionProposalCancel
|
||||
build(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
{
|
||||
sign(publicKey, secretKey);
|
||||
return TransactionProposalCancel{std::make_shared<STTx>(std::move(object_))};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::transactions
|
||||
+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);
|
||||
|
||||
|
||||
+28
-250
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/IntrusivePointer.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/SHAMapHash.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
@@ -18,7 +17,6 @@
|
||||
#include <xrpl/shamap/SHAMapNodeID.h>
|
||||
#include <xrpl/shamap/SHAMapTreeNode.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -42,7 +40,7 @@ class SHAMapSyncFilter;
|
||||
/**
|
||||
* Describes the current state of a given SHAMap
|
||||
*/
|
||||
enum class SHAMapState : std::uint8_t {
|
||||
enum class SHAMapState {
|
||||
/**
|
||||
* The map is in flux and objects can be added and removed.
|
||||
*
|
||||
@@ -122,42 +120,16 @@ private:
|
||||
*/
|
||||
std::uint32_t cowid_ = 1;
|
||||
|
||||
// ledgerSeq_, state_ and full_ are touched on the nodestore fetch path and on a
|
||||
// getMissingNodes() walk, neither of which may block, so pin them lock-free.
|
||||
static_assert(std::atomic<std::uint32_t>::is_always_lock_free);
|
||||
static_assert(std::atomic<SHAMapState>::is_always_lock_free);
|
||||
static_assert(std::atomic<bool>::is_always_lock_free);
|
||||
|
||||
/**
|
||||
* The sequence of the ledger that this map references, if any.
|
||||
*
|
||||
* Written when a map's ledger sequence is established (Ledger::setFull(),
|
||||
* InboundLedger) while a nodestore reader thread reads it. Relaxed either
|
||||
* way: it serves as a lookup hint for a nodestore keyed by hash.
|
||||
*/
|
||||
std::atomic<std::uint32_t> ledgerSeq_ = 0;
|
||||
std::uint32_t ledgerSeq_ = 0;
|
||||
|
||||
SHAMapTreeNodePtr root_;
|
||||
|
||||
/**
|
||||
* The map's state.
|
||||
*
|
||||
* A getMissingNodes() walk writes it, through setInvalid() and
|
||||
* clearSynching(), while whatever drives the acquisition reads it.
|
||||
* The walk runs with the acquisition's lock released, so this is atomic
|
||||
* rather than guarded.
|
||||
*/
|
||||
mutable std::atomic<SHAMapState> state_;
|
||||
mutable SHAMapState state_;
|
||||
SHAMapType const type_;
|
||||
bool backed_ = true; // Map is backed by the database
|
||||
|
||||
/**
|
||||
* Map is believed complete in database.
|
||||
*
|
||||
* Atomic because finishFetch() clears it on any nodestore reader thread,
|
||||
* several of which can run at once.
|
||||
*/
|
||||
mutable std::atomic<bool> full_ = false;
|
||||
bool backed_ = true; // Map is backed by the database
|
||||
mutable bool full_ = false; // Map is believed complete in database
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -180,14 +152,7 @@ public:
|
||||
SHAMap&
|
||||
operator=(SHAMap const&) = delete;
|
||||
|
||||
/**
|
||||
* Take a snapshot of the given map.
|
||||
*
|
||||
* @param other The map to snapshot. An Invalid source yields an Invalid
|
||||
* snapshot.
|
||||
* @param isMutable Whether the snapshot may be modified. Ignored when other
|
||||
* is Invalid.
|
||||
*/
|
||||
// Take a snapshot of the given map:
|
||||
SHAMap(SHAMap const& other, bool isMutable);
|
||||
|
||||
// build new map
|
||||
@@ -225,33 +190,17 @@ public:
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return a new map that is a snapshot of this one.
|
||||
*
|
||||
* Handles copy on write for mutable snapshots. An invalid map yields an
|
||||
* invalid snapshot.
|
||||
*
|
||||
* @param isMutable Whether the snapshot may be modified.
|
||||
* @return The snapshot.
|
||||
*/
|
||||
// Returns a new map that's a snapshot of this one.
|
||||
// Handles copy on write for mutable snapshots.
|
||||
std::shared_ptr<SHAMap>
|
||||
snapShot(bool isMutable) const;
|
||||
|
||||
/**
|
||||
* Mark this map as "should be full": the local server wants every node
|
||||
* it references in durable storage, so a nodestore miss is a gap to report.
|
||||
*
|
||||
* Publishes the ledger sequence before the flag. The sequence store is
|
||||
* relaxed and the flag store is release, and finishFetch() reads the
|
||||
* sequence only after acquiring the flag, so the one miss that withdraws
|
||||
* the flag always reports the sequence stored here. Keeping both stores in
|
||||
* one place is what makes that order impossible to get wrong.
|
||||
*
|
||||
* @param ledgerSeq Sequence of the ledger this map belongs to, which the
|
||||
* gap report names.
|
||||
*/
|
||||
/* Mark this SHAMap as "should be full", indicating
|
||||
that the local server wants all the corresponding nodes
|
||||
in durable storage.
|
||||
*/
|
||||
void
|
||||
setFull(std::uint32_t ledgerSeq);
|
||||
setFull();
|
||||
|
||||
void
|
||||
setLedgerSeq(std::uint32_t lseq);
|
||||
@@ -347,13 +296,9 @@ public:
|
||||
* concurrency, to discover nodes referenced in the
|
||||
* SHAMap but not available locally.
|
||||
*
|
||||
* Only a leaf may occupy a position at or beyond kLeafDepth. A map that
|
||||
* breaks that is marked Invalid and the traversal is abandoned, so callers
|
||||
* ask isValid() to tell an empty result from a satisfied map.
|
||||
*
|
||||
* @param maxNodes The maximum number of found nodes to return
|
||||
* @param filter The filter to use when retrieving nodes
|
||||
* @return The nodes known to be missing, or empty if the map is Invalid
|
||||
* @param return The nodes known to be missing
|
||||
*/
|
||||
std::vector<std::pair<SHAMapNodeID, UInt256>>
|
||||
getMissingNodes(int maxNodes, SHAMapSyncFilter const* filter);
|
||||
@@ -396,9 +341,6 @@ public:
|
||||
* This function is used when receiving the root node of a SHAMap from a peer during ledger
|
||||
* synchronization. The node must already have been deserialized.
|
||||
*
|
||||
* A root offered under a hash the map does not hold names another tree and
|
||||
* is reported as invalid data. A root matching that hash is a duplicate.
|
||||
*
|
||||
* @param hash The expected hash of the root node.
|
||||
* @param rootNode A deserialized root node to add.
|
||||
* @param filter Optional sync filter to track received nodes.
|
||||
@@ -418,10 +360,6 @@ public:
|
||||
* is inserted at the position specified by nodeID. The node must already have been
|
||||
* deserialized.
|
||||
*
|
||||
* A node that no valid tree can hold makes the map Invalid, which is
|
||||
* terminal: the root hash committed to an impossible shape, so no peer
|
||||
* can satisfy it.
|
||||
*
|
||||
* @param nodeID The position in the tree where this node belongs.
|
||||
* @param treeNode A deserialized tree node to add.
|
||||
* @param filter Optional sync filter to track received nodes.
|
||||
@@ -437,57 +375,16 @@ public:
|
||||
SHAMapTreeNodePtr treeNode,
|
||||
SHAMapSyncFilter const* filter);
|
||||
|
||||
/**
|
||||
* Mark this map as immutable, so it can no longer be modified.
|
||||
*
|
||||
* @return false if the map is Invalid and was left unchanged, true
|
||||
* otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
// status functions
|
||||
void
|
||||
setImmutable();
|
||||
|
||||
/**
|
||||
* @return Whether the map is being synced against a hash it was given, so
|
||||
* its hash is fixed while nodes may still be added.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
bool
|
||||
isSynching() const;
|
||||
|
||||
/**
|
||||
* @return Whether the map is settled, with its hash and nodes fixed.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isImmutable() const;
|
||||
|
||||
/**
|
||||
* Mark this map as syncing, fixing its hash while still allowing missing
|
||||
* nodes to be added.
|
||||
*
|
||||
* The map must be freshly constructed. The body asserts that the map is not
|
||||
* Invalid, so a caller that ignores the precondition stops a build with
|
||||
* assertions enabled.
|
||||
*/
|
||||
void
|
||||
setSynching();
|
||||
|
||||
/**
|
||||
* Mark this map as no longer syncing, so it can be modified again.
|
||||
*
|
||||
* Moves only a Synching map. An Immutable map stays settled, a Modifying
|
||||
* map has nothing to clear, and Invalid is terminal.
|
||||
*/
|
||||
void
|
||||
clearSynching();
|
||||
|
||||
/**
|
||||
* Whether the map can still be the map it claims to be.
|
||||
*
|
||||
* A map that is merely missing nodes is valid, and stays valid until
|
||||
* something proves the tree it is syncing against cannot exist.
|
||||
*
|
||||
* @return Whether the map has not been proven impossible.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
bool
|
||||
isValid() const;
|
||||
|
||||
// caution: otherMap must be accessed only by this function
|
||||
@@ -627,51 +524,6 @@ private:
|
||||
using DeltaRef =
|
||||
std::pair<boost::intrusive_ptr<SHAMapItem const>, boost::intrusive_ptr<SHAMapItem const>>;
|
||||
|
||||
/**
|
||||
* The sequence of the ledger this map references, read atomically.
|
||||
*
|
||||
* @return The sequence, or zero if the map references no ledger.
|
||||
*/
|
||||
[[nodiscard]] std::uint32_t
|
||||
ledgerSeq() const;
|
||||
|
||||
/**
|
||||
* The current state, read atomically.
|
||||
*
|
||||
* Orders state_ alone. The tree's nodes carry no ordering guarantees.
|
||||
*
|
||||
* @return The state as of the call, which a concurrent walk may already
|
||||
* have moved past.
|
||||
*/
|
||||
[[nodiscard]] SHAMapState
|
||||
state() const;
|
||||
|
||||
/**
|
||||
* Record that the map is provably not the one it claims to be.
|
||||
*
|
||||
* Private because only the map itself can prove that, from a node that
|
||||
* contradicts the hashes it is syncing against. Cannot fail, since
|
||||
* Invalid outranks every other state; see trySetState().
|
||||
*/
|
||||
void
|
||||
setInvalid();
|
||||
|
||||
/**
|
||||
* Move the map to a new state, atomically.
|
||||
*
|
||||
* With clearSynching(), the only writer of state_ past construction, so
|
||||
* the order between the states lives in one place: Invalid outranks all
|
||||
* of them and is always stored, while every other transition is refused
|
||||
* once the map is Invalid, which is what makes that verdict terminal.
|
||||
* clearSynching() is narrower and moves only Synching.
|
||||
*
|
||||
* @param desired The state to move to.
|
||||
* @return false if the map is Invalid and the requested state is not,
|
||||
* leaving it unchanged; true otherwise.
|
||||
*/
|
||||
bool
|
||||
trySetState(SHAMapState desired);
|
||||
|
||||
// tree node cache operations
|
||||
SHAMapTreeNodePtr
|
||||
cacheLookup(SHAMapHash const& hash) const;
|
||||
@@ -887,120 +739,46 @@ private:
|
||||
};
|
||||
|
||||
inline void
|
||||
SHAMap::setFull(std::uint32_t ledgerSeq)
|
||||
SHAMap::setFull()
|
||||
{
|
||||
ledgerSeq_.store(ledgerSeq, std::memory_order_relaxed);
|
||||
full_.store(true, std::memory_order_release);
|
||||
full_ = true;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::setLedgerSeq(std::uint32_t lseq)
|
||||
{
|
||||
ledgerSeq_.store(lseq, std::memory_order_relaxed);
|
||||
ledgerSeq_ = lseq;
|
||||
}
|
||||
|
||||
inline std::uint32_t
|
||||
SHAMap::ledgerSeq() const
|
||||
{
|
||||
return ledgerSeq_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
inline SHAMapState
|
||||
SHAMap::state() const
|
||||
{
|
||||
return state_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::trySetState(SHAMapState desired)
|
||||
{
|
||||
// Invalid is stored outright: a walk reaching that verdict has to win against a thread
|
||||
// settling the map.
|
||||
if (desired == SHAMapState::Invalid)
|
||||
{
|
||||
state_.store(SHAMapState::Invalid, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
// A failed exchange both reports the state and refreshes expected, so no load is needed
|
||||
// ahead of the loop.
|
||||
auto expected = SHAMapState::Modifying;
|
||||
while (expected != SHAMapState::Invalid)
|
||||
{
|
||||
if (state_.compare_exchange_weak(
|
||||
expected, desired, std::memory_order_acq_rel, std::memory_order_acquire))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool
|
||||
inline void
|
||||
SHAMap::setImmutable()
|
||||
{
|
||||
SOMETIMES(!isValid(), "xrpl::SHAMap::setImmutable : map is invalid");
|
||||
return trySetState(SHAMapState::Immutable);
|
||||
XRPL_ASSERT(state_ != SHAMapState::Invalid, "xrpl::SHAMap::setImmutable : state is valid");
|
||||
state_ = SHAMapState::Immutable;
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::isSynching() const
|
||||
{
|
||||
return state() == SHAMapState::Synching;
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::isImmutable() const
|
||||
{
|
||||
return state() == SHAMapState::Immutable;
|
||||
return state_ == SHAMapState::Synching;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::setSynching()
|
||||
{
|
||||
// Guarded, so this is not a way out of Invalid.
|
||||
if (!trySetState(SHAMapState::Synching))
|
||||
{
|
||||
// Only ever called on a freshly constructed map.
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::setSynching : map is invalid");
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
state_ = SHAMapState::Synching;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::clearSynching()
|
||||
{
|
||||
// Only Synching moves. A walk can end after the ledger settled, and then the map stays
|
||||
// Immutable; a Modifying map has nothing to clear; an invalid map stays invalid. Only that
|
||||
// last refusal is logged, since a concurrent walk is what produces it.
|
||||
auto expected = SHAMapState::Synching;
|
||||
if (state_.compare_exchange_strong(
|
||||
expected, SHAMapState::Modifying, std::memory_order_acq_rel, std::memory_order_acquire))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool const invalid = expected == SHAMapState::Invalid;
|
||||
SOMETIMES(invalid, "xrpl::SHAMap::clearSynching : map is invalid");
|
||||
if (invalid)
|
||||
{
|
||||
JLOG(journal_.warn()) << "Refused to clear synching on an invalid map, root hash "
|
||||
<< root_->getHash();
|
||||
}
|
||||
state_ = SHAMapState::Modifying;
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::isValid() const
|
||||
{
|
||||
return state() != SHAMapState::Invalid;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::setInvalid()
|
||||
{
|
||||
// Through trySetState() like every other transition, so state_ has one writer funnel.
|
||||
trySetState(SHAMapState::Invalid);
|
||||
return state_ != SHAMapState::Invalid;
|
||||
}
|
||||
|
||||
inline void
|
||||
|
||||
@@ -31,19 +31,7 @@ private:
|
||||
*/
|
||||
TaggedPointer hashesAndChildren_;
|
||||
|
||||
// Pin that wrapping fullBelowGen_ in an atomic keeps the packed layout, and that the load
|
||||
// isFullBelow() does once per node of every walk stays lock-free.
|
||||
static_assert(std::atomic<std::uint32_t>::is_always_lock_free);
|
||||
static_assert(sizeof(std::atomic<std::uint32_t>) == sizeof(std::uint32_t));
|
||||
static_assert(alignof(std::atomic<std::uint32_t>) == alignof(std::uint32_t));
|
||||
|
||||
/**
|
||||
* Written from more than one thread, since canonicalization shares a node
|
||||
* between maps and a walk can run with the acquisition lock released (see
|
||||
* SHAMap::state_). Relaxed both ways, since a generation is only compared
|
||||
* for equality and the children it vouches for are published through lock_.
|
||||
*/
|
||||
std::atomic<std::uint32_t> fullBelowGen_ = 0;
|
||||
std::uint32_t fullBelowGen_ = 0;
|
||||
std::uint16_t isBranch_ = 0;
|
||||
|
||||
/**
|
||||
@@ -216,13 +204,13 @@ SHAMapInnerNode::getBranchCount() const
|
||||
inline bool
|
||||
SHAMapInnerNode::isFullBelow(std::uint32_t generation) const
|
||||
{
|
||||
return fullBelowGen_.load(std::memory_order_relaxed) == generation;
|
||||
return fullBelowGen_ == generation;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMapInnerNode::setFullBelowGen(std::uint32_t gen)
|
||||
{
|
||||
fullBelowGen_.store(gen, std::memory_order_relaxed);
|
||||
fullBelowGen_ = gen;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -375,22 +375,6 @@ public:
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
// Interface used by AccountDelete
|
||||
/**
|
||||
* This method removes one Ticket ledger-object from the ledger. AccountDelete uses it to
|
||||
* clean up Ticket objects en masse.
|
||||
*
|
||||
* This method reduces the TicketCount and the OwnerCount of the Owner AccountRoot object. If
|
||||
* a TransactionProposal reserved this Ticket, that proposal is deleted as well: once the
|
||||
* Ticket is gone, its proposed transaction can never be applied (tefNO_TICKET).
|
||||
*
|
||||
* @param view the apply view for making changes.
|
||||
* @param account the account which owns the Ticket object.
|
||||
* @param ticketIndex ticketIndex is used to construct the ledger-entry ID of Ticket object.
|
||||
* @param j the journal reference used to log messages.
|
||||
*
|
||||
* @return the final TER after all invariant checks. `tefBAD_LEDGER` is returned if Ticket,
|
||||
* Owner AccountRoot is not found in the apply-view ledger.
|
||||
*/
|
||||
static TER
|
||||
ticketDelete(
|
||||
ApplyView& view,
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class ReadView;
|
||||
|
||||
/**
|
||||
* @brief Invariants for TransactionProposal ledger entries.
|
||||
*
|
||||
* - Only transaction types implementing proposal lifecycle operations may
|
||||
* create or modify proposals, and a failed one leaves no proposal effect.
|
||||
* - A proposal is deleted only by TransactionProposalCancel or together with
|
||||
* the Ticket it is keyed to, and a deleted Ticket takes the proposal keyed
|
||||
* to it along.
|
||||
* - Collected signer arrays remain sorted, unique, and bounded.
|
||||
*/
|
||||
class ValidTransactionProposal
|
||||
{
|
||||
// The Ticket each deleted proposal was keyed to; a proposal may go with
|
||||
// its Ticket, or by Cancel, and no other way.
|
||||
std::vector<uint256> ticketsOfDeletedProposals_;
|
||||
// The proposal each deleted Ticket was keyed to, whether or not one
|
||||
// existed; none may survive its Ticket.
|
||||
std::vector<Keylet> proposalsOfDeletedTickets_;
|
||||
std::set<uint256> deletedTickets_;
|
||||
std::uint32_t created_ = 0;
|
||||
std::uint32_t modified_ = 0;
|
||||
std::uint32_t deleted_ = 0;
|
||||
bool invalidSignerArrays_ = false;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after);
|
||||
|
||||
[[nodiscard]] bool
|
||||
finalize(STTx const& tx, TER result, XRPAmount, ReadView const& view, beast::Journal const& j)
|
||||
const;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -1,15 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/invariants/AMMInvariant.h>
|
||||
#include <xrpl/tx/invariants/CosignerInvariant.h>
|
||||
#include <xrpl/tx/invariants/DirectoryInvariant.h>
|
||||
#include <xrpl/tx/invariants/FreezeInvariant.h>
|
||||
#include <xrpl/tx/invariants/LoanBrokerInvariant.h>
|
||||
@@ -24,7 +21,6 @@
|
||||
#include <boost/multiprecision/cpp_int.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
@@ -367,27 +363,11 @@ public:
|
||||
* unique to pseudo-accounts. Check that all pseudo-accounts are following the
|
||||
* rules, and that only pseudo-accounts look like pseudo-accounts.
|
||||
*
|
||||
* Post-fixCleanup3_5_0, a pseudo-account also owns only entries that belong
|
||||
* to its own AMM, Vault or LoanBroker:
|
||||
* - AMM: its AMM entry, its holdings of the two pool assets, and the trust
|
||||
* lines of its LP token,
|
||||
* - Vault: its share MPTokenIssuance, its holding of the vault asset, and the
|
||||
* LoanBrokers operating on this vault,
|
||||
* - LoanBroker: its Loans and its holding of the vault asset (the cover).
|
||||
*
|
||||
* A pseudo-account cannot sign transactions, so it could never accept or
|
||||
* remove any other object linked into its owner directory. Such an object
|
||||
* would pin the directory and block deletion of the owning object.
|
||||
*/
|
||||
class ValidPseudoAccounts
|
||||
{
|
||||
std::vector<std::string> errors_;
|
||||
|
||||
// Entries newly linked into owner directories, keyed by the directory
|
||||
// owner. Whether an owner is a pseudo-account is decided in finalize,
|
||||
// where the view is available.
|
||||
std::map<AccountID, std::vector<uint256>> ownerDirAdditions_;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
|
||||
@@ -484,7 +464,6 @@ using InvariantChecks = std::tuple<
|
||||
ValidAmounts,
|
||||
ValidMPTTransfer,
|
||||
ObjectHasPseudoAccount,
|
||||
ValidTransactionProposal,
|
||||
SponsorshipOwnerCountsMatch,
|
||||
SponsorshipAccountCountMatchesField>;
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
@@ -30,7 +29,7 @@ namespace xrpl {
|
||||
* - vault must have MPTokenIssuance for shares
|
||||
* - vault without shares outstanding must have no shares
|
||||
* - loss unrealized does not exceed the difference between assets total and
|
||||
* assets available (Legacy/CashBasis), or AssetsDeployed (FixedPrecision)
|
||||
* assets available
|
||||
* - assets available do not exceed assets total
|
||||
* - vault deposit increases assets and share issuance, and adds to:
|
||||
* total assets, assets available, shares outstanding
|
||||
@@ -47,14 +46,6 @@ namespace xrpl {
|
||||
* - vault withdrawal may not succeed when the vault phase is Investment
|
||||
* - closed-ended loan origination (ttLOAN_SET) may only succeed when the
|
||||
* vault phase is Investment
|
||||
* - FixedPrecision only: AssetsDeployed is non-negative and exactly
|
||||
* representable at the asset's precision; AssetsDeployed changes only for
|
||||
* ttLOAN_SET, ttLOAN_PAY and ttLOAN_MANAGE; AssetsDeployed is zero right
|
||||
* after ttVAULT_CREATE and whenever shares outstanding return to zero; the
|
||||
* stored AssetsTotal cache equals AssetsAvailable + AssetsDeployed, rounded
|
||||
* Downward to the asset's precision
|
||||
* - FixedPrecision only: YieldUnrealized is non-negative; YieldUnrealized
|
||||
* changes only for ttLOAN_SET, ttLOAN_PAY and ttLOAN_MANAGE
|
||||
*
|
||||
* Immutability of VaultKind, SubscriptionDate and RedemptionDate is enforced
|
||||
* by NoModifiedUnmodifiableFields (see InvariantCheck.cpp). From
|
||||
@@ -77,9 +68,6 @@ class ValidVault
|
||||
Number assetsAvailable = 0;
|
||||
Number assetsMaximum = 0;
|
||||
Number lossUnrealized = 0;
|
||||
Number assetsDeployed = 0;
|
||||
Number yieldUnrealized = 0;
|
||||
VaultVersion version = VaultVersion::Legacy;
|
||||
std::optional<std::uint8_t> vaultKind;
|
||||
std::optional<std::uint32_t> subscriptionDate;
|
||||
std::optional<std::uint32_t> redemptionDate;
|
||||
@@ -229,39 +217,6 @@ private:
|
||||
[[nodiscard]] bool
|
||||
finalizeLoanSet(ReadView const& view, beast::Journal const& j) const;
|
||||
|
||||
/**
|
||||
* @brief Check that a vault's AssetsTotal and AssetsAvailable deltas add
|
||||
* up against the real transfer, for deposit/withdraw/clawback.
|
||||
*
|
||||
* Shared body of the ttVAULT_DEPOSIT, ttVAULT_WITHDRAW and
|
||||
* ttVAULT_CLAWBACK "assets outstanding"/"assets available" add-up checks,
|
||||
* which differ only in the rounded/exact deltas plugged in and the verb
|
||||
* used in the log message.
|
||||
*
|
||||
* @param afterVault Snapshot of the vault after the transaction.
|
||||
* @param beforeVault Snapshot of the vault before the transaction.
|
||||
* @param vaultAsset The vault's underlying asset.
|
||||
* @param fix340Enabled Whether fixCleanup3_4_0 is enabled.
|
||||
* @param minScale The posterior scale computed by computeVaultMinScale.
|
||||
* @param roundedVaultDelta The real transfer's delta, rounded to minScale.
|
||||
* @param exactVaultDelta The real transfer's exact (unrounded) delta.
|
||||
* @param verb The transaction name used in the log message ("deposit",
|
||||
* "withdrawal" or "clawback").
|
||||
* @param j Journal for logging.
|
||||
* @return false if either add-up check fails.
|
||||
*/
|
||||
[[nodiscard]] static bool
|
||||
checkTotalsAddUp(
|
||||
Vault const& afterVault,
|
||||
Vault const& beforeVault,
|
||||
Asset const& vaultAsset,
|
||||
bool fix340Enabled,
|
||||
std::int32_t minScale,
|
||||
Number const& roundedVaultDelta,
|
||||
Number const& exactVaultDelta,
|
||||
char const* verb,
|
||||
beast::Journal const& j);
|
||||
|
||||
public:
|
||||
// Compute the coarsest scale required to represent all numbers
|
||||
[[nodiscard]] static std::int32_t
|
||||
|
||||
@@ -20,14 +20,11 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
static TxConsequences
|
||||
makeTxConsequences(PreflightContext const& ctx);
|
||||
|
||||
static XRPAmount
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx);
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
@@ -46,29 +45,9 @@ public:
|
||||
SLE::Ref loanSle,
|
||||
SLE::Ref brokerSle,
|
||||
SLE::Ref vaultSle,
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
* @brief Compute the First-Loss Capital cover amount for a defaulted Loan on a FixedPrecision
|
||||
* Vault, capped at the LoanBroker's CoverAvailable and floored so that AssetsAvailable + cover
|
||||
* is exactly on its posterior grid.
|
||||
*
|
||||
* The LoanBroker absorbs the rounding. When CoverAvailable - cover needs finer digits than the
|
||||
* broker's posterior grid, it rounds to nearest, as the broker's trust line does, so
|
||||
* CoverAvailable can move by up to half a posterior ulp more or less than the cover. When the
|
||||
* two grids differ, no cover is exact on both sides; the Vault side is kept exact.
|
||||
*
|
||||
* The one exception: when AssetsAvailable has digits finer than the 16-digit cover can carry,
|
||||
* no cover cancels them, and the balance writer rounds AssetsAvailable + cover to nearest,
|
||||
* by at most half a posterior ulp. VaultDeposit and LoanPay credits have the same limit.
|
||||
* @param loanSle The Loan being defaulted.
|
||||
* @param brokerSle The Loan's LoanBroker.
|
||||
* @param vaultSle The LoanBroker's Vault.
|
||||
* @return The cover amount, in the Vault's Asset.
|
||||
*/
|
||||
static STAmount
|
||||
calculateDefaultCover(SLE::Ref loanSle, SLE::Ref brokerSle, SLE::Ref vaultSle);
|
||||
|
||||
/**
|
||||
* Helper function that might be needed by other transactors
|
||||
*/
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/ApplyContext.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class TransactionProposalCancel : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
|
||||
|
||||
explicit TransactionProposalCancel(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
|
||||
void
|
||||
visitInvariantEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after) override;
|
||||
|
||||
[[nodiscard]] bool
|
||||
finalizeInvariants(
|
||||
STTx const& tx,
|
||||
TER result,
|
||||
XRPAmount fee,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j) override;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -52,13 +52,6 @@ private:
|
||||
TER
|
||||
applyFee();
|
||||
|
||||
/**
|
||||
* Write the default Feature Extension fee settings into FeeSettings on
|
||||
* `featureSmartEscrow` activation.
|
||||
*/
|
||||
void
|
||||
initializeVMFees();
|
||||
|
||||
TER
|
||||
applyUNLModify();
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -200,23 +200,11 @@ Ledger::Ledger(
|
||||
sle->at(sfReserveIncrement) = *f;
|
||||
sle->at(sfReferenceFeeUnits) = kFeeUnitsDeprecated;
|
||||
}
|
||||
if (std::ranges::find(amendments, featureSmartEscrow) != amendments.end())
|
||||
{
|
||||
sle->at(sfGasLimit) = fees.gasLimit;
|
||||
sle->at(sfBytecodeSizeLimit) = fees.bytecodeSizeLimit;
|
||||
sle->at(sfGasPrice) = fees.gasPrice;
|
||||
}
|
||||
rawInsert(sle);
|
||||
}
|
||||
|
||||
stateMap_.flushDirty(NodeObjectType::AccountNode);
|
||||
// Built locally. See Ledger::setImmutable().
|
||||
if (!setImmutable())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
logicError("Ledger::Ledger(CreateGenesisT, ...): genesis ledger map is invalid");
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
setImmutable();
|
||||
}
|
||||
|
||||
Ledger::Ledger(
|
||||
@@ -227,7 +215,7 @@ Ledger::Ledger(
|
||||
Fees const& fees,
|
||||
Family& family,
|
||||
beast::Journal j)
|
||||
: immutable_(false)
|
||||
: immutable_(true)
|
||||
, txMap_(SHAMapType::TRANSACTION, info.txHash, family)
|
||||
, stateMap_(SHAMapType::STATE, info.accountHash, family)
|
||||
, fees_(fees)
|
||||
@@ -250,20 +238,8 @@ Ledger::Ledger(
|
||||
JLOG(j.warn()) << "Don't have state data root for ledger" << header_.seq;
|
||||
}
|
||||
|
||||
// Loaded locally. See Ledger::setImmutable().
|
||||
if (setMapsImmutable())
|
||||
{
|
||||
immutable_ = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.error()) << "Invalid map for ledger " << header_.seq;
|
||||
UNREACHABLE("xrpl::Ledger::Ledger(LedgerHeader const&, ...) : map is invalid");
|
||||
// Treat it as a damaged ledger: the code below recomputes the hash and re-acquires.
|
||||
loaded = false;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
txMap_.setImmutable();
|
||||
stateMap_.setImmutable();
|
||||
|
||||
if (!setup())
|
||||
loaded = false;
|
||||
@@ -334,50 +310,27 @@ Ledger::Ledger(
|
||||
setup();
|
||||
}
|
||||
|
||||
bool
|
||||
void
|
||||
Ledger::setImmutable(bool rehash)
|
||||
{
|
||||
// A map found structurally invalid during sync must never be made immutable: isValid() tests
|
||||
// only for Invalid, and an immutable ledger is treated as persistable. Asked before anything is
|
||||
// written, so a refusal leaves the header exactly as it was rather than half relabeled.
|
||||
if (!mapsValid())
|
||||
return false;
|
||||
|
||||
// Read here but written to the header below, once the maps are settled: getHash() can unshare a
|
||||
// dirty tree, so it has to run while the map is still mutable, while a write to the header must
|
||||
// wait until both maps have made it. Skipped once the ledger is immutable, since its maps can
|
||||
// no longer change.
|
||||
bool const deriveMapHashes = !immutable_ && rehash;
|
||||
UInt256 const txHash = deriveMapHashes ? txMap_.getHash().asUInt256() : UInt256{};
|
||||
UInt256 const accountHash = deriveMapHashes ? stateMap_.getHash().asUInt256() : UInt256{};
|
||||
|
||||
// Both were valid at the check above, but a concurrent walk can invalidate one in between (see
|
||||
// SHAMap::state_), so the result is checked. setInvalid() outranks Immutable and can land
|
||||
// after both maps have been settled, so this narrows the window it guards.
|
||||
bool const bothImmutable = setMapsImmutable();
|
||||
SOMETIMES(!bothImmutable, "xrpl::Ledger::setImmutable : map invalidated while going immutable");
|
||||
if (!bothImmutable)
|
||||
return false;
|
||||
|
||||
// Written only now, so losing the race above leaves the header describing what the ledger was
|
||||
// built from rather than a map that has since been abandoned. Forced rather than conditional,
|
||||
// since this is the only place the hash transitions to valid.
|
||||
if (deriveMapHashes)
|
||||
// Force update, since this is the only
|
||||
// place the hash transitions to valid
|
||||
if (!immutable_ && rehash)
|
||||
{
|
||||
header_.txHash = txHash;
|
||||
header_.accountHash = accountHash;
|
||||
header_.txHash = txMap_.getHash().asUInt256();
|
||||
header_.accountHash = stateMap_.getHash().asUInt256();
|
||||
}
|
||||
|
||||
if (rehash)
|
||||
header_.hash = calculateLedgerHash(header_);
|
||||
|
||||
// Set last, so isImmutable() reports only a ledger whose maps are both immutable.
|
||||
immutable_ = true;
|
||||
txMap_.setImmutable();
|
||||
stateMap_.setImmutable();
|
||||
setup();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
void
|
||||
Ledger::setAccepted(
|
||||
NetClock::time_point closeTime,
|
||||
NetClock::duration closeResolution,
|
||||
@@ -389,17 +342,7 @@ Ledger::setAccepted(
|
||||
header_.closeTime = closeTime;
|
||||
header_.closeTimeResolution = closeResolution;
|
||||
header_.closeFlags = correctCloseTime ? 0 : kSLcfNoConsensusTime;
|
||||
// Built locally. See Ledger::setImmutable().
|
||||
if (!setImmutable())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.error()) << "Invalid map for accepted ledger " << header_.seq;
|
||||
UNREACHABLE("xrpl::Ledger::setAccepted : map is invalid");
|
||||
return false;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
return true;
|
||||
setImmutable();
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -623,7 +566,6 @@ Ledger::setup()
|
||||
{
|
||||
bool oldFees = false;
|
||||
bool newFees = false;
|
||||
bool extensionFees = false;
|
||||
{
|
||||
auto const baseFee = sle->at(~sfBaseFee);
|
||||
auto const reserveBase = sle->at(~sfReserveBase);
|
||||
@@ -658,20 +600,6 @@ Ledger::setup()
|
||||
assign(fees_.increment, reserveIncrementXRP);
|
||||
newFees = baseFeeXRP || reserveBaseXRP || reserveIncrementXRP;
|
||||
}
|
||||
{
|
||||
auto const gasLimit = sle->at(~sfGasLimit);
|
||||
auto const bytecodeSizeLimit = sle->at(~sfBytecodeSizeLimit);
|
||||
auto const gasPrice = sle->at(~sfGasPrice);
|
||||
|
||||
// An absent field resolves to the protocol constant.
|
||||
if (rules_.enabled(featureSmartEscrow))
|
||||
{
|
||||
fees_.gasLimit = gasLimit.value_or(kDefaultGasLimit);
|
||||
fees_.bytecodeSizeLimit = bytecodeSizeLimit.value_or(kDefaultBytecodeSizeLimit);
|
||||
fees_.gasPrice = gasPrice.value_or(kDefaultGasPrice);
|
||||
}
|
||||
extensionFees = gasLimit || bytecodeSizeLimit || gasPrice;
|
||||
}
|
||||
if (oldFees && newFees)
|
||||
{
|
||||
// Should be all of one or the other, but not both
|
||||
@@ -682,12 +610,6 @@ Ledger::setup()
|
||||
// Can't populate the new fees before the amendment is enabled
|
||||
ret = false;
|
||||
}
|
||||
if (!rules_.enabled(featureSmartEscrow) && extensionFees)
|
||||
{
|
||||
// Can't populate the extension fees before the amendment is
|
||||
// enabled
|
||||
ret = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SHAMapMissingNode const&)
|
||||
|
||||
@@ -435,6 +435,8 @@ PaymentSandbox::issuerSelfDebitHookMPT(
|
||||
std::uint64_t amount,
|
||||
std::int64_t origBalance)
|
||||
{
|
||||
XRPL_ASSERT(amount > 0, "PaymentSandbox::issuerSelfDebitHookMPT: amount must be > 0");
|
||||
|
||||
tab_.issuerSelfDebitMPT(issue, amount, origBalance);
|
||||
}
|
||||
|
||||
|
||||
@@ -511,14 +511,9 @@ doWithdraw(
|
||||
AccountID const& dstAcct,
|
||||
AccountID const& sourceAcct,
|
||||
XRPAmount priorBalance,
|
||||
STAmount const& destinationAmount,
|
||||
STAmount const& sourceAmount,
|
||||
STAmount const& amount,
|
||||
beast::Journal j)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
destinationAmount.asset() == sourceAmount.asset(),
|
||||
"xrpl::doWithdraw : delivered and source amounts use the same asset");
|
||||
|
||||
auto const dstSle = ctx.view.read(keylet::account(dstAcct));
|
||||
|
||||
// Create a trust line or MPToken for a self-destination only when there
|
||||
@@ -528,10 +523,9 @@ doWithdraw(
|
||||
// create+delete MPTokens in the same transaction.
|
||||
if (dstAcct == senderAcct)
|
||||
{
|
||||
if (destinationAmount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
|
||||
if (amount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
|
||||
{
|
||||
if (auto const ter =
|
||||
addEmptyHolding(ctx, senderAcct, priorBalance, destinationAmount.asset(), j);
|
||||
if (auto const ter = addEmptyHolding(ctx, senderAcct, priorBalance, amount.asset(), j);
|
||||
!isTesSuccess(ter) && ter != tecDUPLICATE)
|
||||
return ter;
|
||||
}
|
||||
@@ -546,33 +540,17 @@ doWithdraw(
|
||||
if (accountHolds(
|
||||
ctx.view,
|
||||
sourceAcct,
|
||||
destinationAmount.asset(),
|
||||
amount.asset(),
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j) < sourceAmount)
|
||||
j) < amount)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.error()) << "doWithdraw: source account holds fewer assets than the withdrawal.";
|
||||
JLOG(j.error()) << "doWithdraw: negative balance of broker cover assets.";
|
||||
return tefINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// A transfer fee applies. Redeem the gross amount with the issuer, then
|
||||
// issue the net amount to the destination.
|
||||
if (sourceAmount != destinationAmount)
|
||||
{
|
||||
auto const issuer = destinationAmount.getIssuer();
|
||||
XRPL_ASSERT(
|
||||
!destinationAmount.native() && sourceAcct != issuer && dstAcct != issuer,
|
||||
"xrpl::doWithdraw : transfer fee applies between token holders");
|
||||
|
||||
if (auto const ter = directSendNoFee(ctx.view, sourceAcct, issuer, sourceAmount, false, j);
|
||||
!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
|
||||
return directSendNoFee(ctx.view, issuer, dstAcct, destinationAmount, false, j);
|
||||
}
|
||||
|
||||
// A reserve sponsor only covers tx.Account's own objects, so resolve the
|
||||
// sponsor against the destination. accountSend can auto-create a holding
|
||||
// for dstAcct; keying on the destination ensures a third-party destination's
|
||||
@@ -584,7 +562,7 @@ doWithdraw(
|
||||
// Move the funds directly from the broker's pseudo-account to the
|
||||
// dstAcct
|
||||
return accountSend(
|
||||
ctx.view, sourceAcct, dstAcct, destinationAmount, j, *sponsorSle, WaiveTransferFee::Yes);
|
||||
ctx.view, sourceAcct, dstAcct, amount, j, *sponsorSle, WaiveTransferFee::Yes);
|
||||
}
|
||||
|
||||
TER
|
||||
|
||||
@@ -213,19 +213,49 @@ adjustAmountsByLPTokens(
|
||||
|
||||
if (lpTokensActual < lpTokens)
|
||||
{
|
||||
bool const ammRoundingEnabled = [&] {
|
||||
if (auto const& rules = getCurrentTransactionRules();
|
||||
rules && rules->enabled(fixAMMv1_1))
|
||||
return true;
|
||||
return false;
|
||||
}();
|
||||
|
||||
// Equal trade
|
||||
if (amount2)
|
||||
{
|
||||
Number const fr = lpTokensActual / lpTokens;
|
||||
auto const amountActual = toSTAmount(amount.asset(), fr * amount);
|
||||
auto const amount2Actual = toSTAmount(amount2->asset(), fr * *amount2);
|
||||
if (!ammRoundingEnabled)
|
||||
{
|
||||
return std::make_tuple(
|
||||
amountActual < amount ? amountActual : amount,
|
||||
amount2Actual < amount2 ? amount2Actual : amount2,
|
||||
lpTokensActual);
|
||||
}
|
||||
|
||||
return std::make_tuple(amountActual, amount2Actual, lpTokensActual);
|
||||
}
|
||||
|
||||
// Single trade
|
||||
auto const amountActual = isDeposit == IsDeposit::Yes
|
||||
? ammAssetIn(amountBalance, lptAMMBalance, lpTokensActual, tfee)
|
||||
: ammAssetOut(amountBalance, lptAMMBalance, lpTokensActual, tfee);
|
||||
auto const amountActual = [&] {
|
||||
if (isDeposit == IsDeposit::Yes)
|
||||
{
|
||||
return ammAssetIn(amountBalance, lptAMMBalance, lpTokensActual, tfee);
|
||||
}
|
||||
if (!ammRoundingEnabled)
|
||||
{
|
||||
return ammAssetOut(amountBalance, lptAMMBalance, lpTokens, tfee);
|
||||
}
|
||||
|
||||
return ammAssetOut(amountBalance, lptAMMBalance, lpTokensActual, tfee);
|
||||
}();
|
||||
if (!ammRoundingEnabled)
|
||||
{
|
||||
return amountActual < amount
|
||||
? std::make_tuple(amountActual, std::nullopt, lpTokensActual)
|
||||
: std::make_tuple(amount, std::nullopt, lpTokensActual);
|
||||
}
|
||||
|
||||
return std::make_tuple(amountActual, std::nullopt, lpTokensActual);
|
||||
}
|
||||
@@ -950,7 +980,7 @@ verifyAndAdjustLPTokenBalance(
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::unexpected<TER>(tecAMM_INVALID_TOKENS); // LCOV_EXCL_LINE
|
||||
return std::unexpected<TER>(tecAMM_INVALID_TOKENS);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -66,161 +66,6 @@ canApplyToBrokerCover(
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
|
||||
[[nodiscard]] int
|
||||
getPosteriorBrokerCoverScale(SLE::ConstRef vault, SLE::ConstRef broker, Number const& delta)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::detail::getPosteriorBrokerCoverScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
broker && broker->getType() == ltLOAN_BROKER,
|
||||
"xrpl::detail::getPosteriorBrokerCoverScale : valid LoanBroker sle");
|
||||
|
||||
return posteriorAssetScale(
|
||||
getVaultVersion(vault),
|
||||
vault->at(sfAsset),
|
||||
getVaultBaseScale(vault),
|
||||
broker->at(sfCoverAvailable),
|
||||
delta);
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorBrokerCoverScale(
|
||||
SLE::ConstRef vault,
|
||||
SLE::ConstRef broker,
|
||||
STAmount const& delta,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::detail::roundToPosteriorBrokerCoverScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
broker && broker->getType() == ltLOAN_BROKER,
|
||||
"xrpl::detail::roundToPosteriorBrokerCoverScale : valid LoanBroker sle");
|
||||
XRPL_ASSERT(
|
||||
delta.asset() == vault->at(sfAsset),
|
||||
"xrpl::detail::roundToPosteriorBrokerCoverScale : delta and Vault asset match");
|
||||
if (delta.integral())
|
||||
return delta;
|
||||
return roundToScale(delta, getPosteriorBrokerCoverScale(vault, broker, delta), roundingMode);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorBrokerCoverScale(
|
||||
SLE::ConstRef vault,
|
||||
SLE::ConstRef broker,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::creditToPosteriorBrokerCoverScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
broker && broker->getType() == ltLOAN_BROKER,
|
||||
"xrpl::creditToPosteriorBrokerCoverScale : valid LoanBroker sle");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
if (asset.integral())
|
||||
{
|
||||
NumberRoundModeGuard const rg(roundingMode);
|
||||
return STAmount{asset, raw};
|
||||
}
|
||||
|
||||
Number const reference = broker->at(sfCoverAvailable);
|
||||
return detail::creditToPosteriorScale(
|
||||
asset, reference, getVaultBaseScale(vault), raw, roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
debitToPosteriorBrokerCoverScale(
|
||||
SLE::ConstRef vault,
|
||||
SLE::ConstRef broker,
|
||||
STAmount const& amount,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::debitToPosteriorBrokerCoverScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
broker && broker->getType() == ltLOAN_BROKER,
|
||||
"xrpl::debitToPosteriorBrokerCoverScale : valid LoanBroker sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset),
|
||||
"xrpl::debitToPosteriorBrokerCoverScale : amount and Vault asset match");
|
||||
XRPL_ASSERT(!amount.negative(), "xrpl::debitToPosteriorBrokerCoverScale : non-negative amount");
|
||||
return -detail::roundToPosteriorBrokerCoverScale(vault, broker, -amount, roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
checkOptionalBrokerCoverInflow(SLE::ConstRef vault, SLE::ConstRef broker, STAmount const& amount)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::checkOptionalBrokerCoverInflow : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
broker && broker->getType() == ltLOAN_BROKER,
|
||||
"xrpl::checkOptionalBrokerCoverInflow : valid LoanBroker sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset),
|
||||
"xrpl::checkOptionalBrokerCoverInflow : amount and Vault asset match");
|
||||
XRPL_ASSERT(!amount.negative(), "xrpl::checkOptionalBrokerCoverInflow : non-negative amount");
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
return tesSUCCESS;
|
||||
|
||||
// amount is the effective credit (already floored on the posterior
|
||||
// CoverAvailable grid by creditToPosteriorBrokerCoverScale); rounding it
|
||||
// again as a standalone delta could hide a crossing.
|
||||
STAmount const& rounded = amount;
|
||||
int const baseScale = getVaultBaseScale(vault);
|
||||
// Keep this explicit even though the Open-limit capacity check below rejects
|
||||
// every coarsening transition too. The protocol defines both conditions
|
||||
// independently.
|
||||
if (detail::getPosteriorBrokerCoverScale(vault, broker, rounded) != baseScale)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
|
||||
Number const posterior = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::TowardsZero);
|
||||
return broker->at(sfCoverAvailable) + rounded;
|
||||
}();
|
||||
if (posterior > getVaultOpenLimit(vault))
|
||||
return tecLIMIT_EXCEEDED;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
adjustBrokerDebtTotal(
|
||||
SLE::Ref brokerSle,
|
||||
SLE::ConstRef vaultSle,
|
||||
Number const& delta,
|
||||
int vaultScale)
|
||||
{
|
||||
// On FixedPrecision Vaults, LoanSet adds principal to DebtTotal exactly, LoanPay and default
|
||||
// subtract exact amounts. Thus DebtTotal must not be rounded coarser than the base scale.
|
||||
if (getVaultVersion(vaultSle) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
brokerSle->at(sfDebtTotal) += delta;
|
||||
}
|
||||
else
|
||||
{
|
||||
adjustImpreciseNumber(brokerSle->at(sfDebtTotal), delta, vaultSle->at(sfAsset), vaultScale);
|
||||
}
|
||||
}
|
||||
|
||||
Number
|
||||
minimumBrokerCover(Number const& debtTotal, TenthBips32 coverRateMinimum, SLE::ConstRef vaultSle)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vaultSle && vaultSle->getType() == ltVAULT, "xrpl::minimumBrokerCover : valid Vault sle");
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
|
||||
return roundToAsset(
|
||||
vaultSle->at(sfAsset),
|
||||
tenthBipsOfValue(debtTotal, coverRateMinimum),
|
||||
getVaultBaseScale(vaultSle));
|
||||
}
|
||||
|
||||
bool
|
||||
checkLendingProtocolDependencies(Rules const& rules, STTx const& tx)
|
||||
{
|
||||
@@ -405,13 +250,14 @@ loanPaymentDeltas(LoanPaymentParts const& parts)
|
||||
|
||||
namespace {
|
||||
|
||||
// Cash-basis accounting applies to Vaults created under
|
||||
// featureLendingProtocolV1_1 or a later version. Vaults created before
|
||||
// activation keep instant interest recognition forever.
|
||||
// Cash-basis accounting applies only when featureLendingProtocolV1_1 is
|
||||
// enabled AND the specific Vault was created under it (LEVersion ==
|
||||
// VaultVersion::CashBasis). Vaults created before activation keep instant
|
||||
// interest recognition forever, even after the amendment later turns on.
|
||||
bool
|
||||
cashBasisEnabled(SLE::ConstRef vaultSle)
|
||||
{
|
||||
return getVaultVersion(vaultSle) >= VaultVersion::CashBasis;
|
||||
return getVaultVersion(vaultSle) == VaultVersion::CashBasis;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -1047,14 +1047,6 @@ maxMPTAmount(SLE const& sleIssuance)
|
||||
return sleIssuance[~sfMaximumAmount].value_or(kMaxMpTokenAmount);
|
||||
}
|
||||
|
||||
std::optional<std::int64_t>
|
||||
maxMPTAmount(ReadView const& view, MPTID const& mptID)
|
||||
{
|
||||
if (auto const sle = view.read(keylet::mptokenIssuance(mptID)))
|
||||
return maxMPTAmount(*sle);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::int64_t
|
||||
availableMPTAmount(SLE const& sleIssuance)
|
||||
{
|
||||
|
||||
@@ -1,54 +1,18 @@
|
||||
#include <xrpl/ledger/helpers/ProposalHelpers.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::proposal {
|
||||
|
||||
namespace {
|
||||
|
||||
// Proposals do not nest, so a proposed transaction may carry none of these
|
||||
// types, neither as its own payload nor as a Batch inner transaction.
|
||||
constexpr auto kProposalTxTypes = std::to_array<TxType>({
|
||||
ttTRANSACTION_PROPOSAL_CREATE,
|
||||
ttTRANSACTION_PROPOSAL_CANCEL,
|
||||
});
|
||||
|
||||
} // namespace
|
||||
|
||||
bool
|
||||
isValidProposalTxnType(STObject const& proposedTx)
|
||||
isValidProposal(STObject const& proposedTx)
|
||||
{
|
||||
if (!proposedTx.isFieldPresent(sfTransactionType))
|
||||
return false;
|
||||
|
||||
auto const type = proposedTx.getFieldU16(sfTransactionType);
|
||||
|
||||
if (std::ranges::find(kProposalTxTypes, type) != kProposalTxTypes.end())
|
||||
if (isProposalTx(proposedTx))
|
||||
return false;
|
||||
|
||||
if (isPseudoTx(proposedTx))
|
||||
@@ -58,24 +22,13 @@ isValidProposalTxnType(STObject const& proposedTx)
|
||||
(proposedTx.getFieldU32(sfFlags) & tfInnerBatchTxn) != 0u)
|
||||
return false;
|
||||
|
||||
if (type == ttBATCH && proposedTx.isFieldPresent(sfRawTransactions))
|
||||
if (proposedTx.getFieldU16(sfTransactionType) == ttBATCH &&
|
||||
proposedTx.isFieldPresent(sfRawTransactions))
|
||||
{
|
||||
STArray const& innerTxns = proposedTx.getFieldArray(sfRawTransactions);
|
||||
for (STObject const& inner : innerTxns)
|
||||
{
|
||||
// The only production caller reaches this walk through STTx
|
||||
// construction, which rejects a typeless inner first, so this is
|
||||
// re-checked rather than relied upon.
|
||||
if (!inner.isFieldPresent(sfTransactionType))
|
||||
return false;
|
||||
|
||||
auto const innerType = inner.getFieldU16(sfTransactionType);
|
||||
|
||||
// A nested Batch is rejected alongside nested proposals and
|
||||
// pseudo-transactions: STTx construction forbids inner Batches, so
|
||||
// a proposal must not be able to store one either.
|
||||
if (std::ranges::find(kProposalTxTypes, innerType) != kProposalTxTypes.end() ||
|
||||
innerType == ttBATCH || isPseudoTx(inner))
|
||||
if (isProposalTx(inner) || isPseudoTx(inner))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -83,112 +36,4 @@ isValidProposalTxnType(STObject const& proposedTx)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
isTerminal(
|
||||
ReadView const& view,
|
||||
std::optional<std::uint32_t> expiration,
|
||||
STObject const& proposedTx)
|
||||
{
|
||||
if (hasExpired(view, expiration))
|
||||
return true;
|
||||
|
||||
// Mirror the ordinary transactor's tefMAX_LEDGER rule: view.seq() > bound.
|
||||
// At view.seq() == bound the payload can still be submitted, so the
|
||||
// proposal is still live (On-Chain Cosigner spec §4.5).
|
||||
return proposedTx.isFieldPresent(sfLastLedgerSequence) &&
|
||||
view.seq() > proposedTx.getFieldU32(sfLastLedgerSequence);
|
||||
}
|
||||
|
||||
TER
|
||||
deleteProposal(ApplyView& view, SLE::pointer const& sleProposal, beast::Journal j)
|
||||
{
|
||||
if (!sleProposal || sleProposal->getType() != ltTRANSACTION_PROPOSAL ||
|
||||
!view.exists(Keylet{ltTRANSACTION_PROPOSAL, sleProposal->key()}))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "Invalid TransactionProposal deletion.";
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
AccountID const owner = sleProposal->getAccountID(sfOwner);
|
||||
|
||||
std::uint64_t const page{(*sleProposal)[sfOwnerNode]};
|
||||
if (!view.dirRemove(keylet::ownerDir(owner), page, sleProposal->key(), true))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "Unable to delete TransactionProposal from owner.";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const sleOwner = view.peek(keylet::account(owner));
|
||||
if (!sleOwner)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "Could not find TransactionProposal owner account root.";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// Release the reserve against the Owner or, if the entry carries a
|
||||
// reserve sponsor, against that sponsor.
|
||||
decreaseOwnerCountForObject(
|
||||
view,
|
||||
sleOwner,
|
||||
sleProposal,
|
||||
proposalOwnerCount(sleProposal->getFieldObject(sfProposedTransaction)),
|
||||
j);
|
||||
|
||||
view.erase(sleProposal);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
bool
|
||||
payloadMatches(STObject const& proposedTx, STObject const& tx)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
proposedTx.isFieldPresent(sfTransactionType) && tx.isFieldPresent(sfTransactionType),
|
||||
"xrpl::proposal::payloadMatches : transaction-shaped inputs");
|
||||
|
||||
// The signing-payload serialization omits every kNotSigning field: the
|
||||
// signature containers a proposal lets evolve (TxnSignature, Signers,
|
||||
// BatchSigners, CounterpartySignature, SponsorSignature) plus Signature
|
||||
// and MasterSignature, which the account's own signature does not cover
|
||||
// either (see the header). What remains is the field set fixed at
|
||||
// creation — except SigningPubKey, which signing payloads include but
|
||||
// which is also mutable here (stored empty, filled if the target signs
|
||||
// with its own key). Neutralize it on both sides before comparing.
|
||||
auto const payloadBytes = [](STObject const& obj) {
|
||||
STObject copy{obj};
|
||||
copy.setFieldVL(sfSigningPubKey, Slice{});
|
||||
Serializer s;
|
||||
copy.addWithoutSigningFields(s);
|
||||
return s.getData();
|
||||
};
|
||||
|
||||
return payloadBytes(proposedTx) == payloadBytes(tx);
|
||||
}
|
||||
|
||||
bool
|
||||
canConsumeTicket(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
auto const seqProx = tx.getSeqProxy();
|
||||
if (!seqProx.isTicket())
|
||||
{
|
||||
// The caller only reaches here for a Ticket-based transaction.
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::proposal::canConsumeTicket : tx spends a Sequence");
|
||||
return false;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const sleProposal =
|
||||
view.read(keylet::txProposal(tx.getAccountID(sfAccount), seqProx.value()));
|
||||
if (!sleProposal)
|
||||
return true;
|
||||
|
||||
return payloadMatches(sleProposal->getFieldObject(sfProposedTransaction), tx);
|
||||
}
|
||||
|
||||
} // namespace xrpl::proposal
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/EscrowHelpers.h>
|
||||
#include <xrpl/ledger/helpers/OracleHelpers.h>
|
||||
#include <xrpl/ledger/helpers/ProposalHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -320,8 +319,6 @@ getLedgerEntryOwnerCount(SLE const& sle)
|
||||
UNREACHABLE("AccountRoots are not supported by object sponsorship.");
|
||||
return 0;
|
||||
// LCOV_EXCL_STOP
|
||||
case ltESCROW:
|
||||
return calculateAdditionalReserve(sle[~sfBytecode]);
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/MathUtilities.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
@@ -552,22 +551,6 @@ transferRate(ReadView const& view, STAmount const& amount)
|
||||
return transferRate(view, amount.asset());
|
||||
}
|
||||
|
||||
STAmount
|
||||
addTransferFee(STAmount const& amount, Rate const& rate)
|
||||
{
|
||||
if (rate == kParityRate)
|
||||
return amount;
|
||||
return mulRatio(amount, rate.value, QUALITY_ONE, Number::RoundingMode::Upward);
|
||||
}
|
||||
|
||||
STAmount
|
||||
subtractTransferFee(STAmount const& amount, Rate const& rate)
|
||||
{
|
||||
if (rate == kParityRate)
|
||||
return amount;
|
||||
return mulRatio(amount, QUALITY_ONE, rate.value, Number::RoundingMode::Downward);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// Holding operations
|
||||
@@ -990,11 +973,19 @@ accountSendIOU(
|
||||
SLE::Ref sponsorSle,
|
||||
WaiveTransferFee waiveFee)
|
||||
{
|
||||
if (saAmount < beast::kZero || saAmount.holds<MPTIssue>())
|
||||
if (view.rules().enabled(fixAMMv1_1))
|
||||
{
|
||||
if (saAmount < beast::kZero || saAmount.holds<MPTIssue>())
|
||||
{
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "accountSendIOU: negative amount or invalid asset";
|
||||
return tecINTERNAL;
|
||||
XRPL_ASSERT(
|
||||
saAmount >= beast::kZero && !saAmount.holds<MPTIssue>(),
|
||||
"xrpl::accountSendIOU : minimum amount and not MPT");
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
@@ -1364,8 +1355,9 @@ directSendNoLimitMPT(
|
||||
{
|
||||
// Number math loses precision on large MPT amounts, which can
|
||||
// overcharge the sender. MPTs are integral, so compute the cost
|
||||
// exactly, matching the payment engine.
|
||||
saActual = addTransferFee(saAmount, rate);
|
||||
// exactly and round it up, matching the payment engine.
|
||||
auto const cost = mulRatio(saAmount.mpt(), rate.value, QUALITY_ONE, true);
|
||||
saActual = STAmount(saAmount.asset(), cost.value());
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1490,8 +1482,9 @@ directSendNoLimitMultiMPT(
|
||||
{
|
||||
// Number math loses precision on large MPT amounts, which can
|
||||
// overcharge the sender. MPTs are integral, so compute the
|
||||
// cost exactly, matching the payment engine.
|
||||
actualSend = addTransferFee(amount, rate);
|
||||
// cost exactly and round it up, matching the payment engine.
|
||||
auto const cost = mulRatio(amount.mpt(), rate.value, QUALITY_ONE, true);
|
||||
actualSend = STAmount(amount.asset(), cost.value());
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
@@ -21,480 +17,13 @@
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] int
|
||||
fixedBaseScale(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::fixedBaseScale : valid Vault sle");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
// sfScale is never written for an integral asset (VaultCreate only sets
|
||||
// it when scale != 0, and integral assets always get scale 0), so it
|
||||
// must not be read here; vaultBaseScale ignores the scale argument for
|
||||
// an integral asset anyway.
|
||||
if (asset.integral())
|
||||
return 0;
|
||||
return vaultBaseScale(asset, vault->at(sfScale));
|
||||
}
|
||||
|
||||
// Thin, vault-specific wrapper over posteriorAssetScale: used by
|
||||
// getPosteriorVaultScale and the cash outflow rounding below.
|
||||
[[nodiscard]] int
|
||||
posteriorScale(SLE::ConstRef vault, Number const& reference, Number const& delta)
|
||||
{
|
||||
return detail::posteriorAssetScale(
|
||||
getVaultVersion(vault), vault->at(sfAsset), fixedBaseScale(vault), reference, delta);
|
||||
}
|
||||
|
||||
// FixedPrecision-only counterpart to clampToAssetsTotalScale, for cash
|
||||
// outflows (VaultWithdraw, VaultClawback): rounds the magnitude of delta
|
||||
// toward zero at AssetsAvailable's own posterior scale, the balance the cash
|
||||
// actually leaves, instead of the derived AssetsTotal cache. Same
|
||||
// tecPRECISION_LOSS and integral-asset and magnitude-never-exceeds-delta
|
||||
// rules as clampToAssetsTotalScale. Not part of the public interface; reached
|
||||
// only through clampVaultOutflow.
|
||||
//
|
||||
// For an outflow it is safe to round the delta directly, unlike the credit
|
||||
// case: amount on the posterior grid of AssetsAvailable always leaves
|
||||
// AssetsAvailable minus amount representable, since the posterior grid is
|
||||
// exactly the grid AssetsAvailable itself will canonicalize to after the
|
||||
// subtraction.
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampToAvailableScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
delta.asset() == vault->at(sfAsset),
|
||||
"xrpl::clampToAvailableScale : delta and vault asset match");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::clampToAvailableScale : FixedPrecision Vault");
|
||||
XRPL_ASSERT(delta.negative(), "xrpl::clampToAvailableScale : outflow delta is negative");
|
||||
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
STAmount magnitude = delta.negative() ? -delta : delta;
|
||||
if (asset.integral())
|
||||
return magnitude;
|
||||
|
||||
STAmount const rounded = roundToScale(
|
||||
delta,
|
||||
posteriorScale(vault, vault->at(sfAssetsAvailable), delta),
|
||||
Number::RoundingMode::TowardsZero);
|
||||
STAmount actualDelta = rounded.negative() ? -rounded : rounded;
|
||||
|
||||
XRPL_ASSERT(
|
||||
abs(actualDelta) <= abs(delta),
|
||||
"xrpl::clampToAvailableScale : actual delta smaller or equal to calculated delta");
|
||||
|
||||
// Reject changes below scale precision (1 ULP) to prevent share balance changes
|
||||
// without corresponding asset movements.
|
||||
if (actualDelta <= beast::kZero)
|
||||
return std::unexpected(tecPRECISION_LOSS);
|
||||
|
||||
return actualDelta;
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultKind
|
||||
decodeVaultKind(std::optional<std::uint8_t> vaultKind)
|
||||
{
|
||||
if (vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded))
|
||||
return VaultKind::ClosedEnded;
|
||||
return VaultKind::OpenEnded;
|
||||
}
|
||||
|
||||
// Write cached AssetsTotal from getAssetsTotal, as the exact total rounded
|
||||
// Downward to the Vault asset's 16-digit STAmount representation. Below
|
||||
// coarsening this equals the exact total; above it, the cache is a
|
||||
// deliberate floor of the exact value. Internal to adjustVaultBalances,
|
||||
// the only writer of AssetsAvailable, AssetsDeployed, AssetsTotal,
|
||||
// YieldUnrealized and LossUnrealized on a FixedPrecision Vault after
|
||||
// creation; not part of the public interface.
|
||||
void
|
||||
syncAssetsTotal(SLE::Ref vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::syncAssetsTotal : valid Vault sle");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
|
||||
// adjustVaultBalances writes AssetsAvailable as an already-canonical
|
||||
// STAmount before calling this, so the associateAsset pass a transactor
|
||||
// runs afterwards is always a no-op. If this fires, AssetsAvailable was
|
||||
// written some other way.
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, vault->at(sfAssetsAvailable)} == vault->at(sfAssetsAvailable)),
|
||||
"xrpl::syncAssetsTotal : AssetsAvailable is a 16-digit STAmount value");
|
||||
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
vault->at(sfAssetsTotal) = STAmount{asset, getAssetsTotal(vault)};
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
debitIsNonZeroDust(SLE::ConstRef vault, Number const& amount)
|
||||
{
|
||||
if (amount == 0)
|
||||
return false;
|
||||
|
||||
auto const total = getVaultVersion(vault) == VaultVersion::FixedPrecision
|
||||
? vault->at(sfAssetsAvailable)
|
||||
: getAssetsTotal(vault);
|
||||
auto const asset = vault->at(sfAsset);
|
||||
return STAmount{asset, total - amount} == STAmount{asset, total};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace detail {
|
||||
|
||||
[[nodiscard]] int
|
||||
liveScale(Number const& reference, Asset const& asset, int baseScale)
|
||||
{
|
||||
if (reference == beast::kZero)
|
||||
return baseScale;
|
||||
// Round Downward, not the ambient mode: the live exponent must match the
|
||||
// exponent syncAssetsTotal actually stores (also Downward), regardless of
|
||||
// what rounding mode the caller's own arithmetic is using.
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return std::max(baseScale, scale(reference, asset));
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
posteriorAssetScale(
|
||||
VaultVersion version,
|
||||
Asset const& asset,
|
||||
int baseScale,
|
||||
Number const& reference,
|
||||
Number const& delta)
|
||||
{
|
||||
// ToNearest for the sum and for scale()'s canonicalization of it, as the
|
||||
// Legacy/CashBasis clamp always did; liveScale sets its own mode.
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
Number const posterior = reference + delta;
|
||||
|
||||
switch (version)
|
||||
{
|
||||
case VaultVersion::Legacy:
|
||||
case VaultVersion::CashBasis:
|
||||
return scale(posterior, asset);
|
||||
case VaultVersion::FixedPrecision:
|
||||
return liveScale(posterior, asset, baseScale);
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::detail::posteriorAssetScale : valid VaultVersion");
|
||||
return Number::kMinExponent - 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorScale(
|
||||
Asset const& asset,
|
||||
Number const& reference,
|
||||
int baseScale,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
// Floor the SUM (reference + raw), not just raw. See
|
||||
// creditToPosteriorAvailableScale's doc: a delta floored on its own grid
|
||||
// can still leave a 17-digit sum once the reference has crossed a power
|
||||
// of ten.
|
||||
//
|
||||
// Both reference + raw and flooredSum - reference can carry more digits
|
||||
// than Number holds (the sum is on the posterior grid, the reference on
|
||||
// the finer one). Compute both under roundingMode, not the caller's
|
||||
// ambient mode, so the credit this returns never exceeds raw.
|
||||
//
|
||||
// Take the grid from this same sum. A sum just under a power of ten that
|
||||
// rounds up to it in another mode would pick the coarser grid, and
|
||||
// flooring the unrounded sum on it would land below reference.
|
||||
NumberRoundModeGuard const rg(roundingMode);
|
||||
Number const sum = reference + raw;
|
||||
int const atScale = liveScale(sum, asset, baseScale);
|
||||
Number const flooredSum = roundToAsset(asset, sum, atScale, roundingMode);
|
||||
STAmount const credit{asset, flooredSum - reference};
|
||||
XRPL_ASSERT_PARTS(
|
||||
roundingMode != Number::RoundingMode::Downward ||
|
||||
(credit >= beast::kZero && Number(credit) <= raw),
|
||||
"xrpl::detail::creditToPosteriorScale",
|
||||
"credit is between zero and raw");
|
||||
return credit;
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
getPosteriorVaultScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::detail::getPosteriorVaultScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
delta.asset() == vault->at(sfAsset),
|
||||
"xrpl::detail::getPosteriorVaultScale : delta and Vault asset match");
|
||||
return posteriorScale(vault, getAssetsTotal(vault), delta);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
[[nodiscard]] Number
|
||||
getAssetsTotal(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getAssetsTotal : valid Vault sle");
|
||||
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
return vault->at(sfAssetsTotal);
|
||||
|
||||
// AssetsAvailable is at -Scale (or coarser) and AssetsDeployed is always at
|
||||
// -Scale, so the sum is exact while it fits 19 digits. Fix the mode so a
|
||||
// sum that does not fit rounds the same way for every caller.
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return vault->at(sfAssetsAvailable) + vault->at(sfAssetsDeployed);
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
adjustVaultBalances(SLE::Ref vault, VaultBalanceChange const& change, beast::Journal j)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::adjustVaultBalances : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::adjustVaultBalances : FixedPrecision Vault");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
STAmount const cash = change.cash.value_or(STAmount{asset, 0});
|
||||
XRPL_ASSERT(cash.asset() == asset, "xrpl::adjustVaultBalances : cash and Vault asset match");
|
||||
// This is the mode the ledger's own transfers (trust line / MPT balance
|
||||
// updates) normalize in; computing AA' with any other ambient mode would
|
||||
// not reproduce what the real transfer just did to the backing balance.
|
||||
XRPL_ASSERT(
|
||||
Number::getround() == Number::RoundingMode::ToNearest,
|
||||
"xrpl::adjustVaultBalances : ambient rounding mode is ToNearest");
|
||||
|
||||
// AA' is the exact trust-line/MPT arithmetic the ledger uses for the
|
||||
// transfer itself: a Number sum re-canonicalized to the asset's 16-digit
|
||||
// STAmount precision. MPT and XRP sums are already integral, so this is
|
||||
// a no-op rounding for them.
|
||||
//
|
||||
// AssetsDeployed, LossUnrealized and YieldUnrealized move no cash, so they stay
|
||||
// exact Number sums; the asserts below check each fits 16 digits, rather
|
||||
// than letting an STAmount conversion round it silently.
|
||||
STAmount const availableAfter{asset, Number(vault->at(sfAssetsAvailable)) + Number(cash)};
|
||||
Number const assetsDeployedAfter = Number(vault->at(sfAssetsDeployed)) + change.deployed;
|
||||
Number const lossUnrealizedAfter = Number(vault->at(sfLossUnrealized)) + change.loss;
|
||||
// LoanSet, LoanPay and default each move YieldUnrealized by the change in
|
||||
// the Loan's own TotalValueOutstanding - PrincipalOutstanding -
|
||||
// ManagementFeeOutstanding, so it always equals that sum over the
|
||||
// Vault's Loans and cannot go negative.
|
||||
Number const yieldUnrealizedAfter = Number(vault->at(sfYieldUnrealized)) + change.yield;
|
||||
|
||||
if (availableAfter < beast::kZero || assetsDeployedAfter < beast::kZero ||
|
||||
lossUnrealizedAfter < beast::kZero || yieldUnrealizedAfter < beast::kZero)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "adjustVaultBalances: a balance would become negative."
|
||||
<< " AssetsAvailable: " << Number(availableAfter)
|
||||
<< ", AssetsDeployed: " << assetsDeployedAfter
|
||||
<< ", LossUnrealized: " << lossUnrealizedAfter
|
||||
<< ", YieldUnrealized: " << yieldUnrealizedAfter;
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// The impairment rule: a Loan can only be impaired for as much as it
|
||||
// still owes. Reachable only through LoanManage's impair.
|
||||
if (lossUnrealizedAfter > assetsDeployedAfter)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
|
||||
// Every caller derives principal from a Loan's own PrincipalOutstanding so the new total must
|
||||
// stay representable at the asset's own 16-digit precision too.
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, assetsDeployedAfter} == assetsDeployedAfter),
|
||||
"xrpl::adjustVaultBalances : AssetsDeployed is a 16-digit STAmount value");
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, lossUnrealizedAfter} == lossUnrealizedAfter),
|
||||
"xrpl::adjustVaultBalances : LossUnrealized is a 16-digit STAmount value");
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, yieldUnrealizedAfter} == yieldUnrealizedAfter),
|
||||
"xrpl::adjustVaultBalances : YieldUnrealized is a 16-digit STAmount value");
|
||||
|
||||
vault->at(sfAssetsAvailable) = availableAfter;
|
||||
vault->at(sfAssetsDeployed) = assetsDeployedAfter;
|
||||
vault->at(sfLossUnrealized) = lossUnrealizedAfter;
|
||||
vault->at(sfYieldUnrealized) = yieldUnrealizedAfter;
|
||||
syncAssetsTotal(vault);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
adjustVaultCash(SLE::Ref vault, STAmount const& delta, beast::Journal j)
|
||||
{
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
return adjustVaultBalances(vault, {.cash = delta}, j);
|
||||
|
||||
vault->at(sfAssetsTotal) += delta;
|
||||
vault->at(sfAssetsAvailable) += delta;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
getVaultScale(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultScale : valid Vault sle");
|
||||
|
||||
Number const assetsTotal = getAssetsTotal(vault);
|
||||
switch (getVaultVersion(vault))
|
||||
{
|
||||
case VaultVersion::Legacy:
|
||||
case VaultVersion::CashBasis:
|
||||
return scale(assetsTotal, vault->at(sfAsset));
|
||||
case VaultVersion::FixedPrecision:
|
||||
return detail::liveScale(assetsTotal, vault->at(sfAsset), fixedBaseScale(vault));
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getVaultScale : valid VaultVersion");
|
||||
return Number::kMinExponent - 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
getVaultBaseScale(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultBaseScale : valid Vault sle");
|
||||
|
||||
switch (getVaultVersion(vault))
|
||||
{
|
||||
case VaultVersion::Legacy:
|
||||
case VaultVersion::CashBasis:
|
||||
return getVaultScale(vault);
|
||||
case VaultVersion::FixedPrecision:
|
||||
return fixedBaseScale(vault);
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getVaultBaseScale : valid VaultVersion");
|
||||
return Number::kMinExponent - 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorVaultScale(
|
||||
SLE::ConstRef vault,
|
||||
STAmount const& amount,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::roundToPosteriorVaultScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset),
|
||||
"xrpl::roundToPosteriorVaultScale : amount and Vault asset match");
|
||||
if (amount.integral())
|
||||
return amount;
|
||||
return roundToScale(amount, detail::getPosteriorVaultScale(vault, amount), roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorAvailableScale(
|
||||
SLE::ConstRef vault,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::creditToPosteriorAvailableScale : valid Vault sle");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
if (asset.integral())
|
||||
{
|
||||
NumberRoundModeGuard const rg(roundingMode);
|
||||
return STAmount{asset, raw};
|
||||
}
|
||||
|
||||
Number const reference = vault->at(sfAssetsAvailable);
|
||||
return detail::creditToPosteriorScale(
|
||||
asset, reference, fixedBaseScale(vault), raw, roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
getVaultOpenLimit(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultOpenLimit : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::getVaultOpenLimit : FixedPrecision Vault");
|
||||
return Number{9, 15 + getVaultBaseScale(vault)};
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
checkOptionalVaultInflow(SLE::ConstRef vault, STAmount const& amount)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::checkOptionalVaultInflow : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset),
|
||||
"xrpl::checkOptionalVaultInflow : amount and Vault asset match");
|
||||
XRPL_ASSERT(!amount.negative(), "xrpl::checkOptionalVaultInflow : non-negative amount");
|
||||
if (amount.negative())
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
return tesSUCCESS;
|
||||
|
||||
STAmount const rounded =
|
||||
roundToPosteriorVaultScale(vault, amount, Number::RoundingMode::TowardsZero);
|
||||
int const baseScale = getVaultBaseScale(vault);
|
||||
// Keep this explicit even though a non-negative YieldUnrealized makes the
|
||||
// Open-zone capacity ceiling reject every coarsening transition too. The
|
||||
// protocol defines both conditions independently.
|
||||
if (detail::getPosteriorVaultScale(vault, rounded) != baseScale)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
|
||||
if (vaultOpenZoneCapacity(vault, rounded) > getVaultOpenLimit(vault))
|
||||
return tecLIMIT_EXCEEDED;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
vaultOpenZoneCapacity(SLE::ConstRef vault, Number const& roundedAmount)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::vaultOpenZoneCapacity : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::vaultOpenZoneCapacity : FixedPrecision Vault");
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::TowardsZero);
|
||||
return getAssetsTotal(vault) + vault->at(sfYieldUnrealized) + roundedAmount;
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
checkDebitNotDust(
|
||||
SLE::ConstRef vault,
|
||||
Number const& amount,
|
||||
beast::Journal j,
|
||||
std::string_view name)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (debitIsNonZeroDust(vault, amount))
|
||||
{
|
||||
JLOG(j.debug()) << name << ": amount too small to change stored vault balance";
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
// It's easy to hit this exception from Number with large enough Scale
|
||||
// so we avoid spamming the log and only use debug here.
|
||||
JLOG(j.debug()) //
|
||||
<< name << ": overflow error with"
|
||||
<< " scale=" << (int)vault->at(sfScale) //
|
||||
<< ", assetsTotal=" << vault->at(sfAssetsTotal) << ", amount=" << amount;
|
||||
return tecPATH_DRY;
|
||||
}
|
||||
// LCOV_EXCL_STOP
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<STAmount>
|
||||
assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount const& assets)
|
||||
{
|
||||
@@ -505,7 +34,7 @@ assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
|
||||
if (assets.negative() || assets.asset() != vault->at(sfAsset))
|
||||
return std::nullopt; // LCOV_EXCL_LINE
|
||||
|
||||
Number const assetTotal = getAssetsTotal(vault);
|
||||
Number const assetTotal = vault->at(sfAssetsTotal);
|
||||
STAmount shares{vault->at(sfShareMPTID)};
|
||||
if (assetTotal == 0)
|
||||
{
|
||||
@@ -529,7 +58,7 @@ sharesToAssetsDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
|
||||
if (shares.negative() || shares.asset() != vault->at(sfShareMPTID))
|
||||
return std::nullopt; // LCOV_EXCL_LINE
|
||||
|
||||
Number const assetTotal = getAssetsTotal(vault);
|
||||
Number const assetTotal = vault->at(sfAssetsTotal);
|
||||
STAmount assets{vault->at(sfAsset)};
|
||||
if (assetTotal == 0)
|
||||
{
|
||||
@@ -556,52 +85,37 @@ clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
return magnitude;
|
||||
}
|
||||
Number const assetsTotal = vault->at(sfAssetsTotal);
|
||||
|
||||
// Calculate the scale after applying the delta using ToNearest rounding.
|
||||
// This aligns the delta with scale checks used by vault invariants.
|
||||
int const postScale = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return scale(assetsTotal + delta, asset);
|
||||
}();
|
||||
|
||||
STAmount actualDelta;
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
if (delta.negative())
|
||||
{
|
||||
// FixedPrecision only reaches this branch for credits (VaultDeposit);
|
||||
// outflows go through clampVaultOutflow -> clampToAvailableScale,
|
||||
// which rounds at AssetsAvailable's own posterior scale instead.
|
||||
XRPL_ASSERT(
|
||||
!delta.negative(), "xrpl::clampToAssetsTotalScale : FixedPrecision credit only");
|
||||
STAmount const rounded =
|
||||
roundToPosteriorVaultScale(vault, delta, Number::RoundingMode::TowardsZero);
|
||||
actualDelta = rounded.negative() ? -rounded : rounded;
|
||||
// For withdrawals (debits), floor the magnitude to the target scale
|
||||
// to ensure exact grid alignment without paying out extra assets.
|
||||
actualDelta = roundToScale(magnitude, postScale, Number::RoundingMode::Downward);
|
||||
}
|
||||
else
|
||||
{
|
||||
Number const assetsTotal = getAssetsTotal(vault);
|
||||
|
||||
// Calculate the scale after applying the delta using ToNearest rounding.
|
||||
// This aligns the delta with scale checks used by vault invariants.
|
||||
int const postScale = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return scale(assetsTotal + delta, asset);
|
||||
// For deposits (credits), derive actualDelta from the floored posterior total.
|
||||
// This prevents grid alignment issues from crediting the vault more than deposited.
|
||||
//
|
||||
// Sum using Downward rounding so intermediate precision doesn't round up
|
||||
// and exceed the original requested amount.
|
||||
Number const posterior = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return assetsTotal + magnitude;
|
||||
}();
|
||||
|
||||
if (delta.negative())
|
||||
{
|
||||
// For withdrawals (debits), floor the magnitude to the target scale
|
||||
// to ensure exact grid alignment without paying out extra assets.
|
||||
actualDelta = roundToScale(magnitude, postScale, Number::RoundingMode::Downward);
|
||||
}
|
||||
else
|
||||
{
|
||||
// For deposits (credits), derive actualDelta from the floored posterior total.
|
||||
// This prevents grid alignment issues from crediting the vault more than deposited.
|
||||
//
|
||||
// Sum using Downward rounding so intermediate precision doesn't round up
|
||||
// and exceed the original requested amount.
|
||||
Number const posterior = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return assetsTotal + magnitude;
|
||||
}();
|
||||
|
||||
Number const roundedPosterior =
|
||||
roundToAsset(asset, posterior, postScale, Number::RoundingMode::Downward);
|
||||
actualDelta = STAmount{asset, roundedPosterior - assetsTotal};
|
||||
}
|
||||
Number const roundedPosterior =
|
||||
roundToAsset(asset, posterior, postScale, Number::RoundingMode::Downward);
|
||||
actualDelta = STAmount{asset, roundedPosterior - assetsTotal};
|
||||
}
|
||||
|
||||
XRPL_ASSERT(
|
||||
@@ -616,49 +130,23 @@ clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
return actualDelta;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampVaultOutflow(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
XRPL_ASSERT(delta.negative(), "xrpl::clampVaultOutflow : outflow delta is negative");
|
||||
return getVaultVersion(vault) == VaultVersion::FixedPrecision
|
||||
? clampToAvailableScale(vault, delta)
|
||||
: clampToAssetsTotalScale(vault, delta);
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
vaultBaseScale(Asset const& asset, std::uint8_t scale)
|
||||
{
|
||||
if (asset.integral())
|
||||
return 0;
|
||||
return -static_cast<int>(scale);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isOnVaultBaseGrid(Asset const& asset, Number const& value, int baseScale)
|
||||
{
|
||||
// isRepresentable first: building an STAmount from an out-of-range value
|
||||
// throws, and this checks user-supplied values in preclaim.
|
||||
return isRepresentable(asset, value) && STAmount{asset, value} == value &&
|
||||
roundToAsset(asset, value, baseScale, Number::RoundingMode::TowardsZero) == value;
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
checkAssetsMaximum(SLE::ConstRef vault, Number const& amount)
|
||||
{
|
||||
return isOnVaultBaseGrid(vault->at(sfAsset), amount, getVaultBaseScale(vault))
|
||||
? TER{tesSUCCESS}
|
||||
: TER{tecPRECISION_LOSS};
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive)
|
||||
{
|
||||
Number assetTotal = getAssetsTotal(vault);
|
||||
Number assetTotal = vault->at(sfAssetsTotal);
|
||||
if (waive == WaiveUnrealizedLoss::No)
|
||||
assetTotal -= vault->at(sfLossUnrealized);
|
||||
return assetTotal;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount)
|
||||
{
|
||||
if (amount == 0)
|
||||
return false;
|
||||
return STAmount{asset, total - amount} == STAmount{asset, total};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<STAmount>
|
||||
assetsToSharesWithdraw(
|
||||
SLE::ConstRef vault,
|
||||
@@ -729,42 +217,36 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef
|
||||
return sleToken->getFieldU64(sfMPTAmount) == outstanding;
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultVersion
|
||||
decodeVaultVersion(std::optional<std::uint8_t> leVersion)
|
||||
{
|
||||
if (!leVersion)
|
||||
return VaultVersion::Legacy;
|
||||
if (*leVersion > std::to_underlying(VaultVersion::FixedPrecision))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::decodeVaultVersion : invalid vault version");
|
||||
return VaultVersion::Legacy;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
return static_cast<VaultVersion>(*leVersion);
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultVersion(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultVersion : valid Vault sle");
|
||||
return decodeVaultVersion(vault->at(~sfLEVersion));
|
||||
if (!vault->isFieldPresent(sfLEVersion))
|
||||
return VaultVersion::Legacy;
|
||||
|
||||
auto const version = vault->at(sfLEVersion);
|
||||
if (version > std::to_underlying(VaultVersion::CashBasis))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getVaultVersion : invalid vault version");
|
||||
return VaultVersion::Legacy;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
return static_cast<VaultVersion>(version);
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultDefaultVersion(Rules const& rules)
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] VaultKind
|
||||
decodeVaultKind(std::optional<std::uint8_t> vaultKind)
|
||||
{
|
||||
// FixedPrecision requires featureLendingProtocolV1_1 (closed-ended Vaults and
|
||||
// cash-basis accounting) and fixCleanup3_4_0, so a FixedPrecision Vault always
|
||||
// sees both enabled. fixCleanup3_2_0 is already enabled on the network.
|
||||
if (rules.enabled(featureLendingProtocolV1_1) && rules.enabled(featureLendingProtocolV1_2) &&
|
||||
rules.enabled(fixCleanup3_4_0))
|
||||
return VaultVersion::FixedPrecision;
|
||||
if (rules.enabled(featureLendingProtocolV1_1))
|
||||
return VaultVersion::CashBasis;
|
||||
return VaultVersion::Legacy;
|
||||
if (vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded))
|
||||
return VaultKind::ClosedEnded;
|
||||
return VaultKind::OpenEnded;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
[[nodiscard]] VaultKind
|
||||
getVaultKind(SLE::ConstRef vault)
|
||||
{
|
||||
|
||||
@@ -435,25 +435,18 @@ areMirrorsCurrent(SLE const& issuance, SLE const& mptoken)
|
||||
}
|
||||
|
||||
void
|
||||
setIssuerMirrorCurrent(SLE const& issuance, SLE& mptoken)
|
||||
setIssuerMirrorEpoch(SLE const& issuance, SLE& mptoken)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
issuance.getType() == ltMPTOKEN_ISSUANCE,
|
||||
"xrpl::setIssuerMirrorCurrent : issuance MPTokenIssuance object");
|
||||
"xrpl::setIssuerMirrorEpoch : issuance MPTokenIssuance object");
|
||||
XRPL_ASSERT(
|
||||
mptoken.getType() == ltMPTOKEN, "xrpl::setIssuerMirrorCurrent : mptoken MPToken object");
|
||||
mptoken.getType() == ltMPTOKEN, "xrpl::setIssuerMirrorEpoch : mptoken MPToken object");
|
||||
|
||||
// Unlike the auditor mirror, the issuer mirror is not optional: every
|
||||
// confidential MPToken carries one, so there is no existence check here.
|
||||
//
|
||||
// Record the key and epoch under which the mirror is now encrypted. Both fields remain absent
|
||||
// at epoch 0: such mirrors use the original issuer key. The original key is stored in
|
||||
// sfInitialIssuerEncryptionKey after the first rotation.
|
||||
if (auto const epoch = issuance[~sfIssuerKeyEpoch].value_or(0); epoch != 0)
|
||||
{
|
||||
mptoken[sfIssuerKeyMirrorEpoch] = epoch;
|
||||
mptoken[sfIssuerMirrorEncryptionKey] = issuance[sfIssuerEncryptionKey];
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -473,9 +466,9 @@ setAuditorMirrorEpoch(SLE const& issuance, SLE& mptoken)
|
||||
}
|
||||
|
||||
void
|
||||
setMirrorsCurrent(SLE const& issuance, SLE& mptoken)
|
||||
setMirrorEpochs(SLE const& issuance, SLE& mptoken)
|
||||
{
|
||||
setIssuerMirrorCurrent(issuance, mptoken);
|
||||
setIssuerMirrorEpoch(issuance, mptoken);
|
||||
setAuditorMirrorEpoch(issuance, mptoken);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
#include <xrpl/protocol/MPTAmount.h>
|
||||
|
||||
#include <xrpl/basics/MathUtilities.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
MPTAmount&
|
||||
MPTAmount::operator+=(MPTAmount const& other)
|
||||
{
|
||||
if (auto const result = checkedAdd(value_, other.value()))
|
||||
{
|
||||
value_ = *result;
|
||||
return *this;
|
||||
}
|
||||
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
|
||||
Throw<std::overflow_error>("MPTAmount::operator+= overflow");
|
||||
value_ += other.value();
|
||||
return *this;
|
||||
}
|
||||
@@ -26,13 +12,6 @@ MPTAmount::operator+=(MPTAmount const& other)
|
||||
MPTAmount&
|
||||
MPTAmount::operator-=(MPTAmount const& other)
|
||||
{
|
||||
if (auto const result = checkedSub(value_, other.value()))
|
||||
{
|
||||
value_ = *result;
|
||||
return *this;
|
||||
}
|
||||
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
|
||||
Throw<std::overflow_error>("MPTAmount::operator-= overflow");
|
||||
value_ -= other.value();
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -3,14 +3,10 @@
|
||||
#include <xrpl/beast/utility/Zero.h> // IWYU pragma: keep
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits> // IWYU pragma: keep
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -85,46 +81,13 @@ Quality::ceilInStrict(Amounts const& amount, STAmount const& limit, bool roundUp
|
||||
return ceilInImpl<divRoundStrict>(amount, limit, roundUp, *this);
|
||||
}
|
||||
|
||||
// Returns std::nullopt if a native result can't be represented, instead of
|
||||
// throwing. Pathfinding runs without transaction
|
||||
// rules, so default to the fixed behavior there to match transactions once the
|
||||
// amendment is enabled.
|
||||
template <STAmount (*MulRoundFunc)(STAmount const&, STAmount const&, Asset const&, bool)>
|
||||
static std::optional<STAmount>
|
||||
tryMulRound(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp)
|
||||
{
|
||||
try
|
||||
{
|
||||
return MulRoundFunc(v1, v2, asset, roundUp);
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
// std::overflow_error is a std::runtime_error, so it must be caught
|
||||
// first to keep the handler below from swallowing it. It means the
|
||||
// computation itself overflowed, not that the result is out of range.
|
||||
// Callers handle it, e.g. BookStep removes the offer.
|
||||
throw;
|
||||
}
|
||||
catch (std::runtime_error const&)
|
||||
{
|
||||
if (!asset.native() || !isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
|
||||
throw;
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
template <STAmount (*MulRoundFunc)(STAmount const&, STAmount const&, Asset const&, bool)>
|
||||
static Amounts
|
||||
ceilOutImpl(Amounts const& amount, STAmount const& limit, bool roundUp, Quality const& quality)
|
||||
{
|
||||
if (amount.out > limit)
|
||||
{
|
||||
// An unrepresentable XRP product is larger than amount.in, so the
|
||||
// clamp below would return amount.in anyway.
|
||||
Amounts result(
|
||||
tryMulRound<MulRoundFunc>(limit, quality.rate(), amount.in.asset(), roundUp)
|
||||
.value_or(amount.in),
|
||||
limit);
|
||||
Amounts result(MulRoundFunc(limit, quality.rate(), amount.in.asset(), roundUp), limit);
|
||||
// Clamp in
|
||||
if (result.in > amount.in)
|
||||
result.in = amount.in;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/MathUtilities.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/basics/safe_cast.h>
|
||||
@@ -385,11 +384,10 @@ operator+(STAmount const& v1, STAmount const& v2)
|
||||
return {v1.getFName(), v1.asset(), v2.mantissa(), v2.exponent(), v2.negative()};
|
||||
}
|
||||
|
||||
// XRPAmount and MPTAmount check for int64 overflow under fixCleanup3_5_0.
|
||||
if (v1.native())
|
||||
return {v1.getFName(), (v1.xrp() + v2.xrp()).drops()};
|
||||
return {v1.getFName(), getSNValue(v1) + getSNValue(v2)};
|
||||
if (v1.holds<MPTIssue>())
|
||||
return {v1.asset_, (v1.mpt() + v2.mpt()).value()};
|
||||
return {v1.asset_, v1.mpt().value() + v2.mpt().value()};
|
||||
|
||||
auto x = v1;
|
||||
x = v1.iou() + v2.iou();
|
||||
@@ -445,22 +443,6 @@ getRate(STAmount const& offerOut, STAmount const& offerIn)
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
isRepresentable(Asset const& asset, Number const& value)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Out-of-range values throw std::overflow_error from Number or
|
||||
// IOUAmount, or std::runtime_error from STAmount::canonicalize.
|
||||
[[maybe_unused]] STAmount const amount{asset, value};
|
||||
return true;
|
||||
}
|
||||
catch (std::runtime_error const&)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Safely checks if two STAmount values can be added without overflow,
|
||||
* underflow, or precision loss.
|
||||
@@ -492,7 +474,16 @@ canAdd(STAmount const& a, STAmount const& b)
|
||||
|
||||
// XRP case (overflow & underflow check)
|
||||
if (isXRP(a) && isXRP(b))
|
||||
return checkedAdd(a.xrp().drops(), b.xrp().drops()).has_value();
|
||||
{
|
||||
XRPAmount const aVal = a.xrp();
|
||||
XRPAmount const bVal = b.xrp();
|
||||
|
||||
return !(
|
||||
(bVal > XRPAmount{0} &&
|
||||
aVal > XRPAmount{std::numeric_limits<XRPAmount::value_type>::max()} - bVal) ||
|
||||
(bVal < XRPAmount{0} &&
|
||||
aVal < XRPAmount{std::numeric_limits<XRPAmount::value_type>::min()} - bVal));
|
||||
}
|
||||
|
||||
// IOU case (precision check)
|
||||
auto const ret = std::visit(
|
||||
@@ -509,7 +500,15 @@ canAdd(STAmount const& a, STAmount const& b)
|
||||
|
||||
// MPT (overflow & underflow check)
|
||||
if constexpr (kIsMptissueV<TIss1> && kIsMptissueV<TIss2>)
|
||||
return checkedAdd(a.mpt().value(), b.mpt().value()).has_value();
|
||||
{
|
||||
MPTAmount const aVal = a.mpt();
|
||||
MPTAmount const bVal = b.mpt();
|
||||
return !(
|
||||
(bVal > MPTAmount{0} &&
|
||||
aVal > MPTAmount{std::numeric_limits<MPTAmount::value_type>::max()} - bVal) ||
|
||||
(bVal < MPTAmount{0} &&
|
||||
aVal < MPTAmount{std::numeric_limits<MPTAmount::value_type>::min()} - bVal));
|
||||
}
|
||||
return std::nullopt;
|
||||
},
|
||||
a.asset().value(),
|
||||
@@ -560,7 +559,11 @@ canSubtract(STAmount const& a, STAmount const& b)
|
||||
return false;
|
||||
|
||||
// Check for overflow
|
||||
return checkedSub(aVal.drops(), bVal.drops()).has_value();
|
||||
if (bVal < XRPAmount{0} &&
|
||||
aVal > XRPAmount{std::numeric_limits<XRPAmount::value_type>::max()} + bVal)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// IOU case (no underflow)
|
||||
@@ -583,7 +586,10 @@ canSubtract(STAmount const& a, STAmount const& b)
|
||||
return false;
|
||||
|
||||
// Overflow check
|
||||
return checkedSub(aVal.value(), bVal.value()).has_value();
|
||||
if (bVal < MPTAmount{0} &&
|
||||
aVal > MPTAmount{std::numeric_limits<MPTAmount::value_type>::max()} + bVal)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
return std::nullopt;
|
||||
},
|
||||
@@ -1225,19 +1231,6 @@ divide(STAmount const& num, STAmount const& den, Asset const& asset)
|
||||
if (num == beast::kZero)
|
||||
return {asset};
|
||||
|
||||
// The legacy path below assumes 16-digit mantissas. An MPT mantissa is
|
||||
// the raw 63-bit value and is never scaled down, so an MPT numerator can
|
||||
// overflow the quotient and an MPT denominator shortens it, moving the +5
|
||||
// nudge into a significant digit. Use Number arithmetic under
|
||||
// MPTokensV2 whenever an MPT is involved, as mulRound/divRound do. Pin
|
||||
// the rounding mode so the result does not depend on the caller's.
|
||||
if (isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true) &&
|
||||
(asset.holds<MPTIssue>() || num.holds<MPTIssue>() || den.holds<MPTIssue>()))
|
||||
{
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return STAmount{asset, Number{num} / Number{den}};
|
||||
}
|
||||
|
||||
std::uint64_t numVal = num.mantissa();
|
||||
std::uint64_t denVal = den.mantissa();
|
||||
int numOffset = num.exponent();
|
||||
@@ -1267,21 +1260,11 @@ divide(STAmount const& num, STAmount const& den, Asset const& asset)
|
||||
// numerator by 10^17 (the product is in the range of
|
||||
// 10^32 to 10^33) followed by a division, so the result
|
||||
// is in the range of 10^16 to 10^15.
|
||||
std::uint64_t quotient = muldiv(numVal, kTenTO17, denVal);
|
||||
int offset = numOffset - denOffset - 17;
|
||||
|
||||
// An integral (XRP) numerator is not scaled down to 16 digits, so the
|
||||
// quotient can land in [2^63, 2^64). The STAmount constructor reads
|
||||
// that as a negative IOU mantissa, and the +5 can wrap. Drop a digit
|
||||
// first; the result is canonicalized to 16 digits anyway.
|
||||
if (quotient > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max()) - 5 &&
|
||||
isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true))
|
||||
{
|
||||
quotient /= 10;
|
||||
++offset;
|
||||
}
|
||||
|
||||
return STAmount(asset, quotient + 5, offset, num.negative() != den.negative());
|
||||
return STAmount(
|
||||
asset,
|
||||
muldiv(numVal, kTenTO17, denVal) + 5,
|
||||
numOffset - denOffset - 17,
|
||||
num.negative() != den.negative());
|
||||
}
|
||||
|
||||
STAmount
|
||||
@@ -1487,28 +1470,28 @@ roundNumberResult(
|
||||
// and for materializing the final integral amount.
|
||||
NumberRoundModeGuard const finalRound(roundMode(resultNegative, roundUp));
|
||||
auto result = STAmount{asset, number};
|
||||
// An integral result can't underflow: when roundUp is set, roundMode()
|
||||
// above selects Upward, and materializing a Number into an integral
|
||||
// STAmount honors that mode (Number::operator rep()), so any positive
|
||||
// value rounds up to at least 1.
|
||||
[[maybe_unused]] bool const nonzeroPositiveRoundUp =
|
||||
roundUp && !resultNegative && number != beast::kZero;
|
||||
ALWAYS(
|
||||
!nonzeroPositiveRoundUp || !asset.integral() || result != beast::kZero,
|
||||
"xrpl::roundNumberResult : positive rounded-up integral result is representable");
|
||||
!nonzeroPositiveRoundUp || result != beast::kZero,
|
||||
"xrpl::roundNumberResult : positive rounded-up MPT result is representable");
|
||||
|
||||
if (roundUp && !resultNegative && !result)
|
||||
{
|
||||
// Preserve existing mulRound/divRound behavior for a positive result
|
||||
// too small to represent in the target asset. An IOU result with an
|
||||
// MPT operand (e.g. 1 MPT / 1e95) below the smallest IOU is flushed to
|
||||
// zero by IOUAmount normalization regardless of the rounding mode, so
|
||||
// snap it up to the smallest positive IOU. The only zero case for an
|
||||
// integral result is a zero operand, which the mulRound/divRound
|
||||
// callers handle before reaching this function.
|
||||
// Intended to preserve existing mulRound/divRound behavior for a
|
||||
// positive result too small to represent in the target asset.
|
||||
//
|
||||
// Unreachable in practice: when roundUp is set, roundMode() above
|
||||
// selects Upward, and materializing a Number into an STAmount honors
|
||||
// that mode (Number::operator rep()), so any positive value rounds up
|
||||
// to at least the smallest representable unit. Hence, a positive result
|
||||
// is never !result here; the only zero case is a zero operand, which
|
||||
// the mulRound/divRound callers handle before reaching this function.
|
||||
// LCOV_EXCL_START
|
||||
if (asset.integral())
|
||||
return STAmount{asset, 1}; // LCOV_EXCL_LINE
|
||||
return STAmount{asset, 1};
|
||||
return STAmount{asset, STAmount::kMinValue, STAmount::kMinOffset, false};
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -1557,19 +1540,11 @@ mulRoundImpl(STAmount const& v1, STAmount const& v2, Asset const& asset, bool ro
|
||||
|
||||
bool const resultNegative = v1.negative() != v2.negative();
|
||||
|
||||
// The legacy path below assumes 16-digit mantissas. An MPT mantissa is the
|
||||
// raw 63-bit balance, so any MPT operand (not just an MPT result) can
|
||||
// overflow it: a large MPT amount times an IOU-shaped rate into an XRP or
|
||||
// IOU result is the offer-crossing remainder and partial-fill case. Use
|
||||
// Number arithmetic under MPTokensV2 whenever an MPT is involved.
|
||||
//
|
||||
// Outside a transaction (no current rules, e.g. pathfinding) default to
|
||||
// the Number path: an MPT operand can only reach here through MPT
|
||||
// offers or AMMs, which require MPTokensV2, so this keeps RPC quotes on
|
||||
// the same arithmetic as the transaction they describe.
|
||||
if (isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true) &&
|
||||
(asset.holds<MPTIssue>() || v1.holds<MPTIssue>() || v2.holds<MPTIssue>()))
|
||||
if (asset.holds<MPTIssue>() && isFeatureEnabled(featureMPTokensV2, false))
|
||||
{
|
||||
// MPT DEX can combine 63-bit MPT amounts with IOU-shaped transfer
|
||||
// rates. Use Number arithmetic under MPTokensV2 so the rounded
|
||||
// operation is not limited by the legacy uint64_t scaled mantissa.
|
||||
Number result;
|
||||
{
|
||||
NumberRoundModeGuard const operationRound(roundMode(resultNegative, roundUp));
|
||||
@@ -1667,12 +1642,9 @@ divRoundImpl(STAmount const& num, STAmount const& den, Asset const& asset, bool
|
||||
|
||||
bool const resultNegative = (num.negative() != den.negative());
|
||||
|
||||
// See mulRoundImpl: any MPT operand, not just an MPT result, and the
|
||||
// Number path when there are no current rules.
|
||||
// fixCleanup3_5_0: the legacy path below overflows on large MPT amounts.
|
||||
auto const enabled = isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true) ||
|
||||
isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true);
|
||||
if ((asset.holds<MPTIssue>() || num.holds<MPTIssue>() || den.holds<MPTIssue>()) && enabled)
|
||||
if (asset.holds<MPTIssue>() &&
|
||||
(isFeatureEnabled(featureMPTokensV2, true) || isFeatureEnabled(fixCleanup3_5_0, true)))
|
||||
{
|
||||
// Match the multiply path above: Number performs the rounded
|
||||
// operation, then STAmount materializes the final MPT amount using the
|
||||
@@ -1763,24 +1735,6 @@ divRoundStrict(STAmount const& num, STAmount const& den, Asset const& asset, boo
|
||||
return divRoundImpl<NumberRoundModeGuard>(num, den, asset, roundUp);
|
||||
}
|
||||
|
||||
STAmount
|
||||
mulRatio(STAmount const& amt, std::uint32_t num, std::uint32_t den, Number::RoundingMode rounding)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
rounding == Number::RoundingMode::Upward || rounding == Number::RoundingMode::Downward,
|
||||
"xrpl::mulRatio(STAmount) : rounding is Upward or Downward");
|
||||
bool const roundUp = rounding == Number::RoundingMode::Upward;
|
||||
return amt.asset().visit(
|
||||
[&](Issue const& issue) -> STAmount {
|
||||
if (amt.native())
|
||||
return mulRatio(amt.xrp(), num, den, roundUp);
|
||||
return {mulRatio(amt.iou(), num, den, roundUp), issue};
|
||||
},
|
||||
[&](MPTIssue const& issue) -> STAmount {
|
||||
return {mulRatio(amt.mpt(), num, den, roundUp), issue};
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
STAmount::isZeroAtScale(int scale) const
|
||||
{
|
||||
|
||||
@@ -137,8 +137,6 @@ transResults()
|
||||
MAKE_ERROR(tefBAD_PATH_COUNT, "Malformed: Too many paths."),
|
||||
MAKE_ERROR(tefNO_BYTECODE, "There is no WASM code to run, but a WASM-specific field was included."),
|
||||
MAKE_ERROR(tefBYTECODE_NOT_INCLUDED, "WASM code requires a field that was not included."),
|
||||
MAKE_ERROR(tefTEMP_DISABLED, "The transaction requires logic that is currently temporarily disabled."),
|
||||
MAKE_ERROR(tefEXCEEDS_LIMIT, "The transaction exceeds a limit set by fee voting."),
|
||||
|
||||
MAKE_ERROR(telLOCAL_ERROR, "Local failure."),
|
||||
MAKE_ERROR(telBAD_DOMAIN, "Domain too long."),
|
||||
@@ -211,6 +209,7 @@ transResults()
|
||||
MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."),
|
||||
MAKE_ERROR(temBAD_CIPHERTEXT, "Malformed: Invalid ciphertext."),
|
||||
MAKE_ERROR(temINVALID_BYTECODE, "Malformed: Provided byte code is invalid."),
|
||||
MAKE_ERROR(temTEMP_DISABLED, "The transaction requires logic that is currently temporarily disabled."),
|
||||
|
||||
MAKE_ERROR(terRETRY, "Retry transaction."),
|
||||
MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."),
|
||||
@@ -229,7 +228,6 @@ transResults()
|
||||
MAKE_ERROR(terNO_DELEGATE_PERMISSION, "Delegated account lacks permission to perform this transaction."),
|
||||
MAKE_ERROR(terLOCKED, "Fund is locked."),
|
||||
MAKE_ERROR(terNO_PERMISSION, "No permission to perform requested operation."),
|
||||
MAKE_ERROR(terTICKET_RESERVED, "Ticket is reserved."),
|
||||
|
||||
MAKE_ERROR(tesSUCCESS, "The transaction was applied. Only final in a validated ledger."),
|
||||
};
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <xrpl/basics/MathUtilities.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
XRPAmount&
|
||||
XRPAmount::operator+=(value_type const& rhs)
|
||||
{
|
||||
if (auto const result = checkedAdd(drops_, rhs))
|
||||
{
|
||||
drops_ = *result;
|
||||
return *this;
|
||||
}
|
||||
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
|
||||
Throw<std::overflow_error>("XRPAmount::operator+= overflow");
|
||||
drops_ += rhs;
|
||||
return *this;
|
||||
}
|
||||
|
||||
XRPAmount&
|
||||
XRPAmount::operator-=(value_type const& rhs)
|
||||
{
|
||||
if (auto const result = checkedSub(drops_, rhs))
|
||||
{
|
||||
drops_ = *result;
|
||||
return *this;
|
||||
}
|
||||
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
|
||||
Throw<std::overflow_error>("XRPAmount::operator-= overflow");
|
||||
drops_ -= rhs;
|
||||
return *this;
|
||||
}
|
||||
|
||||
XRPAmount&
|
||||
XRPAmount::operator*=(value_type const& rhs)
|
||||
{
|
||||
if (auto const result = checkedMul(drops_, rhs))
|
||||
{
|
||||
drops_ = *result;
|
||||
return *this;
|
||||
}
|
||||
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
|
||||
Throw<std::overflow_error>("XRPAmount::operator*= overflow");
|
||||
drops_ *= rhs;
|
||||
return *this;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -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());
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
|
||||
#include <boost/smart_ptr/intrusive_ptr.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
@@ -78,23 +77,14 @@ SHAMap::SHAMap(SHAMap const& other, bool isMutable)
|
||||
: f_(other.f_)
|
||||
, journal_(other.f_.journal())
|
||||
, cowid_(other.cowid_ + 1)
|
||||
, ledgerSeq_(other.ledgerSeq())
|
||||
, ledgerSeq_(other.ledgerSeq_)
|
||||
, root_(other.root_)
|
||||
, state_(isMutable ? SHAMapState::Modifying : SHAMapState::Immutable)
|
||||
, type_(other.type_)
|
||||
, backed_(other.backed_)
|
||||
{
|
||||
// A snapshot shares the source's root, so Invalid carries over. Read once into a local, since
|
||||
// the source's state can change while this runs.
|
||||
auto const otherState = other.state();
|
||||
auto const ownState = [&] {
|
||||
if (otherState == SHAMapState::Invalid)
|
||||
return SHAMapState::Invalid;
|
||||
return isMutable ? SHAMapState::Modifying : SHAMapState::Immutable;
|
||||
}();
|
||||
state_.store(ownState, std::memory_order_release);
|
||||
|
||||
// If either map may change, they cannot share nodes
|
||||
if ((ownState != SHAMapState::Immutable) || (otherState != SHAMapState::Immutable))
|
||||
if ((state_ != SHAMapState::Immutable) || (other.state_ != SHAMapState::Immutable))
|
||||
{
|
||||
unshare();
|
||||
}
|
||||
@@ -115,7 +105,7 @@ SHAMap::dirtyUp(NodePathStack& stack, UInt256 const& target, SHAMapTreeNodePtr c
|
||||
// child can be an inner node or a leaf
|
||||
|
||||
XRPL_ASSERT(
|
||||
(state() != SHAMapState::Synching) && (state() != SHAMapState::Immutable),
|
||||
(state_ != SHAMapState::Synching) && (state_ != SHAMapState::Immutable),
|
||||
"xrpl::SHAMap::dirtyUp : valid state");
|
||||
XRPL_ASSERT(child && (child->cowid() == cowid_), "xrpl::SHAMap::dirtyUp : valid child input");
|
||||
|
||||
@@ -180,7 +170,7 @@ SHAMapTreeNodePtr
|
||||
SHAMap::fetchNodeFromDB(SHAMapHash const& hash) const
|
||||
{
|
||||
XRPL_ASSERT(backed_, "xrpl::SHAMap::fetchNodeFromDB : is backed");
|
||||
auto const obj = f_.db().fetchNodeObject(hash.asUInt256(), ledgerSeq());
|
||||
auto const obj = f_.db().fetchNodeObject(hash.asUInt256(), ledgerSeq_);
|
||||
return finishFetch(hash, obj);
|
||||
}
|
||||
|
||||
@@ -193,17 +183,10 @@ SHAMap::finishFetch(SHAMapHash const& hash, std::shared_ptr<NodeObject> const& o
|
||||
{
|
||||
if (!object)
|
||||
{
|
||||
// A missing node disproves full_, so withdraw it and report the gap. The exchange
|
||||
// makes exactly one reader report it, and its acquire pairs with the release store in
|
||||
// setFull(). It needs no release: the only other reader of full_ is this same path,
|
||||
// and a reader that loses the exchange does nothing with what the winner did. The load
|
||||
// ahead of it is relaxed, since all it decides is whether to try the exchange: a stale
|
||||
// true costs one exchange that loses, and a stale false means another reader won, or
|
||||
// setFull() is not visible yet and the gap goes unreported.
|
||||
if (full_.load(std::memory_order_relaxed) &&
|
||||
full_.exchange(false, std::memory_order_acquire))
|
||||
if (full_)
|
||||
{
|
||||
f_.missingNodeAcquireBySeq(ledgerSeq(), hash.asUInt256());
|
||||
full_ = false;
|
||||
f_.missingNodeAcquireBySeq(ledgerSeq_, hash.asUInt256());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -236,7 +219,7 @@ SHAMap::checkFilter(SHAMapHash const& hash, SHAMapSyncFilter const* filter) cons
|
||||
auto node = SHAMapTreeNode::makeFromPrefix(makeSlice(*nodeData), hash);
|
||||
if (node)
|
||||
{
|
||||
filter->gotNode(true, hash, ledgerSeq(), std::move(*nodeData), node->getType());
|
||||
filter->gotNode(true, hash, ledgerSeq_, std::move(*nodeData), node->getType());
|
||||
if (backed_)
|
||||
canonicalize(hash, node);
|
||||
}
|
||||
@@ -416,7 +399,7 @@ SHAMap::descendAsync(
|
||||
{
|
||||
f_.db().asyncFetch(
|
||||
hash.asUInt256(),
|
||||
ledgerSeq(),
|
||||
ledgerSeq_,
|
||||
[this, hash, cb{std::move(callback)}](std::shared_ptr<NodeObject> const& object) {
|
||||
auto const node = finishFetch(hash, object);
|
||||
cb(node, hash);
|
||||
@@ -441,7 +424,7 @@ SHAMap::unshareNode(intr_ptr::SharedPtr<Node> node, SHAMapNodeID const& nodeID)
|
||||
if (node->cowid() != cowid_)
|
||||
{
|
||||
// have a CoW
|
||||
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::unshareNode : not immutable");
|
||||
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::unshareNode : not immutable");
|
||||
node = intr_ptr::staticPointerCast<Node>(node->clone(cowid_));
|
||||
if (nodeID.isRoot())
|
||||
root_ = node;
|
||||
@@ -654,7 +637,7 @@ bool
|
||||
SHAMap::delItem(UInt256 const& id)
|
||||
{
|
||||
// delete the item with this ID
|
||||
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::delItem : not immutable");
|
||||
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::delItem : not immutable");
|
||||
|
||||
NodePathStack stack;
|
||||
walkTowardsKey(id, &stack);
|
||||
@@ -736,7 +719,7 @@ SHAMap::delItem(UInt256 const& id)
|
||||
bool
|
||||
SHAMap::addGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem const> item)
|
||||
{
|
||||
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::addGiveItem : not immutable");
|
||||
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::addGiveItem : not immutable");
|
||||
XRPL_ASSERT(type != SHAMapNodeType::TnInner, "xrpl::SHAMap::addGiveItem : valid type input");
|
||||
|
||||
// add the specified item, does not update
|
||||
@@ -827,7 +810,7 @@ SHAMap::updateGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem cons
|
||||
// can't change the tag but can change the hash
|
||||
UInt256 const tag = item->key();
|
||||
|
||||
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::updateGiveItem : not immutable");
|
||||
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::updateGiveItem : not immutable");
|
||||
|
||||
NodePathStack stack;
|
||||
walkTowardsKey(tag, &stack);
|
||||
@@ -918,7 +901,7 @@ SHAMap::writeNode(NodeObjectType t, SHAMapTreeNodePtr node) const
|
||||
|
||||
Serializer s;
|
||||
node->serializeWithPrefix(s);
|
||||
f_.db().store(t, std::move(s.modData()), node->getHash().asUInt256(), ledgerSeq());
|
||||
f_.db().store(t, std::move(s.modData()), node->getHash().asUInt256(), ledgerSeq_);
|
||||
return node;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <xrpl/shamap/detail/TaggedPointer.h>
|
||||
#include <xrpl/shamap/detail/TaggedPointer.ipp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
@@ -78,9 +77,7 @@ SHAMapInnerNode::clone(std::uint32_t cowid) const
|
||||
auto p = intr_ptr::makeShared<SHAMapInnerNode>(cowid, branchCount);
|
||||
p->hash_ = hash_;
|
||||
p->isBranch_ = isBranch_;
|
||||
// Relaxed, as everywhere. p is not reachable by another thread until this returns.
|
||||
p->fullBelowGen_.store(
|
||||
fullBelowGen_.load(std::memory_order_relaxed), std::memory_order_relaxed);
|
||||
p->fullBelowGen_ = fullBelowGen_;
|
||||
SHAMapHash* cloneHashes = nullptr;
|
||||
SHAMapHash* thisHashes = nullptr;
|
||||
SHAMapTreeNodePtr* cloneChildren = nullptr;
|
||||
|
||||
Loaded 100 of 368 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user