mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-10 21:58:03 +00:00
Compare commits
17 Commits
nb-misc-cl
...
mathbunnyr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d325c335a | ||
|
|
3074723eba | ||
|
|
75c0e58049 | ||
|
|
7681b72b85 | ||
|
|
d79ad12242 | ||
|
|
48158adc8c | ||
|
|
edb341acc9 | ||
|
|
92d9fd8aeb | ||
|
|
1a5ce8377c | ||
|
|
026efd2606 | ||
|
|
2449f951eb | ||
|
|
24400ba70f | ||
|
|
9cbf78ba99 | ||
|
|
b8d8738f81 | ||
|
|
63c97e719f | ||
|
|
3ac26f23c7 | ||
|
|
ff9410bc56 |
10
.github/actions/build-deps/action.yml
vendored
10
.github/actions/build-deps/action.yml
vendored
@@ -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"
|
||||
|
||||
8
.github/scripts/strategy-matrix/linux.json
vendored
8
.github/scripts/strategy-matrix/linux.json
vendored
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"image_tag": "sha-060957e",
|
||||
"image_tag": "sha-1798262",
|
||||
"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
.github/workflows/build-nix-images.yml
vendored
1
.github/workflows/build-nix-images.yml
vendored
@@ -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"
|
||||
|
||||
1
.github/workflows/build-packaging-images.yml
vendored
1
.github/workflows/build-packaging-images.yml
vendored
@@ -5,7 +5,6 @@ on:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- ".github/workflows/build-packaging-images.yml"
|
||||
- "bin/install-packaging-tools.sh"
|
||||
- "package/images/packaging/**"
|
||||
pull_request:
|
||||
|
||||
1
.github/workflows/build-pre-commit-image.yml
vendored
1
.github/workflows/build-pre-commit-image.yml
vendored
@@ -5,7 +5,6 @@ on:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- ".github/workflows/build-pre-commit-image.yml"
|
||||
- "bin/pre-commit/Dockerfile"
|
||||
- "rust-toolchain.toml"
|
||||
pull_request:
|
||||
|
||||
2
.github/workflows/cargo-audit.yml
vendored
2
.github/workflows/cargo-audit.yml
vendored
@@ -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-1798262
|
||||
permissions:
|
||||
contents: read
|
||||
# Needed to open an issue on scheduled failures.
|
||||
|
||||
2
.github/workflows/pre-commit.yml
vendored
2
.github/workflows/pre-commit.yml
vendored
@@ -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" }'
|
||||
|
||||
2
.github/workflows/publish-docs.yml
vendored
2
.github/workflows/publish-docs.yml
vendored
@@ -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-1798262
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
2
.github/workflows/reusable-clang-tidy.yml
vendored
2
.github/workflows/reusable-clang-tidy.yml
vendored
@@ -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-1798262"
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
6
.github/workflows/reusable-rust.yml
vendored
6
.github/workflows/reusable-rust.yml
vendored
@@ -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-1798262
|
||||
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-1798262
|
||||
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-1798262
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
2
.github/workflows/reusable-upload-recipe.yml
vendored
2
.github/workflows/reusable-upload-recipe.yml
vendored
@@ -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-1798262
|
||||
env:
|
||||
REMOTE_NAME: ${{ inputs.remote_name }}
|
||||
CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
71
bin/check-build-context-runtime.sh
Executable file
71
bin/check-build-context-runtime.sh
Executable file
@@ -0,0 +1,71 @@
|
||||
#!/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
|
||||
|
||||
# TODO: Add jq to the Nix packages and use it here instead of Python.
|
||||
folders="$(python3 -c '
|
||||
import json, sys
|
||||
for node in json.load(open(sys.argv[1]))["graph"]["nodes"].values():
|
||||
if node["context"] == "build" and node.get("package_folder"):
|
||||
print(node["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 ]
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
20
docs/build/nix.md
vendored
20
docs/build/nix.md
vendored
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,172 +2,172 @@ 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
|
||||
✅ 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/dykdkqyz48al2mllwjpwgdxyxgkwhk9x-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/m0aqc6dbryn1mvr0m2zsxxgpmahcvp6v-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/dykdkqyz48al2mllwjpwgdxyxgkwhk9x-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/grpzqmx46d3xr3ssrz4ibkl9xr7rvjzj-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/dykdkqyz48al2mllwjpwgdxyxgkwhk9x-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/v9g20lw1q1qq9ljwvh5k580hgd9hmsv8-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).
|
||||
|
||||
|
||||
@@ -2,172 +2,172 @@ 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
|
||||
✅ 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/iv4dxwnzvl0lqj8jfyibwf5ijb8hjw2i-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/7gq05wr155k0pm1b2k73pya3l35hgifl-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/iv4dxwnzvl0lqj8jfyibwf5ijb8hjw2i-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/54pnici9imw8ff0j5m7nig7dwaxrnlhv-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/iv4dxwnzvl0lqj8jfyibwf5ijb8hjw2i-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/0mmdxscn11irlmm7jvp1c0a32712s026-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).
|
||||
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
src/test/app/lending/LendingBatch_test.cpp
Normal file
857
src/test/app/lending/LendingBatch_test.cpp
Normal 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
|
||||
@@ -30,11 +30,8 @@
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/tx/transactors/system/Batch.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -498,8 +495,7 @@ private:
|
||||
// From FIND-001
|
||||
testcase << "Batch Bypass Counterparty";
|
||||
|
||||
bool const lendingBatchEnabled = !std::ranges::any_of(
|
||||
Batch::kDisabledTxTypes, [](auto const& disabled) { return disabled == ttLOAN_SET; });
|
||||
bool const lendingBatchEnabled = features[featureLendingProtocolV1_2];
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
@@ -15,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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user