Compare commits

...
Author SHA1 Message Date
Ayaz Salikhov a9a8ee5333 Merge branch 'develop' into use_llvm_23 2026-10-06 21:09:23 +01: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 c99211a7d2 build: Use LLVM 23 2026-10-06 18:28:10 +01: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
70 changed files with 2368 additions and 622 deletions

No files matched your search

+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"
+4 -4
View File
@@ -1,5 +1,5 @@
{
"image_tag": "sha-060957e",
"image_tag": "sha-ed96e60",
"configs": {
"ubuntu": [
{
@@ -74,7 +74,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,7 +86,7 @@
"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"
}
}
@@ -101,7 +101,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:
+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-ed96e60
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-3a2d19f" }'
+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-ed96e60
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+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-ed96e60
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-ed96e60
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-ed96e60
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-ed96e60
env:
REMOTE_NAME: ${{ inputs.remote_name }}
CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }}
+1
View File
@@ -29,6 +29,7 @@ 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
+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
+8
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 ledger
)
target_sources(xrpld PRIVATE ${rpcspec_instantiations})
if(tests)
file(
GLOB_RECURSE sources
+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.20#6daa13eeb4e6c92b82ea6d78866e1da8%1790720024.368048",
"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.20",
"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
+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
@@ -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>;
-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.
+93 -90
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.32.0
/nix/store/q0g5grbh7zm8gfhhnq5h6fm1rbs9yv3n-conan-2.32.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 version 22.1.8
/nix/store/dggxva67vs7virgsmd6xkwd6p7zsc5pn-clang-wrapper-22.1.8/bin/clang
✅ clang-22
clang version 22.1.7
/nix/store/dagc2rq44gfbr7w7yvvqca3yqpc9gqbq-clang-22/bin/clang-22
clang version 22.1.8
/nix/store/ckz6y10v5dbyswpf9f6hd8kjfv84f920-clang-22/bin/clang-22
✅ clang++
clang version 22.1.7
/nix/store/ff0hrp9r9i3pa5arkdw0sgmzp8d576qi-clang-wrapper-22.1.7/bin/clang++
clang version 22.1.8
/nix/store/dggxva67vs7virgsmd6xkwd6p7zsc5pn-clang-wrapper-22.1.8/bin/clang++
✅ clang++-22
clang version 22.1.7
/nix/store/l5m8clin1npl605wdkd8mr18ggxww3z4-clang++-22/bin/clang++-22
clang version 22.1.8
/nix/store/lsishk8fwbdmlwr5fbwyd3yhhjsm67qr-clang++-22/bin/clang++-22
✅ 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 version 22.1.8
/nix/store/37jhp4j3y339zmkx09qbr3fpfy3q0kl0-clang-tools-22.1.8/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 22.1.8
/nix/store/qg0b99zn522gl3vz9hx9ni9ianb11d1r-clang-apply-replacements-22/bin/clang-apply-replacements-22
✅ clang-format
clang-format version 22.1.7
/nix/store/4zp1rjpj2xijrv4kqpwsy3ixwb2r6nlk-clang-tools-22.1.7/bin/clang-format
clang-format version 22.1.8
/nix/store/37jhp4j3y339zmkx09qbr3fpfy3q0kl0-clang-tools-22.1.8/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 22.1.8
/nix/store/m1al4bygpn7i13q1kalavybwh9ldg6rz-clang-format-22/bin/clang-format-22
✅ clang-tidy
LLVM version 22.1.7
/nix/store/4zp1rjpj2xijrv4kqpwsy3ixwb2r6nlk-clang-tools-22.1.7/bin/clang-tidy
LLVM version 22.1.8
/nix/store/37jhp4j3y339zmkx09qbr3fpfy3q0kl0-clang-tools-22.1.8/bin/clang-tidy
✅ clang-tidy-22
LLVM version 22.1.7
/nix/store/gdrkvpw846lkyzh8y9p3zx50g6ml2v84-clang-tidy-22/bin/clang-tidy-22
LLVM version 22.1.8
/nix/store/h51xrqf8xgx4zil2rm1ydp909858y4xm-clang-tidy-22/bin/clang-tidy-22
✅ 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
/nix/store/a760dc8nldq1715410384bkk83idmm00-run-clang-tidy/bin/run-clang-tidy
✅ run-clang-tidy-22
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/n8yak1ap308gvi7gmrniw0ybsx80fjws-run-clang-tidy-22/bin/run-clang-tidy-22
/nix/store/xwh85m9sa5h8fnpnlmqwfnf2sz9l59mm-run-clang-tidy-22/bin/run-clang-tidy-22
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.
+93 -90
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.32.0
/nix/store/vbr044x1zbq062kkw53zm0sm3slv8g56-conan-2.32.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 version 22.1.8
/nix/store/7z7by1cxa52mk7hjia1pimjvsd6aizhf-clang-wrapper-22.1.8/bin/clang
✅ clang-22
clang version 22.1.7
/nix/store/vcf6ilfwn57828hwzyp6zlyr24j9j6yw-clang-22/bin/clang-22
clang version 22.1.8
/nix/store/pqn36q5paasw7v8sjgz5fd90mfpgkkqr-clang-22/bin/clang-22
✅ clang++
clang version 22.1.7
/nix/store/xjqffrq9i7la058s9865ig71l9sp1ys5-clang-wrapper-22.1.7/bin/clang++
clang version 22.1.8
/nix/store/7z7by1cxa52mk7hjia1pimjvsd6aizhf-clang-wrapper-22.1.8/bin/clang++
✅ clang++-22
clang version 22.1.7
/nix/store/xby0f6gamr7m27zp5cndsvghbp9lgb3c-clang++-22/bin/clang++-22
clang version 22.1.8
/nix/store/m9a3slb0lcixm2csgjz9s0nvj9m5ysnq-clang++-22/bin/clang++-22
✅ 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 version 22.1.8
/nix/store/6jr2pbz9mzxznf47lw1w6ss1arqsakw6-clang-tools-22.1.8/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 22.1.8
/nix/store/mgcaw2063w2d4s61fz023bd0pf3vq5y7-clang-apply-replacements-22/bin/clang-apply-replacements-22
✅ clang-format
clang-format version 22.1.7
/nix/store/s53p2m776iqaz7acgr5csgpsd18w15h7-clang-tools-22.1.7/bin/clang-format
clang-format version 22.1.8
/nix/store/6jr2pbz9mzxznf47lw1w6ss1arqsakw6-clang-tools-22.1.8/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 22.1.8
/nix/store/2iwvl9784yrjfjhfac46z5ihwmbaavk7-clang-format-22/bin/clang-format-22
✅ clang-tidy
LLVM version 22.1.7
/nix/store/s53p2m776iqaz7acgr5csgpsd18w15h7-clang-tools-22.1.7/bin/clang-tidy
LLVM version 22.1.8
/nix/store/6jr2pbz9mzxznf47lw1w6ss1arqsakw6-clang-tools-22.1.8/bin/clang-tidy
✅ clang-tidy-22
LLVM version 22.1.7
/nix/store/wdyd6cb9z1lyi37lbzvwldgcc7yv1n5c-clang-tidy-22/bin/clang-tidy-22
LLVM version 22.1.8
/nix/store/2b7hn8hyp02yfarh8c40c4jsjkm789l0-clang-tidy-22/bin/clang-tidy-22
✅ 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
/nix/store/b1yccjmij9qiha8a8d9yxayjdh5cg2vq-run-clang-tidy/bin/run-clang-tidy
✅ run-clang-tidy-22
usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
/nix/store/f8m0p9ad40brp9ahy4i0h27kqjkya1j9-run-clang-tidy-22/bin/run-clang-tidy-22
/nix/store/v75dyrp7vwdgx5w2fzwsy00lf94qpz0i-run-clang-tidy-22/bin/run-clang-tidy-22
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
+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>
+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;
}
+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;
}
+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();
+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;
+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.
*/
+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
{
+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};
+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
+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{
+50 -6
View File
@@ -4,6 +4,7 @@
#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/server_info/Version.h>
@@ -12,10 +13,16 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ApiVersion.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <rpcspec/Errors.hpp>
#include <rpcspec/Types.hpp>
#include <algorithm>
#include <array>
#include <concepts>
#include <cstddef>
#include <expected>
#include <iterator>
#include <span>
#include <string_view>
@@ -49,7 +56,34 @@ byRef(JsonContext& context, json::Value& result)
return Status();
}
template <typename HandlerImpl>
concept SpecBasedHandler = std::derived_from<HandlerImpl, HandlerFor<typename HandlerImpl::Input>>;
template <typename HandlerImpl>
concept InputlessHandler = requires(HandlerImpl& handler) { handler.process(); };
template <class HandlerImpl, class Output>
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 std::get<ErrorCodeI>(output.error().code);
}
handler.writeResult(object, *output);
injectSpecWarnings(object, warnings);
return Status::kOK;
}
template <class HandlerImpl>
requires SpecBasedHandler<HandlerImpl> or InputlessHandler<HandlerImpl>
Status
handle(JsonContext& context, json::Value& object)
{
@@ -57,18 +91,28 @@ handle(JsonContext& context, json::Value& object)
context.apiVersion >= HandlerImpl::minApiVer &&
context.apiVersion <= HandlerImpl::maxApiVer,
"xrpl::rpc::handle : valid API version");
HandlerImpl handler(context);
auto status = handler.check();
if (status)
if constexpr (SpecBasedHandler<HandlerImpl>)
{
status.inject(object);
auto const input = HandlerImpl::parseInput(context.params, context.apiVersion);
if (not input.has_value())
{
injectSpecError(object, input.error());
return std::get<ErrorCodeI>(input.error().code);
}
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
{
handler.writeResult(object);
HandlerImpl handler(context);
return respond(handler, object, handler.process(), /* warnings= */ {});
}
return status;
}
template <typename HandlerImpl>
+43 -14
View File
@@ -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,28 @@
#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/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)
{
@@ -68,13 +80,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))
@@ -146,7 +158,7 @@ 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
@@ -204,17 +216,17 @@ 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);
}
}
}
@@ -263,7 +275,7 @@ getLedger(T& ledger, uint32_t ledgerIndex, Context const& context)
template <class T>
Status
getLedger(T& ledger, LedgerShortcut shortcut, Context const& context)
getLedger(T& ledger, ::rpc::spec::LedgerShortcut shortcut, Context const& context)
{
if (isValidatedOld(context.ledgerMaster, context.app.config().standalone()))
{
@@ -272,7 +284,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 +298,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");
@@ -321,16 +333,33 @@ getLedger(T& ledger, LedgerShortcut shortcut, Context const& context)
return Status::kOK;
}
// Explicit instantiation of above three functions
template <class T>
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 Status
getLedger<>(std::shared_ptr<ReadView const>&, uint32_t, Context const&);
template Status
getLedger<>(std::shared_ptr<ReadView const>&, LedgerShortcut shortcut, Context const&);
getLedger<>(std::shared_ptr<ReadView const>&, ::rpc::spec::LedgerShortcut shortcut, Context const&);
template Status
getLedger<>(std::shared_ptr<ReadView const>&, UInt256 const&, Context const&);
template Status
getLedger<>(std::shared_ptr<ReadView const>&, ::rpc::spec::LedgerSpecifier const&, Context const&);
// explicit instantiation of ledgerFromSpecifier
template Status
ledgerFromSpecifier<>(
+17 -2
View File
@@ -7,10 +7,10 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/Ledger.h>
#include <xrpl/protocol/LedgerShortcut.h>
#include <xrpl/server/NetworkOPs.h>
#include <org/xrpl/rpc/v1/ledger.pb.h>
#include <rpcspec/Ledger.hpp>
#include <cstdint>
#include <expected>
@@ -76,7 +76,22 @@ getLedger(T& ledger, uint32_t ledgerIndex, Context const& context);
*/
template <class T>
Status
getLedger(T& ledger, LedgerShortcut shortcut, Context const& context);
getLedger(T& ledger, ::rpc::spec::LedgerShortcut shortcut, Context const& context);
/**
* @brief Retrieves the ledger a spec-parsed request selects.
*
* An unspecified selection means the current ledger.
*
* @tparam T Type of the ledger pointer to be filled.
* @param ledger Reference to the ledger pointer to be filled.
* @param specifier The ledger the request selects.
* @param context The RPC context.
* @return Status indicating success or failure of the operation.
*/
template <class T>
Status
getLedger(T& ledger, ::rpc::spec::LedgerSpecifier const& specifier, Context const& context);
/**
* @brief Looks up a ledger from a request and returns a json::Value with either
+20
View File
@@ -1,9 +1,13 @@
#pragma once
#include <xrpld/rpc/detail/JsonObjectView.hpp>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/jss.h>
#include <rpcspec/Errors.hpp>
#include <rpcspec/HandlerFor.hpp>
#include <rpcspec/Types.hpp>
#include <map>
@@ -12,6 +16,22 @@
namespace xrpl::rpc {
template <typename InputT>
using HandlerFor = ::rpc::spec::HandlerFor<InputT, json::Value>;
inline void
injectSpecError(json::Value& object, ::rpc::Status const& status)
{
if (auto const code = std::get<ErrorCodeI>(status.code); status.message.empty())
{
injectError(code, object);
}
else
{
injectError(code, status.message, object);
}
}
// Warnings are grouped by code into one entry each: the code's standard message followed by
// every per-field detail, space separated.
//
+22 -6
View File
@@ -20,13 +20,14 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.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>
#include <xrpl/rdb/RelationalDatabase.h>
#include <xrpl/resource/Fees.h>
#include <rpcspec/Ledger.hpp>
#include <cstdint>
#include <expected>
#include <memory>
@@ -82,9 +83,24 @@ parseDelegateFilter(json::Value const& delegateNode)
using TxnsData = RelationalDatabase::AccountTxs;
using TxnsDataBinary = RelationalDatabase::MetaTxsList;
using TxnDataBinary = RelationalDatabase::TxnMetaLedgerType;
using AccountTxArgs = RelationalDatabase::AccountTxArgs;
using AccountTxResult = RelationalDatabase::AccountTxResult;
using LedgerSpecifier = RelationalDatabase::LedgerSpecifier;
// Moved here from RelationalDatabase, where nothing but this handler used them, so that
// libxrpl does not depend on the spec for LedgerShortcut. Temporary: the spec's
// account_tx Input replaces both once account_tx is migrated to the spec.
using LedgerSpecifier =
std::variant<LedgerRange, ::rpc::spec::LedgerShortcut, std::uint32_t, uint256>;
struct AccountTxArgs
{
AccountID account;
std::optional<LedgerSpecifier> ledger;
bool binary = false;
bool forward = false;
uint32_t limit = 0;
std::optional<RelationalDatabase::AccountTxMarker> marker;
std::optional<DelegateFilter> delegate;
};
// parses args into a ledger specifier, or returns a Json object on error
std::variant<std::optional<LedgerSpecifier>, json::Value>
@@ -148,15 +164,15 @@ parseLedgerArgs(rpc::Context& context, json::Value const& params)
if (ledgerStr == "current" || ledgerStr.empty())
{
ledger = LedgerShortcut::Current;
ledger = ::rpc::spec::LedgerShortcut::Current;
}
else if (ledgerStr == "closed")
{
ledger = LedgerShortcut::Closed;
ledger = ::rpc::spec::LedgerShortcut::Closed;
}
else if (ledgerStr == "validated")
{
ledger = LedgerShortcut::Validated;
ledger = ::rpc::spec::LedgerShortcut::Validated;
}
else
{
+46 -88
View File
@@ -9,6 +9,7 @@
#include <xrpld/rpc/detail/RPCLedgerHelpers.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ErrorCodes.h>
@@ -19,11 +20,11 @@
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/resource/Fees.h>
#include <xrpl/server/LoadFeeTrack.h>
#include <xrpl/shamap/SHAMap.h>
#include <grpcpp/support/status.h>
#include <org/xrpl/rpc/v1/get_ledger.pb.h>
#include <rpcspec/Errors.hpp>
#include <chrono>
#include <exception>
@@ -40,126 +41,83 @@ LedgerHandler::LedgerHandler(JsonContext& context) : context_(context)
{
}
Status
LedgerHandler::check()
std::expected<LedgerHandler::Output, ::rpc::Status>
LedgerHandler::process(Input const& input)
{
auto const& params = context_.params;
Output output;
output.options = (input.full ? static_cast<int>(LedgerFill::Options::Full) : 0) |
(input.expand ? static_cast<int>(LedgerFill::Options::Expand) : 0) |
(input.transactions ? static_cast<int>(LedgerFill::Options::DumpTxrp) : 0) |
(input.accounts ? static_cast<int>(LedgerFill::Options::DumpState) : 0) |
(input.binary ? static_cast<int>(LedgerFill::Options::Binary) : 0) |
(input.ownerFunds ? static_cast<int>(LedgerFill::Options::OwnerFunds) : 0) |
(input.queue ? static_cast<int>(LedgerFill::Options::DumpQueue) : 0);
auto getBool = [&](json::StaticString const& field) -> std::expected<bool, Status> {
if (!params.isMember(field))
{
return false;
}
if (!params[field].isBool())
{
return std::unexpected(RpcInvalidParams);
}
if (input.ledger.isUnspecified())
return output;
return params[field].asBool();
};
if (auto const status = getLedger(output.ledger, input.ledger, context_.get()))
return std::unexpected{::rpc::Status{status.toErrorCode(), status.message()}};
auto const full = getBool(jss::full);
auto const transactions = getBool(jss::transactions);
auto const accounts = getBool(jss::accounts);
auto const expand = getBool(jss::expand);
auto const binary = getBool(jss::binary);
auto const ownerFunds = getBool(jss::owner_funds);
auto const queue = getBool(jss::queue);
if (!full.has_value())
return full.error();
if (!transactions.has_value())
return transactions.error();
if (!accounts.has_value())
return accounts.error();
if (!expand.has_value())
return expand.error();
if (!binary.has_value())
return binary.error();
if (!ownerFunds.has_value())
return ownerFunds.error();
if (!queue.has_value())
return queue.error();
options_ = (*full ? static_cast<int>(LedgerFill::Options::Full) : 0) |
(*expand ? static_cast<int>(LedgerFill::Options::Expand) : 0) |
(*transactions ? static_cast<int>(LedgerFill::Options::DumpTxrp) : 0) |
(*accounts ? static_cast<int>(LedgerFill::Options::DumpState) : 0) |
(*binary ? static_cast<int>(LedgerFill::Options::Binary) : 0) |
(*ownerFunds ? static_cast<int>(LedgerFill::Options::OwnerFunds) : 0) |
(*queue ? static_cast<int>(LedgerFill::Options::DumpQueue) : 0);
bool const needsLedger = params.isMember(jss::ledger) || params.isMember(jss::ledger_hash) ||
params.isMember(jss::ledger_index);
if (!needsLedger)
return Status::kOK;
if (auto s = lookupLedger(ledger_, context_, result_))
return s;
if (*full || *accounts)
if (input.full || input.accounts)
{
// Until some sane way to get full ledgers has been implemented,
// disallow retrieving all state nodes.
if (!isUnlimited(context_.role))
return RpcNoPermission;
if (!isUnlimited(context_.get().role))
return std::unexpected{::rpc::Status{RpcNoPermission}};
if (context_.app.getFeeTrack().isLoadedLocal() && !isUnlimited(context_.role))
{
return RpcTooBusy;
}
context_.loadType = binary ? resource::kFeeMediumBurdenRpc : resource::kFeeHeavyBurdenRpc;
context_.get().loadType =
input.binary ? resource::kFeeMediumBurdenRpc : resource::kFeeHeavyBurdenRpc;
}
if (*queue)
if (input.queue)
{
if (!ledger_ || !ledger_->open())
if (!output.ledger || !output.ledger->open())
{
// It doesn't make sense to request the queue
// with a non-existent or closed/validated ledger.
return RpcInvalidParams;
return std::unexpected{::rpc::Status{RpcInvalidParams}};
}
queueTxs_ = context_.app.getTxQ().getTxs();
output.queueTxs = context_.get().app.getTxQ().getTxs();
}
return Status::kOK;
return output;
}
void
LedgerHandler::writeResult(json::Value& value)
LedgerHandler::writeResult(json::Value& value, Output const& output) const
{
if (ledger_)
if (output.ledger)
{
copyFrom(value, result_);
addJson(value, {*ledger_, &context_, options_, queueTxs_});
auto const& header = output.ledger->header();
if (output.ledger->open())
{
value[jss::ledger_current_index] = header.seq;
}
else
{
value[jss::ledger_hash] = to_string(header.hash);
value[jss::ledger_index] = header.seq;
}
value[jss::validated] = context_.get().ledgerMaster.isValidated(*output.ledger);
addJson(value, {*output.ledger, &context_.get(), output.options, output.queueTxs});
}
else
{
auto& master = context_.app.getLedgerMaster();
auto& master = context_.get().app.getLedgerMaster();
{
auto& closed = value[jss::closed] = json::ValueType::Object;
addJson(closed, {*master.getClosedLedger(), &context_, 0});
addJson(closed, {*master.getClosedLedger(), &context_.get(), 0});
}
{
auto& open = value[jss::open] = json::ValueType::Object;
addJson(open, {*master.getCurrentLedger(), &context_, 0});
addJson(open, {*master.getCurrentLedger(), &context_.get(), 0});
}
}
json::Value warnings{json::ValueType::Array};
if (context_.params.isMember(jss::type))
{
json::Value& w = warnings.append(json::ValueType::Object);
w[jss::id] = WarnRpcFieldsDeprecated;
w[jss::message] =
"Some fields from your request are deprecated. Please check the "
"documentation at "
"https://xrpl.org/docs/references/http-websocket-apis/ "
"and update your request. Field `type` is deprecated.";
}
if (warnings.size() != 0u)
value[jss::warnings] = std::move(warnings);
}
} // namespace rpc
+18 -10
View File
@@ -5,13 +5,18 @@
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/MethodNames.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/Status.h>
#include <xrpld/rpc/detail/Handler.h>
#include <xrpld/rpc/detail/SpecBridge.hpp>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/ApiVersion.h>
#include <rpcspec/Errors.hpp>
#include <rpcspec/handlers/ledger/Types.hpp>
#include <expected>
#include <functional>
#include <memory>
#include <string_view>
#include <vector>
@@ -26,16 +31,23 @@ struct JsonContext;
// full: true | false // optional, defaults to false.
// }
class LedgerHandler
class LedgerHandler : public HandlerFor<::rpc::spec::handlers::ledger::Input>
{
public:
struct Output
{
std::shared_ptr<ReadView const> ledger;
std::vector<TxQ::TxDetails> queueTxs;
int options = 0;
};
explicit LedgerHandler(JsonContext&);
Status
check();
[[nodiscard]] std::expected<Output, ::rpc::Status>
process(Input const& input);
void
writeResult(json::Value&);
writeResult(json::Value& value, Output const& output) const;
// NOLINTBEGIN(readability-identifier-naming)
static constexpr std::string_view name = method::kLedger;
@@ -50,11 +62,7 @@ public:
// NOLINTEND(readability-identifier-naming)
private:
JsonContext& context_;
std::shared_ptr<ReadView const> ledger_;
std::vector<TxQ::TxDetails> queueTxs_;
json::Value result_;
int options_ = 0;
std::reference_wrapper<JsonContext> context_;
};
} // namespace xrpl::rpc
+67 -4
View File
@@ -180,7 +180,23 @@ parseCheck(
json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
return parseObjectID(params, fieldName, "hex string");
if (!params.isObject())
{
return parseObjectID(params, fieldName);
}
auto const account =
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
if (!account)
return std::unexpected(account.error());
auto const sequence =
ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest");
if (!sequence)
return std::unexpected(sequence.error());
auto const seqProxy = SeqProxy::rawSequence(*sequence);
return keylet::check(*account, seqProxy).key;
}
static std::expected<UInt256, json::Value>
@@ -570,7 +586,23 @@ parseNFTokenOffer(
json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
return parseObjectID(params, fieldName, "hex string");
if (!params.isObject())
{
return parseObjectID(params, fieldName);
}
auto const owner =
ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner");
if (!owner)
return std::unexpected(owner.error());
auto const sequence =
ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest");
if (!sequence)
return std::unexpected(sequence.error());
auto const seqProxy = SeqProxy::rawSequence(*sequence);
return keylet::nftokenOffer(*owner, seqProxy).key;
}
static std::expected<UInt256, json::Value>
@@ -638,7 +670,28 @@ parsePayChannel(
json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
return parseObjectID(params, fieldName, "hex string");
if (!params.isObject())
{
return parseObjectID(params, fieldName);
}
auto const account =
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
if (!account)
return std::unexpected(account.error());
auto const destination =
ledger_entry_helpers::requiredAccountID(params, jss::destination, "malformedAddress");
if (!destination)
return std::unexpected(destination.error());
auto const sequence =
ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest");
if (!sequence)
return std::unexpected(sequence.error());
auto const seqProxy = SeqProxy::rawSequence(*sequence);
return keylet::payChannel(*account, *destination, seqProxy).key;
}
static std::expected<UInt256, json::Value>
@@ -726,7 +779,17 @@ parseSignerList(
json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
return parseObjectID(params, fieldName, "hex string");
if (!params.isObject())
{
return parseObjectID(params, fieldName);
}
auto const id =
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress");
if (!id)
return std::unexpected(id.error());
return keylet::signerList(*id).key;
}
static std::expected<UInt256, json::Value>
+22 -11
View File
@@ -4,12 +4,16 @@
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/MethodNames.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/Status.h>
#include <xrpld/rpc/detail/Handler.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ApiVersion.h>
#include <rpcspec/Errors.hpp>
#include <cstdint>
#include <expected>
#include <functional>
#include <string_view>
namespace xrpl::rpc {
@@ -17,21 +21,29 @@ namespace xrpl::rpc {
class VersionHandler
{
public:
explicit VersionHandler(JsonContext& c)
: apiVersion_(c.apiVersion), betaEnabled_(c.app.config().betaRpcApi)
struct Output
{
std::uint32_t apiVersion;
bool betaEnabled;
};
explicit VersionHandler(JsonContext& context) : context_(context)
{
}
static Status
check()
[[nodiscard]] std::expected<Output, ::rpc::Status>
process() const
{
return Status::kOK;
return Output{
.apiVersion = context_.get().apiVersion,
.betaEnabled = context_.get().app.config().betaRpcApi,
};
}
void
writeResult(json::Value& obj) const
static void
writeResult(json::Value& obj, Output const& output)
{
setVersion(obj, apiVersion_, betaEnabled_);
setVersion(obj, output.apiVersion, output.betaEnabled);
}
// NOLINTBEGIN(readability-identifier-naming)
@@ -47,8 +59,7 @@ public:
// NOLINTEND(readability-identifier-naming)
private:
unsigned int apiVersion_;
bool betaEnabled_;
std::reference_wrapper<JsonContext> context_;
};
} // namespace xrpl::rpc