Compare commits

..

2 Commits

Author SHA1 Message Date
tequ
043dcb2a38 URITokenBroker Amendment 2026-08-24 17:34:08 +09:00
Richard Holland
bb244ef772 put release builds into a candidate folder to prevent auto-update scripts running before smoke tests (#761) 2026-06-21 12:12:43 +10:00
17 changed files with 750 additions and 314 deletions

View File

@@ -19,15 +19,6 @@ coverage:
default:
target: auto
threshold: 2%
paths:
# PeerImp is historically hard to exercise in the current unit-test
# harness. Keep this list narrow; new testable code should remain
# covered by the default patch gate.
- "!src/xrpld/overlay/detail/PeerImp.cpp"
historically-untested:
target: 0%
paths:
- "src/xrpld/overlay/detail/PeerImp.cpp"
changes: false
github_checks:

View File

@@ -50,10 +50,6 @@ inputs:
options:
- libstdcxx
- libcxx
conan_deps_cxxflags:
description: 'Extra cxxflags applied to Conan dependency package builds only (NOT the rippled build). JSON object keyed by Conan package pattern, e.g. {"grpc/*":["-Wno-foo"]}. Maps to <pattern>:tools.build:cxxflags.'
required: false
default: '{}'
outputs:
cache-hit:
@@ -85,8 +81,6 @@ runs:
- name: Configure Conan
shell: bash
env:
CONAN_DEPS_CXXFLAGS: ${{ inputs.conan_deps_cxxflags }}
run: |
# Create the default profile directory if it doesn't exist
mkdir -p ~/.conan2/profiles
@@ -111,14 +105,7 @@ runs:
os=${{ inputs.os }}
EOF
# [buildenv] + [conf] sections.
# Linux pins compiler executables; macOS uses the system toolchain.
# conan_deps_cxxflags (matrix-driven) optionally adds package-pattern
# scoped tools.build:cxxflags for Conan dependency builds only - typically
# grpc workarounds for newer clang's stricter diagnostics. Because these
# are profile-pattern scoped (e.g. grpc/*:...), they do NOT affect the
# consumer/rippled toolchain generated for the main build.
NEED_CONF=0
# Add buildenv and conf sections for Linux (not needed for macOS)
if [ "${{ inputs.os }}" = "Linux" ] && [ -n "${{ inputs.cc }}" ]; then
cat >> ~/.conan2/profiles/default <<EOF
@@ -129,38 +116,16 @@ runs:
[conf]
tools.build:compiler_executables={"c": "/usr/bin/${{ inputs.cc }}", "cpp": "/usr/bin/${{ inputs.cxx }}"}
EOF
NEED_CONF=1
fi
if [ -n "${CONAN_DEPS_CXXFLAGS}" ] && [ "${CONAN_DEPS_CXXFLAGS}" != "{}" ]; then
CONAN_DEPS_CXXFLAGS_LINES="$(python3 - <<'PY'
import json
import os
import sys
# Add macOS-specific conf if needed
if [ "${{ inputs.os }}" = "Macos" ]; then
cat >> ~/.conan2/profiles/default <<EOF
raw = os.environ["CONAN_DEPS_CXXFLAGS"]
data = json.loads(raw)
if not isinstance(data, dict):
sys.exit("conan_deps_cxxflags must be a JSON object like {\"grpc/*\": [\"-Wno-...\"]}")
for pattern, flags in data.items():
if not isinstance(pattern, str) or not pattern:
sys.exit("conan_deps_cxxflags keys must be non-empty Conan package patterns")
if pattern == "&":
sys.exit("conan_deps_cxxflags must target dependency package patterns, not the consumer (&)")
if not isinstance(flags, list) or not all(isinstance(flag, str) for flag in flags):
sys.exit(f"{pattern}: cxxflags must be a JSON string list")
if flags:
print(f"{pattern}:tools.build:cxxflags={json.dumps(flags, separators=(',', ':'))}")
PY
)"
if [ -n "${CONAN_DEPS_CXXFLAGS_LINES}" ] && [ "$NEED_CONF" = "0" ]; then
echo "" >> ~/.conan2/profiles/default
echo "[conf]" >> ~/.conan2/profiles/default
fi
if [ -n "${CONAN_DEPS_CXXFLAGS_LINES}" ]; then
printf '%s\n' "${CONAN_DEPS_CXXFLAGS_LINES}" >> ~/.conan2/profiles/default
fi
[conf]
# Workaround for gRPC with newer Apple Clang
tools.build:cxxflags=["-Wno-missing-template-arg-list-after-template-kw"]
EOF
fi
# Display profile for verification

View File

@@ -107,9 +107,6 @@ jobs:
compiler: apple-clang
compiler_version: ${{ steps.detect-compiler.outputs.compiler_version }}
stdlib: libcxx
# grpc 1.50.1 trips clang-19+ -Werror=missing-template-arg-list-after-template-kw
# on Apple Clang. Drop when grpc is bumped past the fix.
conan_deps_cxxflags: '{"grpc/*":["-Wno-missing-template-arg-list-after-template-kw"]}'
- name: Build
uses: ./.github/actions/xahau-ga-build

View File

