mirror of
https://github.com/Xahau/xahaud.git
synced 2026-08-28 10:40:54 +00:00
Compare commits
2 Commits
coverage-l
...
feature/ur
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
043dcb2a38 | ||
|
|
bb244ef772 |
@@ -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:
|
||||
|
||||
51
.github/actions/xahau-ga-dependencies/action.yml
vendored
51
.github/actions/xahau-ga-dependencies/action.yml
vendored
@@ -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
|
||||
|
||||
3
.github/workflows/xahau-ga-macos.yml
vendored
3
.github/workflows/xahau-ga-macos.yml
vendored
@@ -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
|
||||
|
||||
88
.github/workflows/xahau-ga-nix.yml
vendored
88
.github/workflows/xahau-ga-nix.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
|
||||
@@ -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>)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, ({
|
||||
|
||||
@@ -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
|
||||
|
||||
465
src/test/app/URITokenBroker_test.cpp
Normal file
465
src/test/app/URITokenBroker_test.cpp
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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_),
|
||||
|
||||
Reference in New Issue
Block a user