Compare commits

...
Author SHA1 Message Date
Denis Angell b4b680a587 ci: Address review feedback on the supported build and image 2026-10-07 18:55:57 +00:00
Denis Angell 2bb89eeb17 Local merge develop into dangell7/supported-cicd 2026-10-07 18:54:12 +00:00
Alex Kremer d343b542f1 refactor: Migrate handlers to rpc-spec (B) (#8484) 2026-10-07 13:46:37 +00:00
Ayaz Salikhov 7acd719bf7 build: Use images with Clang 23 except for clang-tidy (#8530) 2026-10-07 13:08:20 +00:00
Ayaz Salikhov b4564d5301 chore: Update pre-commit hooks and image (#8528) 2026-10-07 10:59:37 +00:00
Shawn Xie 60195e6d37 fix: Fix MPT partial payment overflow (#8302) 2026-10-06 23:16:13 +00:00
Ayaz Salikhov 3d526d456e build: Use LLVM 23 (#8522) 2026-10-06 21:19:57 +00:00
Denis AngellandMayukha Vadari f05c9f7913 refactor: Extract escrow lock helpers and paychan test helpers (#7882)
Co-authored-by: Mayukha Vadari <[email protected]>
2026-10-06 19:58:52 +00:00
Harshit GuptaandMayukha Vadari c2a4bc3aa1 fix: Validate vetoed parameter type in feature RPC (#7583)
Co-authored-by: Mayukha Vadari <[email protected]>
2026-10-06 19:44:56 +00:00
Mayukha Vadari 718185e3b2 refactor: Use CheckEntry everywhere (#8349) 2026-10-06 19:26:35 +00:00
Ayaz Salikhov 1d7783bb86 build: Make conan retry with Conan Center's source backups (#8524) 2026-10-06 19:23:38 +00:00
Mayukha Vadari 9fd2c552f5 refactor: Use AmendmentsEntry everywhere (#8368) 2026-10-06 17:59:44 +00:00
Ayaz Salikhov 70b8fd301b chore: Update docker images; link Conan-built tools with a static runtime (#8520) 2026-10-06 16:47:35 +00:00
Mayukha Vadari e6564f553d refactor: Use NegativeUNLEntry everywhere (#8365) 2026-10-06 14:23:24 +00:00
Ayaz Salikhov ed96e60ce3 build: Make clang-tools custom in Nix, to match what's being built (#8521) 2026-10-06 13:59:12 +00:00
Alex Kremer 2ebd745a1b refactor: Migrate handlers to rpc-spec (A) (#8345) 2026-10-06 13:19:20 +00:00
Mayukha Vadari cfcbe45b60 test: Declare, not define, entry instantiations in SLEBase test (#8357) 2026-10-06 12:12:22 +00:00
yinyiqian1andBart 9cbf78ba99 test: Add more tests for granular permissions (#8459)
Co-authored-by: Bart <[email protected]>
2026-10-05 23:18:58 +00:00
Timur Yalymov b8d8738f81 fix: Enforce that MPT issuance flags are never cleared (#8152) 2026-10-05 23:17:36 +00:00
63c97e719f feat: Allow lending transactions in Batch (#8244)
Co-authored-by: Cursor <[email protected]>
Co-authored-by: Vito Tumas <[email protected]>
2026-10-05 23:17:26 +00:00
3ac26f23c7 feat: Add full support for all objects in ledger_entry (#6319)
Co-authored-by: Timur Yalymov <[email protected]>
Co-authored-by: Cursor <[email protected]>
Co-authored-by: Vito Tumas <[email protected]>
Co-authored-by: Ayaz Salikhov <[email protected]>
2026-10-05 23:17:14 +00:00
BartandBart ff9410bc56 build: Define XRPL_ASAN, XRPL_TSAN, and XRPL_UBSAN compile definitions (#8483)
Co-authored-by: Bart <[email protected]>
2026-10-05 23:15:08 +00:00
Denis Angell 48ca26c2a4 ci: add the all-amendments supported build workflow 2026-09-12 14:20:05 -04:00
117 changed files with 4068 additions and 2021 deletions

No files matched your search

+72
View File
@@ -108,3 +108,75 @@ endfunction()
function(patch_nix_binary target)
endfunction()
function(rpcspec_generate_instantiations)
set(options)
set(oneValueArgs OUT_VAR VALUE_TYPE VIEW_HEADER INCLUDE_DIR)
set(multiValueArgs HANDLERS)
cmake_parse_arguments(
THIS_FUNCTION_PREFIX
"${options}"
"${oneValueArgs}"
"${multiValueArgs}"
${ARGN}
)
endfunction()
function(corrosion_import_crate)
set(options
ALL_FEATURES
NO_DEFAULT_FEATURES
NO_STD
NO_LINKER_OVERRIDE
NO_USES_TERMINAL
LOCKED
FROZEN
)
set(oneValueArgs MANIFEST_PATH PROFILE IMPORTED_CRATES)
set(multiValueArgs
CRATE_TYPES
CRATES
FEATURES
FLAGS
OVERRIDE_CRATE_TYPE
)
cmake_parse_arguments(
THIS_FUNCTION_PREFIX
"${options}"
"${oneValueArgs}"
"${multiValueArgs}"
${ARGN}
)
endfunction()
function(corrosion_set_env_vars target_name env_var)
endfunction()
function(corrosion_add_cxxbridge cxx_target)
set(options)
set(oneValueArgs CRATE)
set(multiValueArgs FILES)
cmake_parse_arguments(
THIS_FUNCTION_PREFIX
"${options}"
"${oneValueArgs}"
"${multiValueArgs}"
${ARGN}
)
endfunction()
function(_unlink_libgcc_s crate)
endfunction()
function(add_xrpl_crate name)
set(options)
set(oneValueArgs CRATE)
set(multiValueArgs FILES)
cmake_parse_arguments(
THIS_FUNCTION_PREFIX
"${options}"
"${oneValueArgs}"
"${multiValueArgs}"
${ARGN}
)
endfunction()
+9 -1
View File
@@ -44,4 +44,12 @@ runs:
--conf:all tools.build:jobs=${BUILD_NPROC} \
--conf:all tools.build:verbosity="${LOG_VERBOSITY}" \
--conf:all tools.compilation:verbosity="${LOG_VERBOSITY}" \
.
--format=json \
. >"${RUNNER_TEMP}/conan-graph.json"
# Tools that run during the build may only load glibc from the Nix store,
# as their package ID survives a GCC runtime update.
- name: Check build-context packages for Nix store dependencies (Linux)
if: ${{ runner.os == 'Linux' }}
shell: bash
run: ./bin/check-build-context-runtime.sh "${RUNNER_TEMP}/conan-graph.json"
@@ -99,6 +99,8 @@ class LinuxConfig:
suffix: str = ""
extra_cmake_args: str = ""
package: PackageConfig | None = None # set to also package this config
# Flip every amendment to Supported::Yes before building (perf/test only).
force_supported: bool = False
def __post_init__(self) -> None:
if isinstance(self.package, dict):
@@ -186,6 +188,7 @@ class MatrixEntry:
image: str = "" # container image; empty for macOS/Windows (runs natively)
compiler: str = "" # compiler name ("gcc" or "clang"); empty for macOS/Windows
toolchain: str = "" # "nix" for the flake's CI environment; see PlatformConfig
force_supported: bool = False # flip amendments to Supported::Yes before build
@dataclasses.dataclass
@@ -253,6 +256,7 @@ def expand_linux_matrix(linux: LinuxFile, minimal: bool) -> list[MatrixEntry]:
architecture=arch_info,
sanitizers=sanitizer,
compiler=compiler,
force_supported=cfg.force_supported,
)
)
+18 -9
View File
@@ -1,5 +1,5 @@
{
"image_tag": "sha-060957e",
"image_tag": "sha-3d526d4",
"configs": {
"ubuntu": [
{
@@ -17,7 +17,6 @@
"minimal": true,
"benchmark": true
},
{
"compiler": ["gcc"],
"build_type": ["Release"],
@@ -30,7 +29,6 @@
"arch": ["arm64"],
"minimal": false
},
{
"compiler": ["gcc", "clang"],
"build_type": ["Debug", "Release"],
@@ -38,7 +36,6 @@
"minimal": false,
"sanitizers": ["address", "undefinedbehavior"]
},
{
"compiler": ["clang"],
"build_type": ["Debug"],
@@ -62,9 +59,22 @@
"minimal": false,
"suffix": "unity",
"extra_cmake_args": "-Dunity=ON"
},
{
"compiler": ["gcc"],
"build_type": ["Release"],
"arch": ["amd64"],
"minimal": false,
"suffix": "supported",
"force_supported": true,
"extra_cmake_args": "-Dvalidator_keys=ON",
"package": {
"type": "deb",
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-e6055dd",
"variant": "supported"
}
}
],
"debian": [
{
"compiler": ["gcc"],
@@ -74,7 +84,7 @@
"extra_cmake_args": "-Dvalidator_keys=ON",
"package": {
"type": "deb",
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-3a2d19f"
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-e6055dd"
}
},
{
@@ -86,12 +96,11 @@
"extra_cmake_args": "-Dvalidator_keys=ON -Dassert=ON",
"package": {
"type": "deb",
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-3a2d19f",
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-e6055dd",
"variant": "assert"
}
}
],
"rhel": [
{
"compiler": ["gcc"],
@@ -101,7 +110,7 @@
"extra_cmake_args": "-Dvalidator_keys=ON",
"package": {
"type": "rpm",
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-3a2d19f"
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-e6055dd"
}
}
]
-1
View File
@@ -13,7 +13,6 @@ on:
- "!nix/devshell.nix"
- "!nix/check-tools/**"
- "bin/default-loader-path.sh"
- "bin/install-sanitizer-libs.sh"
pull_request:
paths:
- ".github/workflows/build-nix-images.yml"
@@ -5,7 +5,6 @@ on:
branches:
- develop
paths:
- ".github/workflows/build-packaging-images.yml"
- "bin/install-packaging-tools.sh"
- "package/images/packaging/**"
pull_request:
@@ -5,7 +5,6 @@ on:
branches:
- develop
paths:
- ".github/workflows/build-pre-commit-image.yml"
- "bin/pre-commit/Dockerfile"
- "rust-toolchain.toml"
pull_request:
+105
View File
@@ -0,0 +1,105 @@
# Package the "all amendments Supported::Yes" build into a runtime Docker image
# and push it to GHCR, as a drop-in for the rippleci/xrpld image xrpl.js uses
# for standalone testing -- except every amendment is built Supported::Yes.
#
# This does NOT build or package anything: the Trigger workflow already builds
# the supported binary and the supported .deb (the force_supported build config
# and the matching supported package config in linux.json). This workflow waits
# for a successful Trigger run on develop, downloads that run's supported .deb
# artifact, installs it into a slim base (docker/supported.Dockerfile, which
# replicates rippleci's layout), and pushes ghcr.io/xrplf/xrpld/supported.
#
# Perf/test artifact only -- never run it on a production validator.
name: Build supported Docker image
on:
workflow_run:
workflows: ["Trigger"]
types: [completed]
branches: [develop]
# Manual runs: point at a specific completed Trigger run via its run id.
workflow_dispatch:
inputs:
trigger_run_id:
description: "Run id of the Trigger workflow whose supported .deb to package."
required: true
type: string
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
env:
IMAGE_NAME: ghcr.io/xrplf/xrpld/supported
# The supported .deb artifact uploaded by reusable-package.yml:
# <artifact_name>-pkg, where artifact_name carries the -supported suffix.
DEB_ARTIFACT: xrpld-ubuntu-gcc-release-amd64-supported-pkg
SOURCE_RUN_ID: ${{ github.event.workflow_run.id || github.event.inputs.trigger_run_id }}
jobs:
image:
# Only for successful Trigger runs (workflow_run), and only on the canonical
# repo where GITHUB_TOKEN can push to ghcr.io/xrplf/*.
if: ${{ github.repository == 'XRPLF/rippled' && (github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success') }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
- name: Download the supported .deb from the Trigger run
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ env.DEB_ARTIFACT }}
path: dl
run-id: ${{ env.SOURCE_RUN_ID }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Assemble build context
run: |
set -euo pipefail
mkdir -p docker-context
deb="$(find dl -name '*.deb' | head -n1)"
[ -n "${deb}" ] || {
echo "::error::no supported .deb found in run ${SOURCE_RUN_ID}"
exit 1
}
mv "${deb}" docker-context/xrpld.deb
echo "Packaging $(basename "${deb}") into ${IMAGE_NAME}"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Login to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=sha-,format=short
type=raw,value=latest,enable=${{ github.event_name == 'workflow_run' }}
- name: Build and push
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: docker-context
file: docker/supported.Dockerfile
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+1 -1
View File
@@ -34,7 +34,7 @@ permissions:
jobs:
audit:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-3d526d4
permissions:
contents: read
# Needed to open an issue on scheduled failures.
+1 -1
View File
@@ -17,4 +17,4 @@ jobs:
uses: XRPLF/actions/.github/workflows/pre-commit.yml@279ec358f4a1be4088be3e024b07916fa97c75b6
with:
runs_on: ubuntu-latest
container: '{ "image": "ghcr.io/xrplf/xrpld/pre-commit:sha-473fe44" }'
container: '{ "image": "ghcr.io/xrplf/xrpld/pre-commit:sha-70b8fd3" }'
+1 -1
View File
@@ -41,7 +41,7 @@ env:
jobs:
build:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-3d526d4
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -74,6 +74,11 @@ on:
required: false
type: string
default: ""
force_supported:
description: "Flip every amendment to Supported::Yes before building. For perf/test builds only; never for release artifacts."
required: false
type: boolean
default: false
secrets:
CODECOV_TOKEN:
@@ -128,6 +133,20 @@ jobs:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Force all amendments to Supported::Yes
if: ${{ inputs.force_supported && runner.os == 'Linux' }}
run: |
set -euo pipefail
MACRO="include/xrpl/protocol/detail/features.macro"
echo "Flipping Supported::No -> Supported::Yes in ${MACRO}:"
grep -n 'Supported::No,' "${MACRO}" || echo " (none found)"
sed -i 's/Supported::No,/Supported::Yes,/g' "${MACRO}"
if grep -q 'Supported::No,' "${MACRO}"; then
echo "::error::Supported::No entries remain after sed"
exit 1
fi
git diff -- "${MACRO}"
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@b3e255d74d785d053e4903da8ac90983cd7d9e82
with:
@@ -52,5 +52,6 @@ jobs:
sanitizers: ${{ matrix.sanitizers }}
compiler: ${{ matrix.compiler || '' }}
toolchain: ${{ matrix.toolchain || '' }}
force_supported: ${{ matrix.force_supported || false }}
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
needs: [determine-files]
if: ${{ needs.determine-files.outputs.cpp_changed_files != '' || needs.determine-files.outputs.need_full_run == 'true' }}
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-060957e"
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-ed96e60"
permissions:
contents: read
issues: write
+3 -3
View File
@@ -28,7 +28,7 @@ permissions:
jobs:
clippy:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-3d526d4
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -41,7 +41,7 @@ jobs:
coverage:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-3d526d4
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -70,7 +70,7 @@ jobs:
doc:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-3d526d4
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+1 -1
View File
@@ -40,7 +40,7 @@ defaults:
jobs:
upload:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-3d526d4
env:
REMOTE_NAME: ${{ inputs.remote_name }}
CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }}
+6 -5
View File
@@ -60,7 +60,7 @@ repos:
types_or: [c++, c]
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: e2b496dc2bd8340c2524cb9a2d2a943cde1bb6df # frozen: v23.1.1
rev: a9a8a861f30ed207ead7d5a3b7e8032283ba5da7 # frozen: v23.1.2
hooks:
- id: clang-format
args: [--style=file]
@@ -82,9 +82,10 @@ repos:
files: ^crates/.*\.rs$
- repo: https://github.com/BlankSpruce/gersemi-pre-commit
rev: f1c4833f8cf23c6d952673abc73525411a5719e8 # frozen: 0.29.1
rev: 28010ddd6016e1a0f7bd232acb6536ef996ae897 # frozen: 0.29.2
hooks:
- id: gersemi
args: [-i, --warnings-as-errors]
- repo: https://github.com/rbubley/mirrors-prettier
rev: ef4a397f916211b4a39ccf9d3d9cbb6562157251 # frozen: v3.9.9
@@ -95,19 +96,19 @@ repos:
# Scoped to package/: the rest of the repo's Python has pre-existing findings,
# so widening these is its own change.
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: a56c0b927e6465d37cae3e97d35d4d18ab2b96cd # frozen: v0.16.9
rev: f12be1ebaa5351c1fc76472de98db2c3446c8253 # frozen: v0.16.10
hooks:
- id: ruff-check
args: [--fix]
files: ^package/.*\.py$
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 4160603246a6b365d4a2af661c6d71b0a0f50478 # frozen: 26.5.1
rev: 96ae3e5802f3fe2d551e703e18f0a367d1a81ac2 # frozen: 26.10.0
hooks:
- id: black
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 7ff8d35ae36a7d2b968f2f90b4c723e292e594ee # frozen: v2.3.1
rev: 2834ec6639549dd6796205c8f011dedcd587288b # frozen: v2.4.0
hooks:
- id: mypy
args: [--strict]
+6
View File
@@ -29,12 +29,17 @@ Version 3.5.0 is not yet released.
### Additions in 3.5.0
- `subscribe`, `unsubscribe`: Added an optional `mpt_issuances` request field, an array of MPT issuance IDs (hex strings). Subscribers receive a message with `type` `mptTransaction` for each validated transaction whose metadata affects a subscribed issuance; the message has the same fields as the `transactions` stream. MPT issuance subscriptions count toward the per-connection subscription limit. An empty array, a non-array value, or an invalid ID returns `invalidParams`. ([#5671](https://github.com/XRPLF/rippled/pull/5671))
- `ledger_entry`: Add full support for checks, NFT offers, payment channels, and signer lists. ([#6319](https://github.com/XRPLF/rippled/pull/6319))
### Bugfixes in 3.5.0
- `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)
### Bugfixes in 3.5.0
- `feature`: The admin-only `vetoed` field now returns `invalidParams` unless its value is a boolean. [#7583](https://github.com/XRPLF/rippled/pull/7583)
## XRP Ledger server version 3.4.0
Version 3.4.0 is not yet released. These changes are available in the 3.4.0 beta releases.
@@ -55,6 +60,7 @@ Version 3.4.0 is not yet released. These changes are available in the 3.4.0 beta
- `account_lines`: The `peer` field now returns an error if the value is not a string. [#7728](https://github.com/XRPLF/rippled/pull/7728)
- `ledger`: `delivered_amount` is now included in the metadata of successful `AccountDelete` transactions when transactions are expanded (`expand`, or admin-only `full`). Previously it was only added for `Payment` and `CheckCash`, which made `ledger` inconsistent with `tx` and `account_tx`. [#5706](https://github.com/XRPLF/rippled/pull/5706)
- `noripple_check`: The `transactions` field is no longer included in error responses; it is still returned (possibly as an empty array) whenever `transactions` is `true` and the request succeeds. A malformed `account` is now rejected before the ledger is looked up, so that error response no longer carries the `ledger_hash`, `ledger_index`, and `validated` fields ([#6303](https://github.com/XRPLF/rippled/pull/6303)).
- `transaction_entry`: An object or an array in `tx_hash` now returns `malformedRequest`, like any other value that is not a hex hash, instead of an `internal` error.
## XRP Ledger server version 3.3.0
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# Fail if a binary of a build-context Conan package loads anything from the Nix
# store other than glibc, or cannot resolve a library at all.
#
# Only binaries linked by the Nix toolchain are checked, i.e. those recording a
# store path as their interpreter or RUNPATH. Prebuilt upstream binaries (such
# as the ones the cmake package ships) use the system loader instead.
#
# Build-context packages provide the tools that run during the build (protoc,
# grpc_cpp_plugin, ...). Their package ID does not change when a Nix toolchain
# update moves the GCC runtime to a new store path, so a cached binary has to
# get by with the pinned glibc alone. See docs/build/nix.md.
#
# Usage: bin/check-build-context-runtime.sh <graph.json>
# <graph.json> is the output of `conan install --format=json`.
set -euo pipefail
if [ "$#" -ne 1 ]; then
echo "usage: $0 <graph.json>" >&2
exit 2
fi
if [ "$(uname -s)" != "Linux" ]; then
echo "$0: Linux only" >&2
exit 2
fi
folders="$(jq -r '.graph.nodes[] | select(.context == "build" and .package_folder) | .package_folder' "$1" | sort -u)"
checked=0
failed=0
while IFS= read -r file; do
case "$(file -b "${file}")" in
ELF*) ;;
*) continue ;;
esac
[[ "$(readelf -ldW "${file}")" == */nix/store/* ]] || continue
checked=$((checked + 1))
# `ldd` lists the interpreter and every library as the loader resolves them.
if deps="$(ldd "${file}" 2>&1)"; then
bad="$(printf '%s\n' "${deps}" |
grep -E 'not found|/nix/store/' |
grep -vE '/nix/store/[^/]+-glibc-[^/]+/' || true)"
else
case "${deps}" in
*"not a dynamic executable"*) continue ;;
esac
bad="${deps}"
fi
if [ -n "${bad}" ]; then
failed=$((failed + 1))
echo "::error file=${file}::loads a library from the Nix store other than glibc"
echo "${file}"
echo "${bad}" | sed 's/^/ /'
fi
done < <(
# shellcheck disable=SC2086 # one folder per line, no spaces in Conan paths
[ -z "${folders}" ] || find ${folders} -type f \( -perm -u+x -o -name '*.so*' \)
)
echo "Build-context packages: checked ${checked} binaries, ${failed} failed."
[ "${failed}" -eq 0 ]
+17 -6
View File
@@ -20,9 +20,11 @@
# development setups, but not in the macOS CI environment. They are checked
# everywhere except when running in CI on macOS.
#
# Tools that Nix also exposes under a version-suffixed name (`clang-tidy-22`,
# `g++-15`, ...) are probed under both names: a suffixed name can break while
# the plain one still works (see mkVersionedToolLinks in nix/packages.nix).
# Tools that Nix also exposes under a version-suffixed name
# (`clang-tidy-<v>`, `g++-<v>`, ...) are probed under both names:
# a suffixed name can break while the plain one still works
# (see mkVersionedToolLinks in nix/packages.nix).
# The suffix is the major version of the plain `clang` / `gcc` on PATH.
#
# Tools scoped to a single dev shell rather than to commonPackages are checked
# only in that shell, keyed off XRPL_DEVSHELL.
@@ -34,9 +36,16 @@
set -uo pipefail
# Version suffixes of the Nix tool links, tracking nix/packages.nix.
gcc_version=15
llvm_version=22
# major_version <compiler>
# Major version of a compiler on PATH, or "unknown" when it isn't there.
major_version() {
local version
version="$("$1" -dumpversion 2>/dev/null)" || version=""
version="${version%%.*}"
printf '%s' "${version:-unknown}"
}
llvm_version="$(major_version clang)"
missing=()
checked=0
@@ -112,6 +121,7 @@ if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then
check ClangBuildAnalyzer
check curl
check file
check jq
check less
check make
# net-tools netstat reports "net-tools X.Y"; macOS ships BSD netstat with no
@@ -180,6 +190,7 @@ fi
if [ "${os}" = "linux" ]; then
echo
echo "GCC toolchain:"
gcc_version="$(major_version gcc)"
check gcc
check "gcc-${gcc_version}"
check g++
+1 -1
View File
@@ -23,7 +23,7 @@ apt-get clean
rm -rf /var/lib/apt/lists/*
EOF
ARG PRE_COMMIT_VERSION=4.6.0
ARG PRE_COMMIT_VERSION=4.6.2
RUN pip install --no-cache --break-system-packages \
pre-commit==${PRE_COMMIT_VERSION}
+20 -7
View File
@@ -26,8 +26,6 @@ import sys
import tempfile
from pathlib import Path
CLANG_TIDY_VERSION = 22
# Extensions run-clang-tidy can analyse: `.cpp` translation units and, thanks to
# the `verify_headers` build option, `.h`/`.hpp` headers (each has its own
# compile_commands.json entry). `.ipp` fragments have no entry and are skipped.
@@ -39,8 +37,21 @@ TIDY_EXTENSIONS = {".cpp", ".h", ".hpp"}
FILEPATH_RE = re.compile(r"^(\s*(?:-\s+)?FilePath:\s*)'((?:[^']|'')*)'\s*$")
def find_tool(name: str) -> str | None:
for candidate in (f"{name}-{CLANG_TIDY_VERSION}", name):
def clang_tidy_major() -> str | None:
"""Major version of the `clang-tidy` on PATH, which run-clang-tidy invokes."""
if not (clang_tidy := shutil.which("clang-tidy")):
return None
output = subprocess.run(
[clang_tidy, "--version"], capture_output=True, text=True
).stdout
m = re.search(r"LLVM version (\d+)", output)
return m.group(1) if m else None
def find_tool(name: str, version: str | None) -> str | None:
"""Prefer `<name>-<version>`, so a host tool of another version can't win."""
candidates = ([f"{name}-{version}"] if version else []) + [name]
for candidate in candidates:
if path := shutil.which(candidate):
return path
return None
@@ -103,8 +114,9 @@ def main():
if not files:
return 0
run_clang_tidy = find_tool("run-clang-tidy")
clang_apply_replacements = find_tool("clang-apply-replacements")
version = clang_tidy_major()
run_clang_tidy = find_tool("run-clang-tidy", version)
clang_apply_replacements = find_tool("clang-apply-replacements", version)
missing = [
name
for name, path in (
@@ -114,9 +126,10 @@ def main():
if not path
]
if missing:
tried = f" (tried the '-{version}' suffix too)" if version else ""
print(
f"clang-tidy check failed: TIDY is enabled but {' and '.join(missing)} "
f"was not found in PATH (tried the '-{CLANG_TIDY_VERSION}' suffix too).",
f"was not found in PATH{tried}.",
file=sys.stderr,
)
return 1
+9
View File
@@ -292,6 +292,14 @@ if(xrpld)
)
target_sources(xrpld PRIVATE ${sources})
rpcspec_generate_instantiations(
OUT_VAR rpcspec_instantiations
VALUE_TYPE "::json::Value"
VIEW_HEADER "xrpld/rpc/detail/JsonObjectView.hpp"
HANDLERS book_changes ledger transaction_entry
)
target_sources(xrpld PRIVATE ${rpcspec_instantiations})
if(tests)
file(
GLOB_RECURSE sources
@@ -322,6 +330,7 @@ if(xrpld)
# antithesis_instrumentation.h, which is not exported as INTERFACE
target_include_directories(
xrpld
SYSTEM
PRIVATE ${CMAKE_SOURCE_DIR}/external/antithesis-sdk
)
endif()
+6 -1
View File
@@ -77,19 +77,24 @@ if(is_clang)
message(STATUS " Ignorelist: ${ignorelist_path}")
endif()
# Define SANITIZERS macro for BuildInfo.cpp
# Define the SANITIZERS macro for BuildInfo.cpp, plus one of XRPL_ASAN,
# XRPL_TSAN and XRPL_UBSAN per active sanitizer, so that code can test for a
# specific one with #ifdef instead of parsing the dot-joined SANITIZERS string.
set(sanitizers_list)
if(SANITIZERS MATCHES "address")
set(enable_asan ON)
list(APPEND sanitizers_list "ASAN")
target_compile_definitions(common INTERFACE XRPL_ASAN)
endif()
if(SANITIZERS MATCHES "thread")
set(enable_tsan ON)
list(APPEND sanitizers_list "TSAN")
target_compile_definitions(common INTERFACE XRPL_TSAN)
endif()
if(SANITIZERS MATCHES "undefinedbehavior")
set(enable_ubsan ON)
list(APPEND sanitizers_list "UBSAN")
target_compile_definitions(common INTERFACE XRPL_UBSAN)
endif()
if(sanitizers_list)
+6 -2
View File
@@ -3,7 +3,7 @@
"requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1782392402.420688",
"xrpl-rpc-spec/0.1.19#870b2d3abcfbbf13b61c2d3c69495060%1790348286.187549",
"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",
@@ -20,7 +20,7 @@
"libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1782392403.066892",
"jemalloc/5.3.1#1fc58d55316041f10fbc1e8a2eae632a%1776700028.228",
"gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1782392402.791979",
"grpc/1.81.1#b87796a4269034856cbc1a2522db16eb%1788275071.530512",
"grpc/1.81.1#aaa93ab6cda2f2baa6a84490582c8adf%1791284951.826256",
"fast_float/8.2.10#f6f28d6bb22112078e7dbda611caf681%1785888854.601666",
"ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1782307148.15562",
"date/3.0.4#862e11e80030356b53c2c38599ceb32b%1782392402.538492",
@@ -34,11 +34,15 @@
"build_requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
"strawberryperl/5.32.1.1#8d114504d172cfea8ea1662d09b6333e%1782395692.540639",
"re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1782392402.431897",
"protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933",
"openssl/3.6.3#f806de8933e3bf6f01016c6a888cee2e%1783945160.863288",
"nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1782395690.33162",
"msys2/cci.latest#d22fe7b2808f5fd34d0a7923ace9c54f%1770657326.649",
"m4/1.4.19#1727f439cf74e83826ec96d0b4904eee%1784541921.659",
"grpc/1.81.1#aaa93ab6cda2f2baa6a84490582c8adf%1791284951.826256",
"cmake/4.3.3#840cf00ea09777e05c2050a50a82c722%1782392418.696091",
"c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1782392402.681654",
"b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1782392402.624226",
"automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56",
"autoconf/2.71#51077f068e61700d65bb05541ea1e4b0%1731054366.86",
+9 -1
View File
@@ -3,5 +3,13 @@
core:non_interactive=True
core.download:parallel={{ os.cpu_count() }}
core.upload:parallel={{ os.cpu_count() }}
tools.files.download:retry=5
# Fall back to Conan Center's source backups when a recipe's upstream URL is down
# (e.g. the GNU FTP mirrors), see
# https://github.com/conan-io/conan-center-index/issues/28147#issuecomment-3183544772
# The backups are only tried once every upstream URL has used up its retries,
# so keep the retries low.
core.sources:download_urls=["origin", "https://c3i.jfrog.io/artifactory/conan-center-backup-sources"]
tools.files.download:retry=1
tools.files.download:retry_wait=10
# Fail fast on unreachable hosts (default connect timeout is 30s), keep the 60s read timeout.
core.net.http:timeout=(5, 60)
+24 -3
View File
@@ -29,9 +29,9 @@ os.version={{ min_macos_version }}
[conf]
{# The Boost recipe builds with b2, which doesn't use Conan's toolchain files. #}
{# Instead it hand-rolls the compiler for user-config.jam, #}
{# and its fallback probes a version-suffixed binary (e.g. `g++-15`) before plain `g++`. #}
{# Inside the Nix shell the wrapper only provides `g++`/`gcc` (no `-15` suffix), #}
{# so on a host that also has a system `g++-15` the probe escapes Nix #}
{# and its fallback probes a version-suffixed binary (e.g. `g++-<major>`) before plain `g++`. #}
{# Inside the Nix shell the wrapper only provides `g++`/`gcc` (no `-<major>` suffix), #}
{# so on a host that also has a system `g++-<major>` the probe escapes Nix #}
{# and picks the system compiler, which is mismatched with the Nix libraries #}
{# and breaks the build (e.g. Boost.Stacktrace link checks fail). #}
{# Pinning the executables here short-circuits that probe so Boost (and the rest of the toolchain) #}
@@ -49,6 +49,17 @@ tools.build:compiler_executables={'c':'{{ cc_exe }}','cpp':'{{ cxx_exe }}'}
user.package:cppstd_version=23
tools.info.package_id:confs+=["user.package:cppstd_version"]
{% if os == "Linux" and context == "build" %}
{# Build-context executables (protoc, grpc_cpp_plugin, build tools) run during the build #}
{# and would otherwise load libstdc++/libgcc from a Nix store path #}
{# that might change with a Nix toolchain update. #}
{# --as-needed drops the ones they link but don't use, #}
{# such as libatomic for grpc_cpp_plugin on arm64. #}
{% set static_runtime_flags = ["-static-libstdc++", "-static-libgcc", "-Wl,--as-needed"] %}
tools.build:exelinkflags+={{ static_runtime_flags }}
tools.info.package_id:confs+=["tools.build:exelinkflags"]
{% endif %}
{% if os == "Macos" %}
[buildenv]
{# os.version adds -mmacosx-version-min to compiler command lines, #}
@@ -58,3 +69,13 @@ tools.info.package_id:confs+=["user.package:cppstd_version"]
{# Scoped to boost/* since it is the only gap. #}
boost/*:MACOSX_DEPLOYMENT_TARGET={{ min_macos_version }}
{% endif %}
{% if os == "Linux" and context == "build" %}
{# b2 links itself with its own script, which ignores exelinkflags #}
{# and only takes CXXFLAGS when use_cxx_env is set. #}
[options]
b2/*:use_cxx_env=True
[buildenv]
b2/*:CXXFLAGS={{ static_runtime_flags | join(" ") }}
{% endif %}
+3
View File
@@ -5,6 +5,9 @@ include(default)
{% if not sanitizers %}
{# Sanitizers not configured; no additional settings needed #}
{% elif context == "build" %}
{# Build-context packages are tools we run, not code we test, #}
{# so don't instrument them #}
{% else %}
{% if compiler == "msvc" %}
+7 -2
View File
@@ -40,7 +40,7 @@ class Xrpl(ConanFile):
"nudb/2.0.9",
"openssl/3.6.3",
"soci/4.0.3",
"xrpl-rpc-spec/0.1.19",
"xrpl-rpc-spec/0.1.21",
"zlib/1.3.2",
]
@@ -49,7 +49,8 @@ class Xrpl(ConanFile):
]
tool_requires = [
"protobuf/6.33.5",
"grpc/<host_version>",
"protobuf/<host_version>",
]
default_options = {
@@ -123,6 +124,10 @@ class Xrpl(ConanFile):
"xxhash/*:shared": False,
}
# default_options only reach the host context;
# give tool_requires (and their dependencies) the same dependency options.
default_build_options = {k: v for k, v in default_options.items() if "/" in k}
def set_version(self):
self.version = self.version or DEV_VERSION
+5 -6
View File
@@ -80,13 +80,12 @@ function(add_xrpl_crate name)
# `cc` picks its runtime flag from `crt-static` alone, so it compiles a
# crate's C++ with `-MT`; Debug needs `-MTd` (to match cmake/XrplCompiler.cmake).
if(is_msvc)
corrosion_set_env_vars(
${ARG_CRATE}
"$<$<CONFIG:Debug>:CXXFLAGS=-MTd>"
)
corrosion_set_env_vars(${ARG_CRATE} "$<$<CONFIG:Debug>:CXXFLAGS=-MTd>")
endif()
corrosion_add_cxxbridge(${name}_cxxbridge CRATE ${ARG_CRATE} FILES
${ARG_FILES}
corrosion_add_cxxbridge(
${name}_cxxbridge
CRATE ${ARG_CRATE}
FILES ${ARG_FILES}
)
# Generated cxxbridge headers don't exist at configure time; CMake 3.28+
# validates INTERFACE_SOURCES on consuming targets. Clear it to skip the
+28
View File
@@ -0,0 +1,28 @@
# Runtime image for the perf/test xrpld build with all amendments Supported::Yes.
# Installs the .deb into ubuntu:jammy (matching rippleci/xrpld): gives
# /usr/bin/xrpld, /etc/xrpld/xrpld.cfg, and the xrpld user.
# NOT for production validators.
ARG BASE_IMAGE=ubuntu:jammy@sha256:b8b6ee6aa931ecd9d0d952abc34dc0e5f7c6a30c6bb71b079fe399fde0329c02
FROM ${BASE_IMAGE}
# Build context must contain the supported package as xrpld.deb.
COPY xrpld.deb /tmp/xrpld.deb
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates jq /tmp/xrpld.deb; \
rm -rf /var/lib/apt/lists/* /tmp/xrpld.deb
RUN set -eux; \
id -u xrpld >/dev/null 2>&1 || \
useradd --system --home-dir /var/lib/xrpld --shell /sbin/nologin --user-group xrpld
# Symlink for consumers that exec /opt/xrpld/bin/xrpld.
RUN set -eux; \
mkdir -p /var/log/xrpld /var/lib/xrpld /opt/xrpld/bin; \
chown -R xrpld:xrpld /var/log/xrpld /var/lib/xrpld; \
ln -sf /usr/bin/xrpld /opt/xrpld/bin/xrpld
EXPOSE 2459/tcp 5005/tcp 6006/tcp
USER xrpld
ENTRYPOINT ["/usr/bin/xrpld"]
+9 -7
View File
@@ -10,14 +10,15 @@ This document explains how to set one up.
support it — see [compiler support for C++23][cpp23-support].
The versions currently tested in CI are:
| Compiler | Version |
| ----------- | ------------------ |
| GCC | 15.2 |
| Clang | 22 |
| Apple Clang | 21 |
| MSVC | Visual Studio 2026 |
| Compiler | Version |
| ----------- | ------------------------------- |
| GCC | `gccVersion` in [packages.nix] |
| Clang | `llvmVersion` in [packages.nix] |
| Apple Clang | 21 |
| MSVC | Visual Studio 2026 |
LLVM tools (`clang-tidy` and `clang-format`) are also pinned to version 22.
LLVM tools (`clang-tidy` and `clang-format`)
come from the same LLVM release as Clang.
### Older compilers
@@ -156,3 +157,4 @@ 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
+16 -4
View File
@@ -194,10 +194,22 @@ bin/check-nix-store-refs.sh ~/.conan2-nix
```
It works on Linux too, but asserts something narrower there: the toolchain always
writes the store into `PT_INTERP` and `RUNPATH`, and CI builds inside an image
whose store is fixed for its lifetime, so that is fine. Only the binaries
[`PatchNixBinary.cmake`](../../cmake/PatchNixBinary.cmake) retargets to the
system loader have to be clean, and those are what CI checks:
writes the store into `PT_INTERP` and `RUNPATH`. That is fine for the pinned
glibc, whose path does not move, but not for the GCC runtime, which moves with
every GCC update. So [`conan/profiles/default`](../../conan/profiles/default)
links build-context packages, whose executables run during the build, with
`-static-libstdc++ -static-libgcc -Wl,--as-needed`, and
[`conan/profiles/sanitizers`](../../conan/profiles/sanitizers) does not
instrument them. CI checks that they load nothing from the store but glibc, from
the graph `conan install --format=json` writes:
```bash
bin/check-build-context-runtime.sh graph.json
```
Only the binaries [`PatchNixBinary.cmake`](../../cmake/PatchNixBinary.cmake)
retargets to the system loader have to be fully clean, and those are what CI
checks:
```bash
bin/check-nix-store-refs.sh build/xrpld
+1 -1
View File
@@ -6,7 +6,7 @@ project(antithesis-sdk-cpp VERSION 0.4.4 LANGUAGES CXX)
add_library(antithesis-sdk-cpp INTERFACE antithesis_sdk.h)
# Note, both sections below created by xrpld project
target_include_directories(antithesis-sdk-cpp INTERFACE
target_include_directories(antithesis-sdk-cpp SYSTEM INTERFACE
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
)
@@ -17,7 +17,7 @@
namespace xrpl {
template <typename Key>
static std::size_t
std::size_t
extract(Key const& key)
{
return key;
+1 -1
View File
@@ -20,7 +20,7 @@ namespace beast::unit_test {
namespace detail {
template <class String>
static std::string
std::string
makeReason(String const& reason, char const* file, int line)
{
std::string s(reason);
@@ -1,11 +1,17 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/SLEBase.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <map>
#include <set>
namespace xrpl {
@@ -25,6 +31,52 @@ public:
: Base(keylet::amendments(), view, j)
{
}
/**
* Returns the set of amendments this entry reports as enabled.
*
* @return the set of enabled amendments.
*/
[[nodiscard]] std::set<UInt256>
enabledAmendments() const
{
std::set<UInt256> amendments;
if (this->exists() && (*this)->isFieldPresent(sfAmendments))
{
auto const& v = (*this)->getFieldV256(sfAmendments);
amendments.insert_range(v);
}
return amendments;
}
/**
* Returns a map of amendments that have achieved majority, to the time
* majority was reached.
*
* @return a map of amendment to the time majority was reached.
*/
[[nodiscard]] std::map<UInt256, NetClock::time_point>
majorityAmendments() const
{
std::map<UInt256, NetClock::time_point> ret;
if (this->exists() && (*this)->isFieldPresent(sfMajorities))
{
using TimePoint = NetClock::time_point;
using Duration = TimePoint::duration;
auto const majorities = (*this)->getFieldArray(sfMajorities);
for (auto const& m : majorities)
{
ret[m.getFieldH256(sfAmendment)] = TimePoint(Duration(m.getFieldU32(sfCloseTime)));
}
}
return ret;
}
};
using AmendmentsEntryR = AmendmentsEntry<ReadView>;
+291 -3
View File
@@ -2,8 +2,10 @@
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
@@ -27,6 +29,291 @@
namespace xrpl {
/**
* Validate that @p account may lock @p amount of a token for later delivery
* to @p dest.
*
* The lock-side counterpart of escrowUnlockPreclaimHelper: every issuer
* control (locking opt-in, authorization, freeze/lock, transferability,
* spendable balance) that gates locking token value lives here, so any
* transactor that locks funds applies the same rules. The signature is
* view-based rather than PreclaimContext-based so it can also run from
* doApply.
*/
template <ValidIssueType T>
TER
escrowLockPreclaimHelper(
ReadView const& view,
AccountID const& account,
AccountID const& dest,
STAmount const& amount,
beast::Journal j);
template <>
inline TER
escrowLockPreclaimHelper<Issue>(
ReadView const& view,
AccountID const& account,
AccountID const& dest,
STAmount const& amount,
beast::Journal j)
{
auto const& issue = amount.get<Issue>();
auto const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecNO_PERMISSION
if (issuer == account)
return tecNO_PERMISSION;
// If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION
auto const sleIssuer = view.read(keylet::account(issuer));
if (!sleIssuer)
return tecNO_ISSUER;
if (!sleIssuer->isFlag(lsfAllowTrustLineLocking))
return tecNO_PERMISSION;
// If the account does not have a trustline to the issuer, return tecNO_LINE
auto const sleRippleState = view.read(keylet::trustLine(account, issuer, issue.currency));
if (!sleRippleState)
return tecNO_LINE;
STAmount const balance = (*sleRippleState)[sfBalance];
// If balance is positive, issuer must have higher address than account
if (balance > beast::kZero && issuer < account)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If balance is negative, issuer must have lower address than account
if (balance < beast::kZero && issuer > account)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If the issuer has requireAuth set, check if the account is authorized
if (auto const ter = requireAuth(view, issue, account); !isTesSuccess(ter))
return ter;
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(view, issue, dest); !isTesSuccess(ter))
return ter;
// If the issuer has frozen the account, return tecFROZEN
if (isFrozen(view, account, issue))
return tecFROZEN;
// If the issuer has frozen the destination, return tecFROZEN
if (isFrozen(view, dest, issue))
return tecFROZEN;
STAmount const spendableAmount =
accountHolds(view, account, issue.currency, issuer, FreezeHandling::IgnoreFreeze, j);
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::kZero)
return tecINSUFFICIENT_FUNDS;
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
return tecINSUFFICIENT_FUNDS;
// If the amount is not addable to the balance, return tecPRECISION_LOSS
if (!canAdd(spendableAmount, amount))
return tecPRECISION_LOSS;
return tesSUCCESS;
}
template <>
inline TER
escrowLockPreclaimHelper<MPTIssue>(
ReadView const& view,
AccountID const& account,
AccountID const& dest,
STAmount const& amount,
beast::Journal j)
{
AccountID const issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecNO_PERMISSION
if (issuer == account)
return tecNO_PERMISSION;
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION
if (!sleIssuance->isFlag(lsfMPTCanEscrow))
return tecNO_PERMISSION;
// If the issuer is not the same as the issuer of the mpt, return
// tecNO_PERMISSION
if (sleIssuance->getAccountID(sfIssuer) != issuer)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If the account does not have the mpt, return tecOBJECT_NOT_FOUND
if (!view.exists(keylet::mptoken(issuanceKey.key, account)))
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the account is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
!isTesSuccess(ter))
return ter;
// If the issuer has requireAuth set, check if the destination is
// authorized
if (auto const ter = requireAuth(view, mptIssue, dest, AuthType::WeakAuth); !isTesSuccess(ter))
return ter;
// If the issuer has frozen the account, return tecLOCKED
if (isFrozen(view, account, *sleIssuance))
return tecLOCKED;
// If the issuer has frozen the destination, return tecLOCKED
if (isFrozen(view, dest, *sleIssuance))
return tecLOCKED;
// If the mpt cannot be transferred, return tecNO_AUTH
if (auto const ter = canTransfer(view, mptIssue, account, dest); !isTesSuccess(ter))
return ter;
STAmount const spendableAmount = accountHolds(
view,
account,
amount.get<MPTIssue>(),
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
j);
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::kZero)
return tecINSUFFICIENT_FUNDS;
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
return tecINSUFFICIENT_FUNDS;
return tesSUCCESS;
}
template <ValidIssueType T>
TER
escrowLockApplyHelper(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal);
template <>
inline TER
escrowLockApplyHelper<Issue>(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal)
{
// Defensive: Issuer cannot create an escrow
if (issuer == sender)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const ter =
directSendNoFee(view, sender, issuer, amount, !amount.holds<MPTIssue>(), journal);
if (!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
return tesSUCCESS;
}
template <>
inline TER
escrowLockApplyHelper<MPTIssue>(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal)
{
// Defensive: Issuer cannot create an escrow
if (issuer == sender)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const ter = lockEscrowMPT(view, sender, amount, journal);
if (!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
return tesSUCCESS;
}
template <ValidIssueType T>
TER
escrowUnlockPreclaimHelper(
ReadView const& view,
AccountID const& account,
STAmount const& amount,
bool checkFreeze = true);
template <>
inline TER
escrowUnlockPreclaimHelper<Issue>(
ReadView const& view,
AccountID const& account,
STAmount const& amount,
bool checkFreeze)
{
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tesSUCCESS
if (issuer == account)
return tesSUCCESS;
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(view, amount.get<Issue>(), account); !isTesSuccess(ter))
return ter;
// If the issuer has deep frozen the destination, return tecFROZEN
if (checkFreeze &&
isDeepFrozen(view, account, amount.get<Issue>().currency, amount.getIssuer()))
return tecFROZEN;
return tesSUCCESS;
}
template <>
inline TER
escrowUnlockPreclaimHelper<MPTIssue>(
ReadView const& view,
AccountID const& account,
STAmount const& amount,
bool checkFreeze)
{
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tesSUCCESS
if (issuer == account)
return tesSUCCESS;
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the account is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
!isTesSuccess(ter))
return ter;
// If the issuer has frozen the account, return tecLOCKED
if (checkFreeze && isFrozen(view, account, *sleIssuance))
return tecLOCKED;
return tesSUCCESS;
}
//------------------------------------------------------------------------------
template <ValidIssueType T>
TER
escrowUnlockApplyHelper(
@@ -55,9 +342,6 @@ escrowUnlockApplyHelper<Issue>(
bool createAsset,
beast::Journal journal)
{
auto const& issue = amount.get<Issue>();
Keylet const trustLineKey = keylet::trustLine(receiver, issue);
bool const recvLow = issuer > receiver;
bool const senderIssuer = issuer == sender;
bool const receiverIssuer = issuer == receiver;
@@ -67,6 +351,10 @@ escrowUnlockApplyHelper<Issue>(
if (receiverIssuer)
return tesSUCCESS;
auto const& issue = amount.get<Issue>();
Keylet const trustLineKey = keylet::trustLine(receiver, issue);
bool const recvLow = issuer > receiver;
if (!ctx.view.exists(trustLineKey) && createAsset)
{
// Can the account cover the trust line's reserve?
-28
View File
@@ -1,28 +0,0 @@
#pragma once
namespace xrpl {
/**
* @brief Enumeration of ledger shortcuts for specifying which ledger to use.
*
* These shortcuts provide a convenient way to reference commonly used ledgers
* without needing to specify their exact hash or sequence number.
*/
enum class LedgerShortcut {
/**
* The current working ledger (open, not yet closed)
*/
Current,
/**
* The most recently closed ledger (may not be validated)
*/
Closed,
/**
* The most recently validated ledger
*/
Validated
};
} // namespace xrpl
-16
View File
@@ -9,7 +9,6 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <xrpl/protocol/LedgerShortcut.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/TxMeta.h>
#include <xrpl/protocol/TxSearched.h>
@@ -105,21 +104,6 @@ public:
using TxnMetaLedgerType = std::tuple<Blob, Blob, std::uint32_t>;
using MetaTxsList = std::vector<TxnMetaLedgerType>;
using LedgerSequence = uint32_t;
using LedgerHash = UInt256;
using LedgerSpecifier = std::variant<LedgerRange, LedgerShortcut, LedgerSequence, LedgerHash>;
struct AccountTxArgs
{
AccountID account;
std::optional<LedgerSpecifier> ledger;
bool binary = false;
bool forward = false;
uint32_t limit = 0;
std::optional<AccountTxMarker> marker;
std::optional<DelegateFilter> delegate;
};
struct AccountTxResult
{
std::variant<AccountTxs, MetaTxsList> transactions;
@@ -42,6 +42,14 @@ class ValidMPTIssuance
*/
bool referenceHoldingMutated_ = false;
/**
* Flags cleared on an existing MPTokenIssuance, except lsfMPTLocked,
* which tfMPTUnlock clears legitimately. Every other issuance flag is
* fixed at creation or set-once via MPTokenIssuanceSet, so any bit
* accumulated here is a bug. Enforced post-fixCleanup3_5_0.
*/
std::uint32_t issuanceFlagsCleared_ = 0;
/**
* MPTokens and RippleStates deleted during apply. finalize() checks each
* holder's AccountRoot to detect vault pseudo-account holdings deleted
@@ -57,6 +57,12 @@ public:
ReadView const& view,
beast::Journal const& j) override;
/**
* Inner transaction types that preflight rejects before LendingProtocolV1_2.
*
* Once the amendment is enabled the list only matters when replaying older
* ledgers. Remove it when LendingProtocolV1_2 is retired.
*/
static constexpr auto kDisabledTxTypes = std::to_array<TxType>({
ttVAULT_CREATE,
ttVAULT_SET,
+34 -31
View File
@@ -5,8 +5,8 @@ Core build tools:
cmake version 4.4.3
/nix/store/q85csxf4s4shx89zif097h1ql4ax1rrz-cmake-4.4.3/bin/cmake
✅ conan
Conan version 2.32.0
/nix/store/921jqsgbilixmr3xchj95si0jl8pi13z-conan-2.32.0/bin/conan
Conan version 2.33.0
/nix/store/rp0zf22rx3iwnhxp4d53ncirn4lzg68j-conan-2.33.0/bin/conan
✅ git
git version 2.55.0
/nix/store/gw7c7m0dwca5lg4152x9lpmxpj171bzw-git-2.55.0/bin/git
@@ -19,17 +19,17 @@ Development tooling:
ccache version 4.13.6
/nix/store/ydr5nlzxp1djb5256y0zzqa87vz55gsk-ccache-4.13.6/bin/ccache
✅ clang
clang version 22.1.8
/nix/store/dakcxgwz4ixkk6ps50b7vn3dvblyzsnf-clang-wrapper-22.1.8/bin/clang
✅ clang-22
clang version 22.1.8
/nix/store/w826sqb98nnym6kzackg829664iqdlxj-clang-22/bin/clang-22
clang version 23.1.0
/nix/store/2c16z6akvv65znb8330sl5qpklaf3194-clang-wrapper-23.1.0/bin/clang
✅ clang-23
clang version 23.1.0
/nix/store/vfd0q2395vyn0bsjsh1b4068icy1x9ph-clang-23/bin/clang-23
✅ clang++
clang version 22.1.8
/nix/store/dakcxgwz4ixkk6ps50b7vn3dvblyzsnf-clang-wrapper-22.1.8/bin/clang++
✅ clang++-22
clang version 22.1.8
/nix/store/wnwyw3yfqrhpxij65bl3pa1x65q2zg3m-clang++-22/bin/clang++-22
clang version 23.1.0
/nix/store/2c16z6akvv65znb8330sl5qpklaf3194-clang-wrapper-23.1.0/bin/clang++
✅ clang++-23
clang version 23.1.0
/nix/store/0i6kfnif2v4sqyv5fzfg8a9n09rw9lkp-clang++-23/bin/clang++-23
✅ ClangBuildAnalyzer
ClangBuildAnalyzer 1.6.0
/nix/store/c7jjnw29ra78jdlzjww2m374izhy05n8-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer
@@ -39,6 +39,9 @@ Development tooling:
✅ file
file-5.48
/nix/store/y44vpziwbzfm8020dzlqqbybyyy7lb6g-file-5.48/bin/file
✅ jq
jq-1.8.2
/nix/store/wm7v59a2lhj1w2k8jz53spcs1iizm9lv-jq-1.8.2-bin/bin/jq
✅ less
less 710 (PCRE2 regular expressions)
/nix/store/3yn3dx5fwg5i0inqqzwnxq7f6lwhpjqa-less-710/bin/less
@@ -64,23 +67,23 @@ Development tooling:
Zip 3.0
/nix/store/dvawr36npnad4xa6dxi2ss0fw44aa82r-zip-3.0/bin/zip
✅ clang-apply-replacements
clang-apply-replacements version 22.1.8
/nix/store/5bij4zn161lagwyndmrr21vmxjg054nv-clang-tools-22.1.8/bin/clang-apply-replacements
✅ clang-apply-replacements-22
clang-apply-replacements version 22.1.8
/nix/store/w4bz034k4l0w719rrmbwnqcgyjbswy81-clang-apply-replacements-22/bin/clang-apply-replacements-22
clang-apply-replacements version 23.1.0
/nix/store/mva6ngy45m9in3m2lsfccj1y0z6n1f5s-clang-tools-23.1.0/bin/clang-apply-replacements
✅ clang-apply-replacements-23
clang-apply-replacements version 23.1.0
/nix/store/872k02mqr21hxwcpfwaqkxljfj60aak0-clang-apply-replacements-23/bin/clang-apply-replacements-23
✅ clang-format
clang-format version 22.1.8
/nix/store/5bij4zn161lagwyndmrr21vmxjg054nv-clang-tools-22.1.8/bin/clang-format
✅ clang-format-22
clang-format version 22.1.8
/nix/store/w7z346l0r8y36b6lc5i7jzj3w0581l2y-clang-format-22/bin/clang-format-22
clang-format version 23.1.0
/nix/store/mva6ngy45m9in3m2lsfccj1y0z6n1f5s-clang-tools-23.1.0/bin/clang-format
✅ clang-format-23
clang-format version 23.1.0
/nix/store/wf30x6lcya44n2y1fagd6q7x7p4gyl6f-clang-format-23/bin/clang-format-23
✅ clang-tidy
LLVM version 22.1.8
/nix/store/5bij4zn161lagwyndmrr21vmxjg054nv-clang-tools-22.1.8/bin/clang-tidy
✅ clang-tidy-22
LLVM version 22.1.8
/nix/store/0lv25qd3ddgjm3ndn70q61jfr6krlkl7-clang-tidy-22/bin/clang-tidy-22
LLVM version 23.1.0
/nix/store/mva6ngy45m9in3m2lsfccj1y0z6n1f5s-clang-tools-23.1.0/bin/clang-tidy
✅ clang-tidy-23
LLVM version 23.1.0
/nix/store/kgx00aass66g02p1s4gqqf65kq39m4ci-clang-tidy-23/bin/clang-tidy-23
✅ dot
dot - graphviz version 15.1.1 (0)
/nix/store/mc99a6bpbk2waym2cnw6ifmxli4ndb4x-graphviz-15.1.1/bin/dot
@@ -107,10 +110,10 @@ Development tooling:
/nix/store/ivh54xypg9qvd3ha9lis0i8p4nkf8n7a-pre-commit-4.6.2/bin/pre-commit
✅ run-clang-tidy
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/3xss1mm3mh1hzcmg182na6ki1p5rnxf1-run-clang-tidy/bin/run-clang-tidy
✅ run-clang-tidy-22
/nix/store/zjbzr154xan5xg9pmjiv0zkwklyb94wv-run-clang-tidy/bin/run-clang-tidy
✅ run-clang-tidy-23
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/w68r9z07hcq4fwbfyq3yvc7qxm8aqfbl-run-clang-tidy-22/bin/run-clang-tidy-22
/nix/store/svhfrpgx2j581brsqmkpav9b286v22yy-run-clang-tidy-23/bin/run-clang-tidy-23
Rust toolchain:
✅ cargo
@@ -143,4 +146,4 @@ Rust toolchain:
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
✅ All 45 checked tools are present and runnable.
✅ All 46 checked tools are present and runnable.
+99 -96
View File
@@ -2,173 +2,176 @@ Detected OS: linux (Linux x86_64)
Core build tools:
✅ cmake
cmake version 4.1.2
/nix/store/r9941n32g4wyvggz2703dlplbdq8a6rd-cmake-4.1.2/bin/cmake
cmake version 4.4.3
/nix/store/jcvvpih1046akxcwh9hdiak5q24jqcf0-cmake-4.4.3/bin/cmake
✅ conan
Conan version 2.28.1
/nix/store/lxny9y4jvjdws7hgz1mygvb7hjrpmna5-conan-2.28.1/bin/conan
Conan version 2.33.0
/nix/store/b4lnzrqc60glhl9al1nakfc0vd20kdna-conan-2.33.0/bin/conan
✅ git
git version 2.54.0
/nix/store/bcnisk3ydfgv26v2gw3zlky24g00yww2-git-2.54.0/bin/git
git version 2.55.0
/nix/store/lg46w7hrjx7kylsh6645c9l975i8h43b-git-2.55.0/bin/git
✅ python3
Python 3.13.13
/nix/store/60m4rxhg2fldqaak400c0lry96ijrzqn-python3-3.13.13/bin/python3.13
Python 3.14.7
/nix/store/lb41b0anx1f98y9y5s9mdv97gjgsq740-python3-3.14.7/bin/python3.14
Development tooling:
✅ ccache
ccache version 4.13.6
/nix/store/c9wwl7s5i6rsfwvf4v0xbbmzx5m6jgfr-ccache-4.13.6/bin/ccache
/nix/store/p1a1s700dqsw1b3b8d2ba64nqw68zgpy-ccache-4.13.6/bin/ccache
✅ clang
clang version 22.1.7
/nix/store/ff0hrp9r9i3pa5arkdw0sgmzp8d576qi-clang-wrapper-22.1.7/bin/clang
✅ clang-22
clang version 22.1.7
/nix/store/dagc2rq44gfbr7w7yvvqca3yqpc9gqbq-clang-22/bin/clang-22
clang version 23.1.0
/nix/store/mlqh1xrp0zi3i5g9b0zjcz6zlbbzdgb8-clang-wrapper-23.1.0/bin/clang
✅ clang-23
clang version 23.1.0
/nix/store/xgi7q1bady9dk89zvjmiicjzzp1gvm7x-clang-23/bin/clang-23
✅ clang++
clang version 22.1.7
/nix/store/ff0hrp9r9i3pa5arkdw0sgmzp8d576qi-clang-wrapper-22.1.7/bin/clang++
✅ clang++-22
clang version 22.1.7
/nix/store/l5m8clin1npl605wdkd8mr18ggxww3z4-clang++-22/bin/clang++-22
clang version 23.1.0
/nix/store/mlqh1xrp0zi3i5g9b0zjcz6zlbbzdgb8-clang-wrapper-23.1.0/bin/clang++
✅ clang++-23
clang version 23.1.0
/nix/store/4slsaz29v72xgc4g36cp94f0p41a81pv-clang++-23/bin/clang++-23
✅ ClangBuildAnalyzer
ClangBuildAnalyzer 1.6.0
/nix/store/bshlmn8fqw55nsnm581xqlfbahfkykxx-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer
/nix/store/gvx8im1c89vspwb8ixa91lr1dagbi241-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer
✅ curl
curl 8.20.0 (x86_64-pc-linux-gnu) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1
/nix/store/zbwymrp4lcfjc4kkk0n4779v0kjjz58z-curl-8.20.0-bin/bin/curl
curl 8.22.0 (x86_64-pc-linux-gnu) libcurl/8.22.0 OpenSSL/3.5.8 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.70.0 mit-krb5/1.22.2
/nix/store/305w9ipyg497mz9dq31abdf61qk58k29-curl-8.22.0-bin/bin/curl
✅ file
file-5.47
/nix/store/bizyfqdw0h67wzqmp10knmf9s2pqahdb-file-5.47/bin/file
file-5.48
/nix/store/g0fbnccaan8q0rxlw43xnbyb90i94vlc-file-5.48/bin/file
✅ jq
jq-1.8.2
/nix/store/m3y7hqmy20nd1ps6qy8410a326255bf0-jq-1.8.2-bin/bin/jq
✅ less
less 692 (PCRE2 regular expressions)
/nix/store/c6bacbn93qg4a7g9n4czww8rg24dvysr-less-692/bin/less
less 710 (PCRE2 regular expressions)
/nix/store/zi4d0awnc6crz18s177bv9y2yz9al3lq-less-710/bin/less
✅ make
GNU Make 4.4.1
/nix/store/d3bwqm6bymhy3pdgbvf7vxjqfp31m3j1-gnumake-4.4.1/bin/make
/nix/store/z565rgmn9vccv2h0g87z725pw610jmm8-gnumake-4.4.1/bin/make
✅ netstat
net-tools 2.10
/nix/store/jmyzqvgflnswmws7rnxx6g3zbj680xvd-net-tools-2.10/bin/netstat
/nix/store/9i6j9lrsliyz1ww2f8xx18z36wc9n38a-net-tools-2.10/bin/netstat
✅ ninja
1.13.2
/nix/store/7a235m7crqbb4h49sak20fqxpw3n7hr0-ninja-1.13.2/bin/ninja
/nix/store/4nsdla28c4177ssp0hlyv5l2yzj8wcba-ninja-1.13.2/bin/ninja
✅ perl
v5.42.0
/nix/store/6plwsm6pkq79yjv4xvy8csk2pd4hzr67-perl-5.42.0/bin/perl
v5.42.3
/nix/store/8g4dmf7l8q1mf9w4wfmlcl8gvdgmhb8v-perl-5.42.3/bin/perl
✅ pkg-config
0.29.2
/nix/store/1m05k7xgfnw6jc21xxk5681ni3ar97wf-pkg-config-wrapper-0.29.2/bin/pkg-config
/nix/store/b9izmrfjp0qnnyahdwwrzdwi87s1h179-pkg-config-wrapper-0.29.2/bin/pkg-config
✅ vim
VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00)
/nix/store/hvyqx52g4g2fxhgpans3fksjj6lmlyaw-vim-9.2.0389/bin/vim
/nix/store/9swnd5pf8p8w1gwcbhdpm2yx705kbp38-vim-9.2.1001/bin/vim
✅ zip
Zip 3.0
/nix/store/qnd2ag67hrjj0b6vbmisdshf50r6s72n-zip-3.0/bin/zip
/nix/store/5ypvkry3qra47dylc9kfk36r1q9vha80-zip-3.0/bin/zip
✅ clang-apply-replacements
clang-apply-replacements version 22.1.7
/nix/store/4zp1rjpj2xijrv4kqpwsy3ixwb2r6nlk-clang-tools-22.1.7/bin/clang-apply-replacements
✅ clang-apply-replacements-22
clang-apply-replacements version 22.1.7
/nix/store/py2wihg0a96qcppv4hjmww547xabr0fb-clang-apply-replacements-22/bin/clang-apply-replacements-22
clang-apply-replacements version 23.1.0
/nix/store/ipnj8b3s6f00ngi26fi7p5h9lxv6a3s3-clang-tools-23.1.0/bin/clang-apply-replacements
✅ clang-apply-replacements-23
clang-apply-replacements version 23.1.0
/nix/store/lbyc9hrkkymybah7b6n8c7sijlgldfz0-clang-apply-replacements-23/bin/clang-apply-replacements-23
✅ clang-format
clang-format version 22.1.7
/nix/store/4zp1rjpj2xijrv4kqpwsy3ixwb2r6nlk-clang-tools-22.1.7/bin/clang-format
✅ clang-format-22
clang-format version 22.1.7
/nix/store/kz820ccifjlwqnwqjsx7kbiajrgsmbrh-clang-format-22/bin/clang-format-22
clang-format version 23.1.0
/nix/store/ipnj8b3s6f00ngi26fi7p5h9lxv6a3s3-clang-tools-23.1.0/bin/clang-format
✅ clang-format-23
clang-format version 23.1.0
/nix/store/40w351b7i7aqhpl5wmicfnp2ij8k12g9-clang-format-23/bin/clang-format-23
✅ clang-tidy
LLVM version 22.1.7
/nix/store/4zp1rjpj2xijrv4kqpwsy3ixwb2r6nlk-clang-tools-22.1.7/bin/clang-tidy
✅ clang-tidy-22
LLVM version 22.1.7
/nix/store/gdrkvpw846lkyzh8y9p3zx50g6ml2v84-clang-tidy-22/bin/clang-tidy-22
LLVM version 23.1.0
/nix/store/ipnj8b3s6f00ngi26fi7p5h9lxv6a3s3-clang-tools-23.1.0/bin/clang-tidy
✅ clang-tidy-23
LLVM version 23.1.0
/nix/store/40rj38k5ygq7sawkiv0cdaljclbspwz9-clang-tidy-23/bin/clang-tidy-23
✅ dot
dot - graphviz version 12.2.1 (0)
/nix/store/12rgns2296s4qcja778gvcbx61z77rc4-graphviz-12.2.1/bin/dot
dot - graphviz version 15.1.1 (0)
/nix/store/f7r47nc0d13xa4c2xaaxa6dp3pzm9sp1-graphviz-15.1.1/bin/dot
✅ doxygen
1.16.1
/nix/store/k0vzr5lvgq1byraknzwvk51wcgpnsrkh-doxygen-1.16.1/bin/doxygen
1.17.0
/nix/store/q6yx2nsimj9xmckmgw4br2cnapbgnqpk-doxygen-1.17.0/bin/doxygen
✅ gcovr
gcovr 8.4
/nix/store/iyzi7fpyclqrha054adnizvif02lg49x-python3.13-gcovr-8.4/bin/gcovr
/nix/store/shx7mfl8n4kx11icvwyj5x7cmf6z5ahq-python3.14-gcovr-8.4/bin/gcovr
✅ gh
gh version 2.94.0 (nixpkgs)
/nix/store/pidh15szlsb1vc41xdsa3xbdghdazvby-gh-2.94.0/bin/gh
gh version 2.102.0 (2026-09-30)
/nix/store/66h5dr9v94x04myh2kabx06rq9763jfc-gh-2.102.0/bin/gh
✅ git-cliff
git-cliff 2.13.1
/nix/store/1q851fs62shgjhc03fxxdkpzxdjg7k11-git-cliff-2.13.1/bin/git-cliff
git-cliff 2.14.2
/nix/store/sl19b42d69b3w2vb9pxp6jbqk841z8vj-git-cliff-2.14.2/bin/git-cliff
✅ git-lfs
git-lfs/3.7.1 (3.7.1; linux amd64; go 1.26.3)
/nix/store/6ljwpal7b1756708m33vj0crpral7mvl-git-lfs-3.7.1/bin/git-lfs
git-lfs/3.8.0 (3.8.0; linux amd64; go 1.26.8)
/nix/store/rp9ld1f6h83xmf2rb5fpib5wmr46fy9f-git-lfs-3.8.0/bin/git-lfs
✅ gpg
gpg (GnuPG) 2.4.9
/nix/store/wx7vk8babxkgy813r70yc67vcwnmagbx-gnupg-2.4.9/bin/gpg
/nix/store/8gm9aq3sspqmvvh7c0x7djcivj2x7yvn-gnupg-2.4.9/bin/gpg
✅ pre-commit
pre-commit 4.5.1
/nix/store/bj6i9vl34cij5h0r165y40hrjqak0bmz-pre-commit-4.5.1/bin/pre-commit
pre-commit 4.6.2
/nix/store/6blf72f7sdmlrqjggxvk5ij4cxb7fk4c-pre-commit-4.6.2/bin/pre-commit
✅ run-clang-tidy
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/sbg911hs9dbclrzlp04br3iyfpgnaj6r-run-clang-tidy/bin/run-clang-tidy
✅ run-clang-tidy-22
/nix/store/y7isn3dr8bildnp78vk5nhcav63g80dz-run-clang-tidy/bin/run-clang-tidy
✅ run-clang-tidy-23
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/n8yak1ap308gvi7gmrniw0ybsx80fjws-run-clang-tidy-22/bin/run-clang-tidy-22
/nix/store/pm5a8465hs9zb5i116b39i0y3xfpw6n3-run-clang-tidy-23/bin/run-clang-tidy-23
Rust toolchain:
✅ cargo
cargo 1.97.1 (c980f4866 2026-06-30)
/nix/store/88abzp43ywyzql1rhf8jh5aj5n5j7xzr-cargo-1.97.1-x86_64-unknown-linux-gnu/bin/cargo
/nix/store/60b0cz400q34wj2gzav5a41i7y6jzlw3-cargo-1.97.1-x86_64-unknown-linux-gnu/bin/cargo
✅ cargo-audit
cargo-audit-audit 0.22.1
/nix/store/2w9if868piw98xz057sz97jnjvf7hnvf-cargo-audit-0.22.1/bin/cargo-audit
cargo-audit-audit 0.22.2
/nix/store/kksgynfwir3pfii9qkbj3gcc61an0d4s-cargo-audit-0.22.2/bin/cargo-audit
✅ cargo-llvm-cov
cargo-llvm-cov 0.8.5
/nix/store/jjpdf1l6izz6607a346ykra9sndzaw7h-cargo-llvm-cov-0.8.5/bin/cargo-llvm-cov
cargo-llvm-cov 0.9.0
/nix/store/4y8qxx0mlm22hnxlw2cmbcwpxispw02h-cargo-llvm-cov-0.9.0/bin/cargo-llvm-cov
✅ cargo-nextest
cargo-nextest 0.9.137
/nix/store/jhkr7gwyrchkml33gyns9cy0yn7b57qc-cargo-nextest-0.9.137/bin/cargo-nextest
cargo-nextest 0.9.146
/nix/store/q0anzqrazmdr8n1049ca28098q6lv2y8-cargo-nextest-0.9.146/bin/cargo-nextest
✅ clippy-driver
clippy 0.1.97 (8bab26f4f6 2026-07-14)
/nix/store/40d3mzka7r1ps71l0yv2fs6616nbw85m-rust-minimal-1.97.1/bin/clippy-driver
/nix/store/kk0kqdc4f7am21yp30rr8jcrnib5xqzy-rust-minimal-1.97.1/bin/clippy-driver
✅ rust-analyzer
rust-analyzer 1.97.1 (8bab26f 2026-07-14)
/nix/store/lr3m97p3hx1k22a7c44pb0wa7rbayhfi-rust-analyzer-preview-1.97.1-x86_64-unknown-linux-gnu/bin/rust-analyzer
/nix/store/4mglk7qw9jqbv5nwlx9ahdnpcn5sz70r-rust-analyzer-preview-1.97.1-x86_64-unknown-linux-gnu/bin/rust-analyzer
✅ rust-nightly
rustc 1.99.0-nightly (87e5904f5 2026-07-20)
/nix/store/j7kf7a5h4xypzp6x1skg4dsdx2k4fwb3-rust-nightly/bin/rust-nightly
rustc 1.101.0-nightly (282215592 2026-10-04)
/nix/store/qwyzkvba3f9569wjj45b4iyb7nz7qbnr-rust-nightly/bin/rust-nightly
✅ rustc
rustc 1.97.1 (8bab26f4f 2026-07-14)
/nix/store/40d3mzka7r1ps71l0yv2fs6616nbw85m-rust-minimal-1.97.1/bin/rustc
/nix/store/kk0kqdc4f7am21yp30rr8jcrnib5xqzy-rust-minimal-1.97.1/bin/rustc
✅ rustfmt
rustfmt 1.9.0-stable (8bab26f4f6 2026-07-14)
/nix/store/6f1icmb2za20kxn30pgmbv5jq9fnbf4z-rustfmt-preview-1.97.1-x86_64-unknown-linux-gnu/bin/rustfmt
/nix/store/y5gf8a3hix7pnwzdwcfpf3acqhzv3xy6-rustfmt-preview-1.97.1-x86_64-unknown-linux-gnu/bin/rustfmt
GCC toolchain:
✅ gcc
gcc (GCC) 15.2.0
/nix/store/3dd6y3pq00i3r85l45jvz63wjya403nl-gcc-wrapper-15.2.0/bin/gcc
gcc (GCC) 15.3.0
/nix/store/bslmkbaiymxxdc6lwcdb1qyw745nds5j-gcc-wrapper-15.3.0/bin/gcc
✅ gcc-15
gcc (GCC) 15.2.0
/nix/store/d6iri2s6bzqq5ac3fg25j6hgnn1lz44f-gcc-15/bin/gcc-15
gcc (GCC) 15.3.0
/nix/store/h6f2qc8kgwf7r81gwb10csvpjqfc2404-gcc-15/bin/gcc-15
✅ g++
g++ (GCC) 15.2.0
/nix/store/3dd6y3pq00i3r85l45jvz63wjya403nl-gcc-wrapper-15.2.0/bin/g++
g++ (GCC) 15.3.0
/nix/store/bslmkbaiymxxdc6lwcdb1qyw745nds5j-gcc-wrapper-15.3.0/bin/g++
✅ g++-15
g++ (GCC) 15.2.0
/nix/store/gm3msmmxq055lm9gprkfjj9d2gdz1mpg-g++-15/bin/g++-15
g++ (GCC) 15.3.0
/nix/store/jwncv76v61ld9l99v0b1dkmf810if65s-g++-15/bin/g++-15
✅ cpp
cpp (GCC) 15.2.0
/nix/store/3dd6y3pq00i3r85l45jvz63wjya403nl-gcc-wrapper-15.2.0/bin/cpp
cpp (GCC) 15.3.0
/nix/store/bslmkbaiymxxdc6lwcdb1qyw745nds5j-gcc-wrapper-15.3.0/bin/cpp
✅ cpp-15
cpp (GCC) 15.2.0
/nix/store/bn3gmn0m7g4gn2i0yml46fljc7mghiq5-cpp-15/bin/cpp-15
cpp (GCC) 15.3.0
/nix/store/jmi558xj8v7lz2fm6wrradh40dp48zx9-cpp-15/bin/cpp-15
✅ gcov
gcov (GCC) 15.2.0
/nix/store/xvv5sm5i8x0ks6ypfkzl7c4j9srnxz7k-gcc-15.2.0/bin/gcov
gcov (GCC) 15.3.0
/nix/store/n2ab7kf7mm84hna35x463mizcy52pf1h-gcc-15.3.0/bin/gcov
Mold:
✅ mold
mold 2.41.0 (compatible with GNU ld)
/nix/store/2w6fpgxjzzyqmd25wzplm23dfa49a0p2-mold-unwrapped-wrapper-2.41.0/bin/mold
mold 2.42.1 (compatible with GNU ld)
/nix/store/9dcddrfxlfl9n3wfibi5jkmp22bjkvkh-mold-unwrapped-wrapper-2.42.1/bin/mold
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
✅ All 53 checked tools are present and runnable.
✅ All 54 checked tools are present and runnable.
+99 -96
View File
@@ -2,173 +2,176 @@ Detected OS: linux (Linux aarch64)
Core build tools:
✅ cmake
cmake version 4.1.2
/nix/store/nkcpxjifkambzlrwh27a8igvhnbchibg-cmake-4.1.2/bin/cmake
cmake version 4.4.3
/nix/store/39cq2g3d3flq2lmb6hc09grxs4xaax8p-cmake-4.4.3/bin/cmake
✅ conan
Conan version 2.28.1
/nix/store/8i2gyqgc00xvxg9xm6y7n0ilncdv8imw-conan-2.28.1/bin/conan
Conan version 2.33.0
/nix/store/grz6a77mllqzzmg198qagx1r9zhvfijw-conan-2.33.0/bin/conan
✅ git
git version 2.54.0
/nix/store/ixp98f9avf8ikpdrmp40cj33g0dazyp9-git-2.54.0/bin/git
git version 2.55.0
/nix/store/kj6aff4gmz3snpp35rl3gysbl9srd81a-git-2.55.0/bin/git
✅ python3
Python 3.13.13
/nix/store/lqn6mbgzzdrqq2qkwddcmxj9z6amdd86-python3-3.13.13/bin/python3.13
Python 3.14.7
/nix/store/81ag9nxz1flw3xjkv1vvqg8y2ipx2ins-python3-3.14.7/bin/python3.14
Development tooling:
✅ ccache
ccache version 4.13.6
/nix/store/2q39xi2kbi04ibga7635f2sl148d1mzv-ccache-4.13.6/bin/ccache
/nix/store/nzb1lir9sw06fn4635kfx055myqanaz8-ccache-4.13.6/bin/ccache
✅ clang
clang version 22.1.7
/nix/store/xjqffrq9i7la058s9865ig71l9sp1ys5-clang-wrapper-22.1.7/bin/clang
✅ clang-22
clang version 22.1.7
/nix/store/vcf6ilfwn57828hwzyp6zlyr24j9j6yw-clang-22/bin/clang-22
clang version 23.1.0
/nix/store/g8sc13kz6jarzip643hk01832x40v8ms-clang-wrapper-23.1.0/bin/clang
✅ clang-23
clang version 23.1.0
/nix/store/j8qvcg9150s5n09qg8p62q5z3ci02hd9-clang-23/bin/clang-23
✅ clang++
clang version 22.1.7
/nix/store/xjqffrq9i7la058s9865ig71l9sp1ys5-clang-wrapper-22.1.7/bin/clang++
✅ clang++-22
clang version 22.1.7
/nix/store/xby0f6gamr7m27zp5cndsvghbp9lgb3c-clang++-22/bin/clang++-22
clang version 23.1.0
/nix/store/g8sc13kz6jarzip643hk01832x40v8ms-clang-wrapper-23.1.0/bin/clang++
✅ clang++-23
clang version 23.1.0
/nix/store/n62cn5w7fgbslamy3dgm8zhl5yly7zq0-clang++-23/bin/clang++-23
✅ ClangBuildAnalyzer
ClangBuildAnalyzer 1.6.0
/nix/store/h893hd4q1bb6ily2lby5dzyfrrzd2nvj-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer
/nix/store/ml2991pbdhh0qcxqj9331lhpsg449vky-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer
✅ curl
curl 8.20.0 (aarch64-unknown-linux-gnu) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1
/nix/store/i1s0lqwlrmjd2dxzgy2p84cxqqsb0bmk-curl-8.20.0-bin/bin/curl
curl 8.22.0 (aarch64-unknown-linux-gnu) libcurl/8.22.0 OpenSSL/3.5.8 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.70.0 mit-krb5/1.22.2
/nix/store/94c2x2bl3xb7mx43sg1bs1hd7pnw83ac-curl-8.22.0-bin/bin/curl
✅ file
file-5.47
/nix/store/dx973zg9km2w9albsib2vw9wyvacfrlw-file-5.47/bin/file
file-5.48
/nix/store/66sd1x1hls0yijibn00iz08jh4fnas9z-file-5.48/bin/file
✅ jq
jq-1.8.2
/nix/store/kr5py0dcqc9f3l9148qd1axh0z77lld3-jq-1.8.2-bin/bin/jq
✅ less
less 692 (PCRE2 regular expressions)
/nix/store/1blb3s7hhsr77wqi598m6k1qkfp3ms0w-less-692/bin/less
less 710 (PCRE2 regular expressions)
/nix/store/n6z9dv3yjlgvfx1nmrx6qzyv8vzim1l2-less-710/bin/less
✅ make
GNU Make 4.4.1
/nix/store/9ngw1ippk25jjj5fjxv36xbp6iq7rxdx-gnumake-4.4.1/bin/make
/nix/store/q07jxvck60qgrg4p1ywypddh5zz6i9s2-gnumake-4.4.1/bin/make
✅ netstat
net-tools 2.10
/nix/store/7vdsz21f0s499s5yyqzp5s4676q4yxdd-net-tools-2.10/bin/netstat
/nix/store/rfmhd3f8drg3wqyr2mhyrlkd7vyp2pxv-net-tools-2.10/bin/netstat
✅ ninja
1.13.2
/nix/store/8ksx98gsbn5lmlizcmw57yd4sg0k2p58-ninja-1.13.2/bin/ninja
/nix/store/lfkdyggn1ffslfd830l35jrvapr5lzri-ninja-1.13.2/bin/ninja
✅ perl
v5.42.0
/nix/store/5wnly69vv1i3y97al4v3xrqymf9hlzgq-perl-5.42.0/bin/perl
v5.42.3
/nix/store/nyp5q4d2cb0pghi4i7nscb97bqkb6kp3-perl-5.42.3/bin/perl
✅ pkg-config
0.29.2
/nix/store/c7vwy0gl1q0agl2h22gi0m9dg7xxad2l-pkg-config-wrapper-0.29.2/bin/pkg-config
/nix/store/h82n3d0wdrgskdc3yvmz5l3c50h2snvp-pkg-config-wrapper-0.29.2/bin/pkg-config
✅ vim
VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00)
/nix/store/v8c7pvx26irvy9k5sbwd183cyvckzzb3-vim-9.2.0389/bin/vim
/nix/store/h39h0qq4i80sp3yd7sxjn2wf497v5rln-vim-9.2.1001/bin/vim
✅ zip
Zip 3.0
/nix/store/5mh19mvbv9ym2sm9vymyyaac5l2cj2jq-zip-3.0/bin/zip
/nix/store/lfwhq17wfcmjy823fldhlhxj1hjchdzc-zip-3.0/bin/zip
✅ clang-apply-replacements
clang-apply-replacements version 22.1.7
/nix/store/s53p2m776iqaz7acgr5csgpsd18w15h7-clang-tools-22.1.7/bin/clang-apply-replacements
✅ clang-apply-replacements-22
clang-apply-replacements version 22.1.7
/nix/store/bg4kn8z81hk7b9284rjqvr51wpfjqc24-clang-apply-replacements-22/bin/clang-apply-replacements-22
clang-apply-replacements version 23.1.0
/nix/store/jh9xhz6gwxyz3rfx7vy5g4j5d71w940r-clang-tools-23.1.0/bin/clang-apply-replacements
✅ clang-apply-replacements-23
clang-apply-replacements version 23.1.0
/nix/store/1ajzrfr62m3cianfh9i97r30a7vp1xmb-clang-apply-replacements-23/bin/clang-apply-replacements-23
✅ clang-format
clang-format version 22.1.7
/nix/store/s53p2m776iqaz7acgr5csgpsd18w15h7-clang-tools-22.1.7/bin/clang-format
✅ clang-format-22
clang-format version 22.1.7
/nix/store/79v57mzcw8ng8kl7p961ck08ymhp31v7-clang-format-22/bin/clang-format-22
clang-format version 23.1.0
/nix/store/jh9xhz6gwxyz3rfx7vy5g4j5d71w940r-clang-tools-23.1.0/bin/clang-format
✅ clang-format-23
clang-format version 23.1.0
/nix/store/5nfddq7xgbp4zmj8jn7xk1pxyyn5wiwl-clang-format-23/bin/clang-format-23
✅ clang-tidy
LLVM version 22.1.7
/nix/store/s53p2m776iqaz7acgr5csgpsd18w15h7-clang-tools-22.1.7/bin/clang-tidy
✅ clang-tidy-22
LLVM version 22.1.7
/nix/store/wdyd6cb9z1lyi37lbzvwldgcc7yv1n5c-clang-tidy-22/bin/clang-tidy-22
LLVM version 23.1.0
/nix/store/jh9xhz6gwxyz3rfx7vy5g4j5d71w940r-clang-tools-23.1.0/bin/clang-tidy
✅ clang-tidy-23
LLVM version 23.1.0
/nix/store/cawfgsx0vsslc1f007cavmy8md9drb1r-clang-tidy-23/bin/clang-tidy-23
✅ dot
dot - graphviz version 12.2.1 (0)
/nix/store/58rrk4yzwpmyxvl8cqm18h3dhv24zf00-graphviz-12.2.1/bin/dot
dot - graphviz version 15.1.1 (0)
/nix/store/vz4zb8d1rfigzzdn3i0hd4kff28vk02l-graphviz-15.1.1/bin/dot
✅ doxygen
1.16.1
/nix/store/hq32kzwpl89wgr49iq0gmqn9r5n072zq-doxygen-1.16.1/bin/doxygen
1.17.0
/nix/store/6q4z332wc9jnvad71wjj87lgjxidhgzl-doxygen-1.17.0/bin/doxygen
✅ gcovr
gcovr 8.4
/nix/store/sml3xbbfhhlhk6h7jnlg19pdbx9b764b-python3.13-gcovr-8.4/bin/gcovr
/nix/store/v548fjz51k2nmr257vjnd0zp3bxpc2r1-python3.14-gcovr-8.4/bin/gcovr
✅ gh
gh version 2.94.0 (nixpkgs)
/nix/store/7hh2qi0gj2ifbxbl56cjzbiyfc379bji-gh-2.94.0/bin/gh
gh version 2.102.0 (2026-09-30)
/nix/store/vgg6gnq74h76b4r2ac2q9366f2s5qxwx-gh-2.102.0/bin/gh
✅ git-cliff
git-cliff 2.13.1
/nix/store/bidn3pz53yd6qlg711917xx0q10hqmqv-git-cliff-2.13.1/bin/git-cliff
git-cliff 2.14.2
/nix/store/hrlgliq7l3fl2qrdlc197mvscs4n03rd-git-cliff-2.14.2/bin/git-cliff
✅ git-lfs
git-lfs/3.7.1 (3.7.1; linux arm64; go 1.26.3)
/nix/store/4rsklvkbac5bayy0zv12kxyvspi4sshd-git-lfs-3.7.1/bin/git-lfs
git-lfs/3.8.0 (3.8.0; linux arm64; go 1.26.8)
/nix/store/pa2g4xdvr4a73m6mv4cqs0pik1ka1xp5-git-lfs-3.8.0/bin/git-lfs
✅ gpg
gpg (GnuPG) 2.4.9
/nix/store/ka4i8zz5ni3rzqnzcxbfvwr95fk8pn6q-gnupg-2.4.9/bin/gpg
/nix/store/3qc90b7qkxz7na3xnpd2qb8yvcq8zdp6-gnupg-2.4.9/bin/gpg
✅ pre-commit
pre-commit 4.5.1
/nix/store/n981w6hjfar2l81kxbxs2wxl64vwa5kj-pre-commit-4.5.1/bin/pre-commit
pre-commit 4.6.2
/nix/store/5f3av75nc7n7xbf2hlvlagzpp2g8g562-pre-commit-4.6.2/bin/pre-commit
✅ run-clang-tidy
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/4z2fyklg78klallr7x9j02kz92hnxp4m-run-clang-tidy/bin/run-clang-tidy
✅ run-clang-tidy-22
/nix/store/nlg35d9ccslsq5qny7raxyyyrxgk7vsq-run-clang-tidy/bin/run-clang-tidy
✅ run-clang-tidy-23
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/f8m0p9ad40brp9ahy4i0h27kqjkya1j9-run-clang-tidy-22/bin/run-clang-tidy-22
/nix/store/a50nf8hp8p8l8pgcf9fs0j25hij5acw3-run-clang-tidy-23/bin/run-clang-tidy-23
Rust toolchain:
✅ cargo
cargo 1.97.1 (c980f4866 2026-06-30)
/nix/store/6hch2qrr86n2sa2m90lrpxrfxxwbkayl-cargo-1.97.1-aarch64-unknown-linux-gnu/bin/cargo
/nix/store/nci0zxxj2fiq0dlkpqzbnqfh27wz4sq4-cargo-1.97.1-aarch64-unknown-linux-gnu/bin/cargo
✅ cargo-audit
cargo-audit-audit 0.22.1
/nix/store/9rxbrn9aa2r1z96186s69pc7vzizyfch-cargo-audit-0.22.1/bin/cargo-audit
cargo-audit-audit 0.22.2
/nix/store/zldaj03mfz9f21yiby5yymq7v8vqygd7-cargo-audit-0.22.2/bin/cargo-audit
✅ cargo-llvm-cov
cargo-llvm-cov 0.8.5
/nix/store/vwjsi159n89szrx4yh5pc3jlf2gp4fld-cargo-llvm-cov-0.8.5/bin/cargo-llvm-cov
cargo-llvm-cov 0.9.0
/nix/store/iwpcfjah31fgn99ph32ajzz7i22j6al5-cargo-llvm-cov-0.9.0/bin/cargo-llvm-cov
✅ cargo-nextest
cargo-nextest 0.9.137
/nix/store/qb6bcg2fjvm3r9s9j98nmffmf9xwh45s-cargo-nextest-0.9.137/bin/cargo-nextest
cargo-nextest 0.9.146
/nix/store/p5svqr8x1rr7b1i8595pr8qafsr7s6fm-cargo-nextest-0.9.146/bin/cargo-nextest
✅ clippy-driver
clippy 0.1.97 (8bab26f4f6 2026-07-14)
/nix/store/a6p27cg6b8szfixfyvkssx6l0c345zw8-rust-minimal-1.97.1/bin/clippy-driver
/nix/store/ny64zks16ms5d70jk1qsiwsk0fr33ipp-rust-minimal-1.97.1/bin/clippy-driver
✅ rust-analyzer
rust-analyzer 1.97.1 (8bab26f 2026-07-14)
/nix/store/262830dlw2517lnagfx7i7agqgl4fmsd-rust-analyzer-preview-1.97.1-aarch64-unknown-linux-gnu/bin/rust-analyzer
/nix/store/d9s5skhvi1qxj940mylwym2rjicnc2kq-rust-analyzer-preview-1.97.1-aarch64-unknown-linux-gnu/bin/rust-analyzer
✅ rust-nightly
rustc 1.99.0-nightly (87e5904f5 2026-07-20)
/nix/store/c59pxk1yikdlf129qwyg4fplmxcrha0k-rust-nightly/bin/rust-nightly
rustc 1.101.0-nightly (282215592 2026-10-04)
/nix/store/rbsf1gr6f9ikv2izlyh8im117w6hs7h5-rust-nightly/bin/rust-nightly
✅ rustc
rustc 1.97.1 (8bab26f4f 2026-07-14)
/nix/store/a6p27cg6b8szfixfyvkssx6l0c345zw8-rust-minimal-1.97.1/bin/rustc
/nix/store/ny64zks16ms5d70jk1qsiwsk0fr33ipp-rust-minimal-1.97.1/bin/rustc
✅ rustfmt
rustfmt 1.9.0-stable (8bab26f4f6 2026-07-14)
/nix/store/nd8g81wv1smnvdpy4whpcyv2siwjmaan-rustfmt-preview-1.97.1-aarch64-unknown-linux-gnu/bin/rustfmt
/nix/store/dplvgjikmv8909rk0dkqr90ryjg7xn0j-rustfmt-preview-1.97.1-aarch64-unknown-linux-gnu/bin/rustfmt
GCC toolchain:
✅ gcc
gcc (GCC) 15.2.0
/nix/store/rn6svg593xsmn8qcjzk8x9pa1i62c4kb-gcc-wrapper-15.2.0/bin/gcc
gcc (GCC) 15.3.0
/nix/store/g7z555pl99vnwif4jxa05nk1qdc33i1n-gcc-wrapper-15.3.0/bin/gcc
✅ gcc-15
gcc (GCC) 15.2.0
/nix/store/h489d1rmjisfbxh5kmsb0a7c35j8qsdf-gcc-15/bin/gcc-15
gcc (GCC) 15.3.0
/nix/store/vbdsfjk3j1xy5z329sl2d8vp6mq6x09i-gcc-15/bin/gcc-15
✅ g++
g++ (GCC) 15.2.0
/nix/store/rn6svg593xsmn8qcjzk8x9pa1i62c4kb-gcc-wrapper-15.2.0/bin/g++
g++ (GCC) 15.3.0
/nix/store/g7z555pl99vnwif4jxa05nk1qdc33i1n-gcc-wrapper-15.3.0/bin/g++
✅ g++-15
g++ (GCC) 15.2.0
/nix/store/9ywmhz8bmzknrn3pn84g46z8hj3vrmw5-g++-15/bin/g++-15
g++ (GCC) 15.3.0
/nix/store/9ghl998ry4cxlsg6hnq5rn2iipnlkccm-g++-15/bin/g++-15
✅ cpp
cpp (GCC) 15.2.0
/nix/store/rn6svg593xsmn8qcjzk8x9pa1i62c4kb-gcc-wrapper-15.2.0/bin/cpp
cpp (GCC) 15.3.0
/nix/store/g7z555pl99vnwif4jxa05nk1qdc33i1n-gcc-wrapper-15.3.0/bin/cpp
✅ cpp-15
cpp (GCC) 15.2.0
/nix/store/vmjilh1b830qz9yh0a1jj5ads0jxizdk-cpp-15/bin/cpp-15
cpp (GCC) 15.3.0
/nix/store/wj0mv2fv2lhglvpjzhscqpbigkvik6br-cpp-15/bin/cpp-15
✅ gcov
gcov (GCC) 15.2.0
/nix/store/rmwf5hpi1y2m1wpnfvlxmrhksm4djk2j-gcc-15.2.0/bin/gcov
gcov (GCC) 15.3.0
/nix/store/mszfsj00qnv9ygqcqz3w02qzxxskhvlb-gcc-15.3.0/bin/gcov
Mold:
✅ mold
mold 2.41.0 (compatible with GNU ld)
/nix/store/f5qh5a0bx1dslmnf5n5gx0s6aljbswq3-mold-unwrapped-wrapper-2.41.0/bin/mold
mold 2.42.1 (compatible with GNU ld)
/nix/store/gbq334xbibaz4gn184j7gs58m2spgwi7-mold-unwrapped-wrapper-2.42.1/bin/mold
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
✅ All 53 checked tools are present and runnable.
✅ All 54 checked tools are present and runnable.
+1 -1
View File
@@ -7,7 +7,7 @@
...
}:
let
inherit (import ./packages.nix { inherit pkgs; }) commonPackages;
inherit (import ./packages.nix { inherit pkgs customGlibc; }) commonPackages;
# Each forces something absent on the other platform, so both stay lazy.
linux = import ./linux.nix { inherit pkgs customGlibc; };
+14 -12
View File
@@ -31,6 +31,9 @@ let
};
customGccGcov = if pkgs.stdenv.hostPlatform.isLinux then linux.gcov else plainGcov;
# commonPackages whose clang tools parse with the custom toolchain's headers.
customCommonPackages = (import ./packages.nix { inherit pkgs customGlibc; }).commonPackages;
# Whole directory: init.sh locates the profiles relative to itself.
conanDir = ../conan;
@@ -86,11 +89,11 @@ let
version ? null,
versionedTools ? [ ],
extraPackages ? [ ],
warningHook ? "",
# Opt out of PatchNixBinary.cmake retargeting binaries to the system
# loader. The plain toolchain links a newer glibc, so it must not be
# patched; the custom toolchain patches by default.
noPatchNixBinary ? false,
# The stock nixpkgs toolchain: warn that it doesn't match CI, keep the
# clang tools off the custom toolchain, and opt out of PatchNixBinary.cmake
# retargeting binaries to the system loader (the plain toolchain links a
# newer glibc, so it must not be patched).
plain ? false,
}:
let
compilerVersionHook =
@@ -110,7 +113,8 @@ let
in
(pkgs.mkShell.override { inherit stdenv; }) (
{
packages = commonPackages ++ versionedLinks ++ extraPackages;
packages =
(if plain then commonPackages else customCommonPackages) ++ versionedLinks ++ extraPackages;
# Marks a managed dev shell, so the build (XrplSanity.cmake) can tell an
# intentional Nix toolchain from one leaked into a bare shell.
XRPL_DEVSHELL = shellName;
@@ -119,10 +123,10 @@ let
${compilerVersionHook}
${darwinLibresolvHook}
${conanHook}
${warningHook}
${pkgs.lib.optionalString plain plainWarningHook}
'';
}
// pkgs.lib.optionalAttrs noPatchNixBinary { XRPLD_NO_PATCH_NIX_BINARY = "1"; }
// pkgs.lib.optionalAttrs plain { XRPLD_NO_PATCH_NIX_BINARY = "1"; }
);
in
rec {
@@ -180,8 +184,7 @@ rec {
version = gccVersion;
versionedTools = gccVersionedTools;
extraPackages = [ plainGcov ];
warningHook = plainWarningHook;
noPatchNixBinary = true;
plain = true;
};
clang-plain = makeShell {
@@ -190,7 +193,6 @@ rec {
compilerName = "clang";
version = llvmVersion;
versionedTools = clangVersionedTools;
warningHook = plainWarningHook;
noPatchNixBinary = true;
plain = true;
};
}
+1
View File
@@ -146,6 +146,7 @@ in
})
];
clang = customClang;
gccStdenv = customStdenv;
clangStdenv = pkgs.stdenvAdapters.overrideCC pkgs.stdenv customClang;
gcov = customGcov;
+38 -7
View File
@@ -1,14 +1,28 @@
{ pkgs }:
{
pkgs,
# With the custom glibc, the clang tools (clang-tidy, ...) parse code with the
# Linux custom toolchain's headers, i.e. the same glibc and libstdc++ as the
# build. Without it, they use the nixpkgs default compiler's.
customGlibc ? null,
}:
let
# Compiler versions used across the dev shell and the CI environment.
# Docs link here and scripts read the version off the tools;
# only the clang-format rev in .pre-commit-config.yaml
# has to be bumped alongside llvmVersion.
gccVersion = 15;
llvmVersion = 22;
llvmVersion = 23;
gccPackage = pkgs."gcc${toString gccVersion}";
llvmPackages = pkgs."llvmPackages_${toString llvmVersion}";
# Bound explicitly so it tracks llvmPackages above, not the `with pkgs` default.
clangTools = llvmPackages.clang-tools;
# isLinux first: darwin must not evaluate the custom glibc.
clangTools = llvmPackages.clang-tools.override (
pkgs.lib.optionalAttrs (pkgs.stdenv.hostPlatform.isLinux && customGlibc != null) {
inherit (import ./linux.nix { inherit pkgs customGlibc; }) clang;
}
);
# In LLVM 22, run-clang-tidy.py moved from share/clang/ to bin/, so nixpkgs
# clang-tools no longer links it. Wrap it manually.
@@ -16,6 +30,21 @@ let
exec ${pkgs.python3}/bin/python3 ${llvmPackages.clang-unwrapped}/bin/run-clang-tidy "$@"
'';
# Conan 2.33 is the first release whose settings.yml accepts clang 23.
# TODO: drop once nixpkgs ships it.
conan = pkgs.conan.overridePythonAttrs (old: {
version = "2.33.0";
src = pkgs.fetchFromGitHub {
owner = "conan-io";
repo = "conan";
tag = "2.33.0";
hash = "sha256-FDJjesqvPiAUAEjeqQt088Vnet/KXHbUyEPytP9MyG4=";
};
dependencies = old.dependencies ++ [ pkgs.python3Packages.truststore ];
# The upstream test suite takes long and needs network access.
doCheck = false;
});
rust = import ./rust.nix { inherit pkgs; };
# Nix wraps its toolchain so that binaries are exposed only under unsuffixed
@@ -25,10 +54,11 @@ let
# - Conan's Boost recipe looks up `g++-<major>` before plain `g++`.
# - bin/pre-commit/clang_tidy_check.py looks up `run-clang-tidy-<v>` and
# `clang-apply-replacements-<v>` before the unsuffixed names.
# On a host that also has the matching system binary (e.g. Ubuntu's
# `/usr/bin/g++-15` or `clang-tidy-22`) the probe escapes Nix and mixes a
# system tool into the Nix environment. Generate version-suffixed symlinks
# next to a package's tools so those probes resolve to the Nix ones.
# On a host that also has the matching system binary
# (e.g. Ubuntu's `/usr/bin/g++-<v>` or `clang-tidy-<v>`),
# the probe escapes Nix and mixes a system tool into the Nix environment.
# Generate version-suffixed symlinks next to a package's tools
# so those probes resolve to the Nix ones.
#
# Compiler links must point at whichever compiler is active in a given
# environment (the plain stdenv compiler in the dev shell, the custom-glibc
@@ -112,6 +142,7 @@ in
gnumake
gnupg # needed for signing commits & codecov/codecov-action
graphviz
jq
less # needed for git diff
mold
nettools # provides netstat, used to debug failures in CI
+3 -3
View File
@@ -36,7 +36,7 @@ BASE_NAME = "xrpld"
# The flavours that can be built, '' being the plain xrpld package. A variant
# needs a config in linux.json to be built by CI; see package/README.md.
VARIANTS = ("", "assert")
VARIANTS = ("", "assert", "supported")
# Files both packaging systems consume, staged under the same names.
STAGED_FROM_BUILD = ("xrpld", "validator-keys", "validator-keys-LICENSE")
@@ -323,8 +323,8 @@ def main() -> None:
default="",
choices=VARIANTS,
help="the flavour of the package to build: 'assert' produces "
"xrpld-assert, which ships the same paths as xrpld and replaces it "
"(default: the plain xrpld package)",
"xrpld-assert, 'supported' xrpld-supported, each shipping the same "
"paths as xrpld and replacing it (default: the plain xrpld package)",
)
parser.add_argument(
"--channel",
+5 -3
View File
@@ -12,6 +12,7 @@
#include <xrpl/ledger/LedgerTiming.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/FeeSettingsEntry.h>
#include <xrpl/ledger/entries/NegativeUNLEntry.h>
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Fees.h>
@@ -640,7 +641,7 @@ HashSet<PublicKey>
Ledger::negativeUNL() const
{
HashSet<PublicKey> negUnl;
if (auto sle = read(keylet::negativeUNL()); sle && sle->isFieldPresent(sfDisabledValidators))
if (auto const sle = NegativeUNLEntryR(*this); sle && sle->isFieldPresent(sfDisabledValidators))
{
auto const& nUnlData = sle->getFieldArray(sfDisabledValidators);
for (auto const& n : nUnlData)
@@ -664,7 +665,7 @@ Ledger::negativeUNL() const
std::optional<PublicKey>
Ledger::validatorToDisable() const
{
if (auto sle = read(keylet::negativeUNL()); sle && sle->isFieldPresent(sfValidatorToDisable))
if (auto const sle = NegativeUNLEntryR(*this); sle && sle->isFieldPresent(sfValidatorToDisable))
{
auto d = sle->getFieldVL(sfValidatorToDisable);
auto s = makeSlice(d);
@@ -678,7 +679,8 @@ Ledger::validatorToDisable() const
std::optional<PublicKey>
Ledger::validatorToReEnable() const
{
if (auto sle = read(keylet::negativeUNL()); sle && sle->isFieldPresent(sfValidatorToReEnable))
if (auto const sle = NegativeUNLEntryR(*this);
sle && sle->isFieldPresent(sfValidatorToReEnable))
{
auto d = sle->getFieldVL(sfValidatorToReEnable);
auto s = makeSlice(d);
+3 -3
View File
@@ -3,6 +3,7 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/hash/uhash.h>
#include <xrpl/ledger/detail/ReadViewFwdRange.h>
#include <xrpl/ledger/entries/AmendmentsEntry.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/Rules.h>
@@ -72,9 +73,8 @@ makeRulesGivenLedger(
std::optional const digest = ledger.digest(k.key);
if (digest)
{
auto const sle = ledger.read(k);
if (sle)
return Rules(presets, digest, sle->getFieldV256(sfAmendments));
if (AmendmentsEntryR const entry(k, ledger); entry)
return Rules(presets, digest, entry->getFieldV256(sfAmendments));
}
return Rules(presets);
}
+3 -29
View File
@@ -9,6 +9,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/entries/AmendmentsEntry.h>
#include <xrpl/ledger/entries/LedgerHashesEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
@@ -309,40 +310,13 @@ areCompatible(
std::set<UInt256>
getEnabledAmendments(ReadView const& view)
{
std::set<UInt256> amendments;
if (auto const sle = view.read(keylet::amendments()))
{
if (sle->isFieldPresent(sfAmendments))
{
auto const& v = sle->getFieldV256(sfAmendments);
amendments.insert(v.begin(), v.end());
}
}
return amendments;
return AmendmentsEntryR(view).enabledAmendments();
}
MajorityAmendmentsT
getMajorityAmendments(ReadView const& view)
{
MajorityAmendmentsT ret;
if (auto const sle = view.read(keylet::amendments()))
{
if (sle->isFieldPresent(sfMajorities))
{
using Tp = NetClock::time_point;
using D = Tp::duration;
auto const majorities = sle->getFieldArray(sfMajorities);
for (auto const& m : majorities)
ret[m.getFieldH256(sfAmendment)] = Tp(D(m.getFieldU32(sfCloseTime)));
}
}
return ret;
return AmendmentsEntryR(view).majorityAmendments();
}
std::optional<UInt256>
+39 -6
View File
@@ -18,8 +18,10 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
@@ -1337,9 +1339,26 @@ directSendNoLimitMPT(
}
// Sending 3rd party MPTs: transit.
saActual = (waiveFee == WaiveTransferFee::Yes)
? saAmount
: multiply(saAmount, transferRate(view, saAmount.get<MPTIssue>().getMptID()));
if (waiveFee == WaiveTransferFee::Yes)
{
saActual = saAmount;
}
else
{
auto const rate = transferRate(view, saAmount.get<MPTIssue>().getMptID());
if (view.rules().enabled(fixCleanup3_5_0))
{
// Number math loses precision on large MPT amounts, which can
// overcharge the sender. MPTs are integral, so compute the cost
// 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
{
saActual = multiply(saAmount, rate);
}
}
JLOG(j.debug()) << "directSendNoLimitMPT> " << to_string(uSenderID) << " - > "
<< to_string(uReceiverID) << " : deliver=" << saAmount.getFullText()
@@ -1448,9 +1467,23 @@ directSendNoLimitMultiMPT(
}
// Sending 3rd party MPTs: transit.
STAmount const actualSend = (waiveFee == WaiveTransferFee::Yes)
? amount
: multiply(amount, transferRate(view, amount.get<MPTIssue>().getMptID()));
STAmount actualSend = amount;
if (waiveFee != WaiveTransferFee::Yes)
{
auto const rate = transferRate(view, amount.get<MPTIssue>().getMptID());
if (view.rules().enabled(fixCleanup3_5_0))
{
// Number math loses precision on large MPT amounts, which can
// overcharge the sender. MPTs are integral, so compute the
// 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
{
actualSend = multiply(amount, rate);
}
}
actual += actualSend;
takeFromSender += actualSend;
+3 -1
View File
@@ -1642,7 +1642,9 @@ divRoundImpl(STAmount const& num, STAmount const& den, Asset const& asset, bool
bool const resultNegative = (num.negative() != den.negative());
if (asset.holds<MPTIssue>() && isFeatureEnabled(featureMPTokensV2, false))
// fixCleanup3_5_0: the legacy path below overflows on large MPT amounts.
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
+26 -9
View File
@@ -89,17 +89,24 @@ ValidMPTIssuance::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef
if (fix320Enabled && after->isFieldPresent(sfReferenceHolding))
referenceHoldingSetOnCreate_ = true;
}
else if (fix320Enabled)
else
{
// Modified issuance: detect any change to sfReferenceHolding.
bool const beforePresent = before->isFieldPresent(sfReferenceHolding);
bool const afterPresent = after->isFieldPresent(sfReferenceHolding);
if (beforePresent != afterPresent ||
(afterPresent &&
before->getFieldH256(sfReferenceHolding) !=
after->getFieldH256(sfReferenceHolding)))
// lsfMPTLocked is the only issuance flag with a legal clear path
// (tfMPTUnlock); the rest are fixed at creation or set-once.
issuanceFlagsCleared_ |= before->getFlags() & ~after->getFlags() & ~lsfMPTLocked;
if (fix320Enabled)
{
referenceHoldingMutated_ = true;
// Modified issuance: detect any change to sfReferenceHolding.
bool const beforePresent = before->isFieldPresent(sfReferenceHolding);
bool const afterPresent = after->isFieldPresent(sfReferenceHolding);
if (beforePresent != afterPresent ||
(afterPresent &&
before->getFieldH256(sfReferenceHolding) !=
after->getFieldH256(sfReferenceHolding)))
{
referenceHoldingMutated_ = true;
}
}
}
}
@@ -199,6 +206,16 @@ ValidMPTIssuance::finalize(
return false;
}
// Post-fixCleanup3_5_0: no transaction may clear an issuance flag other
// than lsfMPTLocked, so downstream code can trust set-once flags such as
// lsfMPTCanTransfer.
if (rules.enabled(fixCleanup3_5_0) && issuanceFlagsCleared_ != 0)
{
JLOG(j.fatal()) << "Invariant failed: immutable MPTokenIssuance flag cleared: "
<< issuanceFlagsCleared_;
return false;
}
// Deleting an MPToken with a non-zero MPTAmount is rejected.
if (rules.enabled(fixCleanup3_5_0))
{
-23
View File
@@ -13,7 +13,6 @@
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STAmount.h>
@@ -100,28 +99,6 @@ AMMLiquidity<TIn, TOut>::generateFibSeqOffer(TAmounts<TIn, TOut> const& balances
}
namespace {
template <typename T>
constexpr T
maxAmount()
{
if constexpr (std::is_same_v<T, XRPAmount>)
{
return XRPAmount(STAmount::kMaxNative);
}
else if constexpr (std::is_same_v<T, IOUAmount>)
{
return IOUAmount(STAmount::kMaxValue / 2, STAmount::kMaxOffset);
}
else if constexpr (std::is_same_v<T, STAmount>)
{
return STAmount(STAmount::kMaxValue / 2, STAmount::kMaxOffset);
}
else if constexpr (std::is_same_v<T, MPTAmount>)
{
return MPTAmount(kMaxMpTokenAmount);
}
}
template <typename T>
T
maxOut(T const& out, Asset const& asset)
@@ -3,7 +3,9 @@
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/CheckEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
@@ -30,7 +32,7 @@ CheckCancel::preflight(PreflightContext const& ctx)
TER
CheckCancel::preclaim(PreclaimContext const& ctx)
{
auto const sleCheck = ctx.view.read(keylet::check(ctx.tx[sfCheckID]));
CheckEntryR const sleCheck(ctx.tx[sfCheckID], ctx.view);
if (!sleCheck)
{
JLOG(ctx.j.warn()) << "Check does not exist.";
@@ -59,7 +61,7 @@ CheckCancel::preclaim(PreclaimContext const& ctx)
TER
CheckCancel::doApply()
{
auto const sleCheck = view().peek(keylet::check(ctx_.tx[sfCheckID]));
CheckEntryW sleCheck(ctx_.tx[sfCheckID], view(), j_);
if (!sleCheck)
{
// Error should have been caught in preclaim.
@@ -96,10 +98,10 @@ CheckCancel::doApply()
}
// If we succeeded, update the check owner's reserve.
decreaseOwnerCountForObject(view(), srcId, sleCheck, 1, viewJ);
decreaseOwnerCountForObject(view(), srcId, sleCheck.mutableRawSle(), 1, viewJ);
// Remove check from ledger.
view().erase(sleCheck);
sleCheck.erase();
return tesSUCCESS;
}
+18 -12
View File
@@ -7,6 +7,7 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/CheckEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
@@ -87,7 +88,7 @@ CheckCash::preflight(PreflightContext const& ctx)
TER
CheckCash::preclaim(PreclaimContext const& ctx)
{
auto const sleCheck = ctx.view.read(keylet::check(ctx.tx[sfCheckID]));
CheckEntryR const sleCheck(ctx.tx[sfCheckID], ctx.view);
if (!sleCheck)
{
JLOG(ctx.j.warn()) << "Check does not exist.";
@@ -296,8 +297,9 @@ CheckCash::doApply()
// directly on a View.
PaymentSandbox psb(&ctx_.view());
auto sleCheck = psb.peek(keylet::check(ctx_.tx[sfCheckID]));
if (!sleCheck)
std::optional<CheckEntryW> sleCheck;
sleCheck.emplace(ctx_.tx[sfCheckID], psb, j_);
if (!*sleCheck)
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Precheck did not verify check's existence.";
@@ -305,7 +307,7 @@ CheckCash::doApply()
// LCOV_EXCL_STOP
}
AccountID const srcId{sleCheck->getAccountID(sfAccount)};
AccountID const srcId{(*sleCheck)->getAccountID(sfAccount)};
if (!psb.exists(keylet::account(srcId)) || !psb.exists(keylet::account(accountID_)))
{
// LCOV_EXCL_START
@@ -314,7 +316,7 @@ CheckCash::doApply()
// LCOV_EXCL_STOP
}
auto const sponsorCheckSle = getLedgerEntryReserveSponsor(psb, sleCheck);
auto const sponsorCheckSle = getLedgerEntryReserveSponsor(psb, sleCheck->rawSle());
// Preclaim already checked that source has at least the requested
// funds.
@@ -331,7 +333,7 @@ CheckCash::doApply()
if (srcId != accountID_)
{
STAmount const sendMax = sleCheck->at(sfSendMax);
STAmount const sendMax = (*sleCheck)->at(sfSendMax);
// Flow() doesn't do XRP to XRP transfers.
if (sendMax.native())
@@ -569,7 +571,7 @@ CheckCash::doApply()
true, // owner pays transfer fee
OfferCrossing::No,
std::nullopt,
sleCheck->getFieldAmount(sfSendMax),
(*sleCheck)->getFieldAmount(sfSendMax),
std::nullopt, // check does not support domain
viewJ);
@@ -594,7 +596,7 @@ CheckCash::doApply()
// for DeliverMin.
ctx_.deliver(result.actualAmountOut);
sleCheck = psb.peek(keylet::check(ctx_.tx[sfCheckID]));
sleCheck.emplace(ctx_.tx[sfCheckID], psb, j_);
}
}
@@ -602,7 +604,10 @@ CheckCash::doApply()
// check link from destination directory.
if (srcId != accountID_ &&
!psb.dirRemove(
keylet::ownerDir(accountID_), sleCheck->at(sfDestinationNode), sleCheck->key(), true))
keylet::ownerDir(accountID_),
(*sleCheck)->at(sfDestinationNode),
(*sleCheck)->key(),
true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete check from destination.";
@@ -611,7 +616,8 @@ CheckCash::doApply()
}
// Remove check from check owner's directory.
if (!psb.dirRemove(keylet::ownerDir(srcId), sleCheck->at(sfOwnerNode), sleCheck->key(), true))
if (!psb.dirRemove(
keylet::ownerDir(srcId), (*sleCheck)->at(sfOwnerNode), (*sleCheck)->key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete check from owner.";
@@ -620,10 +626,10 @@ CheckCash::doApply()
}
// If we succeeded, update the check owner's reserve.
decreaseOwnerCountForObject(psb, srcId, sleCheck, 1, viewJ);
decreaseOwnerCountForObject(psb, srcId, sleCheck->mutableRawSle(), 1, viewJ);
// Remove check from ledger.
psb.erase(sleCheck);
sleCheck->erase();
psb.apply(ctx_.rawView());
return tesSUCCESS;
@@ -4,6 +4,7 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/entries/CheckEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
@@ -25,7 +26,6 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <memory>
#include <optional>
namespace xrpl {
@@ -201,8 +201,9 @@ CheckCreate::doApply()
// Note that we use the value from the sequence or ticket as the
// Check sequence. For more explanation see comments in SeqProxy.h.
auto const seq = ctx_.tx.getSeqProxy();
Keylet const checkKeylet = keylet::check(accountID_, seq);
auto sleCheck = std::make_shared<SLE>(checkKeylet);
CheckEntryW sleCheck(accountID_, seq, view(), j_);
Keylet const checkKeylet = sleCheck.keylet();
sleCheck.newSLE();
sleCheck->setAccountID(sfAccount, accountID_);
AccountID const dstAccountId = ctx_.tx[sfDestination];
@@ -218,7 +219,7 @@ CheckCreate::doApply()
if (auto const expiry = ctx_.tx[~sfExpiration])
sleCheck->setFieldU32(sfExpiration, *expiry);
view().insert(sleCheck);
sleCheck.insert();
auto viewJ = ctx_.registry.get().getJournal("View");
// If it's not a self-send (and it shouldn't be), add Check to the
@@ -252,7 +253,7 @@ CheckCreate::doApply()
// If we succeeded, the new entry counts against the creator's reserve.
increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, viewJ);
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sleCheck);
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sleCheck.mutableRawSle());
return tesSUCCESS;
}
@@ -6,15 +6,10 @@
#include <xrpl/ledger/entries/EscrowEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/EscrowHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
@@ -35,60 +30,6 @@ EscrowCancel::preflight(PreflightContext const& ctx)
return tesSUCCESS;
}
template <ValidIssueType T>
static TER
escrowCancelPreclaimHelper(
PreclaimContext const& ctx,
AccountID const& account,
STAmount const& amount);
template <>
TER
escrowCancelPreclaimHelper<Issue>(
PreclaimContext const& ctx,
AccountID const& account,
STAmount const& amount)
{
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecINTERNAL
if (issuer == account)
return tecINTERNAL; // LCOV_EXCL_LINE
// If the issuer has requireAuth set, check if the account is authorized
if (auto const ter = requireAuth(ctx.view, amount.get<Issue>(), account); !isTesSuccess(ter))
return ter;
return tesSUCCESS;
}
template <>
TER
escrowCancelPreclaimHelper<MPTIssue>(
PreclaimContext const& ctx,
AccountID const& account,
STAmount const& amount)
{
AccountID const issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecINTERNAL
if (issuer == account)
return tecINTERNAL; // LCOV_EXCL_LINE
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = ctx.view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the account is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth);
!isTesSuccess(ter))
return ter;
return tesSUCCESS;
}
TER
EscrowCancel::preclaim(PreclaimContext const& ctx)
{
@@ -106,7 +47,7 @@ EscrowCancel::preclaim(PreclaimContext const& ctx)
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowCancelPreclaimHelper<T>(ctx, account, amount);
return escrowUnlockPreclaimHelper<T>(ctx.view, account, amount, false);
},
amount.asset().value());
!isTesSuccess(ret))
@@ -10,8 +10,7 @@
#include <xrpl/ledger/entries/EscrowEntry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/EscrowHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
@@ -19,6 +18,7 @@
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/protocol/MPTIssue.h>
@@ -185,162 +185,6 @@ EscrowCreate::preflight(PreflightContext const& ctx)
return tesSUCCESS;
}
template <ValidIssueType T>
static TER
escrowCreatePreclaimHelper(
PreclaimContext const& ctx,
AccountID const& account,
AccountID const& dest,
STAmount const& amount);
template <>
TER
escrowCreatePreclaimHelper<Issue>(
PreclaimContext const& ctx,
AccountID const& account,
AccountID const& dest,
STAmount const& amount)
{
auto const& issue = amount.get<Issue>();
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecNO_PERMISSION
if (issuer == account)
return tecNO_PERMISSION;
// If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION
auto const sleIssuer = ctx.view.read(keylet::account(issuer));
if (!sleIssuer)
return tecNO_ISSUER;
if (!sleIssuer->isFlag(lsfAllowTrustLineLocking))
return tecNO_PERMISSION;
// If the account does not have a trustline to the issuer, return tecNO_LINE
auto const sleRippleState = ctx.view.read(keylet::trustLine(account, issuer, issue.currency));
if (!sleRippleState)
return tecNO_LINE;
STAmount const balance = (*sleRippleState)[sfBalance];
// If balance is positive, issuer must have higher address than account
if (balance > beast::kZero && issuer < account)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If balance is negative, issuer must have lower address than account
if (balance < beast::kZero && issuer > account)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If the issuer has requireAuth set, check if the account is authorized
if (auto const ter = requireAuth(ctx.view, issue, account); !isTesSuccess(ter))
return ter;
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(ctx.view, issue, dest); !isTesSuccess(ter))
return ter;
// If the issuer has frozen the account, return tecFROZEN
if (isFrozen(ctx.view, account, issue))
return tecFROZEN;
// If the issuer has frozen the destination, return tecFROZEN
if (isFrozen(ctx.view, dest, issue))
return tecFROZEN;
STAmount const spendableAmount = accountHolds(
ctx.view, account, issue.currency, issuer, FreezeHandling::IgnoreFreeze, ctx.j);
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::kZero)
return tecINSUFFICIENT_FUNDS;
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
return tecINSUFFICIENT_FUNDS;
// If the amount is not addable to the balance, return tecPRECISION_LOSS
if (!canAdd(spendableAmount, amount))
return tecPRECISION_LOSS;
return tesSUCCESS;
}
template <>
TER
escrowCreatePreclaimHelper<MPTIssue>(
PreclaimContext const& ctx,
AccountID const& account,
AccountID const& dest,
STAmount const& amount)
{
AccountID const issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecNO_PERMISSION
if (issuer == account)
return tecNO_PERMISSION;
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = ctx.view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION
if (!sleIssuance->isFlag(lsfMPTCanEscrow))
return tecNO_PERMISSION;
// If the issuer is not the same as the issuer of the mpt, return
// tecNO_PERMISSION
if (sleIssuance->getAccountID(sfIssuer) != issuer)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If the account does not have the mpt, return tecOBJECT_NOT_FOUND
if (!ctx.view.exists(keylet::mptoken(issuanceKey.key, account)))
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the account is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth);
!isTesSuccess(ter))
return ter;
// If the issuer has requireAuth set, check if the destination is
// authorized
if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth);
!isTesSuccess(ter))
return ter;
// If the issuer has frozen the account, return tecLOCKED
if (isFrozen(ctx.view, account, *sleIssuance))
return tecLOCKED;
// If the issuer has frozen the destination, return tecLOCKED
if (isFrozen(ctx.view, dest, *sleIssuance))
return tecLOCKED;
// If the mpt cannot be transferred, return tecNO_AUTH
if (auto const ter = canTransfer(ctx.view, mptIssue, account, dest); !isTesSuccess(ter))
return ter;
STAmount const spendableAmount = accountHolds(
ctx.view,
account,
amount.get<MPTIssue>(),
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
ctx.j);
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::kZero)
return tecINSUFFICIENT_FUNDS;
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
return tecINSUFFICIENT_FUNDS;
return tesSUCCESS;
}
TER
EscrowCreate::preclaim(PreclaimContext const& ctx)
{
@@ -366,7 +210,7 @@ EscrowCreate::preclaim(PreclaimContext const& ctx)
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowCreatePreclaimHelper<T>(ctx, account, dest, amount);
return escrowLockPreclaimHelper<T>(ctx.view, account, dest, amount, ctx.j);
},
amount.asset().value());
!isTesSuccess(ret))
@@ -375,54 +219,6 @@ EscrowCreate::preclaim(PreclaimContext const& ctx)
return tesSUCCESS;
}
template <ValidIssueType T>
static TER
escrowLockApplyHelper(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal);
template <>
TER
escrowLockApplyHelper<Issue>(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal)
{
// Defensive: Issuer cannot create an escrow
if (issuer == sender)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const ter =
directSendNoFee(view, sender, issuer, amount, !amount.holds<MPTIssue>(), journal);
if (!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
return tesSUCCESS;
}
template <>
TER
escrowLockApplyHelper<MPTIssue>(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal)
{
// Defensive: Issuer cannot create an escrow
if (issuer == sender)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const ter = lockEscrowMPT(view, sender, amount, journal);
if (!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
return tesSUCCESS;
}
TER
EscrowCreate::doApply()
{
@@ -13,15 +13,10 @@
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/EscrowHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
@@ -137,68 +132,6 @@ EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx)
return Transactor::calculateBaseFee(view, tx) + extraFee;
}
template <ValidIssueType T>
static TER
escrowFinishPreclaimHelper(
PreclaimContext const& ctx,
AccountID const& dest,
STAmount const& amount);
template <>
TER
escrowFinishPreclaimHelper<Issue>(
PreclaimContext const& ctx,
AccountID const& dest,
STAmount const& amount)
{
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tesSUCCESS
if (issuer == dest)
return tesSUCCESS;
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(ctx.view, amount.get<Issue>(), dest); !isTesSuccess(ter))
return ter;
// If the issuer has deep frozen the destination, return tecFROZEN
if (isDeepFrozen(ctx.view, dest, amount.get<Issue>().currency, amount.getIssuer()))
return tecFROZEN;
return tesSUCCESS;
}
template <>
TER
escrowFinishPreclaimHelper<MPTIssue>(
PreclaimContext const& ctx,
AccountID const& dest,
STAmount const& amount)
{
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the dest, return tesSUCCESS
if (issuer == dest)
return tesSUCCESS;
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = ctx.view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the destination is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth);
!isTesSuccess(ter))
return ter;
// If the issuer has frozen the destination, return tecLOCKED
if (isFrozen(ctx.view, dest, *sleIssuance))
return tecLOCKED;
return tesSUCCESS;
}
TER
EscrowFinish::preclaim(PreclaimContext const& ctx)
{
@@ -223,7 +156,7 @@ EscrowFinish::preclaim(PreclaimContext const& ctx)
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowFinishPreclaimHelper<T>(ctx, dest, amount);
return escrowUnlockPreclaimHelper<T>(ctx.view, dest, amount);
},
amount.asset().value());
!isTesSuccess(ret))
+45 -11
View File
@@ -18,6 +18,7 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Permissions.h>
#include <xrpl/protocol/Quality.h>
@@ -715,22 +716,55 @@ Payment::doApply()
// Amount to deliver.
STAmount amountDeliver = dstAmount;
// Factor in the transfer rate.
// No rounding. It'll change once MPT integrated into DEX.
STAmount requiredMaxSourceAmount = multiply(dstAmount, rate);
// True if the sender would pay more than SendMax.
bool exceedsSendMax = false;
// Send more than the account wants to pay or less than
// the account wants to deliver (if no SendMax).
// Adjust the amount to deliver.
if (partialPaymentAllowed && requiredMaxSourceAmount > maxSourceAmount)
if (view().rules().enabled(fixCleanup3_5_0))
{
requiredMaxSourceAmount = maxSourceAmount;
// The legacy multiply() and divide() can overflow on large MPT
// amounts, and Number rounding can charge more than SendMax.
// MPTs are integral, so use exact integer arithmetic. The cost
// is rounded up, matching the transfer fee that accountSend
// charges. If the cost overflows, it exceeds any SendMax.
auto const requiredMaxSourceAmount =
tryMulRatio(dstAmount.mpt(), rate.value, QUALITY_ONE, true);
exceedsSendMax =
!requiredMaxSourceAmount || maxSourceAmount.mpt() < *requiredMaxSourceAmount;
// Send more than the account wants to pay or less than
// the account wants to deliver (if no SendMax).
// Adjust the amount to deliver.
if (partialPaymentAllowed && exceedsSendMax)
{
// Round the delivered amount down so that the sender is
// never charged more than SendMax.
auto const delivered =
mulRatio(maxSourceAmount.mpt(), QUALITY_ONE, rate.value, false);
amountDeliver = STAmount(maxSourceAmount.asset(), delivered.value());
if (amountDeliver <= beast::kZero)
return tecPATH_PARTIAL;
exceedsSendMax = false;
}
}
else
{
// Factor in the transfer rate.
// No rounding. It'll change once MPT integrated into DEX.
amountDeliver = divide(maxSourceAmount, rate);
STAmount requiredMaxSourceAmount = multiply(dstAmount, rate);
// Send more than the account wants to pay or less than
// the account wants to deliver (if no SendMax).
// Adjust the amount to deliver.
if (partialPaymentAllowed && requiredMaxSourceAmount > maxSourceAmount)
{
requiredMaxSourceAmount = maxSourceAmount;
// No rounding. It'll change once MPT integrated into DEX.
amountDeliver = divide(maxSourceAmount, rate);
}
exceedsSendMax = requiredMaxSourceAmount > maxSourceAmount;
}
if (requiredMaxSourceAmount > maxSourceAmount ||
(deliverMin && amountDeliver < *deliverMin))
if (exceedsSendMax || (deliverMin && amountDeliver < *deliverMin))
return tecPATH_PARTIAL;
PaymentSandbox pv(&view());
+7 -4
View File
@@ -9,6 +9,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAccount.h> // IWYU pragma: keep
@@ -297,11 +298,13 @@ Batch::preflight(PreflightContext const& ctx)
}
auto const txType = stx.getFieldU16(sfTransactionType);
if (std::ranges::any_of(
kDisabledTxTypes, [txType](auto const& disabled) { return txType == disabled; }))
{
// Pre-LendingProtocolV1_2: SAV and Lending transactions cannot be Batch inners.
// Post-LendingProtocolV1_2: they continue through the normal Batch checks.
bool const rejectedPreV12 = !ctx.rules.enabled(featureLendingProtocolV1_2) &&
std::ranges::any_of(
kDisabledTxTypes, [txType](auto const& disabled) { return txType == disabled; });
if (rejectedPreV12)
return temINVALID_INNER_BATCH;
}
if (!stx.isFlag(tfInnerBatchTxn))
{
+10 -13
View File
@@ -8,9 +8,10 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/AmendmentTable.h>
#include <xrpl/ledger/entries/AmendmentsEntry.h>
#include <xrpl/ledger/entries/FeeSettingsEntry.h>
#include <xrpl/ledger/entries/NegativeUNLEntry.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
@@ -26,7 +27,6 @@
#include <xrpl/tx/Transactor.h>
#include <algorithm>
#include <memory>
namespace xrpl {
@@ -169,14 +169,12 @@ Change::applyAmendment()
{
UInt256 const amendment(ctx_.tx.getFieldH256(sfAmendment));
auto const k = keylet::amendments();
SLE::pointer amendmentObject = view().peek(k);
AmendmentsEntryW amendmentObject(view(), j_);
if (!amendmentObject)
{
amendmentObject = std::make_shared<SLE>(k);
view().insert(amendmentObject);
amendmentObject.newSLE();
amendmentObject.insert();
}
STVector256 amendments = amendmentObject->getFieldV256(sfAmendments);
@@ -253,7 +251,7 @@ Change::applyAmendment()
amendmentObject->setFieldArray(sfMajorities, newMajorities);
}
view().update(amendmentObject);
amendmentObject.update();
return tesSUCCESS;
}
@@ -331,12 +329,11 @@ Change::applyUNLModify()
JLOG(j_.info()) << "N-UNL: applyUNLModify, " << (disabling ? "ToDisable" : "ToReEnable")
<< " seq=" << seq << " validator data:" << strHex(validator);
auto const k = keylet::negativeUNL();
SLE::pointer negUnlObject = view().peek(k);
NegativeUNLEntryW negUnlObject(view(), j_);
if (!negUnlObject)
{
negUnlObject = std::make_shared<SLE>(k);
view().insert(negUnlObject);
negUnlObject.newSLE();
negUnlObject.insert();
}
bool const found = [&] {
@@ -409,7 +406,7 @@ Change::applyUNLModify()
negUnlObject->setFieldVL(sfValidatorToReEnable, validator);
}
view().update(negUnlObject);
negUnlObject.update();
return tesSUCCESS;
}
+1
View File
@@ -12,6 +12,7 @@
#include <test/jtx/offer.h>
#include <test/jtx/paths.h>
#include <test/jtx/pay.h>
#include <test/jtx/paychan.h>
#include <test/jtx/rate.h>
#include <test/jtx/sendmax.h>
#include <test/jtx/seq.h>
+1
View File
@@ -11,6 +11,7 @@
#include <test/jtx/offer.h>
#include <test/jtx/paths.h>
#include <test/jtx/pay.h>
#include <test/jtx/paychan.h>
#include <test/jtx/rate.h>
#include <test/jtx/sendmax.h>
#include <test/jtx/seq.h>
+66 -21
View File
@@ -47,7 +47,6 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
@@ -69,7 +68,6 @@
#include <xrpl/tx/transactors/payment/Payment.h>
#include <xrpl/tx/transactors/system/Batch.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <exception>
@@ -120,15 +118,6 @@ class Batch_test : public beast::unit_test::Suite
return env.rpc("json", "ledger", to_string(params));
}
void
validateInnerTxn(jtx::Env& env, std::string const& batchID, TestLedgerData const& ledgerResult)
{
json::Value const jrr = env.rpc("tx", ledgerResult.txHash)[jss::result];
BEAST_EXPECT(jrr[sfTransactionType.jsonName] == ledgerResult.txType);
BEAST_EXPECT(jrr[jss::meta][sfTransactionResult.jsonName] == ledgerResult.result);
BEAST_EXPECT(jrr[jss::meta][sfParentBatchID.jsonName] == batchID);
}
void
validateClosedLedger(jtx::Env& env, std::vector<TestLedgerData> const& ledgerResults)
{
@@ -144,7 +133,14 @@ class Batch_test : public beast::unit_test::Suite
BEAST_EXPECT(txn[sfTransactionType.jsonName] == ledgerResult.txType);
BEAST_EXPECT(meta[sfTransactionResult.jsonName] == ledgerResult.result);
if (ledgerResult.batchID)
validateInnerTxn(env, *ledgerResult.batchID, ledgerResult);
{
jtx::batch::validateInnerTxn(
env,
*ledgerResult.batchID,
ledgerResult.txHash,
ledgerResult.txType,
ledgerResult.result);
}
}
}
@@ -3140,9 +3136,7 @@ class Batch_test : public beast::unit_test::Suite
{
testcase("loan");
bool const lendingBatchEnabled = !std::ranges::any_of(
Batch::kDisabledTxTypes,
[](auto const& disabled) { return disabled == ttLOAN_BROKER_SET; });
bool const lendingBatchEnabled = features[featureLendingProtocolV1_2];
using namespace test::jtx;
@@ -3291,8 +3285,12 @@ class Batch_test : public beast::unit_test::Suite
batch::Sig(borrower));
}
env.close();
BEAST_EXPECT(env.le(brokerKeylet));
BEAST_EXPECT(!env.le(loanKeylet));
auto const brokerSleBefore = env.le(brokerKeylet);
if (!BEAST_EXPECT(brokerSleBefore))
return;
auto const coverAvailableBefore = brokerSleBefore->at(sfCoverAvailable);
auto const coverDepositAmount = asset(100).value();
{
// LoanSet normally charges at least 2x base fee, but since the
// signature check is done by the batch, it only charges the
@@ -3311,19 +3309,65 @@ class Batch_test : public beast::unit_test::Suite
Fee(kNone),
Seq(kNone)),
lenderSeq + 1),
batch::Inner(manage(lender, loanKeylet.key, tfLoanImpair), lenderSeq + 2),
batch::Inner(
loan_broker::coverDeposit(lender, brokerKeylet.key, coverDepositAmount),
lenderSeq + 2),
batch::Sig(borrower));
}
env.close();
BEAST_EXPECT(env.le(brokerKeylet));
if (auto const sleLoan = env.le(loanKeylet);
lendingBatchEnabled ? BEAST_EXPECT(sleLoan) : !BEAST_EXPECT(!sleLoan))
BEAST_EXPECT(static_cast<bool>(env.le(loanKeylet)) == lendingBatchEnabled);
if (auto const brokerSle = env.le(brokerKeylet); BEAST_EXPECT(brokerSle))
{
BEAST_EXPECT(sleLoan->isFlag(lsfLoanImpaired));
auto const expectedCover = lendingBatchEnabled
? coverAvailableBefore + coverDepositAmount
: coverAvailableBefore;
BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == expectedCover);
}
}
}
void
testLendingAmendment(FeatureBitset features)
{
testcase("lending amendment");
using namespace test::jtx;
// Before LendingProtocolV1_2 a Vault inner transaction rejects the whole batch.
auto const checkVaultBatch = [this](FeatureBitset amendments) {
bool const lendingBatchEnabled = amendments[featureLendingProtocolV1_2];
Env env{*this, amendments};
Account const payer{"payer"};
Account const lender{"lender"};
env.fund(XRP(100'000), payer, lender);
env.close();
Vault const vault{env};
auto [create, vaultKeylet] = vault.create({.owner = lender, .asset = xrpIssue()});
auto const payerSeq = env.seq(payer);
auto const lenderSeq = env.seq(lender);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
submitBatch(
env,
lendingBatchEnabled ? TER{tesSUCCESS} : TER{temINVALID_INNER_BATCH},
batch::outer(payer, payerSeq, batchFee, tfAllOrNothing),
batch::Inner(create, lenderSeq),
batch::Inner(
vault.deposit(
{.depositor = lender, .id = vaultKeylet.key, .amount = XRP(1'000)}),
lenderSeq + 1),
batch::Sig(lender));
env.close();
BEAST_EXPECT(static_cast<bool>(env.le(vaultKeylet)) == lendingBatchEnabled);
};
checkVaultBatch(features - featureLendingProtocolV1_2);
checkVaultBatch(features);
}
void
testObjectCreateSequence(FeatureBitset features)
{
@@ -5915,6 +5959,7 @@ class Batch_test : public beast::unit_test::Suite
testCheckAllSignatures(features);
testAccountSet(features);
testAccountDelete(features);
testLendingAmendment(features);
testLoan(features);
testObjectCreateSequence(features);
testObjectCreateTicket(features);
+150
View File
@@ -18,10 +18,12 @@
#include <test/jtx/offer.h>
#include <test/jtx/paths.h>
#include <test/jtx/pay.h>
#include <test/jtx/permissioned_domains.h>
#include <test/jtx/rate.h>
#include <test/jtx/regkey.h>
#include <test/jtx/sendmax.h>
#include <test/jtx/sig.h>
#include <test/jtx/sponsor.h>
#include <test/jtx/ter.h>
#include <test/jtx/trust.h>
#include <test/jtx/txflags.h>
@@ -1035,6 +1037,15 @@ class Delegate_test : public beast::unit_test::Suite
env(delegate::set(gw, bob, {"PaymentMint"}));
env.close();
// Cross-currency sfSendMax without any flag passes the granular
// template and is rejected by the granular semantic check, because
// the sfSendMax asset (XRP) is not the same as the sfAmount asset (USD).
env(pay(gw, alice, usd(500)),
Sendmax(XRP(1001)),
delegate::As(bob),
Ter(terNO_DELEGATE_PERMISSION));
BEAST_EXPECT(expectOffers(env, carol, 1));
// bob can not send cross currency payment on behalf of the gw,
// even with PaymentMint permission and gw being the issuer.
env(pay(gw, alice, usd(5000)),
@@ -1063,6 +1074,15 @@ class Delegate_test : public beast::unit_test::Suite
env(delegate::set(alice, bob, {"PaymentBurn"}));
env.close();
// Cross-currency sfSendMax without any flag passes the granular
// template and is rejected by the granular semantic check, because
// the sfSendMax asset (XRP) is not the same as the sfAmount asset (USD).
env(pay(alice, gw, usd(500)),
Sendmax(XRP(1001)),
delegate::As(bob),
Ter(terNO_DELEGATE_PERMISSION));
BEAST_EXPECT(expectOffers(env, bob, 1));
// bob can not send cross currency payment on behalf of alice,
// even with PaymentBurn permission and gw being the issuer.
env(pay(alice, gw, usd(5000)),
@@ -1263,6 +1283,34 @@ class Delegate_test : public beast::unit_test::Suite
Domain(UInt256{1}),
delegate::As(bob),
Ter(terNO_DELEGATE_PERMISSION));
// sfPaths is not in the PaymentMint or PaymentBurn template.
env(pay(gw, alice, usd(100)),
Path(~XRP),
delegate::As(bob),
Ter(terNO_DELEGATE_PERMISSION));
env(pay(alice, gw, usd(50)),
Path(~XRP),
delegate::As(bob),
Ter(terNO_DELEGATE_PERMISSION));
// Valid Payment flags that are not in the PaymentMint or PaymentBurn template.
for (auto const flag : {tfPartialPayment, tfNoRippleDirect, tfLimitQuality})
{
env(pay(gw, alice, usd(100)),
Txflags(flag),
delegate::As(bob),
Ter(terNO_DELEGATE_PERMISSION));
env(pay(alice, gw, usd(50)),
Txflags(flag),
delegate::As(bob),
Ter(terNO_DELEGATE_PERMISSION));
}
// The same payments without the extra field or flag succeed.
env(pay(gw, alice, usd(100)), delegate::As(bob));
env(pay(alice, gw, usd(50)), delegate::As(bob));
env.require(Balance(alice, usd(50)));
}
// Delegate account holds no granular permissions for the tx type:
@@ -2266,6 +2314,107 @@ class Delegate_test : public beast::unit_test::Suite
// terNO_DELEGATE_PERMISSION. The set of permitted flags for delegation is defined in
// permissions.macro.
}
// Fields and flags not in permissions.macro template are not permitted for delegation.
{
Env env(*this);
Account const alice{"alice"};
Account const bob{"bob"};
Account const credIssuer{"credIssuer"};
env.fund(XRP(100000), alice, bob, credIssuer);
env.close();
env(pdomain::setTx(credIssuer, {{.issuer = credIssuer, .credType = "credential"}}));
auto const domainID = pdomain::getNewDomain(env.meta());
env.close();
MPTTester mpt(env, alice, {.fund = false});
mpt.create({.flags = tfMPTCanLock | tfMPTCanTransfer});
env.close();
// bob holds every granular permission defined for MPTokenIssuanceSet
env(delegate::set(alice, bob, {"MPTokenIssuanceLock", "MPTokenIssuanceUnlock"}));
env.close();
// Any of those fields or flags not defined in permissions.macro are not permitted for
// delegation.
std::vector<MPTSet> const args = {
{.transferFee = 100},
{.metadata = "test"},
{.domainID = domainID},
{.flags = tfMPTSetCanTrade},
{.flags = tfMPTSetCanClawback},
{.immutableFlags = tifMPTTransferFee},
// a permitted flag does not unlock the extra fields
{.flags = tfMPTLock, .domainID = domainID},
};
for (auto arg : args)
{
arg.account = alice;
arg.delegate = bob;
arg.err = terNO_DELEGATE_PERMISSION;
mpt.set(arg);
env.close();
}
}
}
void
testGranularPermissionWithSponsorCommonFields()
{
testcase("test granular permission with sponsor common fields");
using namespace jtx;
Account const alice{"alice"};
Account const bob{"bob"};
Account const gw{"gw"};
Account const sponsor{"sponsor"};
auto const usd = gw["USD"];
auto const feeAmt = XRP(10);
// Sponsor fields are common fields, so they are permitted in every granular template.
// Fee sponsorship is allowed for a granular delegated transaction.
Env env(*this);
env.fund(XRP(10000), alice, bob, gw, sponsor);
env.trust(usd(200), alice);
env.close();
env(delegate::set(gw, bob, {"PaymentMint"}));
env.close();
// Co-signed: the sponsor account pays the fee.
auto const gwBalance = env.balance(gw);
auto const bobBalance = env.balance(bob);
auto const sponsorBalance = env.balance(sponsor);
env(pay(gw, alice, usd(50)),
delegate::As(bob),
Fee(feeAmt),
sponsor::As(sponsor, spfSponsorFee),
Sig(sfSponsorSignature, sponsor));
env.close();
env.require(Balance(alice, usd(50)));
BEAST_EXPECT(env.balance(gw) == gwBalance);
BEAST_EXPECT(env.balance(bob) == bobBalance);
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - feeAmt);
// Pre-funded: sponsorship(sponsor, bob) pays the fee, bob is sfDelegate.
env(sponsor::set_fee(sponsor, 0, XRP(100)), sponsor::SponseeAcc(bob));
env.close();
auto const sponsorFee = sponsor::sponsorshipFeeBalance(env, sponsor, bob);
env(pay(gw, alice, usd(50)),
delegate::As(bob),
Fee(feeAmt),
sponsor::As(sponsor, spfSponsorFee));
env.close();
env.require(Balance(alice, usd(100)));
BEAST_EXPECT(env.balance(gw) == gwBalance);
BEAST_EXPECT(env.balance(bob) == bobBalance);
BEAST_EXPECT(sponsor::sponsorshipFeeBalance(env, sponsor, bob) == sponsorFee - feeAmt);
// Reserve sponsorship is not tested here: checkSponsor rejects it with temINVALID for any
// delegated transaction before the permission check, regardless of the delegate's
// permissions. It is covered by testDelegateBlockReserveSponsor in Sponsor_test.
}
void
@@ -3002,6 +3151,7 @@ class Delegate_test : public beast::unit_test::Suite
testTrustSetGranular();
testAccountSetGranular();
testMPTokenIssuanceSetGranular();
testGranularPermissionWithSponsorCommonFields();
testSingleSign();
testSingleSignBadSecret();
testMultiSign();
+12 -53
View File
@@ -1,6 +1,7 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/balance.h> // IWYU pragma: keep
#include <test/jtx/escrow.h>
@@ -17,7 +18,6 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/Dir.h>
#include <xrpl/ledger/OpenView.h>
@@ -25,7 +25,6 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
@@ -46,52 +45,6 @@ namespace xrpl::test {
struct EscrowToken_test : public beast::unit_test::Suite
{
static uint64_t
mptEscrowed(jtx::Env const& env, jtx::Account const& account, jtx::MPT const& mpt)
{
auto const sle = env.le(keylet::mptoken(mpt.mpt(), account));
if (sle && sle->isFieldPresent(sfLockedAmount))
return (*sle)[sfLockedAmount];
return 0;
}
static uint64_t
issuerMPTEscrowed(jtx::Env const& env, jtx::MPT const& mpt)
{
auto const sle = env.le(keylet::mptokenIssuance(mpt.mpt()));
if (sle && sle->isFieldPresent(sfLockedAmount))
return (*sle)[sfLockedAmount];
return 0;
}
static jtx::PrettyAmount
issuerBalance(jtx::Env& env, jtx::Account const& account, Issue const& issue)
{
json::Value params;
params[jss::account] = account.human();
auto jrr = env.rpc("json", "gateway_balances", to_string(params));
auto const result = jrr[jss::result];
auto const obligations = result[jss::obligations][to_string(issue.currency)];
if (obligations.isNull())
return {STAmount(issue, 0), account.name()};
STAmount const amount = amountFromString(issue, obligations.asString());
return {amount, account.name()};
}
static jtx::PrettyAmount
issuerEscrowed(jtx::Env& env, jtx::Account const& account, Issue const& issue)
{
json::Value params;
params[jss::account] = account.human();
auto jrr = env.rpc("json", "gateway_balances", to_string(params));
auto const result = jrr[jss::result];
auto const locked = result[jss::locked][to_string(issue.currency)];
if (locked.isNull())
return {STAmount(issue, 0), account.name()};
STAmount const amount = amountFromString(issue, locked.asString());
return {amount, account.name()};
}
void
testIOUEnablement(FeatureBitset features)
{
@@ -3856,10 +3809,15 @@ struct EscrowToken_test : public beast::unit_test::Suite
auto const gw = Account("gw");
for (auto const testFeatures :
{features - featureMPTokensV2 - fixCleanup3_4_0,
{features - featureMPTokensV2 - fixCleanup3_4_0 - fixCleanup3_5_0,
features - featureMPTokensV2 - fixCleanup3_4_0,
features - featureMPTokensV2 - fixCleanup3_5_0,
features - featureMPTokensV2,
(features | featureMPTokensV2) - fixCleanup3_4_0 - fixCleanup3_5_0,
(features | featureMPTokensV2) - fixCleanup3_4_0,
features | featureMPTokensV2})
(features | featureMPTokensV2) - fixCleanup3_5_0,
features | featureMPTokensV2,
features | fixCleanup3_5_0})
{
bool const mptV2 = testFeatures[featureMPTokensV2];
bool const tokenEscrowV1 = testFeatures[fixTokenEscrowV1];
@@ -3867,10 +3825,11 @@ struct EscrowToken_test : public beast::unit_test::Suite
// legacy divideRound(amount, lockedRate, ...) path, which runs when
// fixCleanup3_4_0 is disabled. With fixCleanup3_4_0 the split uses
// mulRatio (128-bit intermediate), which cannot overflow. Without
// it, this large amount overflows unless the MPTokensV2 Number path
// is active. So the finish succeeds when either amendment is enabled.
// it, this large amount overflows unless divideRound takes the
// Number path, which MPTokensV2 or fixCleanup3_5_0 enables. So the
// finish succeeds when any of these amendments is enabled.
bool const cleanup340 = testFeatures[fixCleanup3_4_0];
bool const noOverflow = cleanup340 || mptV2;
bool const noOverflow = cleanup340 || mptV2 || testFeatures[fixCleanup3_5_0];
auto const expectedErr = noOverflow ? Ter(tesSUCCESS) : Ter(tefEXCEPTION);
// Finish with a large MPT amount and non-zero transfer fee. When the
+340 -4
View File
@@ -3864,14 +3864,16 @@ class MPToken_test : public beast::unit_test::Suite
// Bob can pay carol
MPT const mptc = mptAlice;
if (!features[featureMPTokensV2])
if (!features[featureMPTokensV2] && !features[fixCleanup3_5_0])
{
mptAlice.pay(bob, carol, 50);
BEAST_EXPECT(env.balance(carol, mptc) == mptc(50));
}
else
{
// The difference is due to the rounding in MPT/DEX.
// The transfer fee (0.05) is rounded up, so delivering 50
// costs 51, which exceeds the implicit SendMax.
mptAlice.pay(bob, carol, 50, tecPATH_PARTIAL);
// 1 MPTC is the transfer fee paid by bob to the issuer.
env(pay(bob, carol, mptAlice(50)), Txflags(tfPartialPayment));
BEAST_EXPECT(env.balance(carol, mptc) == mptc(49));
@@ -7707,6 +7709,318 @@ class MPToken_test : public beast::unit_test::Suite
}
}
void
testPartialPaymentRounding(FeatureBitset features)
{
testcase("Partial payment rounding");
// With fixCleanup3_5_0, the legacy direct-MPT partial payment rounds the
// delivered amount down instead of to nearest and the transfer fee up,
// so the sender is never charged more than SendMax. This matches V2.
using namespace test::jtx;
Account const alice("alice"); // issuer
Account const bob("bob"); // holder / sender
Account const carol("carol"); // holder / receiver
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
// Transfer fee is 10%
mptAlice.create(
{.transferFee = 10'000, .ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, 780);
auto const mpt = mptAlice["MPT"];
env(pay(bob, carol, mpt(100)), Sendmax(mpt(90)), Txflags(tfPartialPayment));
if (features[featureMPTokensV2] || features[fixCleanup3_5_0])
{
// 81 to carol, 9 to issuer (90 / 1.1 ~ 81.81 (rounded down) =
// 81, 81 * 1.1 = 89.1 (rounded up) = 90). In V2 the payments are
// executed via the payment engine, which rounds the same way.
BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 690));
BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 81));
}
else
{
// 82 to carol, 8 to issuer (90 / 1.1 ~ 81.81 (rounded to nearest) =
// 82)
BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 690));
BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 82));
}
}
void
testPartialPaymentDivideOverflow(FeatureBitset features)
{
testcase("Partial payment divide overflow");
// Regression: legacy MPT partial-payment divide rounding overflow.
//
// Under MPTokensV1 (V2 disabled), a holder-to-holder partial Payment can
// hit STAmount::divide's unchecked `muldiv(...) + 5` wrap and return a
// zero delivered amount that is nevertheless accepted as tesSUCCESS by
// the legacy direct-MPT branch. fixCleanup3_5_0 computes the
// delivered amount with mulRatio (exact 128-bit arithmetic) instead.
// Under MPTokensV2 the payment is routed through RippleCalc, which uses
// Number.
using namespace test::jtx;
Account const alice("alice"); // issuer
Account const bob("bob"); // holder / sender
Account const carol("carol"); // holder / receiver
// The precise sendMax chosen so that
// muldiv(N * 10^17, D) == UINT64_MAX - 2
// where D is the canonicalized rate mantissa for transferFee = 3
// (D == 1'000'030'000'000'000). The subsequent +5 wraps to 2, and
// the resulting STAmount (mantissa=2, offset=-2) canonicalizes to 0
// for an integral MPT asset.
constexpr std::int64_t sendMax = 184'472'974'760'317'629;
// The largest integer k such that k * 1.00003 <= sendMax.
constexpr std::int64_t expectedDelivered = 184'467'440'737'095'516;
bool const fixed = features[featureMPTokensV2] || features[fixCleanup3_5_0];
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create(
{.maxAmt = kMaxMpTokenAmount,
.transferFee = 3,
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, sendMax);
auto const mpt = mptAlice["MPT"];
env(pay(bob, carol, mpt(sendMax)), Sendmax(mpt(sendMax)), Txflags(tfPartialPayment));
auto const bobBal = mptAlice.getBalance(bob);
auto const carolBal = mptAlice.getBalance(carol);
if (!fixed)
{
// divide(sendMax, rate) wraps to MPT(0), accountSendMPT(0)
// short-circuits to tesSUCCESS, and no balances change.
BEAST_EXPECT(bobBal == sendMax);
BEAST_EXPECT(carolBal == 0);
BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(sendMax));
}
else
{
// The transfer fee is redeemed to the issuer, so
// OutstandingAmount shrinks by that fee.
BEAST_EXPECT(carolBal == expectedDelivered);
BEAST_EXPECT(bobBal == 0);
BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(bobBal + carolBal));
}
auto const meta = env.meta();
BEAST_EXPECT(meta->isFieldPresent(sfDeliveredAmount));
if (meta->isFieldPresent(sfDeliveredAmount))
{
BEAST_EXPECT(
meta->getFieldAmount(sfDeliveredAmount) ==
mpt(fixed ? expectedDelivered : 0).value());
}
}
// A larger sendMax makes muldiv(N * 10^17, D) itself exceed
// UINT64_MAX, so the legacy divide() throws instead of wrapping.
{
constexpr std::int64_t largeSendMax = 200'000'000'000'000'000;
// The largest integer k such that k * 1.00003 <= largeSendMax.
constexpr std::int64_t largeExpectedDelivered = 199'994'000'179'994'600;
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create(
{.maxAmt = kMaxMpTokenAmount,
.transferFee = 3,
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, largeSendMax);
auto const mpt = mptAlice["MPT"];
env(pay(bob, carol, mpt(largeSendMax)),
Sendmax(mpt(largeSendMax)),
Txflags(tfPartialPayment),
fixed ? Ter(tesSUCCESS) : Ter(tefEXCEPTION));
auto const bobBal = mptAlice.getBalance(bob);
auto const carolBal = mptAlice.getBalance(carol);
if (!fixed)
{
BEAST_EXPECT(bobBal == largeSendMax);
BEAST_EXPECT(carolBal == 0);
BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(largeSendMax));
}
else
{
BEAST_EXPECT(carolBal == largeExpectedDelivered);
BEAST_EXPECT(bobBal == 0 || bobBal == 1);
BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(bobBal + carolBal));
}
}
// The cost of delivering kMaxMpTokenAmount with a transfer fee doesn't
// fit in an MPT amount. Without the fix, the legacy
// multiply(Amount, rate) throws. With it, or under MPTokensV2, the
// overflowing cost is treated as exceeding SendMax.
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create(
{.maxAmt = kMaxMpTokenAmount,
.transferFee = 3,
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, 1'000);
auto const mpt = mptAlice["MPT"];
// Not a partial payment: the cost exceeds SendMax.
env(pay(bob, carol, mpt(kMaxMpTokenAmount)),
Sendmax(mpt(1'000)),
fixed ? Ter(tecPATH_PARTIAL) : Ter(tefEXCEPTION));
BEAST_EXPECT(mptAlice.getBalance(bob) == 1'000);
BEAST_EXPECT(mptAlice.getBalance(carol) == 0);
// Partial payment: deliver floor(1000 / 1.00003) = 999 for a cost
// of ceil(999 * 1.00003) = 1000.
env(pay(bob, carol, mpt(kMaxMpTokenAmount)),
Sendmax(mpt(1'000)),
Txflags(tfPartialPayment),
fixed ? Ter(tesSUCCESS) : Ter(tefEXCEPTION));
BEAST_EXPECT(mptAlice.getBalance(bob) == (fixed ? 0 : 1'000));
BEAST_EXPECT(mptAlice.getBalance(carol) == (fixed ? 999 : 0));
}
// With the fix, a partial payment that would deliver zero fails
// instead of succeeding without moving any funds.
if (!features[featureMPTokensV2] && features[fixCleanup3_5_0])
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create(
{.transferFee = 3, .ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, 10);
auto const mpt = mptAlice["MPT"];
env(pay(bob, carol, mpt(10)),
Sendmax(mpt(1)),
Txflags(tfPartialPayment),
Ter(tecPATH_PARTIAL));
BEAST_EXPECT(mptAlice.getBalance(bob) == 10);
BEAST_EXPECT(mptAlice.getBalance(carol) == 0);
}
}
void
testPartialPaymentSendMax(FeatureBitset features)
{
testcase("Partial payment does not exceed SendMax");
// A partial payment must not charge bob more than SendMax. Without
// fixCleanup3_5_0, the cost is computed with Number, which rounds large
// amounts when it has only 16 digits (SingleAssetVault and
// LendingProtocol disabled). bob is funded with 1000 extra so that an
// overcharge shows up in his balance.
//
// SendMax = 122'354'741'735'175'090, transfer fee 3 (rate 1.00003):
//
// | Config | bob pays | carol receives |
// |---------------------------------|--------------|------------------|
// | fixCleanup3_5_0 or MPTokensV2 | SendMax | ...998 (floor) |
// | neither, Number with 19 digits | SendMax | ...999 (nearest) |
// | neither, Number with 16 digits | SendMax + 10 | ...999 (nearest) |
using namespace test::jtx;
Account const alice("alice"); // issuer
Account const bob("bob"); // holder / sender
Account const carol("carol"); // holder / receiver
bool const fixed = features[featureMPTokensV2] || features[fixCleanup3_5_0];
bool const smallNumber =
!features[featureSingleAssetVault] && !features[featureLendingProtocol];
// Small enough that the legacy divide() doesn't overflow. Larger
// amounts are covered by testPartialPaymentDivideOverflow.
constexpr std::int64_t sendMax = 122'354'741'735'175'090;
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create(
{.maxAmt = kMaxMpTokenAmount,
.transferFee = 3,
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, sendMax + 1'000);
auto const mpt = mptAlice["MPT"];
env(pay(bob, carol, mpt(sendMax)), Sendmax(mpt(sendMax)), Txflags(tfPartialPayment));
if (fixed)
{
// fixCleanup3_5_0 or MPTokensV2: bob pays exactly SendMax, carol
// receives floor(SendMax / rate)
BEAST_EXPECT(mptAlice.getBalance(bob) == 1'000);
BEAST_EXPECT(mptAlice.getBalance(carol) == 122'351'071'203'038'998);
}
else if (!smallNumber)
{
// Neither, Number with 19 digits: the Number cost is precise
// enough, so bob pays exactly SendMax
BEAST_EXPECT(mptAlice.getBalance(bob) == 1'000);
// The legacy divide() rounds to nearest, so carol receives one
// more than floor(SendMax / rate)
BEAST_EXPECT(mptAlice.getBalance(carol) == 122'351'071'203'038'999);
}
else
{
// Neither, Number with 16 digits: the Number cost is rounded 10
// above SendMax
BEAST_EXPECT(mptAlice.getBalance(bob) == 1'000 - 10);
// The legacy divide() rounds to nearest, so carol receives one
// more than floor(SendMax / rate)
BEAST_EXPECT(mptAlice.getBalance(carol) == 122'351'071'203'038'999);
}
}
public:
void
run() override
@@ -7767,8 +8081,8 @@ public:
// Test Direct Payment
testPayment(all);
testPayment(all | featureSingleAssetVault);
testPayment((all | featureSingleAssetVault) - featureMPTokensV2);
testPayment(all - featureMPTokensV2);
testPayment((all | featureSingleAssetVault) - featureMPTokensV2 - fixCleanup3_5_0);
testPayment(all - featureMPTokensV2 - fixCleanup3_5_0);
testDepositPreauth(all);
testDepositPreauth(all - featureCredentials);
@@ -7826,6 +8140,28 @@ public:
testLockedMPTokenDestroyedIssuance(all - fixCleanup3_4_0);
testLockedMPTokenDestroyedIssuance(all - featureSingleAssetVault);
testLockedMPTokenDestroyedIssuance(all - featureSingleAssetVault - fixCleanup3_4_0);
testPartialPaymentRounding(all);
testPartialPaymentRounding(all - featureMPTokensV2);
testPartialPaymentRounding(all - featureMPTokensV2 - fixCleanup3_5_0);
testPartialPaymentDivideOverflow(all);
testPartialPaymentDivideOverflow(all - featureMPTokensV2);
#ifndef XRPL_UBSAN
// Without the fix, the legacy divide() relies on unsigned wraparound,
// which UBSan reports as unsigned-integer-overflow.
testPartialPaymentDivideOverflow(all - featureMPTokensV2 - fixCleanup3_5_0);
#endif
// Number has a 16-digit mantissa without these amendments.
auto const smallNumber =
all - featureMPTokensV2 - featureSingleAssetVault - featureLendingProtocol;
testPartialPaymentRounding(smallNumber);
testPartialPaymentRounding(smallNumber - fixCleanup3_5_0);
testPartialPaymentSendMax(smallNumber);
testPartialPaymentSendMax(smallNumber - fixCleanup3_5_0);
testPartialPaymentSendMax(smallNumber | featureMPTokensV2);
testPartialPaymentSendMax((smallNumber | featureMPTokensV2) - fixCleanup3_5_0);
testPartialPaymentSendMax(all);
testPartialPaymentSendMax(all - featureMPTokensV2 - fixCleanup3_5_0);
}
};
+1 -25
View File
@@ -9,6 +9,7 @@
#include <test/jtx/deposit.h>
#include <test/jtx/fee.h>
#include <test/jtx/flags.h>
#include <test/jtx/paychan.h>
#include <test/jtx/ter.h>
#include <test/jtx/ticket.h>
#include <test/jtx/txflags.h>
@@ -17,7 +18,6 @@
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/handlers/Handlers.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
@@ -35,13 +35,10 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/PayChan.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
@@ -77,27 +74,6 @@ struct PayChan_test : public beast::unit_test::Suite
return {k.key, view.read(k)};
}
static Buffer
signClaimAuth(
PublicKey const& pk,
SecretKey const& sk,
UInt256 const& channel,
STAmount const& authAmt)
{
Serializer msg;
serializePayChanAuthorization(msg, channel, authAmt.xrp());
return sign(pk, sk, msg.slice());
}
static STAmount
channelAmount(ReadView const& view, UInt256 const& chan)
{
auto const slep = view.read({ltPAYCHAN, chan});
if (!slep)
return XRPAmount{-1};
return (*slep)[sfAmount];
}
static std::optional<std::int64_t>
channelExpiration(ReadView const& view, UInt256 const& chan)
{
+1
View File
@@ -19,6 +19,7 @@
#include <test/jtx/offer.h>
#include <test/jtx/paths.h>
#include <test/jtx/pay.h>
#include <test/jtx/paychan.h>
#include <test/jtx/sendmax.h>
#include <test/jtx/seq.h>
#include <test/jtx/sig.h>
+69 -7
View File
@@ -36,6 +36,7 @@
#include <functional>
#include <initializer_list>
#include <memory>
#include <ranges>
#include <source_location>
#include <string>
#include <tuple>
@@ -1099,6 +1100,62 @@ class InvariantsMPT_test : public InvariantsBase
});
}
// lsfMPTLocked is exempt because tfMPTUnlock clears it legitimately.
// Pre-fixCleanup3_5_0: clearing another issuance flag is allowed.
// Post-fixCleanup3_5_0: clearing another issuance flag trips the
// invariant.
{
std::uint32_t allFlags = 0;
for (auto const flag : std::views::values(getMPTokenIssuanceFlags()))
allFlags |= flag;
allFlags &= ~lsfMPTLocked;
MPTID id{};
Preclose const setup = [&](Account const&, Account const&, Env& env) {
Account const issuer{"issuer"};
env.fund(XRP(10'000), issuer);
env.close();
MPTTester mptt{env, issuer, kMptInitNoFund};
mptt.create({.flags = allFlags});
id = mptt.issuanceID();
env.close();
return true;
};
STTx const tx{ttACCOUNT_SET, [](STObject&) {}};
for (auto const flag : std::views::values(getMPTokenIssuanceFlags()))
{
if (flag == lsfMPTLocked)
continue;
Precheck const clearFlag = [&, flag](
Account const&, Account const&, ApplyContext& ac) {
auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
if (!sleIssuance)
return false;
sleIssuance->setFieldU32(sfFlags, sleIssuance->getFlags() & ~flag);
ac.view().update(sleIssuance);
return true;
};
doInvariantCheck(
makeEnv(all_ - fixCleanup3_5_0),
{},
clearFlag,
XRPAmount{},
tx,
{tesSUCCESS, tesSUCCESS},
setup);
doInvariantCheck(
makeEnv(all_),
{{"immutable MPTokenIssuance flag cleared"}},
clearFlag,
XRPAmount{},
tx,
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
setup);
}
}
// A vault pseudo-account's MPToken cannot be deleted by anything
// other than a VaultDelete transaction. Set up a vault, then have
// an arbitrary tx erase the pseudo's MPToken in precheck.
@@ -1166,6 +1223,12 @@ class InvariantsMPT_test : public InvariantsBase
0u})
{
MPTID id{};
// Issuance flags cannot be cleared after creation (and
// ValidMPTIssuance now rejects it), so the CanTransfer /
// CanTrade rows create the issuance without the bit
// instead of stripping it in precheck.
std::uint32_t const createFlags =
(flag == 0u || flag == lsfMPTLocked) ? kMptDexFlags : (kMptDexFlags & flag);
auto const isSuccess = !gates.any() || flag == 0 ||
(tx == ttPAYMENT && !crossCurrencyPayment && (flag == ~lsfMPTCanTrade)) ||
(tx == ttAMM_WITHDRAW &&
@@ -1187,14 +1250,9 @@ class InvariantsMPT_test : public InvariantsBase
auto issuanceSle = ac.view().peek(keylet::mptokenIssuance(id));
if (!issuanceSle)
return false;
auto const flags = issuanceSle->at(sfFlags);
if (flag == lsfMPTLocked)
{
issuanceSle->at(sfFlags) = flags | lsfMPTLocked;
}
else if (flag != 0u)
{
issuanceSle->at(sfFlags) = flags & flag;
issuanceSle->at(sfFlags) = issuanceSle->at(sfFlags) | lsfMPTLocked;
}
issuanceSle->at(sfOutstandingAmount) = 200;
ac.view().update(issuanceSle);
@@ -1215,7 +1273,11 @@ class InvariantsMPT_test : public InvariantsBase
Account const gw("gw");
env.fund(XRP(1'000), gw);
MPTTester const usd(
{.env = env, .issuer = gw, .holders = {a1, a2}, .pay = 100});
{.env = env,
.issuer = gw,
.holders = {a1, a2},
.pay = 100,
.flags = createFlags});
id = usd.issuanceID();
// Either gate enforces, so both must be off to stay
// advisory. Disable after setting up the MPT; the
+857
View File
@@ -0,0 +1,857 @@
#include <test/app/lending/LoanTestBase.h>
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/JTx.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/batch.h>
#include <test/jtx/fee.h>
#include <test/jtx/flags.h>
#include <test/jtx/offer.h>
#include <test/jtx/owners.h>
#include <test/jtx/pay.h>
#include <test/jtx/seq.h>
#include <test/jtx/sig.h>
#include <test/jtx/tags.h>
#include <test/jtx/ter.h>
#include <test/jtx/trust.h>
#include <test/jtx/vault.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/Units.h>
#include <algorithm>
#include <chrono>
#include <cstdint>
namespace xrpl::test {
// Vault and Lending transactions used as Batch inners under LendingProtocolV1_2.
class LendingBatch_test : public LoanTestBase
{
private:
// Asserts a loan has been paid off in full: no principal, value, or
// remaining payments left on the loan; no debt left on the broker; and the
// vault's available assets back to `depositValue`.
void
assertLoanFullyRepaid(
jtx::Env const& env,
Keylet const& brokerKeylet,
Keylet const& vaultKeylet,
Keylet const& loanKeylet,
STAmount const& depositValue)
{
if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
{
BEAST_EXPECT(loanSle->at(sfPrincipalOutstanding) == 0);
BEAST_EXPECT(loanSle->at(sfTotalValueOutstanding) == 0);
BEAST_EXPECT(loanSle->at(sfPaymentRemaining) == 0);
}
if (auto const brokerSle = env.le(brokerKeylet); BEAST_EXPECT(brokerSle))
BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0);
if (auto const vaultSle = env.le(vaultKeylet); BEAST_EXPECT(vaultSle))
BEAST_EXPECT(vaultSle->at(sfAssetsAvailable) == depositValue);
}
// LoanSet as a batch inner: the counterparty signs the outer batch, so
// this inner carries no signature of its own. Pass paymentTotal >= 2
// wherever the same batch also submits an early tfLoanFullPayment:
// computeFullPayment refuses it with tecKILLED when only the final
// scheduled payment is left.
static json::Value
loanSetInner(
jtx::Env& env,
jtx::Account const& account,
uint256 const& brokerID,
jtx::Account const& counterparty,
STAmount const& principal,
std::uint32_t paymentTotal)
{
using namespace jtx;
using namespace jtx::loan;
return env.json(
set(account, brokerID, principal.value()),
kCounterparty(counterparty.id()),
kPaymentTotal(paymentTotal),
Sig(kNone),
Fee(kNone),
Seq(kNone));
}
// Runs VaultCreate -> Deposit -> Withdraw -> Delete in one tfAllOrNothing
// batch and checks the vault is gone, the owner paid only the batch fee,
// and both owner count and sequence are back where they belong.
void
runVaultLifecycleBatch(jtx::Env& env, jtx::Account const& owner, json::Value const& createTx)
{
using namespace jtx;
Vault const vault{env};
auto const seq = env.seq(owner);
auto const balanceBefore = env.balance(owner);
auto const ownerCountBefore = env.ownerCount(owner);
// The outer Batch consumes owner's current sequence, so the first
// inner (VaultCreate) lands on seq + 1.
auto const vaultKeylet = keylet::vault(owner.id(), SeqProxy::rawSequence(seq + 1));
auto const depositAmount = XRP(1'000);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
env(batch::outer(owner, seq, batchFee, tfAllOrNothing),
batch::Inner(createTx, seq + 1),
batch::Inner(
vault.deposit({.depositor = owner, .id = vaultKeylet.key, .amount = depositAmount}),
seq + 2),
batch::Inner(
vault.withdraw(
{.depositor = owner, .id = vaultKeylet.key, .amount = depositAmount}),
seq + 3),
batch::Inner(vault.del({.owner = owner, .id = vaultKeylet.key}), seq + 4),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(!env.le(vaultKeylet));
BEAST_EXPECT(env.balance(owner) == balanceBefore - batchFee);
BEAST_EXPECT(env.ownerCount(owner) == ownerCountBefore);
BEAST_EXPECT(env.seq(owner) == seq + 5);
}
void
testVaultLifecycle(FeatureBitset features)
{
// A single tfAllOrNothing batch runs an open-ended XRP vault through
// its whole lifecycle: create, deposit, withdraw everything back out,
// and delete.
testcase("vault lifecycle in a batch");
using namespace jtx;
Env env(*this, features);
Account const owner{"owner"};
env.fund(XRP(100'000), owner);
env.close();
auto const createTx = std::get<0>(Vault{env}.create({.owner = owner, .asset = xrpIssue()}));
runVaultLifecycleBatch(env, owner, createTx);
}
void
testClosedEndedVaultLifecycle(FeatureBitset features)
{
// Same lifecycle as testVaultLifecycle, but on a closed-ended vault
// submitted while it is still in the Subscription phase, where
// LendingProtocolV1_1 allows both deposit and withdraw.
testcase("vault lifecycle in a batch, closed-ended vault, subscription phase");
using namespace jtx;
Env env(*this, features);
Account const owner{"owner"};
env.fund(XRP(100'000), owner);
env.close();
auto const createTx = std::get<0>(Vault{env}.createClosedEnded(
{.owner = owner, .asset = xrpIssue(), .subscriptionOffset = std::chrono::seconds{60}}));
runVaultLifecycleBatch(env, owner, createTx);
}
void
testLoanLifecycleOpenEndedVault(FeatureBitset features)
{
// With LendingProtocolV1_1 off, an open-ended vault can host a broker,
// so vault creation, deposit, broker setup, loan origination, and an
// early full payoff all fit in one atomic batch. The borrower signs the
// outer as counterparty to the LoanSet inner and as the account behind
// the LoanPay inner.
testcase("loan lifecycle in a batch, open-ended vault");
using namespace jtx;
Env env(*this, features);
Account const lender{"lender"};
Account const borrower{"borrower"};
env.fund(XRP(1'000'000), lender, borrower);
env.close();
PrettyAsset const asset{xrpIssue(), 1'000'000};
Vault const vault{env};
auto const lenderSeq = env.seq(lender);
auto const borrowerSeq = env.seq(borrower);
auto const lenderBalanceBefore = env.balance(lender);
auto const borrowerBalanceBefore = env.balance(borrower);
auto const depositAmount = asset(50'000);
auto const principal = asset(1'000);
auto const vaultKeylet = keylet::vault(lender.id(), SeqProxy::rawSequence(lenderSeq + 1));
auto const brokerKeylet =
keylet::loanBroker(lender.id(), SeqProxy::rawSequence(lenderSeq + 3));
auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
auto const createTx = std::get<0>(vault.create({.owner = lender, .asset = xrpIssue()}));
// CoverRateMinimum/CoverRateLiquidation are left unset on the
// LoanBrokerSet inner below, which defaults both to zero, so this
// loan needs no first-loss cover deposit from the broker.
auto const loanSetTx = loanSetInner(env, lender, brokerKeylet.key, borrower, principal, 2);
auto const loanPayTx =
loan::pay(borrower, loanKeylet.key, principal.value(), tfLoanFullPayment);
auto const batchFee = batch::calcBatchFee(env, 1, 5);
env(batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::Inner(createTx, lenderSeq + 1),
batch::Inner(
vault.deposit(
{.depositor = lender, .id = vaultKeylet.key, .amount = depositAmount.value()}),
lenderSeq + 2),
batch::Inner(loan_broker::set(lender, vaultKeylet.key), lenderSeq + 3),
batch::Inner(loanSetTx, lenderSeq + 4),
batch::Inner(loanPayTx, borrowerSeq),
batch::Sig(borrower),
Ter(tesSUCCESS));
env.close();
assertLoanFullyRepaid(env, brokerKeylet, vaultKeylet, loanKeylet, depositAmount.value());
// The borrower borrowed the principal and repaid it in full at zero
// interest and fees, so their balance nets to zero; the batch fee is
// paid entirely by the lender, who also funded the vault deposit.
BEAST_EXPECT(env.balance(borrower) == borrowerBalanceBefore);
BEAST_EXPECT(env.balance(lender) == lenderBalanceBefore - batchFee - depositAmount.value());
}
void
testLoanLifecycleClosedEndedVault(FeatureBitset features)
{
// Under LendingProtocolV1_1 a broker can attach only to a closed-ended
// vault, and LoanSet is refused outside the Investment phase, so the
// vault/broker setup and the loan/payoff run as two batches split at
// the Subscription -> Investment boundary.
testcase("loan lifecycle in a batch, closed-ended vault");
using namespace jtx;
Env env(*this, features);
Account const lender{"lender"};
Account const borrower{"borrower"};
env.fund(XRP(1'000'000), lender, borrower);
env.close();
PrettyAsset const asset{xrpIssue(), 1'000'000};
Vault const vault{env};
auto const lenderSeq1 = env.seq(lender);
auto const depositAmount = asset(50'000);
auto const vaultKeylet = keylet::vault(lender.id(), SeqProxy::rawSequence(lenderSeq1 + 1));
auto const brokerKeylet =
keylet::loanBroker(lender.id(), SeqProxy::rawSequence(lenderSeq1 + 3));
auto const created = vault.createClosedEnded(
{.owner = lender, .asset = xrpIssue(), .subscriptionOffset = std::chrono::seconds{60}});
auto const createTx = std::get<0>(created);
auto const subscriptionDate = std::get<2>(created);
// LoanBrokerSet checks only the vault's kind, not its phase, so setting
// up the broker is allowed here in the same batch, while the vault is
// still in the Subscription phase.
auto const batch1Fee = batch::calcBatchFee(env, 0, 3);
env(batch::outer(lender, lenderSeq1, batch1Fee, tfAllOrNothing),
batch::Inner(createTx, lenderSeq1 + 1),
batch::Inner(
vault.deposit(
{.depositor = lender, .id = vaultKeylet.key, .amount = depositAmount.value()}),
lenderSeq1 + 2),
batch::Inner(loan_broker::set(lender, vaultKeylet.key), lenderSeq1 + 3),
Ter(tesSUCCESS));
env.close();
if (auto const vaultSle = env.le(vaultKeylet); BEAST_EXPECT(vaultSle))
BEAST_EXPECT(vaultSle->at(sfAssetsAvailable) == depositAmount.value());
BEAST_EXPECT(env.le(brokerKeylet));
vault.closePastSubscription(subscriptionDate);
auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
auto const principal = asset(1'000);
auto const lenderSeq2 = env.seq(lender);
auto const borrowerSeq = env.seq(borrower);
auto const lenderBalanceBefore = env.balance(lender);
auto const borrowerBalanceBefore = env.balance(borrower);
auto const loanSetTx = loanSetInner(env, lender, brokerKeylet.key, borrower, principal, 2);
auto const loanPayTx =
loan::pay(borrower, loanKeylet.key, principal.value(), tfLoanFullPayment);
auto const batch2Fee = batch::calcBatchFee(env, 1, 2);
env(batch::outer(lender, lenderSeq2, batch2Fee, tfAllOrNothing),
batch::Inner(loanSetTx, lenderSeq2 + 1),
batch::Inner(loanPayTx, borrowerSeq),
batch::Sig(borrower),
Ter(tesSUCCESS));
env.close();
assertLoanFullyRepaid(env, brokerKeylet, vaultKeylet, loanKeylet, depositAmount.value());
BEAST_EXPECT(env.balance(borrower) == borrowerBalanceBefore);
// The deposit already left the lender's balance in batch 1; batch 2
// only costs the batch fee, since LoanSet/LoanPay net to zero XRP.
BEAST_EXPECT(env.balance(lender) == lenderBalanceBefore - batch2Fee);
}
// Handles shared by testArbitrage and testArbitrageRollback.
struct ArbitrageSetup
{
jtx::PrettyAsset iou;
jtx::Account issuer;
jtx::Account lender;
jtx::Account borrower;
jtx::Account mm1;
jtx::Account mm2;
BrokerInfo broker;
};
// Broker with zero cover rates and no debt cap, so LoanSet needs no cover
// deposit. The borrower holds a trust line but no IOU, so the profit is
// exact. mm1 rests an offer selling 400 XRP for 1000 IOU; mm2 is funded
// but places its offer only inside testArbitrage's batch.
ArbitrageSetup
setupArbitrage(jtx::Env& env)
{
using namespace jtx;
Account const issuer{"issuer"};
Account const lender{"lender"};
Account const borrower{"borrower"};
Account const mm1{"mm1"};
Account const mm2{"mm2"};
env.fund(XRP(1'000'000), issuer, lender, borrower, mm1, mm2);
env(fset(issuer, asfDefaultRipple));
env.close();
PrettyAsset const iou = issuer[iouCurrency_];
STAmount const iouLimit = iou(1'000'000'000);
env(trust(lender, iouLimit));
env(trust(borrower, iouLimit));
env(trust(mm1, iouLimit));
env(trust(mm2, iouLimit));
env.close();
BrokerParameters params = BrokerParameters::defaults();
params.vaultDeposit = 100'000;
params.coverRateMin = TenthBips32{0};
params.coverDeposit = 0;
params.debtMax = 0;
params.coverRateLiquidation = TenthBips32{0};
env(pay(issuer, lender, iou(params.vaultDeposit)));
env(pay(issuer, mm2, iou(20'000)));
env.close();
BrokerInfo const broker{createVaultAndBroker(env, iou, lender, params)};
env(offer(mm1, iou(1'000), XRP(400)));
env.close();
return {
.iou = iou,
.issuer = issuer,
.lender = lender,
.borrower = borrower,
.mm1 = mm1,
.mm2 = mm2,
.broker = broker};
}
void
testArbitrage(FeatureBitset features)
{
// A borrower profits risk-free from a loan and two crossing DEX
// offers in one atomic batch. Crossed offers execute the moment both
// rest on the book, so mm2's offer is itself a batch inner, placed
// right after the borrower's buy leg consumes mm1.
testcase("arbitrage across a loan and two crossing offers in a batch");
using namespace jtx;
Env env(*this, features);
auto const setup = setupArbitrage(env);
auto const& iou = setup.iou;
auto const& lender = setup.lender;
auto const& borrower = setup.borrower;
auto const& broker = setup.broker;
auto const& mm2 = setup.mm2;
auto const loanKeylet = nextLoanKeylet(env, broker);
auto const principal = iou(1'000);
BEAST_EXPECT(env.balance(borrower, iou) == iou(0));
auto const borrowerSeq = env.seq(borrower);
auto const mm2Seq = env.seq(mm2);
auto const loanSetTx = loanSetInner(env, borrower, broker.brokerID, lender, principal, 2);
auto const loanPayTx =
loan::pay(borrower, loanKeylet.key, principal.value(), tfLoanFullPayment);
auto const batchFee = batch::calcBatchFee(env, 2, 5);
env(batch::outer(borrower, borrowerSeq, batchFee, tfAllOrNothing),
batch::Inner(loanSetTx, borrowerSeq + 1),
batch::Inner(
offer(borrower, XRP(400), principal.value(), tfFillOrKill), borrowerSeq + 2),
batch::Inner(offer(mm2, XRP(400), iou(1'008)), mm2Seq),
batch::Inner(
offer(borrower, iou(1'008).value(), XRP(400), tfFillOrKill), borrowerSeq + 3),
batch::Inner(loanPayTx, borrowerSeq + 4),
batch::Sig(lender, mm2),
Ter(tesSUCCESS));
env.close();
if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
BEAST_EXPECT(loanSle->at(sfPrincipalOutstanding) == 0);
BEAST_EXPECT(env.balance(borrower, iou) == iou(8));
env.require(Offers(setup.mm1, 0));
env.require(Offers(mm2, 0));
if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0);
if (auto const vaultSle = env.le(broker.vaultKeylet()); BEAST_EXPECT(vaultSle))
{
BEAST_EXPECT(
vaultSle->at(sfAssetsAvailable) == iou(broker.params.vaultDeposit).value());
}
}
void
testArbitrageRollback(FeatureBitset features)
{
// Without a second market maker willing to buy XRP back at the
// higher price, the sell leg's tfFillOrKill offer cannot fill and
// returns tecKILLED. tfAllOrNothing then rolls back every inner,
// including the loan origination; the outer batch itself still
// returns tesSUCCESS.
testcase("arbitrage batch rolls back when the sell offer cannot fill");
using namespace jtx;
Env env(*this, features);
auto const setup = setupArbitrage(env);
auto const& iou = setup.iou;
auto const& lender = setup.lender;
auto const& borrower = setup.borrower;
auto const& broker = setup.broker;
auto const brokerSleBefore = env.le(broker.brokerKeylet());
auto const vaultSleBefore = env.le(broker.vaultKeylet());
if (!BEAST_EXPECT(brokerSleBefore) || !BEAST_EXPECT(vaultSleBefore))
return;
auto const loanSequenceBefore = brokerSleBefore->at(sfLoanSequence);
auto const assetsAvailableBefore = vaultSleBefore->at(sfAssetsAvailable);
auto const loanKeylet = nextLoanKeylet(env, broker);
auto const principal = iou(1'000);
auto const borrowerSeq = env.seq(borrower);
auto const borrowerBalanceBefore = env.balance(borrower);
auto const loanSetTx = loanSetInner(env, borrower, broker.brokerID, lender, principal, 2);
auto const loanPayTx =
loan::pay(borrower, loanKeylet.key, principal.value(), tfLoanFullPayment);
auto const batchFee = batch::calcBatchFee(env, 1, 4);
env(batch::outer(borrower, borrowerSeq, batchFee, tfAllOrNothing),
batch::Inner(loanSetTx, borrowerSeq + 1),
batch::Inner(
offer(borrower, XRP(400), principal.value(), tfFillOrKill), borrowerSeq + 2),
batch::Inner(
offer(borrower, iou(1'008).value(), XRP(400), tfFillOrKill), borrowerSeq + 3),
batch::Inner(loanPayTx, borrowerSeq + 4),
batch::Sig(lender),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(!env.le(loanKeylet));
if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
{
BEAST_EXPECT(brokerSle->at(sfLoanSequence) == loanSequenceBefore);
BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0);
}
BEAST_EXPECT(env.balance(borrower, iou) == iou(0));
BEAST_EXPECT(env.balance(borrower) == borrowerBalanceBefore - batchFee);
env.require(Offers(setup.mm1, 1));
if (auto const vaultSle = env.le(broker.vaultKeylet()); BEAST_EXPECT(vaultSle))
BEAST_EXPECT(vaultSle->at(sfAssetsAvailable) == assetsAvailableBefore);
}
// Originates a loan outside any batch: one payment due in a day, a day of
// grace, zero interest. PaymentTotal 1 is fine because callers only
// impair or default the loan and never pay it off early.
static void
originateLoan(
jtx::Env& env,
BrokerInfo const& broker,
jtx::Account const& lender,
jtx::Account const& borrower,
STAmount const& principal)
{
using namespace jtx;
using namespace jtx::loan;
env(set(lender, broker.brokerID, principal),
kCounterparty(borrower.id()),
kPaymentTotal(1),
kPaymentInterval(86400),
kGracePeriod(86400),
Sig(sfCounterpartySignature, borrower),
Fee(env.current()->fees().base * 2));
env.close();
}
void
testImpairAndCoverDeposit(FeatureBitset features)
{
// Impairing a loan records its exposure as the vault's unrealized
// loss; the broker tops up first-loss cover in the same batch. Under
// fixCleanup3_4_0 LoanManage refuses tfLoanImpair with tecTOO_SOON
// until a payment is late, so the impairment cannot share a batch
// with the LoanSet that creates the loan.
testcase("loan impairment and cover deposit in a batch");
using namespace jtx;
Env env(*this, features);
Account const lender{"lender"};
Account const borrower{"borrower"};
env.fund(XRP(100'000'000), lender, borrower);
env.close();
PrettyAsset const asset{xrpIssue(), 1'000'000};
auto const broker = createVaultAndBroker(env, asset, lender);
auto const loanKeylet = nextLoanKeylet(env, broker);
originateLoan(env, broker, lender, borrower, asset(1'000).value());
auto const loanSleBefore = env.le(loanKeylet);
if (!BEAST_EXPECT(loanSleBefore))
return;
// Advance past NextPaymentDueDate so the payment is late.
using D = NetClock::duration;
using Tp = NetClock::time_point;
env.close(Tp{D{loanSleBefore->at(sfNextPaymentDueDate) + 1}});
auto const brokerSleBefore = env.le(broker.brokerKeylet());
auto const vaultSleBefore = env.le(broker.vaultKeylet());
if (!BEAST_EXPECT(brokerSleBefore) || !BEAST_EXPECT(vaultSleBefore))
return;
auto const coverAvailableBefore = brokerSleBefore->at(sfCoverAvailable);
auto const lossUnrealizedBefore = vaultSleBefore->at(sfLossUnrealized);
auto const additionalCover = asset(500).value();
auto const lenderSeq = env.seq(lender);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
env(batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::Inner(loan::manage(lender, loanKeylet.key, tfLoanImpair), lenderSeq + 1),
batch::Inner(
loan_broker::coverDeposit(lender, broker.brokerID, additionalCover), lenderSeq + 2),
Ter(tesSUCCESS));
env.close();
if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
BEAST_EXPECT(loanSle->isFlag(lsfLoanImpaired));
// Zero interest and fees make the exposure equal the outstanding
// principal under both accounting modes.
if (auto const vaultSle = env.le(broker.vaultKeylet()); BEAST_EXPECT(vaultSle))
{
BEAST_EXPECT(
vaultSle->at(sfLossUnrealized) ==
lossUnrealizedBefore + loanSleBefore->at(sfPrincipalOutstanding));
}
if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == coverAvailableBefore + additionalCover);
}
void
testDefaultAndCoverWithdraw(FeatureBitset features)
{
// A defaulted loan burns first-loss cover per CoverRateLiquidation;
// the remainder can be withdrawn once DebtTotal drops to zero. The
// default runs in its own batch: LoanManage refuses tfLoanDefault
// with tecTOO_SOON until the grace period elapses, so it cannot share
// a batch with the LoanSet that creates the loan.
testcase("loan default and cover withdraw in a batch");
using namespace jtx;
Env env(*this, features);
Account const lender{"lender"};
Account const borrower{"borrower"};
env.fund(XRP(100'000'000), lender, borrower);
env.close();
PrettyAsset const asset{xrpIssue(), 1'000'000};
auto const broker = createVaultAndBroker(env, asset, lender);
auto const loanKeylet = nextLoanKeylet(env, broker);
originateLoan(env, broker, lender, borrower, asset(1'000).value());
auto const loanSleBefore = env.le(loanKeylet);
if (!BEAST_EXPECT(loanSleBefore))
return;
// Advance past NextPaymentDueDate + GracePeriod so the default is no
// longer refused with tecTOO_SOON.
auto const dueDate = loanSleBefore->at(sfNextPaymentDueDate);
auto const gracePeriod = loanSleBefore->at(sfGracePeriod);
using D = NetClock::duration;
using Tp = NetClock::time_point;
env.close(Tp{D{dueDate + gracePeriod + 1}});
auto const brokerSleBefore = env.le(broker.brokerKeylet());
auto const vaultSleBefore = env.le(broker.vaultKeylet());
if (!BEAST_EXPECT(brokerSleBefore) || !BEAST_EXPECT(vaultSleBefore))
return;
auto const coverAvailableBefore = brokerSleBefore->at(sfCoverAvailable);
auto const assetsAvailableBefore = vaultSleBefore->at(sfAssetsAvailable);
auto const assetsTotalBefore = vaultSleBefore->at(sfAssetsTotal);
TenthBips32 const coverRateMin{brokerSleBefore->at(sfCoverRateMinimum)};
TenthBips32 const coverRateLiquidation{brokerSleBefore->at(sfCoverRateLiquidation)};
Number const totalDefaultAmount{loanSleBefore->at(sfPrincipalOutstanding)};
// DefaultCovered = min(MinimumCover x CoverRateLiquidation,
// DefaultAmount, CoverAvailable), matching LoanManage::defaultLoan.
// Zero interest and fees make DefaultAmount equal the outstanding
// principal under both accounting modes, so no
// featureLendingProtocolV1_1 branch is needed here.
Number defaultCovered;
{
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
auto const minimumCover =
tenthBipsOfValue(Number{brokerSleBefore->at(sfDebtTotal)}, coverRateMin);
defaultCovered = std::min(
roundToAsset(
asset.raw(),
std::min(
tenthBipsOfValue(minimumCover, coverRateLiquidation), totalDefaultAmount),
loanSleBefore->at(sfLoanScale)),
Number{coverAvailableBefore});
}
auto const vaultDefaultAmount = totalDefaultAmount - defaultCovered;
auto const lenderSeq = env.seq(lender);
auto const lenderBalanceBefore = env.balance(lender);
auto const withdrawAmount = STAmount{asset.raw(), coverAvailableBefore - defaultCovered};
auto const batchFee = batch::calcBatchFee(env, 0, 2);
env(batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::Inner(loan::manage(lender, loanKeylet.key, tfLoanDefault), lenderSeq + 1),
batch::Inner(
loan_broker::coverWithdraw(lender, broker.brokerID, withdrawAmount), lenderSeq + 2),
Ter(tesSUCCESS));
env.close();
if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
{
BEAST_EXPECT(loanSle->isFlag(lsfLoanDefault));
BEAST_EXPECT(loanSle->at(sfPrincipalOutstanding) == 0);
}
if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
{
BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == 0);
BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0);
}
BEAST_EXPECT(env.balance(lender) == lenderBalanceBefore - batchFee + withdrawAmount);
// The default moves defaultCovered from the broker's cover to the
// vault; the cover withdraw pays out of the broker's own
// pseudo-account and does not touch the vault again.
if (auto const vaultSle = env.le(broker.vaultKeylet()); BEAST_EXPECT(vaultSle))
{
BEAST_EXPECT(vaultSle->at(sfAssetsAvailable) == assetsAvailableBefore + defaultCovered);
BEAST_EXPECT(vaultSle->at(sfAssetsTotal) == assetsTotalBefore - vaultDefaultAmount);
}
}
void
testIndependentVaultChain(FeatureBitset features)
{
// tfIndependent runs every inner regardless of earlier failures.
// VaultWithdraw fails before anything is deposited: the empty vault
// converts the amount to zero shares (tecPRECISION_LOSS). VaultDelete fails
// once the vault holds a deposit (tecHAS_OBLIGATIONS); both are
// tec-class results, so neither stops the inners that follow.
testcase("independent batch runs every inner despite failing inners");
using namespace jtx;
Env env(*this, features);
Account const owner{"owner"};
env.fund(XRP(100'000), owner);
env.close();
Vault const vault{env};
auto const seq = env.seq(owner);
auto const vaultKeylet = keylet::vault(owner.id(), SeqProxy::rawSequence(seq + 1));
auto const createTx = std::get<0>(vault.create({.owner = owner, .asset = xrpIssue()}));
auto const amount = XRP(1'000);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const batchTx = env.jt(
batch::outer(owner, seq, batchFee, tfIndependent),
batch::Inner(createTx, seq + 1),
batch::Inner(
vault.withdraw({.depositor = owner, .id = vaultKeylet.key, .amount = amount}),
seq + 2),
batch::Inner(
vault.deposit({.depositor = owner, .id = vaultKeylet.key, .amount = amount}),
seq + 3),
batch::Inner(vault.del({.owner = owner, .id = vaultKeylet.key}), seq + 4));
env(batchTx, Ter(tesSUCCESS));
env.close();
auto const batchID = strHex(batchTx.stx->getTransactionID());
auto const& innerIDs = batchTx.stx->getBatchTransactionIDs();
if (!BEAST_EXPECT(innerIDs.size() == 4))
return;
batch::validateInnerTxn(env, batchID, strHex(innerIDs[0]), "VaultCreate", "tesSUCCESS");
batch::validateInnerTxn(
env, batchID, strHex(innerIDs[1]), "VaultWithdraw", "tecPRECISION_LOSS");
batch::validateInnerTxn(env, batchID, strHex(innerIDs[2]), "VaultDeposit", "tesSUCCESS");
batch::validateInnerTxn(
env, batchID, strHex(innerIDs[3]), "VaultDelete", "tecHAS_OBLIGATIONS");
if (auto const vaultSle = env.le(vaultKeylet); BEAST_EXPECT(vaultSle))
BEAST_EXPECT(vaultSle->at(sfAssetsTotal) == amount.value());
BEAST_EXPECT(env.seq(owner) == seq + 5);
}
void
testIndependentLoanChain(FeatureBitset features)
{
// Same claim as testIndependentVaultChain, on a loan chain. The
// LoanSet inner leaves the borrower holding only the 1,000 principal,
// so a LoanPay for 2,000 fails preclaim's balance check
// (tecINSUFFICIENT_FUNDS); the cover deposit submitted after it
// still lands.
testcase("independent batch runs every loan inner despite a failing payment");
using namespace jtx;
Env env(*this, features);
Account const issuer{"issuer"};
Account const lender{"lender"};
Account const borrower{"borrower"};
env.fund(XRP(1'000'000), issuer, lender, borrower);
env(fset(issuer, asfDefaultRipple));
env.close();
PrettyAsset const iou = issuer[iouCurrency_];
STAmount const iouLimit = iou(1'000'000'000);
env(trust(lender, iouLimit));
env(trust(borrower, iouLimit));
env.close();
env(pay(issuer, lender, iou(2'000'000)));
env.close();
auto const broker = createVaultAndBroker(env, iou, lender);
auto const loanKeylet = nextLoanKeylet(env, broker);
auto const principal = iou(1'000);
auto const brokerSleBefore = env.le(broker.brokerKeylet());
if (!BEAST_EXPECT(brokerSleBefore))
return;
auto const coverAvailableBefore = brokerSleBefore->at(sfCoverAvailable);
auto const lenderSeq = env.seq(lender);
auto const borrowerSeq = env.seq(borrower);
auto const loanSetTx = loanSetInner(env, lender, broker.brokerID, borrower, principal, 2);
auto const loanPayTx =
loan::pay(borrower, loanKeylet.key, iou(2'000).value(), tfLoanFullPayment);
auto const additionalCover = iou(500);
auto const batchFee = batch::calcBatchFee(env, 1, 3);
auto const batchTx = env.jt(
batch::outer(lender, lenderSeq, batchFee, tfIndependent),
batch::Inner(loanSetTx, lenderSeq + 1),
batch::Inner(loanPayTx, borrowerSeq),
batch::Inner(
loan_broker::coverDeposit(lender, broker.brokerID, additionalCover), lenderSeq + 2),
batch::Sig(borrower));
env(batchTx, Ter(tesSUCCESS));
env.close();
auto const batchID = strHex(batchTx.stx->getTransactionID());
auto const& innerIDs = batchTx.stx->getBatchTransactionIDs();
if (!BEAST_EXPECT(innerIDs.size() == 3))
return;
batch::validateInnerTxn(env, batchID, strHex(innerIDs[0]), "LoanSet", "tesSUCCESS");
batch::validateInnerTxn(
env, batchID, strHex(innerIDs[1]), "LoanPay", "tecINSUFFICIENT_FUNDS");
batch::validateInnerTxn(
env, batchID, strHex(innerIDs[2]), "LoanBrokerCoverDeposit", "tesSUCCESS");
if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
BEAST_EXPECT(loanSle->at(sfPrincipalOutstanding) == principal.value());
if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
{
BEAST_EXPECT(
brokerSle->at(sfCoverAvailable) == coverAvailableBefore + additionalCover.value());
}
}
public:
void
run() override
{
// Batch rejects Vault and Lending inners before LendingProtocolV1_2.
FeatureBitset const lendingBatch{all_ | featureLendingProtocolV1_2};
for (auto const& features : {lendingBatch, lendingBatch | featureLendingProtocolV1_1})
{
testVaultLifecycle(features);
testArbitrage(features);
testArbitrageRollback(features);
testImpairAndCoverDeposit(features);
testDefaultAndCoverWithdraw(features);
testIndependentVaultChain(features);
testIndependentLoanChain(features);
}
testClosedEndedVaultLifecycle(lendingBatch | featureLendingProtocolV1_1);
testLoanLifecycleOpenEndedVault(lendingBatch);
testLoanLifecycleClosedEndedVault(lendingBatch | featureLendingProtocolV1_1);
}
};
BEAST_DEFINE_TESTSUITE(LendingBatch, tx, xrpl);
} // namespace xrpl::test
+1 -5
View File
@@ -30,11 +30,8 @@
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/tx/transactors/system/Batch.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
@@ -498,8 +495,7 @@ private:
// From FIND-001
testcase << "Batch Bypass Counterparty";
bool const lendingBatchEnabled = !std::ranges::any_of(
Batch::kDisabledTxTypes, [](auto const& disabled) { return disabled == ttLOAN_SET; });
bool const lendingBatchEnabled = features[featureLendingProtocolV1_2];
using namespace jtx;
using namespace std::chrono_literals;
+15 -61
View File
@@ -19,8 +19,8 @@
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/PathAsset.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
@@ -367,6 +367,20 @@ checkArraySize(json::Value const& val, unsigned int size);
std::uint32_t
ownerCount(test::jtx::Env const& env, test::jtx::Account const& account);
/* Token (IOU/MPT) Locking */
/******************************************************************************/
uint64_t
mptEscrowed(jtx::Env const& env, jtx::Account const& account, jtx::MPT const& mpt);
uint64_t
issuerMPTEscrowed(jtx::Env const& env, jtx::MPT const& mpt);
jtx::PrettyAmount
issuerBalance(jtx::Env& env, jtx::Account const& account, Issue const& issue);
jtx::PrettyAmount
issuerEscrowed(jtx::Env& env, jtx::Account const& account, Issue const& issue);
// Helper function that returns the sponsored owner count on an account.
std::uint32_t
sponsoredOwnerCount(test::jtx::Env const& env, test::jtx::Account const& account);
@@ -583,66 +597,6 @@ accountBalance(Env& env, Account const& acct);
[[nodiscard]] bool
expectLedgerEntryRoot(Env& env, Account const& acct, STAmount const& expectedValue);
/* Payment Channel */
/******************************************************************************/
namespace paychan {
json::Value
create(
AccountID const& account,
AccountID const& to,
STAmount const& amount,
NetClock::duration const& settleDelay,
PublicKey const& pk,
std::optional<NetClock::time_point> const& cancelAfter = std::nullopt,
std::optional<std::uint32_t> const& dstTag = std::nullopt);
inline json::Value
create(
Account const& account,
Account const& to,
STAmount const& amount,
NetClock::duration const& settleDelay,
PublicKey const& pk,
std::optional<NetClock::time_point> const& cancelAfter = std::nullopt,
std::optional<std::uint32_t> const& dstTag = std::nullopt)
{
return create(account.id(), to.id(), amount, settleDelay, pk, cancelAfter, dstTag);
}
json::Value
fund(
AccountID const& account,
UInt256 const& channel,
STAmount const& amount,
std::optional<NetClock::time_point> const& expiration = std::nullopt);
json::Value
claim(
AccountID const& account,
UInt256 const& channel,
std::optional<STAmount> const& balance = std::nullopt,
std::optional<STAmount> const& amount = std::nullopt,
std::optional<Slice> const& signature = std::nullopt,
std::optional<PublicKey> const& pk = std::nullopt);
UInt256
channel(AccountID const& account, AccountID const& dst, std::uint32_t seqProxyValue);
inline UInt256
channel(Account const& account, Account const& dst, std::uint32_t seqProxyValue)
{
return channel(account.id(), dst.id(), seqProxyValue);
}
STAmount
channelBalance(ReadView const& view, UInt256 const& chan);
bool
channelExists(ReadView const& view, UInt256 const& chan);
} // namespace paychan
/* Crossing Limits */
/******************************************************************************/
+25
View File
@@ -15,6 +15,8 @@
#include <concepts>
#include <cstdint>
#include <optional>
#include <source_location>
#include <string>
#include <utility>
#include <vector>
@@ -59,6 +61,29 @@ calcConfidentialBatchFee(jtx::Env const& env, uint32_t const& numSigners, uint32
json::Value
outer(jtx::Account const& account, uint32_t seq, STAmount const& fee, std::uint32_t flags);
/**
* @brief Expect an inner Batch transaction to be recorded with the given
* type and result, and with ParentBatchID pointing at its outer Batch.
*
* Looks the inner transaction up with the `tx` RPC, so the ledger holding the
* batch must already be closed. Failures are reported at the caller's line.
*
* @param env The test environment.
* @param batchID Hash of the outer Batch transaction.
* @param txHash Hash of the inner transaction.
* @param txType Expected TransactionType, e.g. "Payment".
* @param result Expected TransactionResult, e.g. "tesSUCCESS".
* @param loc Call site to report failures at.
*/
void
validateInnerTxn(
Env& env,
std::string const& batchID,
std::string const& txHash,
std::string const& txType,
std::string const& result,
std::source_location const& loc = std::source_location::current());
/**
* @brief Adds an inner Batch transaction to a JTx and autofills it.
*/
+48 -101
View File
@@ -16,18 +16,15 @@
#include <xrpld/rpc/Role.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/Job.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ApiVersion.h>
@@ -37,13 +34,11 @@
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/PathAsset.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/STPathSet.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/jss.h>
@@ -53,7 +48,6 @@
#include <xrpl/tx/paths/detail/Steps.h>
#include <algorithm>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <memory>
@@ -95,6 +89,54 @@ ownerCount(Env const& env, Account const& account)
return env.ownerCount(account);
}
/* Token (IOU/MPT) Locking */
/******************************************************************************/
uint64_t
mptEscrowed(jtx::Env const& env, jtx::Account const& account, jtx::MPT const& mpt)
{
auto const sle = env.le(keylet::mptoken(mpt.mpt(), account));
if (sle && sle->isFieldPresent(sfLockedAmount))
return (*sle)[sfLockedAmount];
return 0;
}
uint64_t
issuerMPTEscrowed(jtx::Env const& env, jtx::MPT const& mpt)
{
auto const sle = env.le(keylet::mptokenIssuance(mpt.mpt()));
if (sle && sle->isFieldPresent(sfLockedAmount))
return (*sle)[sfLockedAmount];
return 0;
}
jtx::PrettyAmount
issuerBalance(jtx::Env& env, jtx::Account const& account, Issue const& issue)
{
json::Value params;
params[jss::account] = account.human();
auto jrr = env.rpc("json", "gateway_balances", to_string(params));
auto const result = jrr[jss::result];
auto const obligations = result[jss::obligations][to_string(issue.currency)];
if (obligations.isNull())
return {STAmount(issue, 0), account.name()};
STAmount const amount = amountFromString(issue, obligations.asString());
return {amount, account.name()};
}
jtx::PrettyAmount
issuerEscrowed(jtx::Env& env, jtx::Account const& account, Issue const& issue)
{
json::Value params;
params[jss::account] = account.human();
auto jrr = env.rpc("json", "gateway_balances", to_string(params));
auto const result = jrr[jss::result];
auto const locked = result[jss::locked][to_string(issue.currency)];
if (locked.isNull())
return {STAmount(issue, 0), account.name()};
STAmount const amount = amountFromString(issue, locked.asString());
return {amount, account.name()};
}
std::uint32_t
sponsoredOwnerCount(Env const& env, Account const& account)
{
@@ -500,101 +542,6 @@ expectLedgerEntryRoot(Env& env, Account const& acct, STAmount const& expectedVal
return accountBalance(env, acct) == to_string(expectedValue.xrp());
}
/* Payment Channel */
/******************************************************************************/
namespace paychan {
json::Value
create(
AccountID const& account,
AccountID const& to,
STAmount const& amount,
NetClock::duration const& settleDelay,
PublicKey const& pk,
std::optional<NetClock::time_point> const& cancelAfter,
std::optional<std::uint32_t> const& dstTag)
{
json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelCreate;
jv[jss::Account] = to_string(account);
jv[jss::Destination] = to_string(to);
jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
jv[jss::SettleDelay] = settleDelay.count();
jv[sfPublicKey.fieldName] = strHex(pk.slice());
if (cancelAfter)
jv[sfCancelAfter.fieldName] = cancelAfter->time_since_epoch().count();
if (dstTag)
jv[sfDestinationTag.fieldName] = *dstTag;
return jv;
}
json::Value
fund(
AccountID const& account,
UInt256 const& channel,
STAmount const& amount,
std::optional<NetClock::time_point> const& expiration)
{
json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelFund;
jv[jss::Account] = to_string(account);
jv[sfChannel.fieldName] = to_string(channel);
jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
if (expiration)
jv[sfExpiration.fieldName] = expiration->time_since_epoch().count();
return jv;
}
json::Value
claim(
AccountID const& account,
UInt256 const& channel,
std::optional<STAmount> const& balance,
std::optional<STAmount> const& amount,
std::optional<Slice> const& signature,
std::optional<PublicKey> const& pk)
{
json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelClaim;
jv[jss::Account] = to_string(account);
jv["Channel"] = to_string(channel);
if (amount)
jv[jss::Amount] = amount->getJson(JsonOptions::Values::None);
if (balance)
jv["Balance"] = balance->getJson(JsonOptions::Values::None);
if (signature)
jv["Signature"] = strHex(*signature);
if (pk)
jv["PublicKey"] = strHex(pk->slice());
return jv;
}
UInt256
channel(AccountID const& account, AccountID const& dst, std::uint32_t seqProxyValue)
{
auto const seqProxy = SeqProxy::rawSequence(seqProxyValue);
auto const k = keylet::payChannel(account, dst, seqProxy);
return k.key;
}
STAmount
channelBalance(ReadView const& view, UInt256 const& chan)
{
auto const slep = view.read({ltPAYCHAN, chan});
if (!slep)
return XRPAmount{-1};
return (*slep)[sfBalance];
}
bool
channelExists(ReadView const& view, UInt256 const& chan)
{
auto const slep = view.read({ltPAYCHAN, chan});
return bool(slep);
}
} // namespace paychan
/* Crossing Limits */
/******************************************************************************/
+26
View File
@@ -27,6 +27,8 @@
#include <cstdint>
#include <optional>
#include <ostream>
#include <source_location>
#include <string>
#include <utility>
namespace xrpl::test::jtx::batch {
@@ -62,6 +64,30 @@ outer(jtx::Account const& account, uint32_t seq, STAmount const& fee, std::uint3
return jv;
}
void
validateInnerTxn(
Env& env,
std::string const& batchID,
std::string const& txHash,
std::string const& txType,
std::string const& result,
std::source_location const& loc)
{
json::Value const jrr = env.rpc("tx", txHash)[jss::result];
env.test.expect(
jrr[sfTransactionType.jsonName] == txType, "TransactionType", loc.file_name(), loc.line());
env.test.expect(
jrr[jss::meta][sfTransactionResult.jsonName] == result,
"TransactionResult",
loc.file_name(),
loc.line());
env.test.expect(
jrr[jss::meta][sfParentBatchID.jsonName] == batchID,
"ParentBatchID",
loc.file_name(),
loc.line());
}
void
Inner::operator()(Env& env, JTx& jt) const
{
+140
View File
@@ -0,0 +1,140 @@
#include <test/jtx/paychan.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/PayChan.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/jss.h>
#include <cstdint>
#include <optional>
/**
* Paychan operations.
*/
namespace xrpl::test::jtx::paychan {
json::Value
create(
AccountID const& account,
AccountID const& to,
STAmount const& amount,
NetClock::duration const& settleDelay,
PublicKey const& pk,
std::optional<NetClock::time_point> const& cancelAfter,
std::optional<std::uint32_t> const& dstTag)
{
json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelCreate;
jv[jss::Account] = to_string(account);
jv[jss::Destination] = to_string(to);
jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
jv[jss::SettleDelay] = settleDelay.count();
jv[sfPublicKey.fieldName] = strHex(pk.slice());
if (cancelAfter)
jv[sfCancelAfter.fieldName] = cancelAfter->time_since_epoch().count();
if (dstTag)
jv[sfDestinationTag.fieldName] = *dstTag;
return jv;
}
json::Value
fund(
AccountID const& account,
uint256 const& channel,
STAmount const& amount,
std::optional<NetClock::time_point> const& expiration)
{
json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelFund;
jv[jss::Account] = to_string(account);
jv[sfChannel.fieldName] = to_string(channel);
jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
if (expiration)
jv[sfExpiration.fieldName] = expiration->time_since_epoch().count();
return jv;
}
json::Value
claim(
AccountID const& account,
uint256 const& channel,
std::optional<STAmount> const& balance,
std::optional<STAmount> const& amount,
std::optional<Slice> const& signature,
std::optional<PublicKey> const& pk)
{
json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelClaim;
jv[jss::Account] = to_string(account);
jv["Channel"] = to_string(channel);
if (amount)
jv[jss::Amount] = amount->getJson(JsonOptions::Values::None);
if (balance)
jv["Balance"] = balance->getJson(JsonOptions::Values::None);
if (signature)
jv["Signature"] = strHex(*signature);
if (pk)
jv["PublicKey"] = strHex(pk->slice());
return jv;
}
uint256
channel(AccountID const& account, AccountID const& dst, std::uint32_t seqProxyValue)
{
auto const seqProxy = SeqProxy::rawSequence(seqProxyValue);
auto const k = keylet::payChannel(account, dst, seqProxy);
return k.key;
}
STAmount
channelBalance(ReadView const& view, uint256 const& chan)
{
auto const slep = view.read({ltPAYCHAN, chan});
if (!slep)
return XRPAmount{-1};
return (*slep)[sfBalance];
}
STAmount
channelAmount(ReadView const& view, uint256 const& chan)
{
auto const slep = view.read({ltPAYCHAN, chan});
if (!slep)
return XRPAmount{-1};
return (*slep)[sfAmount];
}
bool
channelExists(ReadView const& view, uint256 const& chan)
{
auto const slep = view.read({ltPAYCHAN, chan});
return bool(slep);
}
Buffer
signClaimAuth(
PublicKey const& pk,
SecretKey const& sk,
uint256 const& channel,
STAmount const& authAmt)
{
Serializer msg;
serializePayChanAuthorization(msg, channel, authAmt.xrp());
return sign(pk, sk, msg.slice());
}
} // namespace xrpl::test::jtx::paychan
+88
View File
@@ -0,0 +1,88 @@
#pragma once
#include <test/jtx/Account.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/SecretKey.h>
#include <cstdint>
#include <optional>
/**
* Paychan operations.
*/
namespace xrpl::test::jtx::paychan {
json::Value
create(
AccountID const& account,
AccountID const& to,
STAmount const& amount,
NetClock::duration const& settleDelay,
PublicKey const& pk,
std::optional<NetClock::time_point> const& cancelAfter = std::nullopt,
std::optional<std::uint32_t> const& dstTag = std::nullopt);
inline json::Value
create(
Account const& account,
Account const& to,
STAmount const& amount,
NetClock::duration const& settleDelay,
PublicKey const& pk,
std::optional<NetClock::time_point> const& cancelAfter = std::nullopt,
std::optional<std::uint32_t> const& dstTag = std::nullopt)
{
return create(account.id(), to.id(), amount, settleDelay, pk, cancelAfter, dstTag);
}
json::Value
fund(
AccountID const& account,
uint256 const& channel,
STAmount const& amount,
std::optional<NetClock::time_point> const& expiration = std::nullopt);
json::Value
claim(
AccountID const& account,
uint256 const& channel,
std::optional<STAmount> const& balance = std::nullopt,
std::optional<STAmount> const& amount = std::nullopt,
std::optional<Slice> const& signature = std::nullopt,
std::optional<PublicKey> const& pk = std::nullopt);
uint256
channel(AccountID const& account, AccountID const& dst, std::uint32_t seqProxyValue);
inline uint256
channel(Account const& account, Account const& dst, std::uint32_t seqProxyValue)
{
return channel(account.id(), dst.id(), seqProxyValue);
}
STAmount
channelBalance(ReadView const& view, uint256 const& chan);
STAmount
channelAmount(ReadView const& view, uint256 const& chan);
bool
channelExists(ReadView const& view, uint256 const& chan);
Buffer
signClaimAuth(
PublicKey const& pk,
SecretKey const& sk,
uint256 const& channel,
STAmount const& authAmt);
} // namespace xrpl::test::jtx::paychan
+16
View File
@@ -319,6 +319,22 @@ class AccountTx_test : public beast::unit_test::Suite
BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), RpcLgrNotFound));
}
{
json::Value p{jParams};
p[jss::ledger_index] = "validated";
BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p))));
p[jss::ledger_index] = "closed";
BEAST_EXPECT(hasTxs(env.rpc(apiVersion, "json", "account_tx", to_string(p))));
p[jss::ledger_index] = "current";
BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), RpcLgrNotValidated));
p[jss::ledger_index] = "";
BEAST_EXPECT(isErr(env.rpc("json", "account_tx", to_string(p)), RpcLgrNotValidated));
}
// Ledger Hash
{
json::Value p{jParams};
+11
View File
@@ -506,7 +506,18 @@ class Feature_test : public beast::unit_test::Suite
feature = *(jrr.begin());
BEAST_EXPECTS(feature[jss::name] == kFeatureName, "name");
BEAST_EXPECTS(feature[jss::vetoed].isBool() && !feature[jss::vetoed].asBool(), "vetoed");
auto testInvalidVetoed = [&](auto const& vetoed) {
json::Value params;
params[jss::feature] = kFeatureName;
params[jss::vetoed] = vetoed;
auto const result = env.rpc("json", "feature", to_string(params))[jss::result];
BEAST_EXPECT(result[jss::error] == "invalidParams");
BEAST_EXPECT(result[jss::error_message] == "Invalid parameters.");
};
testInvalidVetoed("false");
testInvalidVetoed(json::Value(json::ValueType::Null));
// anything other than accept or reject is an error
jrr = env.rpc("feature", kFeatureName, "maybe");
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
+168 -8
View File
@@ -92,6 +92,7 @@ std::vector<std::pair<json::StaticString, FieldType>> gMappings{
{jss::authorized, FieldType::AccountField},
{jss::credential_type, FieldType::BlobField},
{jss::currency, FieldType::CurrencyField},
{jss::destination, FieldType::AccountField},
{jss::issuer, FieldType::AccountField},
{jss::oracle_document_id, FieldType::UInt32Field},
{jss::owner, FieldType::AccountField},
@@ -813,7 +814,8 @@ class LedgerEntry_test : public beast::unit_test::Suite
env.fund(XRP(10000), alice);
env.close();
auto const checkId = keylet::check(env.master, SeqProxy::rawSequence(env.seq(env.master)));
auto const checkSeq = env.seq(env.master);
auto const checkId = keylet::check(env.master, SeqProxy::rawSequence(checkSeq));
env(check::create(env.master, alice, XRP(100)));
env.close();
@@ -829,6 +831,30 @@ class LedgerEntry_test : public beast::unit_test::Suite
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Check);
BEAST_EXPECT(jrr[jss::node][sfSendMax.jsonName] == "100000000");
}
{
// Request a check by account and seq.
json::Value jvParams;
jvParams[jss::check] = json::ValueType::Object;
jvParams[jss::check][jss::account] = env.master.human();
jvParams[jss::check][jss::seq] = checkSeq;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Check);
BEAST_EXPECT(jrr[jss::node][sfSendMax.jsonName] == "100000000");
BEAST_EXPECT(jrr[jss::index] == to_string(checkId.key));
}
{
// Request a non-existent check by account and seq.
json::Value jvParams;
jvParams[jss::check] = json::ValueType::Object;
jvParams[jss::check][jss::account] = env.master.human();
jvParams[jss::check][jss::seq] = checkSeq + 1000;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
}
{
// Request an index that is not a check. We'll use alice's
// account root index.
@@ -849,7 +875,13 @@ class LedgerEntry_test : public beast::unit_test::Suite
}
{
// Check malformed cases
runLedgerEntryTest(env, jss::check);
runLedgerEntryTest(
env,
jss::check,
{
{.fieldName = jss::account, .malformedErrorMsg = "malformedAddress"},
{.fieldName = jss::seq, .malformedErrorMsg = "malformedRequest"},
});
}
}
@@ -1533,8 +1565,8 @@ class LedgerEntry_test : public beast::unit_test::Suite
UInt256 const nftokenID0 = token::getNextID(env, issuer, 0, tfTransferable);
env(token::mint(issuer, 0), Txflags(tfTransferable));
env.close();
UInt256 const offerID =
keylet::nftokenOffer(issuer, SeqProxy::rawSequence(env.seq(issuer))).key;
std::uint32_t const offerSeq = env.seq(issuer);
UInt256 const offerID = keylet::nftokenOffer(issuer, SeqProxy::rawSequence(offerSeq)).key;
env(token::createOffer(issuer, nftokenID0, drops(1)),
token::Destination(buyer),
Txflags(tfSellNFToken));
@@ -1549,9 +1581,39 @@ class LedgerEntry_test : public beast::unit_test::Suite
BEAST_EXPECT(jrr[jss::node][sfNFTokenID.jsonName] == to_string(nftokenID0));
BEAST_EXPECT(jrr[jss::node][sfAmount.jsonName] == "1");
}
{
// Request by owner and seq.
json::Value jvParams;
jvParams[jss::nft_offer] = json::ValueType::Object;
jvParams[jss::nft_offer][jss::owner] = issuer.human();
jvParams[jss::nft_offer][jss::seq] = offerSeq;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::NFTokenOffer);
BEAST_EXPECT(jrr[jss::node][sfOwner.jsonName] == issuer.human());
BEAST_EXPECT(jrr[jss::node][sfNFTokenID.jsonName] == to_string(nftokenID0));
BEAST_EXPECT(jrr[jss::node][sfAmount.jsonName] == "1");
BEAST_EXPECT(jrr[jss::index] == to_string(offerID));
}
{
// Request a non-existent offer by owner and seq.
json::Value jvParams;
jvParams[jss::nft_offer] = json::ValueType::Object;
jvParams[jss::nft_offer][jss::owner] = issuer.human();
jvParams[jss::nft_offer][jss::seq] = offerSeq + 1000;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
}
// negative tests
runLedgerEntryTest(env, jss::nft_offer);
runLedgerEntryTest(
env,
jss::nft_offer,
{
{.fieldName = jss::owner, .malformedErrorMsg = "malformedOwner"},
{.fieldName = jss::seq, .malformedErrorMsg = "malformedRequest"},
});
}
void
@@ -1713,13 +1775,14 @@ class LedgerEntry_test : public beast::unit_test::Suite
return jv;
};
auto const payChanSeq = env.seq(alice);
env(payChanCreate(alice, env.master, XRP(57), 18s, alice.pk()));
env.close();
std::string const ledgerHash{to_string(env.closed()->header().hash)};
UInt256 const payChanIndex{
keylet::payChannel(alice, env.master, SeqProxy::rawSequence(env.seq(alice) - 1)).key};
keylet::payChannel(alice, env.master, SeqProxy::rawSequence(payChanSeq)).key};
{
// Request the payment channel using its index.
json::Value jvParams;
@@ -1731,6 +1794,33 @@ class LedgerEntry_test : public beast::unit_test::Suite
BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName] == "0");
BEAST_EXPECT(jrr[jss::node][sfSettleDelay.jsonName] == 18);
}
{
// Request the payment channel by account, destination, and seq.
json::Value jvParams;
jvParams[jss::payment_channel] = json::ValueType::Object;
jvParams[jss::payment_channel][jss::account] = alice.human();
jvParams[jss::payment_channel][jss::destination] = env.master.human();
jvParams[jss::payment_channel][jss::seq] = payChanSeq;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::node][sfAmount.jsonName] == "57000000");
BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName] == "0");
BEAST_EXPECT(jrr[jss::node][sfSettleDelay.jsonName] == 18);
BEAST_EXPECT(jrr[jss::index] == to_string(payChanIndex));
}
{
// Request a non-existent payment channel by account, destination, and seq.
json::Value jvParams;
jvParams[jss::payment_channel] = json::ValueType::Object;
jvParams[jss::payment_channel][jss::account] = alice.human();
jvParams[jss::payment_channel][jss::destination] = env.master.human();
jvParams[jss::payment_channel][jss::seq] = payChanSeq + 1000;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
}
{
// Request an index that is not a payment channel.
json::Value jvParams;
@@ -1743,7 +1833,14 @@ class LedgerEntry_test : public beast::unit_test::Suite
{
// Malformed paychan field
runLedgerEntryTest(env, jss::payment_channel);
runLedgerEntryTest(
env,
jss::payment_channel,
{
{.fieldName = jss::account, .malformedErrorMsg = "malformedAddress"},
{.fieldName = jss::destination, .malformedErrorMsg = "malformedAddress"},
{.fieldName = jss::seq, .malformedErrorMsg = "malformedRequest"},
});
}
}
@@ -1892,7 +1989,70 @@ class LedgerEntry_test : public beast::unit_test::Suite
testcase("Signer List");
using namespace test::jtx;
Env env{*this};
runLedgerEntryTest(env, jss::signer_list);
Account const alice{"alice"};
Account const bogie{"bogie"};
Account const demon{"demon"};
env.fund(XRP(10000), alice);
env.close();
// Attach phantom signers to alice.
env(signers(alice, 1, {{bogie, 1}, {demon, 1}}));
env.close();
std::string const ledgerHash{to_string(env.closed()->header().hash)};
auto const signerListIndex = keylet::signerList(alice).key;
{
// Request by hash.
json::Value jvParams;
jvParams[jss::signer_list] = to_string(signerListIndex);
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::SignerList);
BEAST_EXPECT(jrr[jss::node][sfSignerQuorum.jsonName] == 1);
}
{
// Request by account.
json::Value jvParams;
jvParams[jss::signer_list] = json::ValueType::Object;
jvParams[jss::signer_list][jss::account] = alice.human();
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::SignerList);
BEAST_EXPECT(jrr[jss::node][sfSignerQuorum.jsonName] == 1);
BEAST_EXPECT(jrr[jss::index] == to_string(signerListIndex));
}
{
// Request a non-existent signer list by account.
json::Value jvParams;
jvParams[jss::signer_list] = json::ValueType::Object;
jvParams[jss::signer_list][jss::account] = env.master.human();
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
}
{
// Request an index that is not a signer list.
json::Value jvParams;
jvParams[jss::signer_list] = ledgerHash;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "entryNotFound", "Entry not found.");
}
{
// Malformed signer_list fields
runLedgerEntryTest(
env,
jss::signer_list,
{
{.fieldName = jss::account, .malformedErrorMsg = "malformedAddress"},
});
}
}
void
+13
View File
@@ -103,6 +103,19 @@ class LedgerRPC_test : public beast::unit_test::Suite
jrr[jss::ledger][jss::ledger_index] == std::to_string(env.current()->header().seq));
BEAST_EXPECT(jrr[jss::ledger_current_index] == env.current()->header().seq);
}
{
auto const jrr = env.rpc("json", "ledger", "{}")[jss::result];
BEAST_EXPECT(!jrr.isMember(jss::ledger));
BEAST_EXPECT(jrr[jss::closed][jss::ledger][jss::closed] == true);
BEAST_EXPECT(
jrr[jss::closed][jss::ledger][jss::ledger_index] ==
std::to_string(env.closed()->header().seq));
BEAST_EXPECT(jrr[jss::open][jss::ledger][jss::closed] == false);
BEAST_EXPECT(
jrr[jss::open][jss::ledger][jss::ledger_index] ==
std::to_string(env.current()->header().seq));
}
}
void
+9 -9
View File
@@ -1,4 +1,3 @@
#include <xrpld/rpc/Status.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpl/beast/unit_test/suite.h>
@@ -24,49 +23,50 @@ public:
// Test no type.
json::Value tx = json::ValueType::Object;
auto result = rpc::chooseLedgerEntryType(tx);
BEAST_EXPECT(result.first == rpc::Status::kOK);
BEAST_EXPECT(result.first == ::rpc::Status::kOK);
BEAST_EXPECT(result.second == 0);
// Test empty type.
tx[jss::type] = "";
result = rpc::chooseLedgerEntryType(tx);
BEAST_EXPECT(result.first == rpc::Status{RpcInvalidParams});
BEAST_EXPECT(result.first == ::rpc::Status(RpcInvalidParams, "Invalid field 'type'."));
BEAST_EXPECT(result.second == 0);
// Test type using canonical name in mixedcase.
tx[jss::type] = "MPTokenIssuance";
result = rpc::chooseLedgerEntryType(tx);
BEAST_EXPECT(result.first == rpc::Status::kOK);
BEAST_EXPECT(result.first == ::rpc::Status::kOK);
BEAST_EXPECT(result.second == ltMPTOKEN_ISSUANCE);
// Test type using canonical name in lowercase.
tx[jss::type] = "mptokenissuance";
result = rpc::chooseLedgerEntryType(tx);
BEAST_EXPECT(result.first == rpc::Status::kOK);
BEAST_EXPECT(result.first == ::rpc::Status::kOK);
BEAST_EXPECT(result.second == ltMPTOKEN_ISSUANCE);
// Test type using RPC name with exact match.
tx[jss::type] = "mpt_issuance";
result = rpc::chooseLedgerEntryType(tx);
BEAST_EXPECT(result.first == rpc::Status::kOK);
BEAST_EXPECT(result.first == ::rpc::Status::kOK);
BEAST_EXPECT(result.second == ltMPTOKEN_ISSUANCE);
// Test type using RPC name with inexact match.
tx[jss::type] = "MPT_Issuance";
result = rpc::chooseLedgerEntryType(tx);
BEAST_EXPECT(result.first == rpc::Status{RpcInvalidParams});
BEAST_EXPECT(result.first == ::rpc::Status(RpcInvalidParams, "Invalid field 'type'."));
BEAST_EXPECT(result.second == 0);
// Test invalid type.
tx[jss::type] = 1234;
result = rpc::chooseLedgerEntryType(tx);
BEAST_EXPECT(result.first == rpc::Status{RpcInvalidParams});
BEAST_EXPECT(
result.first == ::rpc::Status(RpcInvalidParams, "Invalid field 'type', not string."));
BEAST_EXPECT(result.second == 0);
// Test unknown type.
tx[jss::type] = "unknown";
result = rpc::chooseLedgerEntryType(tx);
BEAST_EXPECT(result.first == rpc::Status{RpcInvalidParams});
BEAST_EXPECT(result.first == ::rpc::Status(RpcInvalidParams, "Invalid field 'type'."));
BEAST_EXPECT(result.second == 0);
}
-207
View File
@@ -1,207 +0,0 @@
#include <xrpld/rpc/Status.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/jss.h>
#include <algorithm>
#include <cstddef>
#include <exception>
#include <string>
namespace xrpl::rpc {
class codeString_test : public beast::unit_test::Suite
{
private:
template <typename Type>
std::string
codeString(Type t)
{
return Status(t).codeString();
}
void
testOk()
{
testcase("OK");
{
auto s = codeString(Status());
expect(s.empty(), "String for OK status");
}
{
auto s = codeString(Status::kOK);
expect(s.empty(), "String for OK status");
}
{
auto s = codeString(0);
expect(s.empty(), "String for 0 status");
}
{
auto s = codeString(tesSUCCESS);
expect(s.empty(), "String for tesSUCCESS");
}
{
auto s = codeString(RpcSuccess);
expect(s.empty(), "String for rpcSUCCESS");
}
}
void
testError()
{
testcase("error");
{
auto s = codeString(23);
expect(s == "23", s);
}
{
auto s = codeString(temBAD_AMOUNT);
expect(s == "temBAD_AMOUNT: Malformed: Bad amount.", s);
}
{
auto s = codeString(RpcBadSyntax);
expect(s == "badSyntax: Syntax error.", s);
}
}
public:
void
run() override
{
testOk();
testError();
}
};
BEAST_DEFINE_TESTSUITE(codeString, rpc, RPC);
class fillJson_test : public beast::unit_test::Suite
{
private:
json::Value value_;
template <typename Type>
void
fillJson(Type t)
{
value_.clear();
Status(t).fillJson(value_);
}
void
testOk()
{
testcase("OK");
fillJson(Status());
expect(!value_, "Value for empty status");
fillJson(0);
expect(!value_, "Value for 0 status");
fillJson(Status::kOK);
expect(!value_, "Value for OK status");
fillJson(tesSUCCESS);
expect(!value_, "Value for tesSUCCESS");
fillJson(RpcSuccess);
expect(!value_, "Value for rpcSUCCESS");
}
template <typename Type>
void
expectFill(
std::string const& label,
Type status,
Status::Strings messages,
std::string const& message)
{
value_.clear();
fillJson(Status(status, messages));
auto prefix = label + ": ";
expect(bool(value_), prefix + "No value");
auto error = value_[jss::error];
expect(bool(error), prefix + "No error.");
auto code = error[jss::code].asInt();
expect(
status == code,
prefix + "Wrong status " + std::to_string(code) + " != " + std::to_string(status));
auto m = error[jss::message].asString();
expect(m == message, m + " != " + message);
auto d = error[jss::data];
size_t const s1 = d.size();
size_t const s2 = messages.size();
expect(
s1 == s2,
prefix + "Data sizes differ " + std::to_string(s1) + " != " + std::to_string(s2));
for (auto i = 0; i < std::min(s1, s2); ++i)
{
auto ds = d[i].asString();
expect(ds == messages[i], prefix + ds + " != " + messages[i]);
}
}
void
testError()
{
testcase("error");
expectFill("temBAD_AMOUNT", temBAD_AMOUNT, {}, "temBAD_AMOUNT: Malformed: Bad amount.");
expectFill(
"RpcBadSyntax",
RpcBadSyntax,
{"An error.", "Another error."},
"badSyntax: Syntax error.");
expectFill("integer message", 23, {"Stuff."}, "23");
}
void
testThrow()
{
testcase("throw");
try
{
Throw<Status>(Status(temBAD_PATH, {"path=sdcdfd"}));
}
catch (Status const& s)
{
expect(s.toTER() == temBAD_PATH, "temBAD_PATH wasn't thrown");
auto msgs = s.messages();
expect(msgs.size() == 1, "Wrong number of messages");
expect(msgs[0] == "path=sdcdfd", msgs[0]);
}
catch (std::exception const&)
{
expect(false, "Didn't catch a Status");
}
}
public:
void
run() override
{
testOk();
testError();
testThrow();
}
};
BEAST_DEFINE_TESTSUITE(fillJson, rpc, xrpl);
} // namespace xrpl::rpc
+23
View File
@@ -59,6 +59,19 @@ class TransactionEntry_test : public beast::unit_test::Suite
BEAST_EXPECT(result[jss::status] == "error");
}
{
// No ledger selects the current one, not any ledger.
json::Value params{json::ValueType::Object};
params[jss::tx_hash] =
"E2FE8D4AF3FCC3944DDF6CD8CDDC5E3F0AD50863EF8919AFEF10CB6408CD4D05";
auto const result = env.client().invoke("transaction_entry", params)[jss::result];
BEAST_EXPECT(result[jss::error] == "notYetImplemented");
BEAST_EXPECT(result[jss::status] == "error");
BEAST_EXPECT(result.isMember(jss::ledger_current_index));
BEAST_EXPECT(!result.isMember(jss::ledger_hash));
BEAST_EXPECT(result[jss::validated] == false);
}
{
json::Value params{json::ValueType::Object};
params[jss::ledger] = "closed";
@@ -69,6 +82,16 @@ class TransactionEntry_test : public beast::unit_test::Suite
BEAST_EXPECT(result[jss::status] == "error");
}
for (auto const type : {json::ValueType::Object, json::ValueType::Array})
{
json::Value params{json::ValueType::Object};
params[jss::ledger] = "closed";
params[jss::tx_hash] = json::Value{type};
auto const result = env.client().invoke("transaction_entry", params)[jss::result];
BEAST_EXPECT(result[jss::error] == "malformedRequest");
BEAST_EXPECT(result[jss::status] == "error");
}
std::string const txHash{
"E2FE8D4AF3FCC3944DDF6CD8CDDC5E3F0AD50863EF8919AFEF10CB6408CD4D05"};
+19 -8
View File
@@ -38,6 +38,7 @@
#include <xrpl/ledger/entries/XChainOwnedClaimIDEntry.h> // IWYU pragma: keep
#include <xrpl/ledger/entries/XChainOwnedCreateAccountClaimIDEntry.h> // IWYU pragma: keep
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -55,11 +56,11 @@
namespace xrpl {
// The entry classes have no consumers yet, and an un-instantiated class
// template is barely type-checked. Instantiate every one explicitly so the
// compiler actually checks them. Keep this block even once real call sites
// exist: it is what catches a new ledger entry type being added without its
// wrapper class, or the wrapper class existing but never actually being used.
// An un-instantiated class template is barely type-checked. Name every entry
// class in a static_assert below so its class definition is instantiated and
// type-checked; the per-type suites check the member bodies. Keep this block
// even once real call sites exist: it is what catches a new ledger entry type
// being added without its wrapper class.
//
// Driving this off ledger_entries.macro keeps it exhaustive by construction:
// adding a ledger entry type without adding its entry class stops compiling
@@ -76,12 +77,22 @@ template class SLEBase<ApplyView>;
#pragma push_macro("LEDGER_ENTRY")
#undef LEDGER_ENTRY
// No explicit instantiation here, neither a definition nor a declaration.
// A definition would instantiate an entry a second time once its member
// bodies live in its own .cpp (MSVC rejects that with C4661 under /WX), and a
// declaration (`extern template`) stops clang from emitting the inline
// constructors of the header-only entries at -O0, which leaves them undefined
// at link time in the debug builds. Naming the class in a static_assert is
// enough to instantiate its definition, which is all this exhaustiveness
// check needs; the per-type suites exercise the members.
#define LEDGER_ENTRY(tag, value, name, ...) \
template class name##Entry<ReadView>; \
template class name##Entry<ApplyView>; \
static_assert( \
name##Entry<ReadView>::kEntryType == tag && name##Entry<ApplyView>::kEntryType == tag, \
#name "Entry must be bound to " #tag);
#name "Entry must be bound to " #tag); \
static_assert( \
std::is_constructible_v<name##Entry<ReadView>, Keylet const&, ReadView const&> && \
std::is_constructible_v<name##Entry<ApplyView>, Keylet const&, ApplyView&>, \
#name "Entry must be constructible from a Keylet and a view");
#include <xrpl/protocol/detail/ledger_entries.macro>
+14
View File
@@ -311,6 +311,20 @@ TEST(SpecBridge, shared_spec_warns_on_a_deprecated_field)
EXPECT_EQ(warnings[0].code, ::rpc::WarningCode::WarnRpcDeprecated);
}
TEST(SpecBridge, inject_spec_error_uses_default_message)
{
json::Value out{json::ValueType::Object};
rpc::injectSpecError(out, ::rpc::Status{RpcLgrNotFound});
EXPECT_EQ(out, rpc::makeError(RpcLgrNotFound));
}
TEST(SpecBridge, inject_spec_error_uses_custom_message)
{
json::Value out{json::ValueType::Object};
rpc::injectSpecError(out, ::rpc::Status{RpcInvalidParams, "Invalid field 'ledger'."});
EXPECT_EQ(out, rpc::makeError(RpcInvalidParams, "Invalid field 'ledger'."));
}
TEST(SpecBridge, inject_spec_warnings)
{
std::string const kDeprecatedBase{
+3 -2
View File
@@ -2,10 +2,11 @@
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/Status.h>
#include <xrpl/json/json_value.h>
#include <rpcspec/Errors.hpp>
#include <string_view>
namespace xrpl::rpc {
@@ -15,7 +16,7 @@ struct JsonContext;
/**
* Execute an RPC command and store the results in a json::Value.
*/
Status
::rpc::Status
doCommand(rpc::JsonContext&, json::Value&);
Role
-161
View File
@@ -1,161 +0,0 @@
#pragma once
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/TER.h>
#include <exception>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
namespace xrpl::rpc {
/**
* Status represents the results of an operation that might fail.
*
* It wraps the legacy codes TER and error_code_i, providing both a uniform
* interface and a way to attach additional information to existing status
* returns.
*
* A Status can also be used to fill a json::Value with a JSON-RPC 2.0
* error response: see http://www.jsonrpc.org/specification#error_object
*/
struct Status : public std::exception
{
public:
enum class Type { None, TER, ErrorCodeI };
using Code = int;
using Strings = std::vector<std::string>;
static constexpr Code kOK = 0;
Status() = default;
// The constraint allows only integers (not enums). Prevents enum narrowing.
template <typename T>
Status(T code, Strings d = {})
requires(std::is_integral_v<T>)
: code_(code), messages_(std::move(d))
{
}
Status(TER ter, Strings d = {})
: type_(Type::TER), code_(TERtoInt(ter)), messages_(std::move(d))
{
}
Status(ErrorCodeI e, Strings d = {})
: type_(Type::ErrorCodeI), code_(e), messages_(std::move(d))
{
}
Status(ErrorCodeI e, std::string const& s) : type_(Type::ErrorCodeI), code_(e), messages_({s})
{
}
/**
* If the Status is OK, the result is an empty string.
*
* @return a representation of the integer status Code as a string.
*/
[[nodiscard]] std::string
codeString() const;
/**
* Returns true if the Status is *not* OK.
*/
operator bool() const
{
return code_ != kOK;
}
/**
* Returns true if the Status is OK.
*/
bool
operator!() const
{
return !bool(*this);
}
/**
* Returns the Status as a TER.
* This may only be called if type() == Type::TER.
*/
[[nodiscard]] TER
toTER() const
{
XRPL_ASSERT(type_ == Type::TER, "xrpl::rpc::Status::toTER : type is TER");
return TER::fromInt(code_);
}
/**
* Returns the Status as an error_code_i.
* This may only be called if type() == Type::ErrorCodeI.
*/
[[nodiscard]] ErrorCodeI
toErrorCode() const
{
XRPL_ASSERT(
type_ == Type::ErrorCodeI, "xrpl::rpc::Status::toErrorCode : type is error code");
return ErrorCodeI(code_);
}
/**
* Apply the Status to a JsonObject
*/
void
inject(json::Value& object) const
{
if (auto ec = toErrorCode())
{
if (messages_.empty())
{
injectError(ec, object);
}
else
{
injectError(ec, message(), object);
}
}
}
[[nodiscard]] Strings const&
messages() const
{
return messages_;
}
/**
* Return the first message, if any.
*/
[[nodiscard]] std::string
message() const;
[[nodiscard]] Type
type() const
{
return type_;
}
[[nodiscard]] std::string
toString() const;
/**
* Fill a json::Value with an RPC 2.0 response.
* If the Status is OK, fillJson has no effect.
* Not currently used.
*/
void
fillJson(json::Value&);
private:
Type type_ = Type::None;
Code code_ = kOK;
Strings messages_;
};
} // namespace xrpl::rpc
+78 -77
View File
@@ -3,20 +3,26 @@
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/MethodNames.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/Status.h>
#include <xrpld/rpc/detail/SpecBridge.hpp>
#include <xrpld/rpc/handlers/Handlers.h>
#include <xrpld/rpc/handlers/ledger/Ledger.h>
#include <xrpld/rpc/handlers/orderbook/BookChanges.h>
#include <xrpld/rpc/handlers/server_info/Version.h>
#include <xrpld/rpc/handlers/transaction/TransactionEntry.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ApiVersion.h>
#include <rpcspec/Errors.hpp>
#include <rpcspec/Types.hpp>
#include <algorithm>
#include <array>
#include <concepts>
#include <cstddef>
#include <iterator>
#include <expected>
#include <span>
#include <string_view>
#include <utility>
@@ -34,7 +40,7 @@ using Method = Handler::Method;
* names a plain function instead of returning a closure over it.
*/
template <json::Value (*Function)(JsonContext&)>
Status
::rpc::Status
byRef(JsonContext& context, json::Value& result)
{
result = Function(context);
@@ -46,53 +52,66 @@ byRef(JsonContext& context, json::Value& result)
// LCOV_EXCL_STOP
}
return Status();
}
template <class HandlerImpl>
Status
handle(JsonContext& context, json::Value& object)
{
XRPL_ASSERT(
context.apiVersion >= HandlerImpl::minApiVer &&
context.apiVersion <= HandlerImpl::maxApiVer,
"xrpl::rpc::handle : valid API version");
HandlerImpl handler(context);
auto status = handler.check();
if (status)
{
status.inject(object);
}
else
{
handler.writeResult(object);
}
return status;
return ::rpc::Status::kOK;
}
template <typename HandlerImpl>
constexpr Handler
handlerFrom()
{
static_assert(HandlerImpl::minApiVer <= HandlerImpl::maxApiVer);
static_assert(HandlerImpl::maxApiVer <= rpc::kApiMaximumValidVersion);
static_assert(rpc::kApiMinimumSupportedVersion <= HandlerImpl::minApiVer);
concept SpecBasedHandler = std::derived_from<HandlerImpl, HandlerFor<typename HandlerImpl::Input>>;
return {
HandlerImpl::name,
Method::of<&handle<HandlerImpl>>(),
HandlerImpl::role,
HandlerImpl::condition,
HandlerImpl::minApiVer,
HandlerImpl::maxApiVer,
};
template <typename HandlerImpl>
concept InputlessHandler = requires(HandlerImpl& handler) { handler.process(); };
template <class HandlerImpl, class Output>
::rpc::Status
respond(
HandlerImpl const& handler,
json::Value& object,
std::expected<Output, ::rpc::Status> const& output,
::rpc::spec::Warnings const& warnings)
{
if (not output.has_value())
{
injectSpecError(object, output.error());
return output.error();
}
json::Value result;
handler.writeResult(result, *output);
injectSpecWarnings(result, warnings);
object = std::move(result);
return ::rpc::Status::kOK;
}
// The handlers that name the function they dispatch to. The order is free:
// getHandler() searches kHandlers below, which is this array and the next one
// sorted together.
constexpr auto kFunctionHandlerArray = std::to_array<Handler>({
template <class HandlerImpl>
::rpc::Status
handle(JsonContext& context, json::Value& object)
requires SpecBasedHandler<HandlerImpl> or InputlessHandler<HandlerImpl>
{
if constexpr (SpecBasedHandler<HandlerImpl>)
{
auto const input = HandlerImpl::parseInput(context.params, context.apiVersion);
if (not input.has_value())
{
injectSpecError(object, input.error());
return input.error();
}
HandlerImpl handler(context);
auto const output = handler.process(*input);
auto const warnings = output.has_value()
? HandlerImpl::spec(context.apiVersion).check(context.params)
: ::rpc::spec::Warnings{};
return respond(handler, object, output, warnings);
}
else
{
HandlerImpl handler(context);
return respond(handler, object, handler.process(), /* warnings= */ {});
}
}
constexpr auto kHandlerArray = std::to_array<Handler>({
// Request-response methods
{
.name = method::kAccountInfo,
@@ -157,7 +176,7 @@ constexpr auto kFunctionHandlerArray = std::to_array<Handler>({
},
{
.name = method::kBookChanges,
.valueMethod = Method::of<&byRef<&doBookChanges>>(),
.valueMethod = Method::of<&handle<BookChangesHandler>>(),
.role = Role::USER,
.condition = Condition::NoCondition,
},
@@ -241,6 +260,12 @@ constexpr auto kFunctionHandlerArray = std::to_array<Handler>({
.condition = Condition::NoCondition,
.hasCommandLineForm = false,
},
{
.name = method::kLedger,
.valueMethod = Method::of<&handle<LedgerHandler>>(),
.role = Role::USER,
.condition = Condition::NoCondition,
},
{
.name = method::kLedgerAccept,
.valueMethod = Method::of<&byRef<&doLedgerAccept>>(),
@@ -448,7 +473,7 @@ constexpr auto kFunctionHandlerArray = std::to_array<Handler>({
},
{
.name = method::kTransactionEntry,
.valueMethod = Method::of<&byRef<&doTransactionEntry>>(),
.valueMethod = Method::of<&handle<TransactionEntryHandler>>(),
.role = Role::USER,
.condition = Condition::NoCondition,
},
@@ -511,6 +536,12 @@ constexpr auto kFunctionHandlerArray = std::to_array<Handler>({
.role = Role::USER,
.condition = Condition::NoCondition,
},
{
.name = method::kVersion,
.valueMethod = Method::of<&handle<VersionHandler>>(),
.role = Role::USER,
.condition = Condition::NoCondition,
},
{
.name = method::kWalletPropose,
.valueMethod = Method::of<&byRef<&doWalletPropose>>(),
@@ -532,39 +563,9 @@ constexpr auto kFunctionHandlerArray = std::to_array<Handler>({
},
});
// The class-based handlers, which carry their name and API range as static
// members rather than as a table entry, so they cannot go in the array above.
constexpr auto kClassHandlerArray = std::to_array<Handler>({
handlerFrom<LedgerHandler>(),
handlerFrom<VersionHandler>(),
});
/**
* Join the two handler arrays above into one.
*
* Handler has no default constructor, so every entry is built in place from an
* index pack rather than the array being sized and then copied into. The packs
* come from the arrays themselves, so adding a handler to either needs no change
* here.
*
* @return kFunctionHandlerArray followed by kClassHandlerArray.
*/
constexpr auto
joinHandlers()
{
constexpr auto kFunctionIndices = std::make_index_sequence<std::size(kFunctionHandlerArray)>{};
constexpr auto kClassIndices = std::make_index_sequence<std::size(kClassHandlerArray)>{};
return []<std::size_t... Function, std::size_t... Class>(
std::index_sequence<Function...>, std::index_sequence<Class...>) {
return std::array<Handler, sizeof...(Function) + sizeof...(Class)>{
kFunctionHandlerArray[Function]..., kClassHandlerArray[Class]...};
}(kFunctionIndices, kClassIndices);
}
// The whole dispatch table.
constexpr auto kHandlers = [] {
auto all = joinHandlers();
auto all = kHandlerArray;
// Sorted by name, so a handler can be found by binary search.
std::ranges::sort(all, {}, &Handler::name);
+4 -3
View File
@@ -2,7 +2,6 @@
#include <xrpld/rpc/RPCHandler.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/Status.h>
#include <xrpld/rpc/detail/Tuning.h>
#include <xrpl/basics/Log.h>
@@ -13,6 +12,8 @@
#include <xrpl/protocol/jss.h>
#include <xrpl/server/NetworkOPs.h>
#include <rpcspec/Errors.hpp>
#include <span>
#include <string_view>
@@ -47,7 +48,7 @@ struct Handler
class Method
{
public:
using Function = Status (*)(JsonContext&, json::Value&);
using Function = ::rpc::Status (*)(JsonContext&, json::Value&);
/**
* Build a Method that calls a given function.
@@ -69,7 +70,7 @@ struct Handler
* @param result The object the function writes its reply into.
* @return The status the function returns.
*/
Status
::rpc::Status
operator()(JsonContext& context, json::Value& result) const
{
return fn_(context, result);
+4 -3
View File
@@ -5,7 +5,6 @@
#include <xrpld/core/Config.h>
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/Status.h>
#include <xrpld/rpc/detail/Handler.h>
#include <xrpld/rpc/detail/Tuning.h>
@@ -18,6 +17,8 @@
#include <xrpl/protocol/jss.h>
#include <xrpl/resource/Fees.h>
#include <rpcspec/Errors.hpp>
#include <atomic>
#include <chrono>
#include <cstdint>
@@ -154,7 +155,7 @@ fillHandler(JsonContext& context, Handler const*& result)
return RpcSuccess;
}
Status
::rpc::Status
callMethod(JsonContext& context, Handler::Method method, std::string_view name, json::Value& result)
{
static std::atomic<std::uint64_t> kRequestId{0};
@@ -190,7 +191,7 @@ callMethod(JsonContext& context, Handler::Method method, std::string_view name,
} // namespace
Status
::rpc::Status
doCommand(rpc::JsonContext& context, json::Value& result)
{
Handler const* handler = nullptr;
+6 -13
View File
@@ -3,7 +3,6 @@
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/DeliveredAmount.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/Status.h>
#include <xrpld/rpc/detail/Tuning.h>
#include <xrpl/basics/Log.h>
@@ -12,7 +11,6 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
@@ -33,6 +31,8 @@
#include <boost/algorithm/string/predicate.hpp>
#include <rpcspec/Errors.hpp>
#include <algorithm>
#include <array>
#include <cstdint>
@@ -370,10 +370,10 @@ keypairForSignature(json::Value const& params, json::Value& error, unsigned int
return generateKeyPair(*keyType, *seed);
}
std::pair<rpc::Status, LedgerEntryType>
std::pair<::rpc::Status, LedgerEntryType>
chooseLedgerEntryType(json::Value const& params)
{
std::pair<rpc::Status, LedgerEntryType> result{rpc::Status::kOK, ltANY};
std::pair<::rpc::Status, LedgerEntryType> result{::rpc::Status::kOK, ltANY};
if (params.isMember(jss::type))
{
static constexpr auto kTypes =
@@ -392,10 +392,7 @@ chooseLedgerEntryType(json::Value const& params)
auto const& p = params[jss::type];
if (!p.isString())
{
result.first = rpc::Status{RpcInvalidParams, "Invalid field 'type', not string."};
XRPL_ASSERT(
result.first.type() == rpc::Status::Type::ErrorCodeI,
"xrpl::rpc::chooseLedgerEntryType : first valid result type");
result.first = ::rpc::Status{RpcInvalidParams, "Invalid field 'type', not string."};
return result;
}
@@ -408,11 +405,7 @@ chooseLedgerEntryType(json::Value const& params)
});
if (iter == kTypes.end())
{
result.first = rpc::Status{RpcInvalidParams, "Invalid field 'type'."};
XRPL_ASSERT(
result.first.type() == rpc::Status::Type::ErrorCodeI,
"xrpl::rpc::chooseLedgerEntryType : second valid result "
"type");
result.first = ::rpc::Status{RpcInvalidParams, "Invalid field 'type'."};
return result;
}
result.second = std::get<2>(*iter);
+3 -2
View File
@@ -1,7 +1,6 @@
#pragma once
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/Status.h>
#include <xrpld/rpc/detail/Tuning.h>
#include <xrpl/basics/UnorderedContainers.h>
@@ -21,6 +20,8 @@
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/server/NetworkOPs.h>
#include <rpcspec/Errors.hpp>
#include <cstdint>
#include <optional>
#include <utility>
@@ -147,7 +148,7 @@ parseXrplLibSeed(json::Value const& params);
* @param params The JSON value containing RPC parameters.
* @return A pair consisting of the RPC status and the chosen LedgerEntryType.
*/
std::pair<rpc::Status, LedgerEntryType>
std::pair<::rpc::Status, LedgerEntryType>
chooseLedgerEntryType(json::Value const& params);
/**
+83 -47
View File
@@ -6,7 +6,7 @@
#include <xrpld/app/ledger/LedgerToJson.h>
#include <xrpld/app/main/Application.h>
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/Status.h>
#include <xrpld/rpc/detail/SpecBridge.hpp>
#include <xrpld/rpc/detail/Tuning.h>
#include <xrpl/basics/base_uint.h>
@@ -16,7 +16,6 @@
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/LedgerShortcut.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/RippleLedgerHash.h>
#include <xrpl/protocol/jss.h>
@@ -25,15 +24,29 @@
#include <org/xrpl/rpc/v1/get_ledger_data.pb.h> // IWYU pragma: keep
#include <org/xrpl/rpc/v1/get_ledger_entry.pb.h> // IWYU pragma: keep
#include <org/xrpl/rpc/v1/ledger.pb.h>
#include <rpcspec/Errors.hpp>
#include <rpcspec/Ledger.hpp>
#include <cstdint>
#include <expected>
#include <memory>
#include <utility>
#include <variant>
namespace xrpl::rpc {
namespace {
template <typename... Ts>
struct Overload : Ts...
{
using Ts::operator()...;
constexpr Overload(Ts&&... ts) : Ts(std::forward<Ts>(ts))...
{
}
};
bool
isValidatedOld(LedgerMaster& ledgerMaster, bool standalone)
{
@@ -44,7 +57,7 @@ isValidatedOld(LedgerMaster& ledgerMaster, bool standalone)
}
template <class T>
Status
::rpc::Status
ledgerFromHash(
T& ledger,
json::Value hash,
@@ -58,7 +71,7 @@ ledgerFromHash(
}
template <class T>
Status
::rpc::Status
ledgerFromIndex(
T& ledger,
json::Value indexValue,
@@ -68,13 +81,13 @@ ledgerFromIndex(
auto const index = indexValue.asString();
if (index == "current" || index.empty())
return getLedger(ledger, LedgerShortcut::Current, context);
return getLedger(ledger, ::rpc::spec::LedgerShortcut::Current, context);
if (index == "validated")
return getLedger(ledger, LedgerShortcut::Validated, context);
return getLedger(ledger, ::rpc::spec::LedgerShortcut::Validated, context);
if (index == "closed")
return getLedger(ledger, LedgerShortcut::Closed, context);
return getLedger(ledger, ::rpc::spec::LedgerShortcut::Closed, context);
std::uint32_t iVal = 0;
if (!beast::lexicalCastChecked(iVal, index))
@@ -84,7 +97,7 @@ ledgerFromIndex(
}
template <class T>
Status
::rpc::Status
ledgerFromRequest(T& ledger, JsonContext const& context)
{
ledger.reset();
@@ -146,12 +159,12 @@ ledgerFromRequest(T& ledger, JsonContext const& context)
}
// nothing specified, `index` has a default setting
return getLedger(ledger, LedgerShortcut::Current, context);
return getLedger(ledger, ::rpc::spec::LedgerShortcut::Current, context);
}
} // namespace
template <class T, class R>
Status
::rpc::Status
ledgerFromRequest(T& ledger, GRPCContext<R> const& context)
{
R const& request = context.params;
@@ -159,25 +172,25 @@ ledgerFromRequest(T& ledger, GRPCContext<R> const& context)
}
// explicit instantiation of above function
template Status
template ::rpc::Status
ledgerFromRequest<>(
std::shared_ptr<ReadView const>&,
GRPCContext<org::xrpl::rpc::v1::GetLedgerEntryRequest> const&);
// explicit instantiation of above function
template Status
template ::rpc::Status
ledgerFromRequest<>(
std::shared_ptr<ReadView const>&,
GRPCContext<org::xrpl::rpc::v1::GetLedgerDataRequest> const&);
// explicit instantiation of above function
template Status
template ::rpc::Status
ledgerFromRequest<>(
std::shared_ptr<ReadView const>&,
GRPCContext<org::xrpl::rpc::v1::GetLedgerRequest> const&);
template <class T>
Status
::rpc::Status
ledgerFromSpecifier(
T& ledger,
org::xrpl::rpc::v1::LedgerSpecifier const& specifier,
@@ -204,36 +217,36 @@ ledgerFromSpecifier(
auto const shortcut = specifier.shortcut();
if (shortcut == org::xrpl::rpc::v1::LedgerSpecifier::SHORTCUT_VALIDATED)
{
return getLedger(ledger, LedgerShortcut::Validated, context);
return getLedger(ledger, ::rpc::spec::LedgerShortcut::Validated, context);
}
if (shortcut == org::xrpl::rpc::v1::LedgerSpecifier::SHORTCUT_CURRENT ||
shortcut == org::xrpl::rpc::v1::LedgerSpecifier::SHORTCUT_UNSPECIFIED)
{
return getLedger(ledger, LedgerShortcut::Current, context);
return getLedger(ledger, ::rpc::spec::LedgerShortcut::Current, context);
}
if (shortcut == org::xrpl::rpc::v1::LedgerSpecifier::SHORTCUT_CLOSED)
{
return getLedger(ledger, LedgerShortcut::Closed, context);
return getLedger(ledger, ::rpc::spec::LedgerShortcut::Closed, context);
}
}
}
return Status::kOK;
return ::rpc::Status::kOK;
}
template <class T>
Status
::rpc::Status
getLedger(T& ledger, UInt256 const& ledgerHash, Context const& context)
{
ledger = context.ledgerMaster.getLedgerByHash(ledgerHash);
if (ledger == nullptr)
return {RpcLgrNotFound, "ledgerNotFound"};
return Status::kOK;
return ::rpc::Status::kOK;
}
template <class T>
Status
::rpc::Status
getLedger(T& ledger, uint32_t ledgerIndex, Context const& context)
{
ledger = context.ledgerMaster.getLedgerBySeq(ledgerIndex);
@@ -258,12 +271,12 @@ getLedger(T& ledger, uint32_t ledgerIndex, Context const& context)
return {RpcNotSynced, "notSynced"};
}
return Status::kOK;
return ::rpc::Status::kOK;
}
template <class T>
Status
getLedger(T& ledger, LedgerShortcut shortcut, Context const& context)
::rpc::Status
getLedger(T& ledger, ::rpc::spec::LedgerShortcut shortcut, Context const& context)
{
if (isValidatedOld(context.ledgerMaster, context.app.config().standalone()))
{
@@ -272,7 +285,7 @@ getLedger(T& ledger, LedgerShortcut shortcut, Context const& context)
return {RpcNotSynced, "notSynced"};
}
if (shortcut == LedgerShortcut::Validated)
if (shortcut == ::rpc::spec::LedgerShortcut::Validated)
{
ledger = context.ledgerMaster.getValidatedLedger();
if (ledger == nullptr)
@@ -286,12 +299,12 @@ getLedger(T& ledger, LedgerShortcut shortcut, Context const& context)
}
else
{
if (shortcut == LedgerShortcut::Current)
if (shortcut == ::rpc::spec::LedgerShortcut::Current)
{
ledger = context.ledgerMaster.getCurrentLedger();
XRPL_ASSERT(ledger->open(), "xrpl::rpc::getLedger : current is open");
}
else if (shortcut == LedgerShortcut::Closed)
else if (shortcut == ::rpc::spec::LedgerShortcut::Closed)
{
ledger = context.ledgerMaster.getClosedLedger();
XRPL_ASSERT(!ledger->open(), "xrpl::rpc::getLedger : closed is not open");
@@ -318,21 +331,38 @@ getLedger(T& ledger, LedgerShortcut shortcut, Context const& context)
return {RpcNotSynced, "notSynced"};
}
}
return Status::kOK;
return ::rpc::Status::kOK;
}
// Explicit instantiation of above three functions
template Status
template <class T>
::rpc::Status
getLedger(T& ledger, ::rpc::spec::LedgerSpecifier const& specifier, Context const& context)
{
return std::visit(
Overload{
[&](std::monostate) {
return getLedger(ledger, ::rpc::spec::kDefaultLedgerShortcut, context);
},
[&](auto const& value) { return getLedger(ledger, value, context); },
},
specifier.value);
}
// Explicit instantiation of above four functions
template ::rpc::Status
getLedger<>(std::shared_ptr<ReadView const>&, uint32_t, Context const&);
template Status
getLedger<>(std::shared_ptr<ReadView const>&, LedgerShortcut shortcut, Context const&);
template ::rpc::Status
getLedger<>(std::shared_ptr<ReadView const>&, ::rpc::spec::LedgerShortcut shortcut, Context const&);
template Status
template ::rpc::Status
getLedger<>(std::shared_ptr<ReadView const>&, UInt256 const&, Context const&);
template ::rpc::Status
getLedger<>(std::shared_ptr<ReadView const>&, ::rpc::spec::LedgerSpecifier const&, Context const&);
// explicit instantiation of ledgerFromSpecifier
template Status
template ::rpc::Status
ledgerFromSpecifier<>(
std::shared_ptr<ReadView const>&,
org::xrpl::rpc::v1::LedgerSpecifier const&,
@@ -357,18 +387,12 @@ ledgerFromSpecifier<>(
// return value. Otherwise, the object contains the field "validated" and
// optionally the fields "ledger_hash", "ledger_index" and
// "ledger_current_index", if they are defined.
Status
lookupLedger(
std::shared_ptr<ReadView const>& ledger,
JsonContext const& context,
json::Value& result)
void
injectLedgerFields(ReadView const& ledger, Context const& context, json::Value& result)
{
if (auto status = ledgerFromRequest(ledger, context))
return status;
auto const& info = ledger.header();
auto& info = ledger->header();
if (!ledger->open())
if (!ledger.open())
{
result[jss::ledger_hash] = to_string(info.hash);
result[jss::ledger_index] = info.seq;
@@ -378,8 +402,20 @@ lookupLedger(
result[jss::ledger_current_index] = info.seq;
}
result[jss::validated] = context.ledgerMaster.isValidated(*ledger);
return Status::kOK;
result[jss::validated] = context.ledgerMaster.isValidated(ledger);
}
::rpc::Status
lookupLedger(
std::shared_ptr<ReadView const>& ledger,
JsonContext const& context,
json::Value& result)
{
if (auto status = ledgerFromRequest(ledger, context))
return status;
injectLedgerFields(*ledger, context, result);
return ::rpc::Status::kOK;
}
json::Value
@@ -387,7 +423,7 @@ lookupLedger(std::shared_ptr<ReadView const>& ledger, JsonContext const& context
{
json::Value result;
if (auto status = lookupLedger(ledger, context, result))
status.inject(result);
injectSpecError(result, status);
return result;
}
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff. Show more