@@ -72,26 +72,15 @@ jobs:
"job_type": "build"
},
{
# Latest stable Clang for the most accurate source-based
# coverage mapping (newer language features, fewer bugs in
# llvm-cov region inference). Pulled from apt.llvm.org since
# Ubuntu 24.04 default repos cap at clang-18.
"compiler_id": "clang-20-libcxx",
"compiler": "clang",
"cc": "clang-20",
"cxx": "clang++-20",
"compiler_version": 20,
"stdlib": "libcxx",
"compiler_id": "gcc-13-libstdcxx",
"compiler": "gcc",
"cc": "gcc-13",
"cxx": "g++-13",
"gcov": "gcov-13",
"compiler_version": 13,
"stdlib": "default",
"configuration": "Debug",
"job_type": "coverage",
"coverage_tool": "llvm",
"coverage_format": "lcov",
# grpc 1.50.1 uses `Foo::template Bar(...)` without an
# angle-bracket arg list; clang-19+ promoted that to
# -Werror. Drop when grpc is bumped past the fix.
"conan_deps_cxxflags": {
"grpc/*": ["-Wno-missing-template-arg-list-after-template-kw"]
}
"job_type": "coverage"
},
{
"compiler_id": "clang-14-libstdcxx-gcc11",
@@ -142,7 +131,7 @@ jobs:
# Minimal matrix for PRs and feature branches
minimal_matrix = [
full_matrix[1], # gcc-13 (middle-ground gcc)
full_matrix[2], # clang-20 llvm-cov coverage
full_matrix[2], # gcc-13 coverage
full_matrix[3] # clang-14 (mature, stable clang)
]
@@ -218,9 +207,9 @@ jobs:
# Select the appropriate matrix
if use_full:
if force_full:
print(f"Using FULL matrix (7 configs (build x6 + clang-20 llvm-cov coverage)) - forced by [ci-nix-full-matrix] tag")
print(f"Using FULL matrix (7 configs) - forced by [ci-nix-full-matrix] tag")
else:
print(f"Using FULL matrix (7 configs (build x6 + clang-20 llvm-cov coverage)) - targeting main branch")
print(f"Using FULL matrix (7 configs) - targeting main branch")
matrix = full_matrix
else:
print(f"Using MINIMAL matrix (3 configs) - feature branch/PR")
@@ -268,25 +257,9 @@ jobs:
- name: Install build dependencies
run: |
# Bump apt's default 3 retries; papers over short upstream blips
# like the recurring ppa.launchpadcontent.net outages.
echo 'Acquire::Retries "5";' > /etc/apt/apt.conf.d/80-retries
apt-get update
apt-get install -y software-properties-common
add-apt-repository ppa:ubuntu-toolchain-r/test -y
# apt.llvm.org for Clang versions newer than what Ubuntu 24.04 ships
# (24.04 default repos cap at clang-18). The bootstrap script adds
# the LLVM apt source for the requested version and runs apt-get update.
if [ "${{ matrix.compiler }}" = "clang" ] && [ "${{ matrix.compiler_version }}" -ge 19 ]; then
apt-get install -y wget gnupg lsb-release
wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh
chmod +x /tmp/llvm.sh
# `all` installs clang + libllvm + lldb + lld + the llvm-N package
# (which provides llvm-profdata-N / llvm-cov-N for coverage runs).
/tmp/llvm.sh ${{ matrix.compiler_version }} all
fi
apt-get update
apt-get install -y git python3 python-is-python3 pipx
pipx ensurepath
@@ -359,16 +332,10 @@ jobs:
pipx install "conan>=2.0,<3"
echo "$HOME/.local/bin" >> $GITHUB_PATH
# Install coverage tooling
# Install gcovr for coverage jobs
if [ "${{ matrix.job_type }}" = "coverage" ]; then
pipx install "gcovr>=7,<9"
apt-get install -y curl lcov
if [ "${{ matrix.coverage_tool }}" = "llvm" ]; then
# Native LLVM source-based coverage: llvm-profdata + llvm-cov.
# The clang-N package doesn't pull these in; the llvm-N package does.
apt-get install -y "llvm-${{ matrix.compiler_version }}"
else
pipx install "gcovr>=7,<9"
fi
fi
- name: Check environment
@@ -381,15 +348,10 @@ jobs:
which ${{ matrix.cxx }} && ${{ matrix.cxx }} --version || echo "${{ matrix.cxx }} not found"
which ccache && ccache --version || echo "ccache not found"
# Check coverage tooling
# Check gcovr for coverage jobs
if [ "${{ matrix.job_type }}" = "coverage" ]; then
if [ "${{ matrix.coverage_tool }}" = "llvm" ]; then
which "llvm-profdata-${{ matrix.compiler_version }}" && "llvm-profdata-${{ matrix.compiler_version }}" --version || echo "llvm-profdata not found"
which "llvm-cov-${{ matrix.compiler_version }}" && "llvm-cov-${{ matrix.compiler_version }}" --version || echo "llvm-cov not found"
else
which gcov && gcov --version || echo "gcov not found"
which gcovr && gcovr --version || echo "gcovr not found"
fi
which gcov && gcov --version || echo "gcov not found"
which gcovr && gcovr --version || echo "gcovr not found"
fi
echo "---- Full Environment ----"
@@ -416,7 +378,6 @@ jobs:
cc: ${{ matrix.cc }}
cxx: ${{ matrix.cxx }}
stdlib: ${{ matrix.stdlib }}
conan_deps_cxxflags: ${{ matrix.conan_deps_cxxflags && toJson(matrix.conan_deps_cxxflags) || '{}' }}
gha_cache_enabled: 'false' # Disable caching for self hosted runner
- name: Build
@@ -449,9 +410,8 @@ jobs:
cache_version: ${{ env.CACHE_VERSION }}
main_branch: ${{ env.MAIN_BRANCH_NAME }}
stdlib: ${{ matrix.stdlib }}
# Coverage builds are slower due to instrumentation; use fewer parallel jobs to avoid flakiness.
# Use *_FLAGS_DEBUG so the build action's stdlib flag (e.g. -stdlib=libc++) in CMAKE_CXX_FLAGS isn't clobbered.
cmake-args: '-Dcoverage=ON -Dcoverage_tool=${{ matrix.coverage_tool }} -Dcoverage_format=${{ matrix.coverage_format }} -Dcoverage_test_parallelism=$(($(nproc)/2)) -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_CXX_FLAGS_DEBUG="-g -O0" -DCMAKE_C_FLAGS_DEBUG="-g -O0"'
# Coverage builds are slower due to instrumentation; use fewer parallel jobs to avoid flakiness
cmake-args: '-Dcoverage=ON -Dcoverage_format=xml -Dcoverage_test_parallelism=$(($(nproc)/2)) -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_CXX_FLAGS="-O0" -DCMAKE_C_FLAGS="-O0"'
cmake-target: 'coverage'
ccache_max_size: '100G'
@@ -483,26 +443,22 @@ jobs:
- name: Move coverage report
if: matrix.job_type == 'coverage'
shell: bash
env:
COVERAGE_FILE: ${{ matrix.coverage_tool == 'llvm' && 'coverage.lcov' || 'coverage.xml' }}
run: |
mv "${{ env.build_dir }}/${COVERAGE_FILE}" ./
echo "COVERAGE_FILE=${COVERAGE_FILE}" >> "$GITHUB_ENV"
mv "${{ env.build_dir }}/coverage.xml" ./
- name: Archive coverage report
if: matrix.job_type == 'coverage'
uses: actions/upload-artifact@v4
with:
name: ${{ env.COVERAGE_FILE }}-${{ matrix.compiler_id }}
path: ${{ env.COVERAGE_FILE }}
name: coverage.xml
path: coverage.xml
retention-days: 30
- name: Upload coverage report
if: matrix.job_type == 'coverage'
uses: codecov/codecov-action@v5
with:
files: ${{ env.COVERAGE_FILE }}
flags: ${{ matrix.coverage_tool }}
files: coverage.xml
fail_ci_if_error: true
disable_search: true
verbose: true

View File

@@ -95,8 +95,16 @@ if [[ "$4" == "" ]]; then
echo "Non GH, local building, no Action runner magic"
else
# GH Action, runner
cp /io/release-build/xahaud /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
cp /io/release-build/release.info /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
if [[ "$(git rev-parse --abbrev-ref HEAD)" == "release" ]]; then
echo "building on the release branch... placing it in builds/candidate"
mkdir /data/builds/candidate
cp /io/release-build/xahaud /data/builds/candidate/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
cp /io/release-build/release.info /data/builds/candidate/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
else
echo "building non-release branch, placing it in builds root"
cp /io/release-build/xahaud /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
cp /io/release-build/release.info /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
fi
echo "Published build to: http://build.xahau.tech/"
echo $(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
fi

View File

@@ -1,156 +0,0 @@
#[===================================================================[
Native LLVM source-based code coverage helper.
Drives the -fprofile-instr-generate / -fcoverage-mapping pipeline:
1. Run instrumented binary with LLVM_PROFILE_FILE=...%m-%p.profraw
2. llvm-profdata merge -sparse -> coverage.profdata
3. llvm-cov export/show/report -> final report
Output filename per coverage_format:
lcov -> coverage.lcov
json -> coverage.json
txt | text -> coverage.txt
html | html-details -> <NAME>/index.html
#]===================================================================]
include(CMakeParseArguments)
# Locate llvm-profdata / llvm-cov, preferring versioned variants matching the
# Clang we're building with so we don't accidentally pair clang-20 with
# llvm-cov-14 (profile format mismatch -> hard failure).
function(_find_llvm_cov_tools)
if(LLVM_PROFDATA_PATH AND LLVM_COV_PATH)
return()
endif()
string(REGEX MATCH "^[0-9]+" _major "${CMAKE_CXX_COMPILER_VERSION}")
set(_pd_names llvm-profdata)
set(_cov_names llvm-cov)
if(_major)
list(PREPEND _pd_names "llvm-profdata-${_major}")
list(PREPEND _cov_names "llvm-cov-${_major}")
endif()
# Only delegate to xcrun when the *compiler* is AppleClang. On macOS with
# Homebrew/system clang-N, xcrun would resolve to Xcode's llvm tools which
# could be a different version - exactly the mismatch we want to avoid.
if(APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
execute_process(COMMAND xcrun -f llvm-profdata
OUTPUT_VARIABLE _pd_xcrun OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET RESULT_VARIABLE _pd_rc)
if(_pd_rc EQUAL 0 AND _pd_xcrun)
set(LLVM_PROFDATA_PATH "${_pd_xcrun}" CACHE FILEPATH "llvm-profdata" FORCE)
endif()
execute_process(COMMAND xcrun -f llvm-cov
OUTPUT_VARIABLE _cov_xcrun OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET RESULT_VARIABLE _cov_rc)
if(_cov_rc EQUAL 0 AND _cov_xcrun)
set(LLVM_COV_PATH "${_cov_xcrun}" CACHE FILEPATH "llvm-cov" FORCE)
endif()
endif()
if(NOT LLVM_PROFDATA_PATH)
find_program(LLVM_PROFDATA_PATH NAMES ${_pd_names})
endif()
if(NOT LLVM_COV_PATH)
find_program(LLVM_COV_PATH NAMES ${_cov_names})
endif()
endfunction()
function(setup_target_for_coverage_llvm)
set(oneValueArgs NAME FORMAT)
set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES)
cmake_parse_arguments(Cov "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
_find_llvm_cov_tools()
if(NOT LLVM_PROFDATA_PATH)
message(FATAL_ERROR "llvm-profdata not found (needed for coverage_tool=llvm)")
endif()
if(NOT LLVM_COV_PATH)
message(FATAL_ERROR "llvm-cov not found (needed for coverage_tool=llvm)")
endif()
if(NOT Cov_FORMAT)
set(Cov_FORMAT lcov)
endif()
set(_profraw_dir "${PROJECT_BINARY_DIR}/${Cov_NAME}-profraw")
set(_profdata "${PROJECT_BINARY_DIR}/${Cov_NAME}.profdata")
# Resolve binary path: accept either an absolute path or a bare target name
# (resolved against PROJECT_BINARY_DIR). Splice the resolved path back into
# Cov_EXECUTABLE so the run command invokes it via absolute path - bare
# names aren't on PATH and the build dir isn't `.` either.
list(GET Cov_EXECUTABLE 0 _exec_name)
if(IS_ABSOLUTE "${_exec_name}")
set(_binary "${_exec_name}")
else()
set(_binary "${PROJECT_BINARY_DIR}/${_exec_name}")
list(REMOVE_AT Cov_EXECUTABLE 0)
list(PREPEND Cov_EXECUTABLE "${_binary}")
endif()
# llvm-cov takes a single -ignore-filename-regex; OR our excludes together.
set(_ignore_regex "")
foreach(EXC IN LISTS Cov_EXCLUDE)
if(_ignore_regex)
string(APPEND _ignore_regex "|")
endif()
string(APPEND _ignore_regex "${EXC}")
endforeach()
set(_filter "")
if(_ignore_regex)
set(_filter "-ignore-filename-regex='${_ignore_regex}'")
endif()
# Pick llvm-cov subcommand + output file for the requested format. Each
# branch builds a single shell command string that we'll hand to bash -c.
if(Cov_FORMAT STREQUAL "lcov")
set(_output "${PROJECT_BINARY_DIR}/coverage.lcov")
set(_report_sh "${LLVM_COV_PATH} export -instr-profile='${_profdata}' -format=lcov ${_filter} '${_binary}' > '${_output}'")
elseif(Cov_FORMAT STREQUAL "json")
set(_output "${PROJECT_BINARY_DIR}/coverage.json")
set(_report_sh "${LLVM_COV_PATH} export -instr-profile='${_profdata}' -format=text ${_filter} '${_binary}' > '${_output}'")
elseif(Cov_FORMAT STREQUAL "txt" OR Cov_FORMAT STREQUAL "text")
set(_output "${PROJECT_BINARY_DIR}/coverage.txt")
set(_report_sh "${LLVM_COV_PATH} report -instr-profile='${_profdata}' ${_filter} '${_binary}' > '${_output}'")
elseif(Cov_FORMAT STREQUAL "html" OR Cov_FORMAT STREQUAL "html-details")
set(_output "${PROJECT_BINARY_DIR}/${Cov_NAME}/index.html")
set(_report_sh "${LLVM_COV_PATH} show -instr-profile='${_profdata}' -format=html -output-dir='${PROJECT_BINARY_DIR}/${Cov_NAME}' ${_filter} '${_binary}'")
else()
message(FATAL_ERROR "coverage_tool=llvm: unsupported coverage_format '${Cov_FORMAT}' (use lcov|json|txt|html)")
endif()
set(_merge_sh "${LLVM_PROFDATA_PATH} merge -sparse -o '${_profdata}' '${_profraw_dir}'/*.profraw")
if(CODE_COVERAGE_VERBOSE)
message(STATUS "[coverage:llvm] binary: ${_binary}")
message(STATUS "[coverage:llvm] profraw: ${_profraw_dir}")
message(STATUS "[coverage:llvm] profdata: ${_profdata}")
message(STATUS "[coverage:llvm] format: ${Cov_FORMAT}")
message(STATUS "[coverage:llvm] output: ${_output}")
if(_ignore_regex)
message(STATUS "[coverage:llvm] ignore: ${_ignore_regex}")
endif()
message(STATUS "[coverage:llvm] merge: ${_merge_sh}")
message(STATUS "[coverage:llvm] report: ${_report_sh}")
endif()
# %m: hash of the binary, %p: pid. Wipe the dir up front so stale profraw
# files can't leak into a fresh merge.
add_custom_target(${Cov_NAME}
COMMAND ${CMAKE_COMMAND} -E rm -rf "${_profraw_dir}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${_profraw_dir}"
COMMAND ${CMAKE_COMMAND} -E env
"LLVM_PROFILE_FILE=${_profraw_dir}/rippled-%m-%p.profraw"
${Cov_EXECUTABLE} ${Cov_EXECUTABLE_ARGS}
COMMAND bash -c "${_merge_sh}"
COMMAND bash -c "${_report_sh}"
BYPRODUCTS ${_output}
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
DEPENDS ${Cov_DEPENDENCIES}
VERBATIM
COMMENT "Running llvm-cov (${Cov_FORMAT}) -> ${_output}"
)
endfunction()

View File

@@ -11,21 +11,6 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
return()
endif()
if(coverage_tool STREQUAL "llvm")
include(CodeCoverageLLVM)
setup_target_for_coverage_llvm(
NAME coverage
FORMAT ${coverage_format}
EXECUTABLE rippled
EXECUTABLE_ARGS --unittest$<$<BOOL:${coverage_test}>:=${coverage_test}> --unittest-jobs ${coverage_test_parallelism} --quiet --unittest-log
EXCLUDE "src/test" "include/xrpl/beast/test" "include/xrpl/beast/unit_test" "${CMAKE_BINARY_DIR}/pb-xrpl.libpb"
DEPENDENCIES rippled
)
return()
endif()
# coverage_tool == "gcov" (default): existing gcovr-driven pipeline.
include(CodeCoverage)
# The instructions for these commands come from the `CodeCoverage` module,

View File

@@ -28,17 +28,15 @@ target_compile_options (opts
$<$<AND:$<BOOL:${is_gcc}>,$<COMPILE_LANGUAGE:CXX>>:-Wsuggest-override>
$<$<BOOL:${is_gcc}>:-Wno-maybe-uninitialized>
$<$<BOOL:${perf}>:-fno-omit-frame-pointer>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage -fprofile-abs-path>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},llvm>>:-g -fprofile-instr-generate -fcoverage-mapping>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>>:-g --coverage -fprofile-abs-path>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>>:-g --coverage>
$<$<BOOL:${profile}>:-pg>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${profile}>>:-p>)
target_link_libraries (opts
INTERFACE
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage -fprofile-abs-path>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},llvm>>:-g -fprofile-instr-generate -fcoverage-mapping>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>>:-g --coverage -fprofile-abs-path>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>>:-g --coverage>
$<$<BOOL:${profile}>:-pg>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${profile}>>:-p>)

View File

@@ -29,25 +29,13 @@ if(is_gcc OR is_clang)
"Unit tests parallelism for the purpose of coverage report.")
set(coverage_format "html-details" CACHE STRING
"Output format of the coverage report.")
set(coverage_tool "gcov" CACHE STRING
"Coverage instrumentation tool: 'gcov' (default, gcc/clang via --coverage + gcovr) or 'llvm' (clang only, native source-based coverage via -fprofile-instr-generate).")
set_property(CACHE coverage_tool PROPERTY STRINGS "gcov" "llvm")
if(NOT coverage_tool MATCHES "^(gcov|llvm)$")
message(FATAL_ERROR "coverage_tool must be 'gcov' or 'llvm', got '${coverage_tool}'")
endif()
set(coverage_extra_args "" CACHE STRING
"Additional arguments to pass to gcovr (gcov tool only).")
"Additional arguments to pass to gcovr.")
set(coverage_test "" CACHE STRING
"On gcc & clang, the specific unit test(s) to run for coverage. Default is all tests.")
if(coverage_test AND NOT coverage)
set(coverage ON CACHE BOOL "gcc/clang only" FORCE)
endif()
# Validate after coverage_test may have flipped coverage on, otherwise
# `-Dcoverage_tool=llvm -Dcoverage_test=Foo` on gcc would silently slip
# past the Clang guard and produce a broken instrumentation combo.
if(coverage AND coverage_tool STREQUAL "llvm" AND NOT is_clang)
message(FATAL_ERROR "coverage_tool=llvm requires Clang (got ${CMAKE_CXX_COMPILER_ID})")
endif()
option(wextra "compile with extra gcc/clang warnings enabled" ON)
else()
set(profile OFF CACHE BOOL "gcc/clang only" FORCE)

View File

@@ -242,6 +242,7 @@
#define sfLockingChainDoor ((8U << 16U) + 22U)
#define sfIssuingChainDoor ((8U << 16U) + 23U)
#define sfSubject ((8U << 16U) + 24U)
#define sfBrokerAccount ((8U << 16U) + 98U)
#define sfInform ((8U << 16U) + 99U)
#define sfIndexes ((19U << 16U) + 1U)
#define sfHashes ((19U << 16U) + 2U)

View File

@@ -34,6 +34,7 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(URITokenBroker, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (GuardDepth32, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(NamedHooks, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -319,6 +319,7 @@ TYPED_SFIELD(sfAttestationRewardAccount, ACCOUNT, 21)
TYPED_SFIELD(sfLockingChainDoor, ACCOUNT, 22)
TYPED_SFIELD(sfIssuingChainDoor, ACCOUNT, 23)
TYPED_SFIELD(sfSubject, ACCOUNT, 24)
TYPED_SFIELD(sfBrokerAccount, ACCOUNT, 98)
TYPED_SFIELD(sfInform, ACCOUNT, 99)
// vector of 256-bit

View File

@@ -314,6 +314,7 @@ TRANSACTION(ttURITOKEN_BURN, 46, URITokenBurn, ({
TRANSACTION(ttURITOKEN_BUY, 47, URITokenBuy, ({
{sfURITokenID, soeREQUIRED},
{sfAmount, soeREQUIRED},
{sfBrokerAccount, soeOPTIONAL},
}))
TRANSACTION(ttURITOKEN_CREATE_SELL_OFFER, 48, URITokenCreateSellOffer, ({

View File

@@ -7033,6 +7033,130 @@ private:
testTSHStrongWeak(env, tshNONE, __LINE__);
}
}
// A broker is an explicitly discovered weak TSH. This is required for
// XAH sales because native balance changes are not found by the generic
// balance-based weak-TSH discovery.
if (features[featureURITokenBroker] && features[fixXahauV1])
{
for (bool const collectEnabled : {false, true})
{
test::jtx::Env env{
*this,
network::makeNetworkConfig(
21337, "10", "1000000", "200000"),
features};
auto const issuer = Account("alice");
auto const owner = Account("bob");
auto const buyer = Account("carol");
auto const broker = Account("dave");
env.fund(XRP(1000), issuer, owner, buyer, broker);
env.close();
std::string const uri =
collectEnabled ? "broker-collect" : "broker-no-collect";
auto const tid = uritoken::tokenid(issuer, uri);
std::string const hexid{strHex(tid)};
env(uritoken::mint(issuer, uri),
uritoken::dest(owner),
uritoken::amt(XRP(1)),
ter(tesSUCCESS));
env.close();
env(uritoken::buy(owner, hexid),
uritoken::amt(XRP(1)),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
env(uritoken::sell(owner, hexid),
uritoken::dest(buyer),
uritoken::amt(XRP(1)),
ter(tesSUCCESS));
env.close();
if (collectEnabled)
addWeakTSH(env, broker);
setTSHHook(env, broker, false);
auto buy = uritoken::buy(buyer, hexid);
buy[sfAmount.jsonName] =
XRP(2).value().getJson(JsonOptions::none);
buy[sfBrokerAccount.jsonName] = broker.human();
env(buy, fee(XRP(1)), ter(tesSUCCESS));
env.close();
testTSHStrongWeak(
env, collectEnabled ? tshWEAK : tshNONE, __LINE__);
}
}
// If the token issuer is also the broker, TSH strengths are ORed and
// the account executes exactly once: weak for a non-burnable token and
// strong for a burnable token.
if (features[featureURITokenBroker] && features[fixXahauV1])
{
for (bool const burnable : {false, true})
{
test::jtx::Env env{
*this,
network::makeNetworkConfig(
21337, "10", "1000000", "200000"),
features};
auto const issuer = Account("alice");
auto const owner = Account("bob");
auto const buyer = Account("carol");
env.fund(XRP(1000), issuer, owner, buyer);
env.close();
std::string const uri =
burnable ? "broker-issuer-burnable" : "broker-issuer";
auto const tid = uritoken::tokenid(issuer, uri);
std::string const hexid{strHex(tid)};
env(uritoken::mint(issuer, uri),
uritoken::dest(owner),
uritoken::amt(XRP(1)),
txflags(burnable ? tfBurnable : 0),
ter(tesSUCCESS));
env.close();
env(uritoken::buy(owner, hexid),
uritoken::amt(XRP(1)),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
env(uritoken::sell(owner, hexid),
uritoken::dest(buyer),
uritoken::amt(XRP(1)),
ter(tesSUCCESS));
env.close();
if (!burnable)
addWeakTSH(env, issuer);
setTSHHook(env, issuer, burnable);
auto buy = uritoken::buy(buyer, hexid);
buy[sfAmount.jsonName] =
XRP(2).value().getJson(JsonOptions::none);
buy[sfBrokerAccount.jsonName] = issuer.human();
env(buy, fee(XRP(1)), ter(tesSUCCESS));
env.close();
testTSHStrongWeak(
env, burnable ? tshSTRONG : tshWEAK, __LINE__);
Json::Value params;
params[jss::transaction] =
env.tx()->getJson(JsonOptions::none)[jss::hash];
auto const result = env.rpc("json", "tx", to_string(params));
auto const executions =
result[jss::result][jss::meta][sfHookExecutions.jsonName];
BEAST_EXPECT(executions.size() == 1);
}
}
}
void

View File

@@ -0,0 +1,465 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2026 XRPL-Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx.h>
#include <test/jtx/AMM.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
namespace test {
struct URITokenBroker_test : public beast::unit_test::suite
{
static Json::Value
brokeredBuy(
jtx::Account const& buyer,
std::string const& tokenID,
STAmount const& amount,
jtx::Account const& broker)
{
auto tx = jtx::uritoken::buy(buyer, tokenID);
tx[sfAmount.jsonName] = amount.getJson(JsonOptions::none);
tx[sfBrokerAccount.jsonName] = broker.human();
return tx;
}
static std::string
mintAndSell(
jtx::Env& env,
jtx::Account const& seller,
std::string const& uri,
STAmount const& amount,
std::optional<jtx::Account> const& destination = std::nullopt)
{
auto const id = strHex(jtx::uritoken::tokenid(seller, uri));
env(jtx::uritoken::mint(seller, uri));
if (destination)
env(jtx::uritoken::sell(seller, id),
jtx::uritoken::amt(amount),
jtx::uritoken::dest(*destination));
else
env(jtx::uritoken::sell(seller, id), jtx::uritoken::amt(amount));
env.close();
return id;
}
static std::shared_ptr<SLE const>
token(jtx::Env const& env, uint256 const& id)
{
return env.le(Keylet{ltURI_TOKEN, id});
}
static std::shared_ptr<SLE const>
token(jtx::Env const& env, std::string const& id)
{
uint256 tokenID;
if (!tokenID.parseHex(id))
return nullptr;
return token(env, tokenID);
}
void
testAmendment(FeatureBitset const& features)
{
testcase("amendment");
using namespace jtx;
Account const seller{"seller"};
Account const buyer{"buyer"};
Account const broker{"broker"};
// A broker-less buy remains valid when the broker amendment is off.
{
Env env{*this, features - featureURITokenBroker};
env.fund(XRP(1000), seller, buyer, broker);
auto const id = mintAndSell(env, seller, "no-broker", XRP(10));
env(uritoken::buy(buyer, id), uritoken::amt(XRP(10)));
}
// BrokerAccount requires both URITokenBroker and fixXahauV1.
for (auto const disabled : {featureURITokenBroker, fixXahauV1})
{
Env env{*this, features - disabled};
env.fund(XRP(1000), seller, buyer, broker);
auto const id =
mintAndSell(env, seller, to_string(disabled), XRP(10));
env(brokeredBuy(buyer, id, XRP(11), broker), ter(temDISABLED));
}
}
void
testMalformed(FeatureBitset const& features)
{
testcase("preflight");
using namespace jtx;
Env env{*this, features};
Account const seller{"seller"};
Account const buyer{"buyer"};
env.fund(XRP(1000), seller, buyer);
auto const id = mintAndSell(env, seller, "preflight", XRP(10));
env(brokeredBuy(buyer, id, XRP(11), buyer), ter(temREDUNDANT));
auto zero = uritoken::buy(buyer, id);
zero[sfAmount.jsonName] = XRP(11).value().getJson(JsonOptions::none);
zero[sfBrokerAccount.jsonName] = "rrrrrrrrrrrrrrrrrrrrrhoLvTp";
env(zero, ter(temMALFORMED));
// BrokerAccount is only part of the URITokenBuy template.
auto mint = uritoken::mint(seller, "broker-on-mint");
mint[sfBrokerAccount.jsonName] = buyer.human();
env(mint, ter(temMALFORMED));
// BrokerAccount belongs only to URITokenBuy's serialization template.
auto payment = pay(buyer, seller, XRP(1));
payment[sfBrokerAccount.jsonName] = seller.human();
env(payment, ter(temMALFORMED));
}
void
testPreclaim(FeatureBitset const& features)
{
testcase("preclaim");
using namespace jtx;
Account const seller{"seller"};
Account const buyer{"buyer"};
Account const broker{"broker"};
Account const missing{"missing"};
// Missing broker account.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer);
env.memoize(missing);
auto const id = mintAndSell(env, seller, "missing", XRP(10));
env(brokeredBuy(buyer, id, XRP(11), missing), ter(tecNO_TARGET));
}
// The seller cannot also receive the broker spread.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer);
auto const id = mintAndSell(env, seller, "seller", XRP(10));
env(brokeredBuy(buyer, id, XRP(11), seller), ter(tecNO_PERMISSION));
}
// A brokered sale requires both a positive price and positive spread.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer, broker);
auto const zeroID = mintAndSell(env, seller, "zero", XRP(0), buyer);
env(brokeredBuy(buyer, zeroID, XRP(1), broker),
ter(tecNO_PERMISSION));
// The legacy destination-restricted zero-price buy is unchanged.
env(uritoken::buy(buyer, zeroID), uritoken::amt(XRP(0)));
auto const id = mintAndSell(env, seller, "spread", XRP(10));
env(brokeredBuy(buyer, id, XRP(9), broker),
ter(tecINSUFFICIENT_PAYMENT));
env(brokeredBuy(buyer, id, XRP(10), broker), ter(tecNO_PERMISSION));
}
// An IOU broker must already have a trust line.
{
Env env{*this, features};
Account const gw{"gateway"};
auto const USD = gw["USD"];
env.fund(XRP(1000), seller, buyer, broker, gw);
env.trust(USD(1000), seller, buyer);
env.close();
env(pay(gw, buyer, USD(100)));
auto const id = mintAndSell(env, seller, "no-line", USD(10));
env(brokeredBuy(buyer, id, USD(11), broker), ter(tecNO_LINE));
}
// AMM pseudo-accounts cannot be named as brokers.
{
Env env{*this, features | featureAMM};
Account const gw{"gateway"};
auto const USD = gw["USD"];
env.fund(XRP(30'000), seller, buyer, gw);
env.trust(USD(30'000), seller, buyer);
env.close();
env(pay(gw, seller, USD(20'000)));
AMM amm{env, seller, XRP(10'000), USD(10'000)};
auto const id = mintAndSell(env, seller, "amm", XRP(10));
auto tx = uritoken::buy(buyer, id);
tx[sfAmount.jsonName] = XRP(11).value().getJson(JsonOptions::none);
tx[sfBrokerAccount.jsonName] = toBase58(amm.ammAccount());
env(tx, ter(tecNO_PERMISSION));
}
}
void
testXAH(FeatureBitset const& features)
{
testcase("XAH settlement");
using namespace jtx;
Env env{*this, features};
Account const seller{"seller"};
Account const buyer{"buyer"};
Account const broker{"broker"};
env.fund(XRP(1000), seller, buyer, broker);
env(fset(seller, asfDepositAuth));
env(fset(broker, asfDepositAuth));
env.close();
auto const id = mintAndSell(env, seller, "xah", XRP(100));
auto const sellerBefore = env.balance(seller);
auto const buyerBefore = env.balance(buyer);
auto const brokerBefore = env.balance(broker);
auto const buyerOwners = env.ownerCount(buyer);
auto const brokerOwners = env.ownerCount(broker);
auto const fee = env.current()->fees().base;
env(brokeredBuy(buyer, id, XRP(110), broker));
env.close();
BEAST_EXPECT(env.balance(seller) == sellerBefore + XRP(100));
BEAST_EXPECT(env.balance(broker) == brokerBefore + XRP(10));
BEAST_EXPECT(env.balance(buyer) == buyerBefore - XRP(110) - fee);
BEAST_EXPECT(env.ownerCount(buyer) == buyerOwners + 1);
BEAST_EXPECT(env.ownerCount(broker) == brokerOwners);
auto const sle = token(env, id);
if (!BEAST_EXPECT(sle))
return;
BEAST_EXPECT((*sle)[sfOwner] == buyer.id());
BEAST_EXPECT(!sle->isFieldPresent(sfAmount));
BEAST_EXPECT(!sle->isFieldPresent(sfDestination));
}
void
testXAHReserve(FeatureBitset const& features)
{
testcase("XAH reserve boundary");
using namespace jtx;
Env env{*this, features};
Account const seller{"seller"};
Account const buyer{"buyer"};
Account const broker{"broker"};
env.fund(XRP(1000), seller, broker);
env.fund(XRP(300), buyer);
env.close();
auto const id = mintAndSell(env, seller, "reserve", XRP(1));
auto const sellerBefore = env.balance(seller);
auto const brokerBefore = env.balance(broker);
// The buyer needs the next owner reserve plus the full signed amount,
// not merely the seller's one-XAH listing amount.
env(brokeredBuy(buyer, id, XRP(51), broker),
ter(tecINSUFFICIENT_FUNDS));
BEAST_EXPECT(env.balance(seller) == sellerBefore);
BEAST_EXPECT(env.balance(broker) == brokerBefore);
auto const sle = token(env, id);
BEAST_EXPECT(sle && (*sle)[sfOwner] == seller.id());
}
void
testIOU(FeatureBitset const& features)
{
testcase("IOU settlement");
using namespace jtx;
Account const seller{"seller"};
Account const buyer{"buyer"};
Account const broker{"broker"};
Account const gw{"gateway"};
auto const USD = gw["USD"];
// Both recipients bear the transfer rate; buyer debit remains exact.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer, broker, gw);
env(rate(gw, 1.25));
env.trust(USD(1000), seller, buyer, broker);
env.close();
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
env(trust(gw, broker["USD"](1000), tfClearNoRipple));
env.close();
env(pay(gw, buyer, USD(1000)));
auto const id = mintAndSell(env, seller, "rate", USD(100));
auto const buyerBefore = env.balance(buyer, USD.issue());
auto const brokerOwners = env.ownerCount(broker);
env(brokeredBuy(buyer, id, USD(110), broker));
env.close();
BEAST_EXPECT(env.balance(seller, USD.issue()) == USD(80));
BEAST_EXPECT(env.balance(broker, USD.issue()) == USD(8));
BEAST_EXPECT(
env.balance(buyer, USD.issue()) == buyerBefore - USD(110));
BEAST_EXPECT(env.ownerCount(broker) == brokerOwners);
}
// The issuer may be the broker and needs no trust line.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer, gw);
env(rate(gw, 1.25));
env.trust(USD(1000), seller, buyer);
env.close();
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
env.close();
env(pay(gw, buyer, USD(1000)));
auto const id = mintAndSell(env, seller, "issuer", USD(100));
auto const buyerBefore = env.balance(buyer, USD.issue());
env(brokeredBuy(buyer, id, USD(110), gw));
env.close();
BEAST_EXPECT(env.balance(seller, USD.issue()) == USD(80));
BEAST_EXPECT(
env.balance(buyer, USD.issue()) == buyerBefore - USD(110));
}
// Existing line limits are not consulted and no owner object is added.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer, broker, gw);
env.trust(USD(1000), seller, buyer);
env.trust(USD(1), broker);
env.close();
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
env(trust(gw, broker["USD"](1), tfClearNoRipple));
env.close();
env(pay(gw, buyer, USD(100)));
env(pay(gw, broker, USD(1)));
auto const id = mintAndSell(env, seller, "limit", USD(10));
auto const brokerOwners = env.ownerCount(broker);
env(brokeredBuy(buyer, id, USD(20), broker));
env.close();
BEAST_EXPECT(env.balance(broker, USD.issue()) == USD(11));
BEAST_EXPECT(env.ownerCount(broker) == brokerOwners);
}
}
void
testIOUPermissions(FeatureBitset const& features)
{
testcase("IOU permissions");
using namespace jtx;
Account const seller{"seller"};
Account const buyer{"buyer"};
Account const broker{"broker"};
Account const gw{"gateway"};
auto const USD = gw["USD"];
// A frozen broker line rejects the broker leg atomically.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer, broker, gw);
env.trust(USD(1000), seller, buyer, broker);
env.close();
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
env(pay(gw, buyer, USD(100)));
env(trust(gw, broker["USD"](1000), tfSetFreeze | tfClearNoRipple));
env.close();
auto const id = mintAndSell(env, seller, "freeze", USD(10));
auto const buyerBefore = env.balance(buyer, USD.issue());
env(brokeredBuy(buyer, id, USD(11), broker), ter(tecFROZEN));
BEAST_EXPECT(env.balance(seller, USD.issue()) == USD(0));
BEAST_EXPECT(env.balance(broker, USD.issue()) == USD(0));
BEAST_EXPECT(env.balance(buyer, USD.issue()) == buyerBefore);
}
// Under RequireAuth, an existing but unauthorized broker line fails.
{
Env env{*this, features};
auto const sellerUSD = seller["USD"];
auto const buyerUSD = buyer["USD"];
env.fund(XRP(1000), seller, buyer, broker, gw);
env(fset(gw, asfRequireAuth));
env.close();
env.trust(USD(1000), seller, buyer, broker);
env(trust(gw, sellerUSD(1000)),
txflags(tfSetfAuth | tfClearNoRipple));
env(trust(gw, buyerUSD(1000)),
txflags(tfSetfAuth | tfClearNoRipple));
env(trust(gw, broker["USD"](1000)), txflags(tfClearNoRipple));
env.close();
env(pay(gw, buyer, USD(100)));
auto const id = mintAndSell(env, seller, "auth", USD(10));
env(brokeredBuy(buyer, id, USD(11), broker), ter(tecNO_AUTH));
}
// NoRipple on the broker leg rejects the whole transaction; the
// already-computed seller leg must also be rolled back.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer, broker, gw);
env.trust(USD(1000), seller, buyer, broker);
env.close();
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
env(pay(gw, buyer, USD(100)));
env(trust(gw, broker["USD"](1000), tfSetNoRipple));
env.close();
auto const id = mintAndSell(env, seller, "no-ripple", USD(10));
auto const sellerBefore = env.balance(seller, USD.issue());
auto const buyerBefore = env.balance(buyer, USD.issue());
auto const brokerBefore = env.balance(broker, USD.issue());
env(brokeredBuy(buyer, id, USD(11), broker), ter(tecPATH_DRY));
BEAST_EXPECT(env.balance(seller, USD.issue()) == sellerBefore);
BEAST_EXPECT(env.balance(buyer, USD.issue()) == buyerBefore);
BEAST_EXPECT(env.balance(broker, USD.issue()) == brokerBefore);
auto const sle = token(env, id);
BEAST_EXPECT(sle && (*sle)[sfOwner] == seller.id());
}
}
public:
void
run() override
{
auto const features = jtx::supported_amendments();
testAmendment(features);
testMalformed(features);
testPreclaim(features);
testXAH(features);
testXAHReserve(features);
testIOU(features);
testIOUPermissions(features);
}
};
BEAST_DEFINE_TESTSUITE(URITokenBroker, app, ripple);
} // namespace test
} // namespace ripple

View File

@@ -200,6 +200,10 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
issuer,
(ut->getFlags() & lsfBurnable) ? tshSTRONG : tshWEAK);
if (rv.rules().enabled(featureURITokenBroker) &&
tx.isFieldPresent(sfBrokerAccount))
ADD_TSH(tx.getAccountID(sfBrokerAccount), tshWEAK);
break;
}

View File

@@ -29,6 +29,33 @@
namespace ripple {
TER
checkBroker(
ReadView const& view,
AccountID const& broker,
AccountID const& owner,
STAmount const& saleAmount,
STAmount const& purchaseAmount)
{
auto const sleBroker = view.read(keylet::account(broker));
if (!sleBroker)
return tecNO_TARGET;
if (sleBroker->isFieldPresent(sfAMMID))
return tecNO_PERMISSION;
if (broker == owner || saleAmount == beast::zero ||
purchaseAmount == saleAmount)
return tecNO_PERMISSION;
if (!purchaseAmount.native() && broker != purchaseAmount.getIssuer() &&
!view.exists(keylet::line(
broker, purchaseAmount.getIssuer(), purchaseAmount.getCurrency())))
return tecNO_LINE;
return tesSUCCESS;
}
NotTEC
URIToken::preflight(PreflightContext const& ctx)
{
@@ -138,6 +165,23 @@ URIToken::preflight(PreflightContext const& ctx)
ctx.tx.getAccountID(sfAccount) == ctx.tx.getAccountID(sfDestination))
return temREDUNDANT;
if (ctx.tx.isFieldPresent(sfBrokerAccount))
{
if (tt != ttURITOKEN_BUY)
return temMALFORMED;
if (!ctx.rules.enabled(featureURITokenBroker) ||
!ctx.rules.enabled(fixXahauV1))
return temDISABLED;
auto const broker = ctx.tx.getAccountID(sfBrokerAccount);
if (broker == beast::zero)
return temMALFORMED;
if (broker == ctx.tx.getAccountID(sfAccount))
return temREDUNDANT;
}
return preflight2(ctx);
}
@@ -235,6 +279,21 @@ URIToken::preclaim(PreclaimContext const& ctx)
if (purchaseAmount < saleAmount)
return tecINSUFFICIENT_PAYMENT;
if (ctx.tx.isFieldPresent(sfBrokerAccount))
{
if (!ctx.view.rules().enabled(featureURITokenBroker) || !fixV1)
return temDISABLED;
if (TER const result = checkBroker(
ctx.view,
ctx.tx.getAccountID(sfBrokerAccount),
*owner,
*saleAmount,
purchaseAmount);
!isTesSuccess(result))
return result;
}
if (fixV1)
{
if (purchaseAmount.native() && saleAmount->native())
@@ -483,6 +542,11 @@ URIToken::doApply()
if (purchaseAmount.issue() != saleAmount->issue())
return temBAD_CURRENCY;
std::optional<AccountID> const broker = ctx_.tx[~sfBrokerAccount];
if (broker &&
(!sb.rules().enabled(featureURITokenBroker) || !fixV1))
return temDISABLED;
if (fixV1)
{
// this is the reworked version of the buy routine
@@ -490,6 +554,14 @@ URIToken::doApply()
if (purchaseAmount < saleAmount)
return tecINSUFFICIENT_PAYMENT;
if (broker)
{
if (TER const result = checkBroker(
sb, *broker, *owner, *saleAmount, purchaseAmount);
!isTesSuccess(result))
return result;
}
// if it's an xrp sale/purchase then no trustline needed
if (purchaseAmount.native())
{
@@ -525,6 +597,23 @@ URIToken::doApply()
return result;
}
if (broker)
{
if (TER result = trustTransferAllowed(
sb,
{account_, *broker},
purchaseAmount.issue(),
j);
!isTesSuccess(result))
{
JLOG(j.trace()) << "URIToken::doApply broker "
"trustTransferAllowed result="
<< result;
return result;
}
}
if (STAmount availableFunds{accountFunds(
sb, account_, purchaseAmount, fhZERO_IF_FROZEN, j)};
purchaseAmount > availableFunds)
@@ -536,13 +625,31 @@ URIToken::doApply()
sb,
account_,
*owner,
purchaseAmount,
broker ? *saleAmount : purchaseAmount,
j,
WaiveTransferFee::No,
false);
!isTesSuccess(result))
return result;
if (broker)
{
STAmount const spread = purchaseAmount - *saleAmount;
if (spread <= beast::zero)
return tecINTERNAL; // LCOV_EXCL_LINE
if (TER result = accountSend(
sb,
account_,
*broker,
spread,
j,
WaiveTransferFee::No,
false);
!isTesSuccess(result))
return result;
}
// add token to new owner dir
auto const newPage = sb.dirInsert(
keylet::ownerDir(account_),