Compare commits

..

22 Commits

Author SHA1 Message Date
Valon Mamudi
f50a0a9258 Merge branch 'develop' into nudbBlockSize 2025-08-28 00:06:12 +02:00
Valon Mamudi
b4eabf384e removed trailing whitespaces 2025-08-27 23:59:45 +02:00
Ed Hennis
2f155d9273 Merge branch 'develop' into nudbBlockSize 2025-08-26 17:01:43 -04:00
Valon Mamudi
53fcfda242 Remove unnecessary warning text as EXPERIMENTAL tag is self-explanatory 2025-08-12 22:50:52 +02:00
Valon Mamudi
540f0aff7f Apply clang-format to NuDBFactory_test.cpp 2025-08-12 22:32:56 +02:00
Valon Mamudi
20e0fefa2c Enhance nudb_block_size documentation with performance testing recommendation
- Move 'Default is 4096.' to parameter definition line for better organization
- Add performance testing recommendation for non-default block sizes
- Maintain visual spacing in documentation structure
- Net result: +2 content lines with improved readability
2025-08-12 20:22:21 +02:00
Valon Mamudi
0db558cf83 Enhance nudb_block_size documentation with detailed performance characteristics
- Expand 4096 bytes description with memory footprint and latency details
- Add sequential throughput and I/O operation details for 8192-16384 bytes
- Include enterprise environment recommendations for 32768 bytes
- Provide comprehensive guidance for block size selection based on hardware
2025-08-12 20:15:29 +02:00
Valon Mamudi
6385985ee2 Merge branch 'develop' into nudbBlockSize 2025-08-12 17:52:10 +02:00
Valon Mamudi
4d7173b5d9 Mark nudb_block_size as experimental in configuration documentation
Add clear warning that nudb_block_size is an experimental feature that may
affect database performance and stability, as suggested in code review.
2025-08-12 17:47:27 +02:00
Valon Mamudi
b3bc48999b Resolve merge conflict 2025-08-12 17:32:27 +02:00
Ed Hennis
aea76c8693 Some improvements:
- Get the default value from NuDB.
- Throw on error instead of using the default value.
- Check the parsed block size in unit tests.
- Add some more BEAST_EXPECT checks.
2025-08-12 08:25:33 +02:00
Bronek Kozicki
9d20f27a55 Merge branch 'develop' into nudbBlockSize 2025-08-08 16:24:00 +01:00
Valon Mamudi
5026b64180 Merge branch 'develop' into nudbBlockSize 2025-06-25 00:04:52 +02:00
Valon Mamudi
9fd0f72039 Merge branch 'develop' into nudbBlockSize 2025-06-18 01:10:22 +02:00
Valon Mamudi
45f024ddef Merge branch 'develop' into nudbBlockSize 2025-06-10 22:53:13 +02:00
Valon Mamudi
ec25b9d24a format NuDBFactory_test.cpp with clang-format 2025-06-10 22:52:37 +02:00
Valon Mamudi
f8687226ea Add comprehensive test coverage for NuDBFactory block size validation
- Test default block size behavior
- Test valid block sizes (4096, 8192, 16384, 32768)
- Test invalid block size fallback to default
- Test log message verification for valid/invalid values
- Test power of 2 validation logic
- Test configuration parsing edge cases
- Test data persistence across different block sizes
2025-06-10 22:49:25 +02:00
Valon Mamudi
ec530a9b0c Merge branch 'XRPLF:develop' into nudbBlockSize 2025-06-07 00:01:14 +02:00
Valon Mamudi
086b9f62d4 Improve NuDB block size configuration with early return pattern and 32K support
- Implement early return pattern in parseBlockSize function to reduce nesting
- Fix unqualified assignment by using properly scoped const variable
- Decreased maximum block size limit from 64K to 32K (32768 bytes)
- Update configuration documentation to reflect correct 32K maximum
- Add guidance for 32K block size usage in high-performance scenarios
- Apply clang-format fixes to resolve CI formatting checks

This enhances NuDB performance configurability while maintaining code quality
and following modern C++ best practices. The 32K limit reflects the actual
maximum supported by NuDB as confirmed by testing.
2025-06-07 00:00:11 +02:00
Valon Mamudi
1eb4b08592 Merge branch 'develop' into nudbBlockSize 2025-06-03 20:46:54 +02:00
Valon Mamudi
a7c9c69fbd Merge branch 'develop' into nudbBlockSize 2025-06-03 18:03:53 +02:00
Valon Mamudi
6e11a3f1a3 Add configurable NuDB block size feature
- Implement parseBlockSize() function with validation
- Add nudb_block_size configuration parameter
- Support block sizes from 4K to 64K (power of 2)
- Add comprehensive logging and error handling
- Maintain backward compatibility with 4K default
2025-06-03 17:45:35 +02:00
75 changed files with 3068 additions and 8848 deletions

View File

@@ -1,5 +1,7 @@
# This action installs and optionally uploads Conan dependencies to a remote
# repository. The dependencies will only be uploaded if the credentials are
# provided.
name: Build Conan dependencies
description: "Install Conan dependencies, optionally forcing a rebuild of all dependencies."
# Note that actions do not support 'type' and all inputs are strings, see
# https://docs.github.com/en/actions/reference/workflows-and-actions/metadata-syntax#inputs.
@@ -10,10 +12,28 @@ inputs:
build_type:
description: 'The build type to use ("Debug", "Release").'
required: true
conan_remote_name:
description: "The name of the Conan remote to use."
required: true
conan_remote_url:
description: "The URL of the Conan endpoint to use."
required: true
conan_remote_username:
description: "The username for logging into the Conan remote. If not provided, the dependencies will not be uploaded."
required: false
default: ""
conan_remote_password:
description: "The password for logging into the Conan remote. If not provided, the dependencies will not be uploaded."
required: false
default: ""
force_build:
description: 'Force building of all dependencies ("true", "false").'
required: false
default: "false"
force_upload:
description: 'Force uploading of all dependencies ("true", "false").'
required: false
default: "false"
runs:
using: composite
@@ -31,3 +51,12 @@ runs:
--options:host '&:xrpld=True' \
--settings:all build_type=${{ inputs.build_type }} \
--format=json ..
- name: Upload Conan dependencies
if: ${{ inputs.conan_remote_username != '' && inputs.conan_remote_password != '' }}
shell: bash
working-directory: ${{ inputs.build_dir }}
run: |
echo "Logging into Conan remote '${{ inputs.conan_remote_name }}' at ${{ inputs.conan_remote_url }}."
conan remote login ${{ inputs.conan_remote_name }} "${{ inputs.conan_remote_username }}" --password "${{ inputs.conan_remote_password }}"
echo 'Uploading dependencies.'
conan upload '*' --confirm --check ${{ inputs.force_upload == 'true' && '--force' || '' }} --remote=${{ inputs.conan_remote_name }}

View File

@@ -1,7 +1,6 @@
# This action build and tests the binary. The Conan dependencies must have
# already been installed (see the build-deps action).
name: Build and Test
description: "Build and test the binary."
# Note that actions do not support 'type' and all inputs are strings, see
# https://docs.github.com/en/actions/reference/workflows-and-actions/metadata-syntax#inputs.

View File

@@ -1,43 +0,0 @@
name: Setup Conan
description: "Set up Conan configuration, profile, and remote."
inputs:
conan_remote_name:
description: "The name of the Conan remote to use."
required: false
default: xrplf
conan_remote_url:
description: "The URL of the Conan endpoint to use."
required: false
default: https://conan.ripplex.io
runs:
using: composite
steps:
- name: Set up Conan configuration
shell: bash
run: |
echo 'Installing configuration.'
cat conan/global.conf ${{ runner.os == 'Linux' && '>>' || '>' }} $(conan config home)/global.conf
echo 'Conan configuration:'
conan config show '*'
- name: Set up Conan profile
shell: bash
run: |
echo 'Installing profile.'
conan config install conan/profiles/default -tf $(conan config home)/profiles/
echo 'Conan profile:'
conan profile show
- name: Set up Conan remote
shell: bash
run: |
echo "Adding Conan remote '${{ inputs.conan_remote_name }}' at ${{ inputs.conan_remote_url }}."
conan remote add --index 0 --force ${{ inputs.conan_remote_name }} ${{ inputs.conan_remote_url }}
echo 'Listing Conan remotes.'
conan remote list

43
.github/scripts/strategy-matrix/generate.py vendored Executable file → Normal file
View File

@@ -2,17 +2,7 @@
import argparse
import itertools
import json
from pathlib import Path
from dataclasses import dataclass
THIS_DIR = Path(__file__).parent.resolve()
@dataclass
class Config:
architecture: list[dict]
os: list[dict]
build_type: list[str]
cmake_args: list[str]
import re
'''
Generate a strategy matrix for GitHub Actions CI.
@@ -28,9 +18,9 @@ We will further set additional CMake arguments as follows:
- Certain Debian Bookworm configurations will change the reference fee, enable
codecov, and enable voidstar in PRs.
'''
def generate_strategy_matrix(all: bool, config: Config) -> list:
def generate_strategy_matrix(all: bool, architecture: list[dict], os: list[dict], build_type: list[str], cmake_args: list[str]) -> dict:
configurations = []
for architecture, os, build_type, cmake_args in itertools.product(config.architecture, config.os, config.build_type, config.cmake_args):
for architecture, os, build_type, cmake_args in itertools.product(architecture, os, build_type, cmake_args):
# The default CMake target is 'all' for Linux and MacOS and 'install'
# for Windows, but it can get overridden for certain configurations.
cmake_target = 'install' if os["distro_name"] == 'windows' else 'all'
@@ -168,30 +158,21 @@ def generate_strategy_matrix(all: bool, config: Config) -> list:
'architecture': architecture,
})
return configurations
def read_config(file: Path) -> Config:
config = json.loads(file.read_text())
if config['architecture'] is None or config['os'] is None or config['build_type'] is None or config['cmake_args'] is None:
raise Exception('Invalid configuration file.')
return Config(**config)
return {'include': configurations}
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-a', '--all', help='Set to generate all configurations (generally used when merging a PR) or leave unset to generate a subset of configurations (generally used when committing to a PR).', action="store_true")
parser.add_argument('-c', '--config', help='Path to the JSON file containing the strategy matrix configurations.', required=False, type=Path)
parser.add_argument('-c', '--config', help='Path to the JSON file containing the strategy matrix configurations.', required=True, type=str)
args = parser.parse_args()
matrix = []
if args.config is None or args.config == '':
matrix += generate_strategy_matrix(args.all, read_config(THIS_DIR / "linux.json"))
matrix += generate_strategy_matrix(args.all, read_config(THIS_DIR / "macos.json"))
matrix += generate_strategy_matrix(args.all, read_config(THIS_DIR / "windows.json"))
else:
matrix += generate_strategy_matrix(args.all, read_config(args.config))
# Load the JSON configuration file.
config = None
with open(args.config, 'r') as f:
config = json.load(f)
if config['architecture'] is None or config['os'] is None or config['build_type'] is None or config['cmake_args'] is None:
raise Exception('Invalid configuration file.')
# Generate the strategy matrix.
print(f'matrix={json.dumps({"include": matrix})}')
print(f'matrix={json.dumps(generate_strategy_matrix(args.all, config['architecture'], config['os'], config['build_type'], config['cmake_args']))}')

View File

@@ -13,6 +13,14 @@ on:
required: false
type: string
default: ".build"
conan_remote_name:
description: "The name of the Conan remote to use."
required: true
type: string
conan_remote_url:
description: "The URL of the Conan endpoint to use."
required: true
type: string
dependencies_force_build:
description: "Force building of all dependencies."
required: false
@@ -37,6 +45,12 @@ on:
codecov_token:
description: "The Codecov token to use for uploading coverage reports."
required: false
conan_remote_username:
description: "The username for logging into the Conan remote. If not provided, the dependencies will not be uploaded."
required: false
conan_remote_password:
description: "The password for logging into the Conan remote. If not provided, the dependencies will not be uploaded."
required: false
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.os }}
@@ -49,10 +63,20 @@ defaults:
jobs:
# Generate the strategy matrix to be used by the following job.
generate-matrix:
uses: ./.github/workflows/reusable-strategy-matrix.yml
with:
os: ${{ inputs.os }}
strategy_matrix: ${{ inputs.strategy_matrix }}
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: 3.13
- name: Generate strategy matrix
working-directory: .github/scripts/strategy-matrix
id: generate
run: python generate.py ${{ inputs.strategy_matrix == 'all' && '--all' || '' }} --config=${{ inputs.os }}.json >> "${GITHUB_OUTPUT}"
outputs:
matrix: ${{ steps.generate.outputs.matrix }}
# Build and test the binary.
build-test:
@@ -77,18 +101,28 @@ jobs:
echo 'CMake arguments: ${{ matrix.cmake_args }}'
echo 'CMake target: ${{ matrix.cmake_target }}'
echo 'Config name: ${{ matrix.config_name }}'
- name: Cleanup workspace
if: ${{ runner.os == 'macOS' }}
uses: XRPLF/actions/.github/actions/cleanup-workspace@3f044c7478548e3c32ff68980eeb36ece02b364e
- name: Clean workspace (MacOS)
if: ${{ inputs.os == 'macos' }}
run: |
WORKSPACE=${{ github.workspace }}
echo "Cleaning workspace '${WORKSPACE}'."
if [ -z "${WORKSPACE}" ] || [ "${WORKSPACE}" = "/" ]; then
echo "Invalid working directory '${WORKSPACE}'."
exit 1
fi
find "${WORKSPACE}" -depth 1 | xargs rm -rfv
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Prepare runner
uses: XRPLF/actions/.github/actions/prepare-runner@638e0dc11ea230f91bd26622fb542116bb5254d5
- name: Set up Python (Windows)
if: ${{ inputs.os == 'windows' }}
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
disable_ccache: false
python-version: 3.13
- name: Install build tools (Windows)
if: ${{ inputs.os == 'windows' }}
run: |
echo 'Installing build tools.'
pip install wheel conan
- name: Check configuration (Windows)
if: ${{ inputs.os == 'windows' }}
run: |
@@ -100,6 +134,11 @@ jobs:
echo 'Checking Conan version.'
conan --version
- name: Install build tools (MacOS)
if: ${{ inputs.os == 'macos' }}
run: |
echo 'Installing build tools.'
brew install --quiet cmake conan ninja coreutils
- name: Check configuration (Linux and MacOS)
if: ${{ inputs.os == 'linux' || inputs.os == 'macos' }}
run: |
@@ -123,17 +162,51 @@ jobs:
echo 'Checking nproc version.'
nproc --version
- name: Set up Conan home directory (MacOS)
if: ${{ inputs.os == 'macos' }}
run: |
echo 'Setting up Conan home directory.'
export CONAN_HOME=${{ github.workspace }}/.conan
mkdir -p ${CONAN_HOME}
- name: Set up Conan home directory (Windows)
if: ${{ inputs.os == 'windows' }}
run: |
echo 'Setting up Conan home directory.'
set CONAN_HOME=${{ github.workspace }}\.conan
mkdir -p %CONAN_HOME%
- name: Set up Conan configuration
run: |
echo 'Installing configuration.'
cat conan/global.conf ${{ inputs.os == 'linux' && '>>' || '>' }} $(conan config home)/global.conf
- name: Setup Conan
uses: ./.github/actions/setup-conan
echo 'Conan configuration:'
conan config show '*'
- name: Set up Conan profile
run: |
echo 'Installing profile.'
conan config install conan/profiles/default -tf $(conan config home)/profiles/
echo 'Conan profile:'
conan profile show
- name: Set up Conan remote
shell: bash
run: |
echo "Adding Conan remote '${{ inputs.conan_remote_name }}' at ${{ inputs.conan_remote_url }}."
conan remote add --index 0 --force ${{ inputs.conan_remote_name }} ${{ inputs.conan_remote_url }}
echo 'Listing Conan remotes.'
conan remote list
- name: Build dependencies
uses: ./.github/actions/build-deps
with:
build_dir: ${{ inputs.build_dir }}
build_type: ${{ matrix.build_type }}
conan_remote_name: ${{ inputs.conan_remote_name }}
conan_remote_url: ${{ inputs.conan_remote_url }}
conan_remote_username: ${{ secrets.conan_remote_username }}
conan_remote_password: ${{ secrets.conan_remote_password }}
force_build: ${{ inputs.dependencies_force_build }}
force_upload: ${{ inputs.dependencies_force_upload }}
- name: Build and test binary
uses: ./.github/actions/build-test
with:

View File

@@ -17,10 +17,41 @@ jobs:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/ci/tools-rippled-pre-commit
steps:
# The $GITHUB_WORKSPACE and ${{ github.workspace }} might not point to the
# same directory for jobs running in containers. The actions/checkout step
# is *supposed* to checkout into $GITHUB_WORKSPACE and then add it to
# safe.directory (see instructions at https://github.com/actions/checkout)
# but that is apparently not happening for some container images. We
# therefore preemptively add both directories to safe.directory. See also
# https://github.com/actions/runner/issues/2058 for more details.
- name: Configure git safe.directory
run: |
git config --global --add safe.directory $GITHUB_WORKSPACE
git config --global --add safe.directory ${{ github.workspace }}
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Prepare runner
uses: XRPLF/actions/.github/actions/prepare-runner@638e0dc11ea230f91bd26622fb542116bb5254d5
- name: Check configuration
run: |
echo 'Checking path.'
echo ${PATH} | tr ':' '\n'
echo 'Checking environment variables.'
env | sort
echo 'Checking pre-commit version.'
pre-commit --version
echo 'Checking clang-format version.'
clang-format --version
echo 'Checking NPM version.'
npm --version
echo 'Checking Node.js version.'
node --version
echo 'Checking prettier version.'
prettier --version
- name: Format code
run: pre-commit run --show-diff-on-failure --color=always --all-files
- name: Check for differences

View File

@@ -9,14 +9,12 @@ on:
inputs:
conan_remote_name:
description: "The name of the Conan remote to use."
required: false
required: true
type: string
default: xrplf
conan_remote_url:
description: "The URL of the Conan endpoint to use."
required: false
required: true
type: string
default: https://conan.ripplex.io
secrets:
clio_notify_token:
description: "The GitHub token to notify Clio about new versions."
@@ -52,25 +50,21 @@ jobs:
echo "channel=pr_${{ github.event.pull_request.number }}" >> "${GITHUB_OUTPUT}"
echo 'Extracting version.'
echo "version=$(cat src/libxrpl/protocol/BuildInfo.cpp | grep "versionString =" | awk -F '"' '{print $2}')" >> "${GITHUB_OUTPUT}"
- name: Calculate conan reference
id: conan_ref
- name: Add Conan remote
run: |
echo "conan_ref=${{ steps.generate.outputs.version }}@${{ steps.generate.outputs.user }}/${{ steps.generate.outputs.channel }}" >> "${GITHUB_OUTPUT}"
- name: Set up Conan
uses: ./.github/actions/setup-conan
with:
conan_remote_name: ${{ inputs.conan_remote_name }}
conan_remote_url: ${{ inputs.conan_remote_url }}
echo "Adding Conan remote '${{ inputs.conan_remote_name }}' at ${{ inputs.conan_remote_url }}."
conan remote add --index 0 --force ${{ inputs.conan_remote_name }} ${{ inputs.conan_remote_url }}
echo 'Listing Conan remotes.'
conan remote list
- name: Log into Conan remote
run: conan remote login ${{ inputs.conan_remote_name }} "${{ secrets.conan_remote_username }}" --password "${{ secrets.conan_remote_password }}"
- name: Upload package
run: |
conan export --user=${{ steps.generate.outputs.user }} --channel=${{ steps.generate.outputs.channel }} .
conan upload --confirm --check --remote=${{ inputs.conan_remote_name }} xrpl/${{ steps.conan_ref.outputs.conan_ref }}
conan upload --confirm --check --remote=${{ inputs.conan_remote_name }} xrpl/${{ steps.generate.outputs.version }}@${{ steps.generate.outputs.user }}/${{ steps.generate.outputs.channel }}
outputs:
conan_ref: ${{ steps.conan_ref.outputs.conan_ref }}
channel: ${{ steps.generate.outputs.channel }}
version: ${{ steps.generate.outputs.version }}
notify:
needs: upload
@@ -82,5 +76,5 @@ jobs:
run: |
gh api --method POST -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" \
/repos/xrplf/clio/dispatches -f "event_type=check_libxrpl" \
-F "client_payload[conan_ref]=${{ needs.upload.outputs.conan_ref }}" \
-F "client_payload[pr_url]=${{ github.event.pull_request.html_url }}"
-F "client_payload[version]=${{ needs.upload.outputs.version }}@${{ needs.upload.outputs.user }}/${{ needs.upload.outputs.channel }}" \
-F "client_payload[pr]=${{ github.event.pull_request.number }}"

View File

@@ -23,27 +23,35 @@ defaults:
run:
shell: bash
env:
CONAN_REMOTE_NAME: xrplf
CONAN_REMOTE_URL: https://conan.ripplex.io
jobs:
# This job determines whether the rest of the workflow should run. It runs
# when the PR is not a draft (which should also cover merge-group) or
# has the 'DraftRunCI' label.
# This job determines whether the workflow should run. It runs when the PR is
# not a draft or has the 'DraftRunCI' label.
should-run:
if: ${{ !github.event.pull_request.draft || contains(github.event.pull_request.labels.*.name, 'DraftRunCI') }}
runs-on: ubuntu-latest
steps:
- name: No-op
run: true
# This job checks whether any files have changed that should cause the next
# jobs to run. We do it this way rather than using `paths` in the `on:`
# section, because all required checks must pass, even for changes that do not
# modify anything that affects those checks. We would therefore like to make
# the checks required only if the job runs, but GitHub does not support that
# directly. By always executing the workflow on new commits and by using the
# changed-files action below, we ensure that Github considers any skipped jobs
# to have passed, and in turn the required checks as well.
any-changed:
needs: should-run
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Determine changed files
# This step checks whether any files have changed that should
# cause the next jobs to run. We do it this way rather than
# using `paths` in the `on:` section, because all required
# checks must pass, even for changes that do not modify anything
# that affects those checks. We would therefore like to make the
# checks required only if the job runs, but GitHub does not
# support that directly. By always executing the workflow on new
# commits and by using the changed-files action below, we ensure
# that Github considers any skipped jobs to have passed, and in
# turn the required checks as well.
id: changes
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
with:
@@ -71,66 +79,54 @@ jobs:
tests/**
CMakeLists.txt
conanfile.py
conan.lock
- name: Check whether to run
# This step determines whether the rest of the workflow should
# run. The rest of the workflow will run if this job runs AND at
# least one of:
# * Any of the files checked in the `changes` step were modified
# * The PR is NOT a draft and is labeled "Ready to merge"
# * The workflow is running from the merge queue
id: go
env:
FILES: ${{ steps.changes.outputs.any_changed }}
DRAFT: ${{ github.event.pull_request.draft }}
READY: ${{ contains(github.event.pull_request.labels.*.name, 'Ready to merge') }}
MERGE: ${{ github.event_name == 'merge_group' }}
run: |
echo "go=${{ (env.DRAFT != 'true' && env.READY == 'true') || env.FILES == 'true' || env.MERGE == 'true' }}" >> "${GITHUB_OUTPUT}"
cat "${GITHUB_OUTPUT}"
outputs:
go: ${{ steps.go.outputs.go == 'true' }}
changed: ${{ steps.changes.outputs.any_changed }}
check-format:
needs: should-run
if: needs.should-run.outputs.go == 'true'
needs: any-changed
if: needs.any-changed.outputs.changed == 'true'
uses: ./.github/workflows/check-format.yml
check-levelization:
needs: should-run
if: needs.should-run.outputs.go == 'true'
needs: any-changed
if: needs.any-changed.outputs.changed == 'true'
uses: ./.github/workflows/check-levelization.yml
# This job works around the limitation that GitHub Actions does not support
# using environment variables as inputs for reusable workflows.
generate-outputs:
needs: any-changed
if: needs.any-changed.outputs.changed == 'true'
runs-on: ubuntu-latest
steps:
- name: No-op
run: true
outputs:
conan_remote_name: ${{ env.CONAN_REMOTE_NAME }}
conan_remote_url: ${{ env.CONAN_REMOTE_URL }}
build-test:
needs: should-run
if: needs.should-run.outputs.go == 'true'
needs: generate-outputs
uses: ./.github/workflows/build-test.yml
strategy:
matrix:
os: [linux, macos, windows]
with:
conan_remote_name: ${{ needs.generate-outputs.outputs.conan_remote_name }}
conan_remote_url: ${{ needs.generate-outputs.outputs.conan_remote_url }}
os: ${{ matrix.os }}
secrets:
codecov_token: ${{ secrets.CODECOV_TOKEN }}
notify-clio:
needs:
- should-run
- generate-outputs
- build-test
if: needs.should-run.outputs.go == 'true'
uses: ./.github/workflows/notify-clio.yml
with:
conan_remote_name: ${{ needs.generate-outputs.outputs.conan_remote_name }}
conan_remote_url: ${{ needs.generate-outputs.outputs.conan_remote_url }}
secrets:
clio_notify_token: ${{ secrets.CLIO_NOTIFY_TOKEN }}
conan_remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
conan_remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
passed:
if: failure() || cancelled()
needs:
- build-test
- check-format
- check-levelization
runs-on: ubuntu-latest
steps:
- name: Fail
run: false

View File

@@ -32,7 +32,6 @@ on:
- "tests/**"
- "CMakeLists.txt"
- "conanfile.py"
- "conan.lock"
# Run at 06:32 UTC on every day of the week from Monday through Friday. This
# will force all dependencies to be rebuilt, which is useful to verify that
@@ -66,18 +65,54 @@ defaults:
run:
shell: bash
env:
CONAN_REMOTE_NAME: xrplf
CONAN_REMOTE_URL: https://conan.ripplex.io
jobs:
check-missing-commits:
if: ${{ github.event_name == 'push' && github.ref_type == 'branch' && contains(fromJSON('["develop", "release"]'), github.ref_name) }}
uses: ./.github/workflows/check-missing-commits.yml
# This job works around the limitation that GitHub Actions does not support
# using environment variables as inputs for reusable workflows. It also sets
# outputs that depend on the event that triggered the workflow.
generate-outputs:
runs-on: ubuntu-latest
steps:
- name: Check inputs and set outputs
id: generate
run: |
if [[ '${{ github.event_name }}' == 'push' ]]; then
echo 'dependencies_force_build=false' >> "${GITHUB_OUTPUT}"
echo 'dependencies_force_upload=false' >> "${GITHUB_OUTPUT}"
elif [[ '${{ github.event_name }}' == 'schedule' ]]; then
echo 'dependencies_force_build=true' >> "${GITHUB_OUTPUT}"
echo 'dependencies_force_upload=false' >> "${GITHUB_OUTPUT}"
else
echo 'dependencies_force_build=${{ inputs.dependencies_force_build }}' >> "${GITHUB_OUTPUT}"
echo 'dependencies_force_upload=${{ inputs.dependencies_force_upload }}' >> "${GITHUB_OUTPUT}"
fi
outputs:
conan_remote_name: ${{ env.CONAN_REMOTE_NAME }}
conan_remote_url: ${{ env.CONAN_REMOTE_URL }}
dependencies_force_build: ${{ steps.generate.outputs.dependencies_force_build }}
dependencies_force_upload: ${{ steps.generate.outputs.dependencies_force_upload }}
build-test:
needs: generate-outputs
uses: ./.github/workflows/build-test.yml
strategy:
matrix:
os: [linux, macos, windows]
with:
conan_remote_name: ${{ needs.generate-outputs.outputs.conan_remote_name }}
conan_remote_url: ${{ needs.generate-outputs.outputs.conan_remote_url }}
dependencies_force_build: ${{ needs.generate-outputs.outputs.dependencies_force_build == 'true' }}
dependencies_force_upload: ${{ needs.generate-outputs.outputs.dependencies_force_upload == 'true' }}
os: ${{ matrix.os }}
strategy_matrix: "minimal"
strategy_matrix: "all"
secrets:
codecov_token: ${{ secrets.CODECOV_TOKEN }}
conan_remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
conan_remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}

View File

@@ -1,38 +0,0 @@
name: Generate strategy matrix
on:
workflow_call:
inputs:
os:
description: 'The operating system to use for the build ("linux", "macos", "windows").'
required: false
type: string
strategy_matrix:
# TODO: Support additional strategies, e.g. "ubuntu" for generating all Ubuntu configurations.
description: 'The strategy matrix to use for generating the configurations ("minimal", "all").'
required: false
type: string
default: "minimal"
outputs:
matrix:
description: "The generated strategy matrix."
value: ${{ jobs.generate-matrix.outputs.matrix }}
jobs:
generate-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.generate.outputs.matrix }}
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: 3.13
- name: Generate strategy matrix
working-directory: .github/scripts/strategy-matrix
id: generate
run: ./generate.py ${{ inputs.strategy_matrix == 'all' && '--all' || '' }} ${{ inputs.os != '' && format('--config={0}.json', inputs.os) || '' }} >> "${GITHUB_OUTPUT}"

View File

@@ -1,83 +0,0 @@
name: Upload Conan Dependencies
on:
schedule:
- cron: "0 3 * * 2-6"
workflow_dispatch:
inputs:
force_source_build:
description: "Force source build of all dependencies"
required: false
default: false
type: boolean
force_upload:
description: "Force upload of all dependencies"
required: false
default: false
type: boolean
pull_request:
branches: [develop]
paths:
# This allows testing changes to the upload workflow in a PR
- .github/workflows/upload-conan-deps.yml
push:
branches: [develop]
paths:
- .github/workflows/upload-conan-deps.yml
- .github/workflows/reusable-strategy-matrix.yml
- .github/actions/build-deps/action.yml
- ".github/scripts/strategy-matrix/**"
- conanfile.py
- conan.lock
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
generate-matrix:
uses: ./.github/workflows/reusable-strategy-matrix.yml
with:
strategy_matrix: ${{ github.event_name == 'pull_request' && 'minimal' || 'all' }}
run-upload-conan-deps:
needs:
- generate-matrix
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
max-parallel: 10
runs-on: ${{ matrix.architecture.runner }}
container: ${{ contains(matrix.architecture.platform, 'linux') && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version) || null }}
steps:
- name: Cleanup workspace
if: ${{ runner.os == 'macOS' }}
uses: XRPLF/actions/.github/actions/cleanup-workspace@3f044c7478548e3c32ff68980eeb36ece02b364e
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Prepare runner
uses: XRPLF/actions/.github/actions/prepare-runner@638e0dc11ea230f91bd26622fb542116bb5254d5
with:
disable_ccache: false
- name: Setup Conan
uses: ./.github/actions/setup-conan
- name: Build dependencies
uses: ./.github/actions/build-deps
with:
build_dir: .build
build_type: ${{ matrix.build_type }}
force_build: ${{ github.event_name == 'schedule' || github.event.inputs.force_source_build == 'true' }}
- name: Login to Conan
if: github.repository_owner == 'XRPLF' && github.event_name != 'pull_request'
run: conan remote login -p ${{ secrets.CONAN_PASSWORD }} ${{ inputs.conan_remote_name }} ${{ secrets.CONAN_USERNAME }}
- name: Upload Conan packages
if: github.repository_owner == 'XRPLF' && github.event_name != 'pull_request' && github.event_name != 'schedule'
run: conan upload "*" -r=${{ inputs.conan_remote_name }} --confirm ${{ github.event.inputs.force_upload == 'true' && '--force' || '' }}

View File

@@ -1,5 +1,18 @@
# To run pre-commit hooks, first install pre-commit:
# - `pip install pre-commit==${PRE_COMMIT_VERSION}`
# - `pip install pre-commit-hooks==${PRE_COMMIT_HOOKS_VERSION}`
#
# Depending on your system, you can use `brew install` or `apt install` as well
# for installing the pre-commit package, but `pip` is needed to install the
# hooks; you can also use `pipx` if you prefer.
# Next, install the required formatters:
# - `pip install clang-format==${CLANG_VERSION}`
# - `npm install prettier@${PRETTIER_VERSION}`
#
# See https://github.com/XRPLF/ci/blob/main/.github/workflows/tools-rippled.yml
# for the versions used in the CI pipeline. You will need to have the exact same
# versions of the tools installed on your system to produce the same results as
# the pipeline.
#
# Then, run the following command to install the git hook scripts:
# - `pre-commit install`
@@ -7,33 +20,45 @@
# - `pre-commit run --all-files`
# To manually run a specific hook, use:
# - `pre-commit run <hook_id> --all-files`
# To run the hooks against only the staged files, use:
# To run the hooks against only the files changed in the current commit, use:
# - `pre-commit run`
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: mixed-line-ending
- id: check-merge-conflict
args: [--assume-in-merge]
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: 7d85583be209cb547946c82fbe51f4bc5dd1d017 # frozen: v18.1.8
- repo: local
hooks:
- id: clang-format
args: [--style=file]
"types_or": [c++, c, proto]
- repo: https://github.com/rbubley/mirrors-prettier
rev: 5ba47274f9b181bce26a5150a725577f3c336011 # frozen: v3.6.2
name: clang-format
language: system
entry: clang-format -i
files: '\.(cpp|hpp|h|ipp|proto)$'
- id: trailing-whitespace
name: trailing-whitespace
entry: trailing-whitespace-fixer
language: system
types: [text]
- id: end-of-file
name: end-of-file
entry: end-of-file-fixer
language: system
types: [text]
- id: mixed-line-ending
name: mixed-line-ending
entry: mixed-line-ending
language: system
types: [text]
- id: check-merge-conflict
name: check-merge-conflict
entry: check-merge-conflict --assume-in-merge
language: system
types: [text]
- repo: local
hooks:
- id: prettier
name: prettier
language: system
entry: prettier --ignore-unknown --write
exclude: |
(?x)^(
external/.*|
.github/scripts/levelization/results/.*\.txt|
conan\.lock
.github/scripts/levelization/results/.*\.txt
)$

View File

@@ -158,10 +158,6 @@ updated dependencies with the newer version. However, if we switch to a newer
version that no longer requires a patch, no action is required on your part, as
the new recipe will be automatically pulled from the official Conan Center.
> [!NOTE]
> You might need to add `--lockfile=""` to your `conan install` command
> to avoid automatic use of the existing `conan.lock` file when you run `conan export` manually on your machine
### Conan profile tweaks
#### Missing compiler version
@@ -470,21 +466,6 @@ tools.build:cxxflags=['-DBOOST_ASIO_DISABLE_CONCEPTS']
The location of `rippled` binary in your build directory depends on your
CMake generator. Pass `--help` to see the rest of the command line options.
#### Conan lockfile
To achieve reproducible dependencies, we use [Conan lockfile](https://docs.conan.io/2/tutorial/versioning/lockfiles.html).
The `conan.lock` file in the repository contains a "snapshot" of the current dependencies.
It is implicitly used when running `conan` commands, you don't need to specify it.
You have to update this file every time you add a new dependency or change a revision or version of an existing dependency.
To do that, run the following command in the repository root:
```bash
conan lock create . -o '&:jemalloc=True' -o '&:rocksdb=True'
```
## Coverage report
The coverage report is intended for developers using compilers GCC
@@ -583,8 +564,7 @@ After any updates or changes to dependencies, you may need to do the following:
```
3. Re-run [conan export](#patched-recipes) if needed.
4. [Regenerate lockfile](#conan-lockfile).
5. Re-run [conan install](#build-and-test).
4. Re-run [conan install](#build-and-test).
### `protobuf/port_def.inc` file not found

View File

@@ -6,7 +6,7 @@ The [XRP Ledger](https://xrpl.org/) is a decentralized cryptographic ledger powe
## XRP
[XRP](https://xrpl.org/xrp.html) is a public, counterparty-free crypto-asset native to the XRP Ledger, and is designed as a gas token for network services and to bridge different currencies. XRP is traded on the open-market and is available for anyone to access. The XRP Ledger was created in 2012 with a finite supply of 100 billion units of XRP.
[XRP](https://xrpl.org/xrp.html) is a public, counterparty-free asset native to the XRP Ledger, and is designed to bridge the many different currencies in use worldwide. XRP is traded on the open-market and is available for anyone to access. The XRP Ledger was created in 2012 with a finite supply of 100 billion units of XRP.
## rippled
@@ -23,19 +23,19 @@ If you are interested in running an **API Server** (including a **Full History S
- **[Censorship-Resistant Transaction Processing][]:** No single party decides which transactions succeed or fail, and no one can "roll back" a transaction after it completes. As long as those who choose to participate in the network keep it healthy, they can settle transactions in seconds.
- **[Fast, Efficient Consensus Algorithm][]:** The XRP Ledger's consensus algorithm settles transactions in 4 to 5 seconds, processing at a throughput of up to 1500 transactions per second. These properties put XRP at least an order of magnitude ahead of other top digital assets.
- **[Finite XRP Supply][]:** When the XRP Ledger began, 100 billion XRP were created, and no more XRP will ever be created. The available supply of XRP decreases slowly over time as small amounts are destroyed to pay transaction fees.
- **[Responsible Software Governance][]:** A team of full-time developers at Ripple & other organizations maintain and continually improve the XRP Ledger's underlying software with contributions from the open-source community. Ripple acts as a steward for the technology and an advocate for its interests.
- **[Finite XRP Supply][]:** When the XRP Ledger began, 100 billion XRP were created, and no more XRP will ever be created. The available supply of XRP decreases slowly over time as small amounts are destroyed to pay transaction costs.
- **[Responsible Software Governance][]:** A team of full-time, world-class developers at Ripple maintain and continually improve the XRP Ledger's underlying software with contributions from the open-source community. Ripple acts as a steward for the technology and an advocate for its interests, and builds constructive relationships with governments and financial institutions worldwide.
- **[Secure, Adaptable Cryptography][]:** The XRP Ledger relies on industry standard digital signature systems like ECDSA (the same scheme used by Bitcoin) but also supports modern, efficient algorithms like Ed25519. The extensible nature of the XRP Ledger's software makes it possible to add and disable algorithms as the state of the art in cryptography advances.
- **[Modern Features][]:** Features like Escrow, Checks, and Payment Channels support financial applications atop of the XRP Ledger. This toolbox of advanced features comes with safety features like a process for amending the network and separate checks against invariant constraints.
- **[Modern Features for Smart Contracts][]:** Features like Escrow, Checks, and Payment Channels support cutting-edge financial applications including the [Interledger Protocol](https://interledger.org/). This toolbox of advanced features comes with safety features like a process for amending the network and separate checks against invariant constraints.
- **[On-Ledger Decentralized Exchange][]:** In addition to all the features that make XRP useful on its own, the XRP Ledger also has a fully-functional accounting system for tracking and trading obligations denominated in any way users want, and an exchange built into the protocol. The XRP Ledger can settle long, cross-currency payment paths and exchanges of multiple currencies in atomic transactions, bridging gaps of trust with XRP.
[Censorship-Resistant Transaction Processing]: https://xrpl.org/transaction-censorship-detection.html#transaction-censorship-detection
[Fast, Efficient Consensus Algorithm]: https://xrpl.org/consensus-research.html#consensus-research
[Finite XRP Supply]: https://xrpl.org/what-is-xrp.html
[Responsible Software Governance]: https://xrpl.org/contribute-code.html#contribute-code-to-the-xrp-ledger
[Secure, Adaptable Cryptography]: https://xrpl.org/cryptographic-keys.html#cryptographic-keys
[Modern Features]: https://xrpl.org/use-specialized-payment-types.html
[On-Ledger Decentralized Exchange]: https://xrpl.org/decentralized-exchange.html#decentralized-exchange
[Censorship-Resistant Transaction Processing]: https://xrpl.org/xrp-ledger-overview.html#censorship-resistant-transaction-processing
[Fast, Efficient Consensus Algorithm]: https://xrpl.org/xrp-ledger-overview.html#fast-efficient-consensus-algorithm
[Finite XRP Supply]: https://xrpl.org/xrp-ledger-overview.html#finite-xrp-supply
[Responsible Software Governance]: https://xrpl.org/xrp-ledger-overview.html#responsible-software-governance
[Secure, Adaptable Cryptography]: https://xrpl.org/xrp-ledger-overview.html#secure-adaptable-cryptography
[Modern Features for Smart Contracts]: https://xrpl.org/xrp-ledger-overview.html#modern-features-for-smart-contracts
[On-Ledger Decentralized Exchange]: https://xrpl.org/xrp-ledger-overview.html#on-ledger-decentralized-exchange
## Source Code

View File

@@ -975,6 +975,47 @@
# number of ledger records online. Must be greater
# than or equal to ledger_history.
#
# Optional keys for NuDB only:
#
# nudb_block_size EXPERIMENTAL: Block size in bytes for NuDB storage.
# Must be a power of 2 between 4096 and 32768. Default is 4096.
#
# This parameter controls the fundamental storage unit
# size for NuDB's internal data structures. The choice
# of block size can significantly impact performance
# depending on your storage hardware and filesystem:
#
# - 4096 bytes: Optimal for most standard SSDs and
# traditional filesystems (ext4, NTFS, HFS+).
# Provides good balance of performance and storage
# efficiency. Recommended for most deployments.
# Minimizes memory footprint and provides consistent
# low-latency access patterns across diverse hardware.
#
# - 8192-16384 bytes: May improve performance on
# high-end NVMe SSDs and copy-on-write filesystems
# like ZFS or Btrfs that benefit from larger block
# alignment. Can reduce metadata overhead for large
# databases. Offers better sequential throughput and
# reduced I/O operations at the cost of higher memory
# usage per operation.
#
# - 32768 bytes (32K): Maximum supported block size
# for high-performance scenarios with very fast
# storage. May increase memory usage and reduce
# efficiency for smaller databases. Best suited for
# enterprise environments with abundant RAM.
#
# Performance testing is recommended before deploying
# any non-default block size in production environments.
#
# Note: This setting cannot be changed after database
# creation without rebuilding the entire database.
# Choose carefully based on your hardware and expected
# database size.
#
# Example: nudb_block_size=4096
#
# These keys modify the behavior of online_delete, and thus are only
# relevant if online_delete is defined and non-zero:
#
@@ -1471,6 +1512,7 @@ secure_gateway = 127.0.0.1
[node_db]
type=NuDB
path=/var/lib/rippled/db/nudb
nudb_block_size=4096
online_delete=512
advisory_delete=0

View File

@@ -101,9 +101,6 @@
# 2025-05-12, Jingchen Wu
# - add -fprofile-update=atomic to ensure atomic profile generation
#
# 2025-08-28, Bronek Kozicki
# - fix "At least one COMMAND must be given" CMake warning from policy CMP0175
#
# USAGE:
#
# 1. Copy this file into your cmake modules path.
@@ -218,12 +215,12 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)")
set(COVERAGE_C_COMPILER_FLAGS "${COVERAGE_COMPILER_FLAGS} -fprofile-abs-path")
endif()
check_cxx_compiler_flag(-fprofile-update=atomic HAVE_cxx_fprofile_update)
check_cxx_compiler_flag(-fprofile-update HAVE_cxx_fprofile_update)
if(HAVE_cxx_fprofile_update)
set(COVERAGE_CXX_COMPILER_FLAGS "${COVERAGE_COMPILER_FLAGS} -fprofile-update=atomic")
endif()
check_c_compiler_flag(-fprofile-update=atomic HAVE_c_fprofile_update)
check_c_compiler_flag(-fprofile-update HAVE_c_fprofile_update)
if(HAVE_c_fprofile_update)
set(COVERAGE_C_COMPILER_FLAGS "${COVERAGE_COMPILER_FLAGS} -fprofile-update=atomic")
endif()
@@ -449,7 +446,7 @@ function(setup_target_for_coverage_gcovr)
# Show info where to find the report
add_custom_command(TARGET ${Coverage_NAME} POST_BUILD
COMMAND echo
COMMAND ;
COMMENT "Code coverage report saved in ${GCOVR_OUTPUT_FILE} formatted as ${Coverage_FORMAT}"
)
endfunction() # setup_target_for_coverage_gcovr

View File

@@ -14,6 +14,12 @@ find_package(Boost 1.82 REQUIRED
add_library(ripple_boost INTERFACE)
add_library(Ripple::boost ALIAS ripple_boost)
if(XCODE)
target_include_directories(ripple_boost BEFORE INTERFACE ${Boost_INCLUDE_DIRS})
target_compile_options(ripple_boost INTERFACE --system-header-prefix="boost/")
else()
target_include_directories(ripple_boost SYSTEM BEFORE INTERFACE ${Boost_INCLUDE_DIRS})
endif()
target_link_libraries(ripple_boost
INTERFACE

View File

@@ -1,56 +0,0 @@
{
"version": "0.5",
"requires": [
"zlib/1.3.1#b8bc2603263cf7eccbd6e17e66b0ed76%1756234269.497",
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1756234289.683",
"sqlite3/3.49.1#8631739a4c9b93bd3d6b753bac548a63%1756234266.869",
"soci/4.0.3#a9f8d773cd33e356b5879a4b0564f287%1756234262.318",
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1756234314.246",
"rocksdb/10.0.1#85537f46e538974d67da0c3977de48ac%1756234304.347",
"re2/20230301#dfd6e2bf050eb90ddd8729cfb4c844a4%1756234257.976",
"protobuf/3.21.12#d927114e28de9f4691a6bbcdd9a529d1%1756234251.614",
"openssl/3.5.2#0c5a5e15ae569f45dff57adcf1770cf7%1756234259.61",
"nudb/2.0.9#c62cfd501e57055a7e0d8ee3d5e5427d%1756234237.107",
"lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1756234228.999",
"libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1756223727.64",
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1756230911.03",
"libarchive/3.8.1#5cf685686322e906cb42706ab7e099a8%1756234256.696",
"jemalloc/5.3.0#e951da9cf599e956cebc117880d2d9f8%1729241615.244",
"grpc/1.50.1#02291451d1e17200293a409410d1c4e1%1756234248.958",
"doctest/2.4.11#a4211dfc329a16ba9f280f9574025659%1756234220.819",
"date/3.0.4#f74bbba5a08fa388256688743136cb6f%1756234217.493",
"c-ares/1.34.5#b78b91e7cfb1f11ce777a285bbf169c6%1756234217.915",
"bzip2/1.0.8#00b4a4658791c1f06914e087f0e792f5%1756234261.716",
"boost/1.88.0#8852c0b72ce8271fb8ff7c53456d4983%1756223752.326",
"abseil/20230802.1#f0f91485b111dc9837a68972cb19ca7b%1756234220.907"
],
"build_requires": [
"zlib/1.3.1#b8bc2603263cf7eccbd6e17e66b0ed76%1756234269.497",
"strawberryperl/5.32.1.1#707032463aa0620fa17ec0d887f5fe41%1756234281.733",
"protobuf/3.21.12#d927114e28de9f4691a6bbcdd9a529d1%1756234251.614",
"nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1756234232.901",
"msys2/cci.latest#5b73b10144f73cc5bfe0572ed9be39e1%1751977009.857",
"m4/1.4.19#b38ced39a01e31fef5435bc634461fd2%1700758725.451",
"cmake/3.31.8#dde3bde00bb843687e55aea5afa0e220%1756234232.89",
"b2/5.3.3#107c15377719889654eb9a162a673975%1756234226.28",
"automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56",
"autoconf/2.71#51077f068e61700d65bb05541ea1e4b0%1731054366.86"
],
"python_requires": [],
"overrides": {
"protobuf/3.21.12": [
null,
"protobuf/3.21.12"
],
"lz4/1.9.4": [
"lz4/1.10.0"
],
"boost/1.83.0": [
"boost/1.88.0"
],
"sqlite3/3.44.2": [
"sqlite3/3.49.1"
]
},
"config_requires": []
}

View File

@@ -150,24 +150,6 @@ public:
return (mantissa_ < 0) ? -1 : (mantissa_ ? 1 : 0);
}
Number
truncate() const noexcept
{
if (exponent_ >= 0 || mantissa_ == 0)
return *this;
Number ret = *this;
while (ret.exponent_ < 0 && ret.mantissa_ != 0)
{
ret.exponent_ += 1;
ret.mantissa_ /= rep(10);
}
// We are guaranteed that normalize() will never throw an exception
// because exponent is either negative or zero at this point.
ret.normalize();
return ret;
}
friend constexpr bool
operator>(Number const& x, Number const& y) noexcept
{

View File

@@ -157,12 +157,7 @@ enum error_code_i {
// Pathfinding
rpcDOMAIN_MALFORMED = 97,
// ledger_entry
rpcENTRY_NOT_FOUND = 98,
rpcUNEXPECTED_LEDGER_TYPE = 99,
rpcLAST =
rpcUNEXPECTED_LEDGER_TYPE // rpcLAST should always equal the last code.
rpcLAST = rpcDOMAIN_MALFORMED // rpcLAST should always equal the last code.
};
/** Codes returned in the `warnings` array of certain RPC commands.

View File

@@ -349,19 +349,6 @@ permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept;
Keylet
permissionedDomain(uint256 const& domainID) noexcept;
Keylet
subscription(
AccountID const& account,
AccountID const& dest,
std::uint32_t const& seq) noexcept;
inline Keylet
subscription(uint256 const& key) noexcept
{
return {ltSUBSCRIPTION, key};
}
} // namespace keylet
// Everything below is deprecated and should be removed in favor of keylets:

View File

@@ -62,6 +62,7 @@ enum LedgerEntryType : std::uint16_t
#undef LEDGER_ENTRY
#pragma pop_macro("LEDGER_ENTRY")
//---------------------------------------------------------------------------
/** A special type, matching any ledger entry type.

View File

@@ -122,13 +122,6 @@ std::size_t constexpr maxDataPayloadLength = 256;
/** Vault withdrawal policies */
std::uint8_t constexpr vaultStrategyFirstComeFirstServe = 1;
/** Default IOU scale factor for a Vault */
std::uint8_t constexpr vaultDefaultIOUScale = 6;
/** Maximum scale factor for a Vault. The number is chosen to ensure that
1 IOU can be always converted to shares.
10^19 > maxMPTokenAmount (2^64-1) > 10^18 */
std::uint8_t constexpr vaultMaximumIOUScale = 18;
/** Maximum recursion depth for vault shares being put as an asset inside
* another vault; counted from 0 */
std::uint8_t constexpr maxAssetCheckDepth = 5;

View File

@@ -32,10 +32,9 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(Subscription, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (PriceOracleOrder, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (MPTDeliveredAmount, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (AMMClawbackRounding, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (AMMClawbackRounding, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (EnforceNFTokenTrustlineV2, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (AMMv1_3, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -499,30 +499,9 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
{sfLossUnrealized, soeREQUIRED},
{sfShareMPTID, soeREQUIRED},
{sfWithdrawalPolicy, soeREQUIRED},
{sfScale, soeDEFAULT},
// no SharesTotal ever (use MPTIssuance.sfOutstandingAmount)
// no PermissionedDomainID ever (use MPTIssuance.sfDomainID)
}))
/** A ledger object representing a subscription.
\sa keylet::mptoken
*/
LEDGER_ENTRY(ltSUBSCRIPTION, 0x0085, Subscription, subscription, ({
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
{sfSequence, soeREQUIRED},
{sfOwnerNode, soeREQUIRED},
{sfAccount, soeREQUIRED},
{sfDestination, soeREQUIRED},
{sfDestinationTag, soeOPTIONAL},
{sfAmount, soeREQUIRED},
{sfBalance, soeREQUIRED},
{sfFrequency, soeREQUIRED},
{sfNextClaimTime, soeREQUIRED},
{sfExpiration, soeOPTIONAL},
{sfDestinationNode, soeREQUIRED},
}))
#undef EXPAND
#undef LEDGER_ENTRY_DUPLICATE

View File

@@ -114,9 +114,6 @@ TYPED_SFIELD(sfVoteWeight, UINT32, 48)
TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50)
TYPED_SFIELD(sfOracleDocumentID, UINT32, 51)
TYPED_SFIELD(sfPermissionValue, UINT32, 52)
TYPED_SFIELD(sfFrequency, UINT32, 53)
TYPED_SFIELD(sfStartTime, UINT32, 54)
TYPED_SFIELD(sfNextClaimTime, UINT32, 55)
// 64-bit integers (common)
TYPED_SFIELD(sfIndexNext, UINT64, 1)
@@ -200,7 +197,6 @@ TYPED_SFIELD(sfHookSetTxnID, UINT256, 33)
TYPED_SFIELD(sfDomainID, UINT256, 34)
TYPED_SFIELD(sfVaultID, UINT256, 35)
TYPED_SFIELD(sfParentBatchID, UINT256, 36)
TYPED_SFIELD(sfSubscriptionID, UINT256, 37)
// number (common)
TYPED_SFIELD(sfNumber, NUMBER, 1)

View File

@@ -483,7 +483,6 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, Delegation::delegatable, ({
{sfDomainID, soeOPTIONAL},
{sfWithdrawalPolicy, soeOPTIONAL},
{sfData, soeOPTIONAL},
{sfScale, soeOPTIONAL},
}))
/** This transaction updates a single asset vault. */
@@ -526,28 +525,6 @@ TRANSACTION(ttBATCH, 71, Batch, Delegation::notDelegatable, ({
{sfBatchSigners, soeOPTIONAL},
}))
/** This transaction type batches together transactions. */
TRANSACTION(ttSUBSCRIPTION_SET, 72, SubscriptionSet, Delegation::delegatable, ({
{sfDestination, soeOPTIONAL},
{sfAmount, soeREQUIRED, soeMPTSupported},
{sfFrequency, soeOPTIONAL},
{sfStartTime, soeOPTIONAL},
{sfExpiration, soeOPTIONAL},
{sfDestinationTag, soeOPTIONAL},
{sfSubscriptionID, soeOPTIONAL},
}))
/** This transaction type batches together transactions. */
TRANSACTION(ttSUBSCRIPTION_CANCEL, 73, SubscriptionCancel, Delegation::delegatable, ({
{sfSubscriptionID, soeREQUIRED},
}))
/** This transaction type batches together transactions. */
TRANSACTION(ttSUBSCRIPTION_CLAIM, 74, SubscriptionClaim, Delegation::delegatable, ({
{sfAmount, soeREQUIRED, soeMPTSupported},
{sfSubscriptionID, soeREQUIRED},
}))
/** This system-generated transaction type is used to update the status of the various amendments.
For details, see: https://xrpl.org/amendments.html

View File

@@ -68,13 +68,9 @@ JSS(Flags); // in/out: TransactionSign; field.
JSS(Holder); // field.
JSS(Invalid); //
JSS(Issuer); // in: Credential transactions
JSS(IssuingChainDoor); // field.
JSS(IssuingChainIssue); // field.
JSS(LastLedgerSequence); // in: TransactionSign; field
JSS(LastUpdateTime); // field.
JSS(LimitAmount); // field.
JSS(LockingChainDoor); // field.
JSS(LockingChainIssue); // field.
JSS(NetworkID); // field.
JSS(LPTokenOut); // in: AMM Liquidity Provider deposit tokens
JSS(LPTokenIn); // in: AMM Liquidity Provider withdraw tokens
@@ -99,7 +95,6 @@ JSS(Signer); // field.
JSS(Signers); // field.
JSS(SigningPubKey); // field.
JSS(Subject); // in: Credential transactions
JSS(SubscriptionID); // in: Subscription transactions
JSS(TakerGets); // field.
JSS(TakerPays); // field.
JSS(TradingFee); // in/out: AMM trading fee
@@ -284,7 +279,6 @@ JSS(fee_mult_max); // in: TransactionSign
JSS(fee_ref); // out: NetworkOPs, DEPRECATED
JSS(fetch_pack); // out: NetworkOPs
JSS(FIELDS); // out: RPC server_definitions
JSS(Frequency); // in: Subscription transactions
// matches definitions.json format
JSS(first); // out: rpc/Version
JSS(finished);

View File

@@ -24,7 +24,6 @@
#include <xrpl/json/json_value.h>
#include <xrpl/json/json_writer.h>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <string>
@@ -686,9 +685,7 @@ Value::isConvertibleTo(ValueType other) const
(other == intValue && value_.real_ >= minInt &&
value_.real_ <= maxInt) ||
(other == uintValue && value_.real_ >= 0 &&
value_.real_ <= maxUInt &&
std::fabs(round(value_.real_) - value_.real_) <
std::numeric_limits<double>::epsilon()) ||
value_.real_ <= maxUInt) ||
other == realValue || other == stringValue ||
other == booleanValue;

View File

@@ -36,7 +36,7 @@ namespace BuildInfo {
// and follow the format described at http://semver.org/
//------------------------------------------------------------------------------
// clang-format off
char const* const versionString = "2.6.0"
char const* const versionString = "2.6.0-rc3"
// clang-format on
#if defined(DEBUG) || defined(SANITIZER)

View File

@@ -117,10 +117,7 @@ constexpr static ErrorInfo unorderedErrorInfos[]{
{rpcORACLE_MALFORMED, "oracleMalformed", "Oracle request is malformed.", 400},
{rpcBAD_CREDENTIALS, "badCredentials", "Credentials do not exist, are not accepted, or have expired.", 400},
{rpcTX_SIGNED, "transactionSigned", "Transaction should not be signed.", 400},
{rpcDOMAIN_MALFORMED, "domainMalformed", "Domain is malformed.", 400},
{rpcENTRY_NOT_FOUND, "entryNotFound", "Entry not found.", 400},
{rpcUNEXPECTED_LEDGER_TYPE, "unexpectedLedgerType", "Unexpected ledger type.", 400},
};
{rpcDOMAIN_MALFORMED, "domainMalformed", "Domain is malformed.", 400}};
// clang-format on
// Sort and validate unorderedErrorInfos at compile time. Should be

View File

@@ -96,7 +96,6 @@ enum class LedgerNameSpace : std::uint16_t {
PERMISSIONED_DOMAIN = 'm',
DELEGATE = 'E',
VAULT = 'V',
SUBSCRIPTION = 'U',
// No longer used or supported. Left here to reserve the space
// to avoid accidental reuse.
@@ -581,17 +580,6 @@ permissionedDomain(uint256 const& domainID) noexcept
return {ltPERMISSIONED_DOMAIN, domainID};
}
Keylet
subscription(
AccountID const& account,
AccountID const& dest,
std::uint32_t const& seq) noexcept
{
return {
ltSUBSCRIPTION,
indexHash(LedgerNameSpace::SUBSCRIPTION, account, dest, seq)};
}
} // namespace keylet
} // namespace ripple

View File

@@ -27,7 +27,6 @@
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STXChainBridge.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/jss.h>
#include <boost/format/free_funcs.hpp>
@@ -99,10 +98,12 @@ STXChainBridge::STXChainBridge(SField const& name, Json::Value const& v)
};
checkExtra(v);
Json::Value const& lockingChainDoorStr = v[jss::LockingChainDoor];
Json::Value const& lockingChainIssue = v[jss::LockingChainIssue];
Json::Value const& issuingChainDoorStr = v[jss::IssuingChainDoor];
Json::Value const& issuingChainIssue = v[jss::IssuingChainIssue];
Json::Value const& lockingChainDoorStr =
v[sfLockingChainDoor.getJsonName()];
Json::Value const& lockingChainIssue = v[sfLockingChainIssue.getJsonName()];
Json::Value const& issuingChainDoorStr =
v[sfIssuingChainDoor.getJsonName()];
Json::Value const& issuingChainIssue = v[sfIssuingChainIssue.getJsonName()];
if (!lockingChainDoorStr.isString())
{
@@ -160,10 +161,10 @@ Json::Value
STXChainBridge::getJson(JsonOptions jo) const
{
Json::Value v;
v[jss::LockingChainDoor] = lockingChainDoor_.getJson(jo);
v[jss::LockingChainIssue] = lockingChainIssue_.getJson(jo);
v[jss::IssuingChainDoor] = issuingChainDoor_.getJson(jo);
v[jss::IssuingChainIssue] = issuingChainIssue_.getJson(jo);
v[sfLockingChainDoor.getJsonName()] = lockingChainDoor_.getJson(jo);
v[sfLockingChainIssue.getJsonName()] = lockingChainIssue_.getJson(jo);
v[sfIssuingChainDoor.getJsonName()] = issuingChainDoor_.getJson(jo);
v[sfIssuingChainIssue.getJsonName()] = issuingChainIssue_.getJson(jo);
return v;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -720,30 +720,6 @@ public:
BEAST_EXPECT(res2 == STAmount{7518784});
}
void
test_truncate()
{
BEAST_EXPECT(Number(25, +1).truncate() == Number(250, 0));
BEAST_EXPECT(Number(25, 0).truncate() == Number(25, 0));
BEAST_EXPECT(Number(25, -1).truncate() == Number(2, 0));
BEAST_EXPECT(Number(25, -2).truncate() == Number(0, 0));
BEAST_EXPECT(Number(99, -2).truncate() == Number(0, 0));
BEAST_EXPECT(Number(-25, +1).truncate() == Number(-250, 0));
BEAST_EXPECT(Number(-25, 0).truncate() == Number(-25, 0));
BEAST_EXPECT(Number(-25, -1).truncate() == Number(-2, 0));
BEAST_EXPECT(Number(-25, -2).truncate() == Number(0, 0));
BEAST_EXPECT(Number(-99, -2).truncate() == Number(0, 0));
BEAST_EXPECT(Number(0, 0).truncate() == Number(0, 0));
BEAST_EXPECT(Number(0, 30000).truncate() == Number(0, 0));
BEAST_EXPECT(Number(0, -30000).truncate() == Number(0, 0));
BEAST_EXPECT(Number(100, -30000).truncate() == Number(0, 0));
BEAST_EXPECT(Number(100, -30000).truncate() == Number(0, 0));
BEAST_EXPECT(Number(-100, -30000).truncate() == Number(0, 0));
BEAST_EXPECT(Number(-100, -30000).truncate() == Number(0, 0));
}
void
run() override
{
@@ -764,7 +740,6 @@ public:
test_stream();
test_inc_dec();
test_toSTAmount();
test_truncate();
}
};

View File

@@ -67,7 +67,6 @@
#include <test/jtx/sendmax.h>
#include <test/jtx/seq.h>
#include <test/jtx/sig.h>
#include <test/jtx/subscription.h>
#include <test/jtx/tag.h>
#include <test/jtx/tags.h>
#include <test/jtx/ter.h>

View File

@@ -74,10 +74,6 @@ public:
/** @} */
/** Create an Account from an account ID. Should only be used when the
* secret key is unavailable, such as for pseudo-accounts. */
explicit Account(std::string name, AccountID const& id);
enum AcctStringType { base58Seed, other };
/** Create an account from a base58 seed string. Throws on invalid seed. */
Account(AcctStringType stringType, std::string base58SeedStr);

View File

@@ -86,14 +86,6 @@ Account::Account(AcctStringType stringType, std::string base58SeedStr)
{
}
Account::Account(std::string name, AccountID const& id)
: Account(name, randomKeyPair(KeyType::secp256k1), privateCtorTag{})
{
// override the randomly generated values
id_ = id;
human_ = toBase58(id_);
}
IOU
Account::operator[](std::string const& s) const
{

View File

@@ -1,107 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
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/subscription.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
namespace ripple {
namespace test {
namespace jtx {
/** Subscription operations. */
namespace subscription {
void
start_time::operator()(Env& env, JTx& jt) const
{
jt.jv[sfStartTime.jsonName] = value_.time_since_epoch().count();
}
Json::Value
create(
jtx::Account const& account,
jtx::Account const& destination,
STAmount const& amount,
NetClock::duration const& frequency,
std::optional<NetClock::time_point> const& expiration)
{
Json::Value jv;
jv[jss::TransactionType] = jss::SubscriptionSet;
jv[jss::Account] = to_string(account.id());
jv[jss::Destination] = to_string(destination.id());
jv[jss::Amount] = amount.getJson(JsonOptions::none);
jv[jss::Frequency] = frequency.count();
jv[jss::Flags] = tfFullyCanonicalSig;
if (expiration)
jv[sfExpiration.jsonName] = expiration->time_since_epoch().count();
return jv;
}
Json::Value
update(
jtx::Account const& account,
uint256 const& subscriptionId,
STAmount const& amount,
std::optional<NetClock::time_point> const& expiration)
{
Json::Value jv;
jv[jss::TransactionType] = jss::SubscriptionSet;
jv[jss::Account] = to_string(account.id());
jv[jss::SubscriptionID] = to_string(subscriptionId);
jv[jss::Amount] = amount.getJson(JsonOptions::none);
jv[jss::Flags] = tfFullyCanonicalSig;
if (expiration)
jv[sfExpiration.jsonName] = expiration->time_since_epoch().count();
return jv;
}
Json::Value
cancel(jtx::Account const& account, uint256 const& subscriptionId)
{
Json::Value jv;
jv[jss::TransactionType] = jss::SubscriptionCancel;
jv[jss::Account] = to_string(account.id());
jv[jss::SubscriptionID] = to_string(subscriptionId);
jv[jss::Flags] = tfFullyCanonicalSig;
return jv;
}
Json::Value
claim(
jtx::Account const& account,
uint256 const& subscriptionId,
STAmount const& amount)
{
Json::Value jv;
jv[jss::TransactionType] = jss::SubscriptionClaim;
jv[jss::Account] = to_string(account.id());
jv[jss::SubscriptionID] = to_string(subscriptionId);
jv[jss::Amount] = amount.getJson(JsonOptions::none);
jv[jss::Flags] = tfFullyCanonicalSig;
return jv;
}
} // namespace subscription
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -44,10 +44,10 @@ bridge(
Issue const& issuingChainIssue)
{
Json::Value jv;
jv[jss::LockingChainDoor] = lockingChainDoor.human();
jv[jss::LockingChainIssue] = to_json(lockingChainIssue);
jv[jss::IssuingChainDoor] = issuingChainDoor.human();
jv[jss::IssuingChainIssue] = to_json(issuingChainIssue);
jv[sfLockingChainDoor.getJsonName()] = lockingChainDoor.human();
jv[sfLockingChainIssue.getJsonName()] = to_json(lockingChainIssue);
jv[sfIssuingChainDoor.getJsonName()] = issuingChainDoor.human();
jv[sfIssuingChainIssue.getJsonName()] = to_json(issuingChainIssue);
return jv;
}
@@ -60,10 +60,10 @@ bridge_rpc(
Issue const& issuingChainIssue)
{
Json::Value jv;
jv[jss::LockingChainDoor] = lockingChainDoor.human();
jv[jss::LockingChainIssue] = to_json(lockingChainIssue);
jv[jss::IssuingChainDoor] = issuingChainDoor.human();
jv[jss::IssuingChainIssue] = to_json(issuingChainIssue);
jv[sfLockingChainDoor.getJsonName()] = lockingChainDoor.human();
jv[sfLockingChainIssue.getJsonName()] = to_json(lockingChainIssue);
jv[sfIssuingChainDoor.getJsonName()] = issuingChainDoor.human();
jv[sfIssuingChainIssue.getJsonName()] = to_json(issuingChainIssue);
return jv;
}

View File

@@ -1,79 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2019 Ripple Labs Inc.
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.
*/
//==============================================================================
#ifndef RIPPLE_TEST_JTX_SUBSCRIPTION_H_INCLUDED
#define RIPPLE_TEST_JTX_SUBSCRIPTION_H_INCLUDED
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
namespace ripple {
namespace test {
namespace jtx {
/** Subscription operations. */
namespace subscription {
Json::Value
create(
jtx::Account const& account,
jtx::Account const& destination,
STAmount const& amount,
NetClock::duration const& frequency,
std::optional<NetClock::time_point> const& expiration = std::nullopt);
Json::Value
update(
jtx::Account const& account,
uint256 const& subscriptionId,
STAmount const& amount,
std::optional<NetClock::time_point> const& expiration = std::nullopt);
Json::Value
cancel(jtx::Account const& account, uint256 const& subscriptionId);
Json::Value
claim(
jtx::Account const& account,
uint256 const& subscriptionId,
STAmount const& amount);
/** Set the "StartTime" time tag on a JTx */
class start_time
{
private:
NetClock::time_point value_;
public:
explicit start_time(NetClock::time_point const& value) : value_(value)
{
}
void
operator()(Env&, JTx& jtx) const;
};
} // namespace subscription
} // namespace jtx
} // namespace test
} // namespace ripple
#endif

View File

@@ -0,0 +1,478 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012, 2013 Ripple Labs Inc.
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/nodestore/TestBase.h>
#include <test/unit_test/SuiteJournal.h>
#include <xrpld/nodestore/DummyScheduler.h>
#include <xrpld/nodestore/Manager.h>
#include <xrpl/basics/BasicConfig.h>
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/beast/utility/temp_dir.h>
#include <memory>
#include <sstream>
namespace ripple {
namespace NodeStore {
class NuDBFactory_test : public TestBase
{
private:
// Helper function to create a Section with specified parameters
Section
createSection(std::string const& path, std::string const& blockSize = "")
{
Section params;
params.set("type", "nudb");
params.set("path", path);
if (!blockSize.empty())
params.set("nudb_block_size", blockSize);
return params;
}
// Helper function to create a backend and test basic functionality
bool
testBackendFunctionality(
Section const& params,
std::size_t expectedBlocksize)
{
try
{
DummyScheduler scheduler;
test::SuiteJournal journal("NuDBFactory_test", *this);
auto backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
if (!BEAST_EXPECT(backend))
return false;
if (!BEAST_EXPECT(backend->getBlockSize() == expectedBlocksize))
return false;
backend->open();
if (!BEAST_EXPECT(backend->isOpen()))
return false;
// Test basic store/fetch functionality
auto batch = createPredictableBatch(10, 12345);
storeBatch(*backend, batch);
Batch copy;
fetchCopyOfBatch(*backend, &copy, batch);
backend->close();
return areBatchesEqual(batch, copy);
}
catch (...)
{
return false;
}
}
// Helper function to test log messages
void
testLogMessage(
Section const& params,
beast::severities::Severity level,
std::string const& expectedMessage)
{
test::StreamSink sink(level);
beast::Journal journal(sink);
DummyScheduler scheduler;
auto backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
std::string logOutput = sink.messages().str();
BEAST_EXPECT(logOutput.find(expectedMessage) != std::string::npos);
}
// Helper function to test power of two validation
void
testPowerOfTwoValidation(std::string const& size, bool shouldWork)
{
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), size);
test::StreamSink sink(beast::severities::kWarning);
beast::Journal journal(sink);
DummyScheduler scheduler;
auto backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
std::string logOutput = sink.messages().str();
bool hasWarning =
logOutput.find("Invalid nudb_block_size") != std::string::npos;
BEAST_EXPECT(hasWarning == !shouldWork);
}
public:
void
testDefaultBlockSize()
{
testcase("Default block size (no nudb_block_size specified)");
beast::temp_dir tempDir;
auto params = createSection(tempDir.path());
// Should work with default 4096 block size
BEAST_EXPECT(testBackendFunctionality(params, 4096));
}
void
testValidBlockSizes()
{
testcase("Valid block sizes");
std::vector<std::size_t> validSizes = {4096, 8192, 16384, 32768};
for (auto const& size : validSizes)
{
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), to_string(size));
BEAST_EXPECT(testBackendFunctionality(params, size));
}
// Empty value is ignored by the config parser, so uses the
// default
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), "");
BEAST_EXPECT(testBackendFunctionality(params, 4096));
}
void
testInvalidBlockSizes()
{
testcase("Invalid block sizes");
std::vector<std::string> invalidSizes = {
"2048", // Too small
"1024", // Too small
"65536", // Too large
"131072", // Too large
"5000", // Not power of 2
"6000", // Not power of 2
"10000", // Not power of 2
"0", // Zero
"-1", // Negative
"abc", // Non-numeric
"4k", // Invalid format
"4096.5" // Decimal
};
for (auto const& size : invalidSizes)
{
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), size);
// Fails
BEAST_EXPECT(!testBackendFunctionality(params, 4096));
}
// Test whitespace cases separately since lexical_cast may handle them
std::vector<std::string> whitespaceInvalidSizes = {
"4096 ", // Trailing space - might be handled by lexical_cast
" 4096" // Leading space - might be handled by lexical_cast
};
for (auto const& size : whitespaceInvalidSizes)
{
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), size);
// Fails
BEAST_EXPECT(!testBackendFunctionality(params, 4096));
}
}
void
testLogMessages()
{
testcase("Log message verification");
// Test valid custom block size logging
{
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), "8192");
testLogMessage(
params,
beast::severities::kInfo,
"Using custom NuDB block size: 8192");
}
// Test invalid block size failure
{
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), "5000");
test::StreamSink sink(beast::severities::kWarning);
beast::Journal journal(sink);
DummyScheduler scheduler;
try
{
auto backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
fail();
}
catch (std::exception const& e)
{
std::string logOutput{e.what()};
BEAST_EXPECT(
logOutput.find("Invalid nudb_block_size: 5000") !=
std::string::npos);
BEAST_EXPECT(
logOutput.find(
"Must be power of 2 between 4096 and 32768") !=
std::string::npos);
}
}
// Test non-numeric value failure
{
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), "invalid");
test::StreamSink sink(beast::severities::kWarning);
beast::Journal journal(sink);
DummyScheduler scheduler;
try
{
auto backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
fail();
}
catch (std::exception const& e)
{
std::string logOutput{e.what()};
BEAST_EXPECT(
logOutput.find("Invalid nudb_block_size value: invalid") !=
std::string::npos);
}
}
}
void
testPowerOfTwoValidation()
{
testcase("Power of 2 validation logic");
// Test edge cases around valid range
std::vector<std::pair<std::string, bool>> testCases = {
{"4095", false}, // Just below minimum
{"4096", true}, // Minimum valid
{"4097", false}, // Just above minimum, not power of 2
{"8192", true}, // Valid power of 2
{"8193", false}, // Just above valid power of 2
{"16384", true}, // Valid power of 2
{"32768", true}, // Maximum valid
{"32769", false}, // Just above maximum
{"65536", false} // Power of 2 but too large
};
for (auto const& [size, shouldWork] : testCases)
{
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), size);
// We test the validation logic by catching exceptions for invalid
// values
test::StreamSink sink(beast::severities::kWarning);
beast::Journal journal(sink);
DummyScheduler scheduler;
try
{
auto backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
BEAST_EXPECT(shouldWork);
}
catch (std::exception const& e)
{
std::string logOutput{e.what()};
BEAST_EXPECT(
logOutput.find("Invalid nudb_block_size") !=
std::string::npos);
}
}
}
void
testBothConstructorVariants()
{
testcase("Both constructor variants work with custom block size");
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), "16384");
DummyScheduler scheduler;
test::SuiteJournal journal("NuDBFactory_test", *this);
// Test first constructor (without nudb::context)
{
auto backend1 = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
BEAST_EXPECT(backend1 != nullptr);
BEAST_EXPECT(testBackendFunctionality(params, 16384));
}
// Test second constructor (with nudb::context)
// Note: This would require access to nudb::context, which might not be
// easily testable without more complex setup. For now, we test that
// the factory can create backends with the first constructor.
}
void
testConfigurationParsing()
{
testcase("Configuration parsing edge cases");
// Test that whitespace is handled correctly
std::vector<std::string> validFormats = {
"8192" // Basic valid format
};
// Test whitespace handling separately since lexical_cast behavior may
// vary
std::vector<std::string> whitespaceFormats = {
" 8192", // Leading space - may or may not be handled by
// lexical_cast
"8192 " // Trailing space - may or may not be handled by
// lexical_cast
};
// Test basic valid format
for (auto const& format : validFormats)
{
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), format);
test::StreamSink sink(beast::severities::kInfo);
beast::Journal journal(sink);
DummyScheduler scheduler;
auto backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
// Should log success message for valid values
std::string logOutput = sink.messages().str();
bool hasSuccessMessage =
logOutput.find("Using custom NuDB block size") !=
std::string::npos;
BEAST_EXPECT(hasSuccessMessage);
}
// Test whitespace formats - these should work if lexical_cast handles
// them
for (auto const& format : whitespaceFormats)
{
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), format);
// Use a lower threshold to capture both info and warning messages
test::StreamSink sink(beast::severities::kDebug);
beast::Journal journal(sink);
DummyScheduler scheduler;
try
{
auto backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
fail();
}
catch (...)
{
// Fails
BEAST_EXPECT(!testBackendFunctionality(params, 8192));
}
}
}
void
testDataPersistence()
{
testcase("Data persistence with different block sizes");
std::vector<std::string> blockSizes = {
"4096", "8192", "16384", "32768"};
for (auto const& size : blockSizes)
{
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), size);
DummyScheduler scheduler;
test::SuiteJournal journal("NuDBFactory_test", *this);
// Create test data
auto batch = createPredictableBatch(50, 54321);
// Store data
{
auto backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
backend->open();
storeBatch(*backend, batch);
backend->close();
}
// Retrieve data in new backend instance
{
auto backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
backend->open();
Batch copy;
fetchCopyOfBatch(*backend, &copy, batch);
BEAST_EXPECT(areBatchesEqual(batch, copy));
backend->close();
}
}
}
void
run() override
{
testDefaultBlockSize();
testValidBlockSizes();
testInvalidBlockSizes();
testLogMessages();
testPowerOfTwoValidation();
testBothConstructorVariants();
testConfigurationParsing();
testDataPersistence();
}
};
BEAST_DEFINE_TESTSUITE(NuDBFactory, ripple_core, ripple);
} // namespace NodeStore
} // namespace ripple

View File

@@ -183,7 +183,7 @@ private:
boost::asio::ip::make_address("172.1.1." + std::to_string(rid_)));
PublicKey key(std::get<0>(randomKeyPair(KeyType::ed25519)));
auto consumer = overlay.resourceManager().newInboundEndpoint(remote);
auto [slot, _] = overlay.peerFinder().new_inbound_slot(local, remote);
auto slot = overlay.peerFinder().new_inbound_slot(local, remote);
auto const peer = std::make_shared<PeerTest>(
env.app(),
slot,

View File

@@ -20,7 +20,6 @@
#include <test/unit_test/SuiteJournal.h>
#include <xrpld/core/Config.h>
#include <xrpld/peerfinder/PeerfinderManager.h>
#include <xrpld/peerfinder/detail/Logic.h>
#include <xrpl/basics/chrono.h>
@@ -99,7 +98,7 @@ public:
if (!list.empty())
{
BEAST_EXPECT(list.size() == 1);
auto const [slot, _] = logic.new_outbound_slot(list.front());
auto const slot = logic.new_outbound_slot(list.front());
BEAST_EXPECT(logic.onConnected(
slot, beast::IP::Endpoint::from_string("65.0.0.2:5")));
logic.on_closed(slot);
@@ -140,7 +139,7 @@ public:
if (!list.empty())
{
BEAST_EXPECT(list.size() == 1);
auto const [slot, _] = logic.new_outbound_slot(list.front());
auto const slot = logic.new_outbound_slot(list.front());
if (!BEAST_EXPECT(logic.onConnected(
slot, beast::IP::Endpoint::from_string("65.0.0.2:5"))))
return;
@@ -159,7 +158,6 @@ public:
BEAST_EXPECT(n <= (seconds + 59) / 60);
}
// test accepting an incoming slot for an already existing outgoing slot
void
test_duplicateOutIn()
{
@@ -168,6 +166,8 @@ public:
TestChecker checker;
TestStopwatch clock;
Logic<TestChecker> logic(clock, store, checker, journal_);
logic.addFixedPeer(
"test", beast::IP::Endpoint::from_string("65.0.0.1:5"));
{
Config c;
c.autoConnect = false;
@@ -176,24 +176,28 @@ public:
logic.config(c);
}
auto const remote = beast::IP::Endpoint::from_string("65.0.0.1:5");
auto const [slot1, r] = logic.new_outbound_slot(remote);
BEAST_EXPECT(slot1 != nullptr);
BEAST_EXPECT(r == Result::success);
BEAST_EXPECT(logic.connectedAddresses_.count(remote.address()) == 1);
auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1024");
auto const [slot2, r2] = logic.new_inbound_slot(local, remote);
BEAST_EXPECT(logic.connectedAddresses_.count(remote.address()) == 1);
BEAST_EXPECT(r2 == Result::duplicatePeer);
if (!BEAST_EXPECT(slot2 == nullptr))
logic.on_closed(slot2);
logic.on_closed(slot1);
auto const list = logic.autoconnect();
if (BEAST_EXPECT(!list.empty()))
{
BEAST_EXPECT(list.size() == 1);
auto const remote = list.front();
auto const slot1 = logic.new_outbound_slot(remote);
if (BEAST_EXPECT(slot1 != nullptr))
{
BEAST_EXPECT(
logic.connectedAddresses_.count(remote.address()) == 1);
auto const local =
beast::IP::Endpoint::from_string("65.0.0.2:1024");
auto const slot2 = logic.new_inbound_slot(local, remote);
BEAST_EXPECT(
logic.connectedAddresses_.count(remote.address()) == 1);
if (!BEAST_EXPECT(slot2 == nullptr))
logic.on_closed(slot2);
logic.on_closed(slot1);
}
}
}
// test establishing outgoing slot for an already existing incoming slot
void
test_duplicateInOut()
{
@@ -202,6 +206,8 @@ public:
TestChecker checker;
TestStopwatch clock;
Logic<TestChecker> logic(clock, store, checker, journal_);
logic.addFixedPeer(
"test", beast::IP::Endpoint::from_string("65.0.0.1:5"));
{
Config c;
c.autoConnect = false;
@@ -210,202 +216,33 @@ public:
logic.config(c);
}
auto const remote = beast::IP::Endpoint::from_string("65.0.0.1:5");
auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1024");
auto const [slot1, r] = logic.new_inbound_slot(local, remote);
BEAST_EXPECT(slot1 != nullptr);
BEAST_EXPECT(r == Result::success);
BEAST_EXPECT(logic.connectedAddresses_.count(remote.address()) == 1);
auto const [slot2, r2] = logic.new_outbound_slot(remote);
BEAST_EXPECT(r2 == Result::duplicatePeer);
BEAST_EXPECT(logic.connectedAddresses_.count(remote.address()) == 1);
if (!BEAST_EXPECT(slot2 == nullptr))
logic.on_closed(slot2);
logic.on_closed(slot1);
}
void
test_peerLimitExceeded()
{
testcase("peer limit exceeded");
TestStore store;
TestChecker checker;
TestStopwatch clock;
Logic<TestChecker> logic(clock, store, checker, journal_);
auto const list = logic.autoconnect();
if (BEAST_EXPECT(!list.empty()))
{
Config c;
c.autoConnect = false;
c.listeningPort = 1024;
c.ipLimit = 2;
logic.config(c);
BEAST_EXPECT(list.size() == 1);
auto const remote = list.front();
auto const local =
beast::IP::Endpoint::from_string("65.0.0.2:1024");
auto const slot1 = logic.new_inbound_slot(local, remote);
if (BEAST_EXPECT(slot1 != nullptr))
{
BEAST_EXPECT(
logic.connectedAddresses_.count(remote.address()) == 1);
auto const slot2 = logic.new_outbound_slot(remote);
BEAST_EXPECT(
logic.connectedAddresses_.count(remote.address()) == 1);
if (!BEAST_EXPECT(slot2 == nullptr))
logic.on_closed(slot2);
logic.on_closed(slot1);
}
}
auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1024");
auto const [slot, r] = logic.new_inbound_slot(
local, beast::IP::Endpoint::from_string("55.104.0.2:1025"));
BEAST_EXPECT(slot != nullptr);
BEAST_EXPECT(r == Result::success);
auto const [slot1, r1] = logic.new_inbound_slot(
local, beast::IP::Endpoint::from_string("55.104.0.2:1026"));
BEAST_EXPECT(slot1 != nullptr);
BEAST_EXPECT(r1 == Result::success);
auto const [slot2, r2] = logic.new_inbound_slot(
local, beast::IP::Endpoint::from_string("55.104.0.2:1027"));
BEAST_EXPECT(r2 == Result::ipLimitExceeded);
if (!BEAST_EXPECT(slot2 == nullptr))
logic.on_closed(slot2);
logic.on_closed(slot1);
logic.on_closed(slot);
}
void
test_activate_duplicate_peer()
{
testcase("test activate duplicate peer");
TestStore store;
TestChecker checker;
TestStopwatch clock;
Logic<TestChecker> logic(clock, store, checker, journal_);
{
Config c;
c.autoConnect = false;
c.listeningPort = 1024;
c.ipLimit = 2;
logic.config(c);
}
auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1024");
PublicKey const pk1(randomKeyPair(KeyType::secp256k1).first);
auto const [slot, rSlot] = logic.new_outbound_slot(
beast::IP::Endpoint::from_string("55.104.0.2:1025"));
BEAST_EXPECT(slot != nullptr);
BEAST_EXPECT(rSlot == Result::success);
auto const [slot2, r2Slot] = logic.new_outbound_slot(
beast::IP::Endpoint::from_string("55.104.0.2:1026"));
BEAST_EXPECT(slot2 != nullptr);
BEAST_EXPECT(r2Slot == Result::success);
BEAST_EXPECT(logic.onConnected(slot, local));
BEAST_EXPECT(logic.onConnected(slot2, local));
BEAST_EXPECT(logic.activate(slot, pk1, false) == Result::success);
// activating a different slot with the same node ID (pk) must fail
BEAST_EXPECT(
logic.activate(slot2, pk1, false) == Result::duplicatePeer);
logic.on_closed(slot);
// accept the same key for a new slot after removing the old slot
BEAST_EXPECT(logic.activate(slot2, pk1, false) == Result::success);
logic.on_closed(slot2);
}
void
test_activate_inbound_disabled()
{
testcase("test activate inbound disabled");
TestStore store;
TestChecker checker;
TestStopwatch clock;
Logic<TestChecker> logic(clock, store, checker, journal_);
{
Config c;
c.autoConnect = false;
c.listeningPort = 1024;
c.ipLimit = 2;
logic.config(c);
}
PublicKey const pk1(randomKeyPair(KeyType::secp256k1).first);
auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1024");
auto const [slot, rSlot] = logic.new_inbound_slot(
local, beast::IP::Endpoint::from_string("55.104.0.2:1025"));
BEAST_EXPECT(slot != nullptr);
BEAST_EXPECT(rSlot == Result::success);
BEAST_EXPECT(
logic.activate(slot, pk1, false) == Result::inboundDisabled);
{
Config c;
c.autoConnect = false;
c.listeningPort = 1024;
c.ipLimit = 2;
c.inPeers = 1;
logic.config(c);
}
// new inbound slot must succeed when inbound connections are enabled
BEAST_EXPECT(logic.activate(slot, pk1, false) == Result::success);
// creating a new inbound slot must succeed as IP Limit is not exceeded
auto const [slot2, r2Slot] = logic.new_inbound_slot(
local, beast::IP::Endpoint::from_string("55.104.0.2:1026"));
BEAST_EXPECT(slot2 != nullptr);
BEAST_EXPECT(r2Slot == Result::success);
PublicKey const pk2(randomKeyPair(KeyType::secp256k1).first);
// an inbound slot exceeding inPeers limit must fail
BEAST_EXPECT(logic.activate(slot2, pk2, false) == Result::full);
logic.on_closed(slot2);
logic.on_closed(slot);
}
void
test_addFixedPeer_no_port()
{
testcase("test addFixedPeer no port");
TestStore store;
TestChecker checker;
TestStopwatch clock;
Logic<TestChecker> logic(clock, store, checker, journal_);
try
{
logic.addFixedPeer(
"test", beast::IP::Endpoint::from_string("65.0.0.2"));
fail("invalid endpoint successfully added");
}
catch (std::runtime_error const& e)
{
pass();
}
}
void
test_onConnected_self_connection()
{
testcase("test onConnected self connection");
TestStore store;
TestChecker checker;
TestStopwatch clock;
Logic<TestChecker> logic(clock, store, checker, journal_);
auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1234");
auto const [slot, r] = logic.new_outbound_slot(local);
BEAST_EXPECT(slot != nullptr);
BEAST_EXPECT(r == Result::success);
// Must fail when a slot is to our own IP address
BEAST_EXPECT(!logic.onConnected(slot, local));
logic.on_closed(slot);
}
void
test_config()
{
// if peers_max is configured then peers_in_max and peers_out_max
// are ignored
// if peers_max is configured then peers_in_max and peers_out_max are
// ignored
auto run = [&](std::string const& test,
std::optional<std::uint16_t> maxPeers,
std::optional<std::uint16_t> maxIn,
@@ -445,21 +282,13 @@ public:
Counts counts;
counts.onConfig(config);
BEAST_EXPECT(
counts.out_max() == expectOut && counts.in_max() == expectIn &&
counts.out_max() == expectOut &&
counts.inboundSlots() == expectIn &&
config.ipLimit == expectIpLimit);
TestStore store;
TestChecker checker;
TestStopwatch clock;
Logic<TestChecker> logic(clock, store, checker, journal_);
logic.config(config);
BEAST_EXPECT(logic.config() == config);
};
// if max_peers == 0 => maxPeers = 21,
// else if max_peers < 10 => maxPeers = 10 else maxPeers =
// max_peers
// else if max_peers < 10 => maxPeers = 10 else maxPeers = max_peers
// expectOut => if legacy => max(0.15 * maxPeers, 10),
// if legacy && !wantIncoming => maxPeers else max_out_peers
// expectIn => if legacy && wantIncoming => maxPeers - outPeers
@@ -535,11 +364,6 @@ public:
test_duplicateInOut();
test_config();
test_invalid_config();
test_peerLimitExceeded();
test_activate_duplicate_peer();
test_activate_inbound_disabled();
test_addFixedPeer_no_port();
test_onConnected_self_connection();
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,284 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
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.
*/
//==============================================================================
#ifndef RIPPLE_APP_MISC_SUBSCRIPTIONHELPERS_H_INCLUDED
#define RIPPLE_APP_MISC_SUBSCRIPTIONHELPERS_H_INCLUDED
#include <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/paths/Flow.h>
#include <xrpld/app/tx/detail/MPTokenAuthorize.h>
#include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/ReadView.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/scope.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/STAccount.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
template <ValidIssueType T>
static TER
canTransferTokenHelper(
ReadView const& view,
AccountID const& account,
AccountID const& dest,
STAmount const& amount,
beast::Journal const& j);
template <>
TER
canTransferTokenHelper<Issue>(
ReadView const& view,
AccountID const& account,
AccountID const& dest,
STAmount const& amount,
beast::Journal const& j)
{
AccountID issuer = amount.getIssuer();
if (issuer == account)
{
JLOG(j.trace())
<< "canTransferTokenHelper: Issuer is the same as the account.";
return tesSUCCESS;
}
// If the issuer does not exist, return tecNO_ISSUER
auto const sleIssuer = view.read(keylet::account(issuer));
if (!sleIssuer)
{
JLOG(j.trace()) << "canTransferTokenHelper: Issuer does not exist.";
return tecNO_ISSUER;
}
// If the account does not have a trustline to the issuer, return tecNO_LINE
auto const sleRippleState =
view.read(keylet::line(account, issuer, amount.getCurrency()));
if (!sleRippleState)
{
JLOG(j.trace()) << "canTransferTokenHelper: Trust line does not exist.";
return tecNO_LINE;
}
STAmount const balance = (*sleRippleState)[sfBalance];
// If balance is positive, issuer must have higher address than account
if (balance > beast::zero && issuer < account)
{
JLOG(j.trace()) << "canTransferTokenHelper: Invalid trust line state.";
return tecNO_PERMISSION;
}
// If balance is negative, issuer must have lower address than account
if (balance < beast::zero && issuer > account)
{
JLOG(j.trace()) << "canTransferTokenHelper: Invalid trust line state.";
return tecNO_PERMISSION;
}
// If the issuer has requireAuth set, check if the account is authorized
if (auto const ter = requireAuth(view, amount.issue(), account);
ter != tesSUCCESS)
{
JLOG(j.trace()) << "canTransferTokenHelper: Account is not authorized";
return ter;
}
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(view, amount.issue(), dest);
ter != tesSUCCESS)
{
JLOG(j.trace())
<< "canTransferTokenHelper: Destination is not authorized.";
return ter;
}
// If the issuer has frozen the account, return tecFROZEN
if (isFrozen(view, account, amount.issue()) ||
isDeepFrozen(
view, account, amount.issue().currency, amount.issue().account))
{
JLOG(j.trace()) << "canTransferTokenHelper: Account is frozen.";
return tecFROZEN;
}
// If the issuer has frozen the destination, return tecFROZEN
if (isFrozen(view, dest, amount.issue()) ||
isDeepFrozen(
view, dest, amount.issue().currency, amount.issue().account))
{
JLOG(j.trace()) << "canTransferTokenHelper: Destination is frozen.";
return tecFROZEN;
}
STAmount const spendableAmount = accountHolds(
view, account, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j);
// If the balance is less than or equal to 0, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::zero)
{
JLOG(j.trace()) << "canTransferTokenHelper: Spendable amount is less "
"than or equal to 0.";
return tecINSUFFICIENT_FUNDS;
}
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
{
JLOG(j.trace()) << "canTransferTokenHelper: Spendable amount is less "
"than the amount.";
return tecINSUFFICIENT_FUNDS;
}
// If the amount is not addable to the balance, return tecPRECISION_LOSS
if (!canAdd(spendableAmount, amount))
return tecPRECISION_LOSS;
return tesSUCCESS;
}
template <>
TER
canTransferTokenHelper<MPTIssue>(
ReadView const& view,
AccountID const& account,
AccountID const& dest,
STAmount const& amount,
beast::Journal const& j)
{
AccountID issuer = amount.getIssuer();
if (issuer == account)
{
JLOG(j.trace())
<< "canTransferTokenHelper: Issuer is the same as the account.";
return tesSUCCESS;
}
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey =
keylet::mptIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = view.read(issuanceKey);
if (!sleIssuance)
{
JLOG(j.trace())
<< "canTransferTokenHelper: MPT issuance does not exist.";
return tecOBJECT_NOT_FOUND;
}
// If the issuer is not the same as the issuer of the mpt, return
// tecNO_PERMISSION
if (sleIssuance->getAccountID(sfIssuer) != issuer)
{
JLOG(j.trace()) << "canTransferTokenHelper: Issuer is not the same as "
"the issuer of the MPT.";
return tecNO_PERMISSION;
}
// If the account does not have the mpt, return tecOBJECT_NOT_FOUND
if (!view.exists(keylet::mptoken(issuanceKey.key, account)))
{
JLOG(j.trace())
<< "canTransferTokenHelper: Account does not have the MPT.";
return tecOBJECT_NOT_FOUND;
}
// If the issuer has requireAuth set, check if the account is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter =
requireAuth(view, mptIssue, account, AuthType::WeakAuth);
ter != tesSUCCESS)
{
JLOG(j.trace()) << "canTransferTokenHelper: Account is not authorized.";
return ter;
}
// If the issuer has requireAuth set, check if the destination is
// authorized
if (auto const ter = requireAuth(view, mptIssue, dest, AuthType::WeakAuth);
ter != tesSUCCESS)
{
JLOG(j.trace())
<< "canTransferTokenHelper: Destination is not authorized.";
return ter;
}
// If the issuer has locked the account, return tecLOCKED
if (isFrozen(view, account, mptIssue))
{
JLOG(j.trace()) << "canTransferTokenHelper: Account is locked.";
return tecLOCKED;
}
// If the issuer has locked the destination, return tecLOCKED
if (isFrozen(view, dest, mptIssue))
{
JLOG(j.trace()) << "canTransferTokenHelper: Destination is locked.";
return tecLOCKED;
}
// If the mpt cannot be transferred, return tecNO_AUTH
if (auto const ter = canTransfer(view, mptIssue, account, dest);
ter != tesSUCCESS)
{
JLOG(j.trace()) << "canTransferTokenHelper: MPT cannot be transferred.";
return ter;
}
STAmount const spendableAmount = accountHolds(
view,
account,
amount.get<MPTIssue>(),
fhIGNORE_FREEZE,
ahIGNORE_AUTH,
j);
// If the balance is less than or equal to 0, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::zero)
{
JLOG(j.trace()) << "canTransferTokenHelper: Spendable amount is less "
"than or equal to 0.";
return tecINSUFFICIENT_FUNDS;
}
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
{
JLOG(j.trace()) << "canTransferTokenHelper: Spendable amount is less "
"than the amount.";
return tecINSUFFICIENT_FUNDS;
}
// If the amount is not addable to the balance, return tecPRECISION_LOSS
if (!canAdd(spendableAmount, amount))
return tecPRECISION_LOSS;
return tesSUCCESS;
}
} // namespace ripple
#endif

View File

@@ -166,7 +166,7 @@ private:
int addFlags,
std::function<bool(void)> const& continueCallback);
// Compute the liquidity for a path. Return tesSUCCESS if it has enough
// Compute the liquidity for a path. Return tesSUCCESS if it has has enough
// liquidity to be worth keeping, otherwise an error.
TER
getPathLiquidity(

View File

@@ -543,7 +543,6 @@ LedgerEntryTypesMatch::visitEntry(
case ltCREDENTIAL:
case ltPERMISSIONED_DOMAIN:
case ltVAULT:
case ltSUBSCRIPTION:
break;
default:
invalidTypeAdded_ = true;
@@ -1511,15 +1510,6 @@ ValidMPTIssuance::finalize(
if (tx.getTxnType() == ttESCROW_FINISH)
return true;
if (tx.getTxnType() == ttSUBSCRIPTION_CLAIM)
return true;
if ((tx.getTxnType() == ttVAULT_CLAWBACK ||
tx.getTxnType() == ttVAULT_WITHDRAW) &&
mptokensDeleted_ == 1 && mptokensCreated_ == 0 &&
mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0)
return true;
}
if (mptIssuancesCreated_ != 0)

View File

@@ -1,106 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
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 <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/paths/Flow.h>
#include <xrpld/app/tx/detail/SubscriptionCancel.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/scope.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/STAccount.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
NotTEC
SubscriptionCancel::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureSubscription))
return temDISABLED;
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;
if (ctx.tx.getFlags() & tfUniversalMask)
return temINVALID_FLAG;
return preflight2(ctx);
}
TER
SubscriptionCancel::preclaim(PreclaimContext const& ctx)
{
auto const sleSub = ctx.view.read(
keylet::subscription(ctx.tx.getFieldH256(sfSubscriptionID)));
if (!sleSub)
{
JLOG(ctx.j.debug())
<< "SubscriptionCancel: Subscription does not exist.";
return tecNO_ENTRY;
}
return tesSUCCESS;
}
TER
SubscriptionCancel::doApply()
{
Sandbox sb(&ctx_.view());
auto const sleSub =
sb.peek(keylet::subscription(ctx_.tx.getFieldH256(sfSubscriptionID)));
if (!sleSub)
{
JLOG(ctx_.journal.debug())
<< "SubscriptionCancel: Subscription does not exist.";
return tecINTERNAL;
}
AccountID const account{sleSub->getAccountID(sfAccount)};
AccountID const dstAcct{sleSub->getAccountID(sfDestination)};
auto viewJ = ctx_.app.journal("View");
std::uint64_t const ownerPage{(*sleSub)[sfOwnerNode]};
if (!sb.dirRemove(
keylet::ownerDir(account), ownerPage, sleSub->key(), true))
{
JLOG(j_.fatal()) << "Unable to delete subscription from source.";
return tefBAD_LEDGER;
}
std::uint64_t const destPage{(*sleSub)[sfDestinationNode]};
if (!sb.dirRemove(keylet::ownerDir(dstAcct), destPage, sleSub->key(), true))
{
JLOG(j_.fatal()) << "Unable to delete subscription from destination.";
return tefBAD_LEDGER;
}
auto const sleSrc = sb.peek(keylet::account(account));
sb.erase(sleSub);
adjustOwnerCount(sb, sleSrc, -1, viewJ);
sb.apply(ctx_.rawView());
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -1,48 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
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.
*/
//==============================================================================
#ifndef RIPPLE_TX_SUBSCRIPTIONCANCEL_H_INCLUDED
#define RIPPLE_TX_SUBSCRIPTIONCANCEL_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class SubscriptionCancel : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit SubscriptionCancel(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
#endif // RIPPLE_TX_SUBSCRIPTIONCANCEL_H_INCLUDED

View File

@@ -1,426 +0,0 @@
#include <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/misc/SubscriptionHelpers.h>
#include <xrpld/app/paths/Flow.h>
#include <xrpld/app/tx/detail/MPTokenAuthorize.h>
#include <xrpld/app/tx/detail/SubscriptionClaim.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/scope.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/STAccount.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
NotTEC
SubscriptionClaim::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureSubscription))
return temDISABLED;
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;
if (ctx.tx.getFlags() & tfUniversalMask)
return temINVALID_FLAG;
return preflight2(ctx);
}
TER
SubscriptionClaim::preclaim(PreclaimContext const& ctx)
{
auto const sleSub = ctx.view.read(
keylet::subscription(ctx.tx.getFieldH256(sfSubscriptionID)));
if (!sleSub)
{
JLOG(ctx.j.trace())
<< "SubscriptionClaim: Subscription does not exist.";
return tecNO_ENTRY;
}
// Only claim a subscription with this account as the destination.
AccountID const dest = sleSub->getAccountID(sfDestination);
if (ctx.tx[sfAccount] != dest)
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: Cashing a subscription with "
"wrong Destination.";
return tecNO_PERMISSION;
}
AccountID const account = sleSub->getAccountID(sfAccount);
if (account == dest)
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: Malformed transaction: "
"Cashing subscription to self.";
return tecINTERNAL;
}
{
auto const sleSrc = ctx.view.read(keylet::account(account));
auto const sleDst = ctx.view.read(keylet::account(dest));
if (!sleSrc || !sleDst)
{
JLOG(ctx.j.trace())
<< "SubscriptionClaim: source or destination not in ledger";
return tecNO_ENTRY;
}
}
{
STAmount const amount = ctx.tx.getFieldAmount(sfAmount);
STAmount const sleAmount = sleSub->getFieldAmount(sfAmount);
if (amount.asset() != sleAmount.asset())
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: Subscription claim does "
"not match subscription currency.";
return tecWRONG_ASSET;
}
if (amount > sleAmount)
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: Claim amount exceeds "
"subscription amount.";
return temBAD_AMOUNT;
}
// Time/period context
std::uint32_t const currentTime =
ctx.view.info().parentCloseTime.time_since_epoch().count();
std::uint32_t const nextClaimTime =
sleSub->getFieldU32(sfNextClaimTime);
std::uint32_t const frequency = sleSub->getFieldU32(sfFrequency);
// Determine effective available balance:
// - If we have crossed into a later period AND the previous period had
// a partial
// balance remaining (carryover not allowed), then the effective
// period rolls forward once and its balance resets to sleAmount.
// - Otherwise we operate on the period at nextClaimTime with its stored
// balance.
STAmount balance = sleSub->getFieldAmount(sfBalance);
bool const arrears = currentTime >= nextClaimTime + frequency;
if (arrears && balance != sleAmount)
{
// We will effectively operate on (nextClaimTime + frequency) with a
// full balance.
balance = sleAmount;
}
if (amount > balance)
{
JLOG(ctx.j.trace())
<< "SubscriptionClaim: Claim amount exceeds remaining "
"balance for this period.";
return tecINSUFFICIENT_FUNDS;
}
if (isXRP(amount))
{
if (xrpLiquid(ctx.view, account, 0, ctx.j) < amount)
return tecINSUFFICIENT_FUNDS;
}
else
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return canTransferTokenHelper<T>(
ctx.view, account, dest, amount, ctx.j);
},
amount.asset().value());
!isTesSuccess(ret))
return ret;
}
}
// Must be at or past the start of the effective period.
if (!hasExpired(ctx.view, sleSub->getFieldU32(sfNextClaimTime)))
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: The subscription has not "
"reached the next claim time.";
return tecTOO_SOON;
}
return tesSUCCESS;
}
template <ValidIssueType T>
static TER
doTransferTokenHelper(
ApplyView& view,
std::shared_ptr<SLE> const& sleDest,
STAmount const& xrpBalance,
STAmount const& amount,
AccountID const& issuer,
AccountID const& sender,
AccountID const& receiver,
bool createAsset,
beast::Journal journal);
template <>
TER
doTransferTokenHelper<Issue>(
ApplyView& view,
std::shared_ptr<SLE> const& sleDest,
STAmount const& xrpBalance,
STAmount const& amount,
AccountID const& issuer,
AccountID const& sender,
AccountID const& receiver,
bool createAsset,
beast::Journal journal)
{
Keylet const trustLineKey = keylet::line(receiver, amount.issue());
bool const recvLow = issuer > receiver;
// Review Note: We could remove this and just say to use batch to auth the
// token first
if (!view.exists(trustLineKey) && createAsset && issuer != receiver)
{
// Can the account cover the trust line's reserve?
if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)};
xrpBalance < view.fees().accountReserve(ownerCount + 1))
{
JLOG(journal.trace())
<< "doTransferTokenHelper: Trust line does not exist. "
"Insufficent reserve to create line.";
return tecNO_LINE_INSUF_RESERVE;
}
Currency const currency = amount.getCurrency();
STAmount initialBalance(amount.issue());
initialBalance.setIssuer(noAccount());
// clang-format off
if (TER const ter = trustCreate(
view, // payment sandbox
recvLow, // is dest low?
issuer, // source
receiver, // destination
trustLineKey.key, // ledger index
sleDest, // Account to add to
false, // authorize account
(sleDest->getFlags() & lsfDefaultRipple) == 0,
false, // freeze trust line
false, // deep freeze trust line
initialBalance, // zero initial balance
Issue(currency, receiver), // limit of zero
0, // quality in
0, // quality out
journal); // journal
!isTesSuccess(ter))
{
JLOG(journal.trace()) << "doTransferTokenHelper: Failed to create trust line: " << transToken(ter);
return ter;
}
// clang-format on
view.update(sleDest);
}
if (!view.exists(trustLineKey) && issuer != receiver)
return tecNO_LINE;
auto const ter = accountSend(
view, sender, receiver, amount, journal, WaiveTransferFee::No);
if (ter != tesSUCCESS)
{
JLOG(journal.trace()) << "doTransferTokenHelper: Failed to send token: "
<< transToken(ter);
return ter; // LCOV_EXCL_LINE
}
return tesSUCCESS;
}
template <>
TER
doTransferTokenHelper<MPTIssue>(
ApplyView& view,
std::shared_ptr<SLE> const& sleDest,
STAmount const& xrpBalance,
STAmount const& amount,
AccountID const& issuer,
AccountID const& sender,
AccountID const& receiver,
bool createAsset,
beast::Journal journal)
{
auto const mptID = amount.get<MPTIssue>().getMptID();
auto const issuanceKey = keylet::mptIssuance(mptID);
if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && createAsset)
{
if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)};
xrpBalance < view.fees().accountReserve(ownerCount + 1))
{
JLOG(journal.trace())
<< "doTransferTokenHelper: MPT does not exist. "
"Insufficent reserve to create MPT.";
return tecINSUFFICIENT_RESERVE;
}
if (auto const ter =
MPTokenAuthorize::createMPToken(view, mptID, receiver, 0);
!isTesSuccess(ter))
{
JLOG(journal.trace())
<< "doTransferTokenHelper: Failed to create MPT: "
<< transToken(ter);
return ter;
}
// Update owner count.
adjustOwnerCount(view, sleDest, 1, journal);
}
if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)))
{
JLOG(journal.trace()) << "doTransferTokenHelper: MPT does not exist.";
return tecNO_PERMISSION;
}
auto const ter = accountSend(
view, sender, receiver, amount, journal, WaiveTransferFee::No);
if (ter != tesSUCCESS)
{
JLOG(journal.trace())
<< "doTransferTokenHelper: Failed to send MPT: " << transToken(ter);
return ter; // LCOV_EXCL_LINE
}
return tesSUCCESS;
}
TER
SubscriptionClaim::doApply()
{
PaymentSandbox psb(&ctx_.view());
auto viewJ = ctx_.app.journal("View");
auto sleSub =
psb.peek(keylet::subscription(ctx_.tx.getFieldH256(sfSubscriptionID)));
if (!sleSub)
{
JLOG(j_.trace()) << "SubscriptionClaim: Subscription does not exist.";
return tecINTERNAL;
}
AccountID const account = sleSub->getAccountID(sfAccount);
if (!psb.exists(keylet::account(account)))
{
JLOG(j_.trace()) << "SubscriptionClaim: Account does not exist.";
return tecINTERNAL;
}
AccountID const dest = sleSub->getAccountID(sfDestination);
if (!psb.exists(keylet::account(dest)))
{
JLOG(j_.trace()) << "SubscriptionClaim: Account does not exist.";
return tecINTERNAL;
}
if (dest != ctx_.tx.getAccountID(sfAccount))
{
JLOG(j_.trace()) << "SubscriptionClaim: Account is not the "
"destination of the subscription.";
return tecNO_PERMISSION;
}
STAmount const sleAmount = sleSub->getFieldAmount(sfAmount);
STAmount const deliverAmount = ctx_.tx.getFieldAmount(sfAmount);
// Pull current period info
std::uint32_t const currentTime =
psb.info().parentCloseTime.time_since_epoch().count();
std::uint32_t nextClaimTime = sleSub->getFieldU32(sfNextClaimTime);
std::uint32_t const frequency = sleSub->getFieldU32(sfFrequency);
STAmount availableBalance = sleSub->getFieldAmount(sfBalance);
bool const arrears = currentTime >= nextClaimTime + frequency;
// If we crossed into a later period and the previous period was partially
// used, forfeit the leftover and roll forward exactly one period; reset the
// balance.
if (arrears && availableBalance != sleAmount)
{
nextClaimTime += frequency;
availableBalance = sleAmount;
// Reflect the rollover immediately in the SLE so subsequent logic is
// consistent.
sleSub->setFieldU32(sfNextClaimTime, nextClaimTime);
sleSub->setFieldAmount(sfBalance, availableBalance);
}
// Enforce available balance for the effective period.
if (deliverAmount > availableBalance)
{
JLOG(j_.trace()) << "SubscriptionClaim: Claim amount exceeds remaining "
<< "balance for this period.";
return tecINTERNAL;
}
// Perform the transfer
if (isXRP(deliverAmount))
{
if (TER const ter{
transferXRP(psb, account, dest, deliverAmount, viewJ)};
ter != tesSUCCESS)
{
return ter;
}
}
else
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return doTransferTokenHelper<T>(
psb,
psb.peek(keylet::account(dest)),
mPriorBalance,
deliverAmount,
deliverAmount.getIssuer(),
account,
dest,
true, // create asset
viewJ);
},
deliverAmount.asset().value());
!isTesSuccess(ret))
return ret;
}
// Update balance and period pointer
STAmount const newBalance = availableBalance - deliverAmount;
if (newBalance == sleAmount.zeroed())
{
// Full period claimed: advance exactly one period and reset next period
// balance.
nextClaimTime += frequency;
sleSub->setFieldU32(sfNextClaimTime, nextClaimTime);
sleSub->setFieldAmount(sfBalance, sleAmount);
}
else
{
// Partial claim within the same effective period.
sleSub->setFieldAmount(sfBalance, newBalance);
// Do not advance nextClaimTime; if we had a rollover-forfeit above,
// we already moved nextClaimTime forward exactly once.
}
psb.update(sleSub);
if (sleSub->isFieldPresent(sfExpiration) &&
psb.info().parentCloseTime.time_since_epoch().count() >=
sleSub->getFieldU32(sfExpiration))
{
psb.erase(sleSub);
}
psb.apply(ctx_.rawView());
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -1,48 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
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.
*/
//==============================================================================
#ifndef RIPPLE_TX_SUBSCRIPTIONCLAIM_H_INCLUDED
#define RIPPLE_TX_SUBSCRIPTIONCLAIM_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class SubscriptionClaim : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit SubscriptionClaim(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
#endif // RIPPLE_TX_SUBSCRIPTIONCLAIM_H_INCLUDED

View File

@@ -1,337 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
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 <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/misc/SubscriptionHelpers.h>
#include <xrpld/app/paths/Flow.h>
#include <xrpld/app/tx/detail/SubscriptionSet.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/scope.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/STAccount.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
template <ValidIssueType T>
static NotTEC
setPreflightHelper(PreflightContext const& ctx);
template <>
NotTEC
setPreflightHelper<Issue>(PreflightContext const& ctx)
{
STAmount const amount = ctx.tx[sfAmount];
if (amount.native() || amount <= beast::zero)
return temBAD_AMOUNT;
if (badCurrency() == amount.getCurrency())
return temBAD_CURRENCY;
return tesSUCCESS;
}
template <>
NotTEC
setPreflightHelper<MPTIssue>(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureMPTokensV1))
return temDISABLED;
auto const amount = ctx.tx[sfAmount];
if (amount.native() || amount.mpt() > MPTAmount{maxMPTokenAmount} ||
amount <= beast::zero)
return temBAD_AMOUNT;
return tesSUCCESS;
}
NotTEC
SubscriptionSet::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureSubscription))
return temDISABLED;
if (ctx.tx.getFlags() & tfUniversalMask)
return temINVALID_FLAG;
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;
if (ctx.tx.isFieldPresent(sfSubscriptionID))
{
// update
if (!ctx.tx.isFieldPresent(sfAmount))
{
JLOG(ctx.j.trace())
<< "SubscriptionSet: Malformed transaction: SubscriptionID "
"is present, but Amount is not.";
return temMALFORMED;
}
if (ctx.tx.isFieldPresent(sfDestination) ||
ctx.tx.isFieldPresent(sfFrequency) ||
ctx.tx.isFieldPresent(sfStartTime))
{
JLOG(ctx.j.trace())
<< "SubscriptionSet: Malformed transaction: SubscriptionID "
"is present, but optional fields are also present.";
return temMALFORMED;
}
}
else
{
// create
if (!ctx.tx.isFieldPresent(sfDestination) ||
!ctx.tx.isFieldPresent(sfAmount) ||
!ctx.tx.isFieldPresent(sfFrequency))
{
JLOG(ctx.j.trace())
<< "SubscriptionSet: Malformed transaction: SubscriptionID "
"is not present, and required fields are not present.";
return temMALFORMED;
}
if (ctx.tx.getAccountID(sfDestination) ==
ctx.tx.getAccountID(sfAccount))
{
JLOG(ctx.j.trace())
<< "SubscriptionSet: Malformed transaction: Account "
"is the same as the destination.";
return temDST_IS_SRC;
}
}
STAmount const amount = ctx.tx.getFieldAmount(sfAmount);
if (amount.native())
{
if (!isLegalNet(amount) || amount <= beast::zero)
{
JLOG(ctx.j.trace())
<< "SubscriptionSet: Malformed transaction: bad amount: "
<< amount.getFullText();
return temBAD_AMOUNT;
}
}
else
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return setPreflightHelper<T>(ctx);
},
amount.asset().value());
!isTesSuccess(ret))
return ret;
}
return preflight2(ctx);
}
TER
SubscriptionSet::preclaim(PreclaimContext const& ctx)
{
STAmount const amount = ctx.tx.getFieldAmount(sfAmount);
AccountID const account = ctx.tx.getAccountID(sfAccount);
AccountID const dest = ctx.tx.getAccountID(sfDestination);
if (ctx.tx.isFieldPresent(sfSubscriptionID))
{
// update
auto sle = ctx.view.read(
keylet::subscription(ctx.tx.getFieldH256(sfSubscriptionID)));
if (!sle)
{
JLOG(ctx.j.trace())
<< "SubscriptionSet: Subscription does not exist.";
return tecNO_ENTRY;
}
if (sle->getAccountID(sfAccount) != ctx.tx.getAccountID(sfAccount))
{
JLOG(ctx.j.trace()) << "SubscriptionSet: Account is not the "
"owner of the subscription.";
return tecNO_PERMISSION;
}
}
else
{
// create
auto const sleDest =
ctx.view.read(keylet::account(ctx.tx.getAccountID(sfDestination)));
if (!sleDest)
{
JLOG(ctx.j.trace())
<< "SubscriptionSet: Destination account does not exist.";
return tecNO_DST;
}
auto const flags = sleDest->getFlags();
if ((flags & lsfRequireDestTag) && !ctx.tx[~sfDestinationTag])
return tecDST_TAG_NEEDED;
if (ctx.tx.getFieldU32(sfFrequency) <= 0)
{
JLOG(ctx.j.trace())
<< "SubscriptionSet: The frequency is less than or equal to 0.";
return temMALFORMED;
}
}
if (!isXRP(amount))
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return canTransferTokenHelper<T>(
ctx.view, account, dest, amount, ctx.j);
},
amount.asset().value());
!isTesSuccess(ret))
return ret;
}
return tesSUCCESS;
}
TER
SubscriptionSet::doApply()
{
Sandbox sb(&ctx_.view());
AccountID const account = ctx_.tx.getAccountID(sfAccount);
auto const sleAccount = sb.peek(keylet::account(account));
if (!sleAccount)
{
JLOG(ctx_.journal.trace())
<< "SubscriptionSet: Account does not exist.";
return tecINTERNAL;
}
if (ctx_.tx.isFieldPresent(sfSubscriptionID))
{
// update
auto sle = sb.peek(
keylet::subscription(ctx_.tx.getFieldH256(sfSubscriptionID)));
sle->setFieldAmount(sfAmount, ctx_.tx.getFieldAmount(sfAmount));
if (ctx_.tx.isFieldPresent(sfExpiration))
{
auto const currentTime =
sb.info().parentCloseTime.time_since_epoch().count();
auto const expiration = ctx_.tx.getFieldU32(sfExpiration);
if (expiration < currentTime)
{
JLOG(ctx_.journal.trace())
<< "SubscriptionSet: The expiration time is in the past.";
return temBAD_EXPIRATION;
}
sle->setFieldU32(sfExpiration, ctx_.tx.getFieldU32(sfExpiration));
}
sb.update(sle);
}
else
{
auto const currentTime =
sb.info().parentCloseTime.time_since_epoch().count();
auto startTime = currentTime;
auto nextClaimTime = currentTime;
// create
{
auto const balance = STAmount((*sleAccount)[sfBalance]).xrp();
auto const reserve =
sb.fees().accountReserve((*sleAccount)[sfOwnerCount] + 1);
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
}
AccountID const dest = ctx_.tx.getAccountID(sfDestination);
Keylet const subKeylet =
keylet::subscription(account, dest, ctx_.tx.getSeqProxy().value());
auto sle = std::make_shared<SLE>(subKeylet);
sle->setAccountID(sfAccount, account);
sle->setAccountID(sfDestination, dest);
if (ctx_.tx.isFieldPresent(sfDestinationTag))
sle->setFieldU32(
sfDestinationTag, ctx_.tx.getFieldU32(sfDestinationTag));
sle->setFieldAmount(sfAmount, ctx_.tx.getFieldAmount(sfAmount));
sle->setFieldAmount(sfBalance, ctx_.tx.getFieldAmount(sfAmount));
sle->setFieldU32(sfFrequency, ctx_.tx.getFieldU32(sfFrequency));
if (ctx_.tx.isFieldPresent(sfStartTime))
{
startTime = ctx_.tx.getFieldU32(sfStartTime);
nextClaimTime = startTime;
if (startTime < currentTime)
{
JLOG(ctx_.journal.trace())
<< "SubscriptionSet: The start time is in the past.";
return temMALFORMED;
}
}
sle->setFieldU32(sfNextClaimTime, nextClaimTime);
if (ctx_.tx.isFieldPresent(sfExpiration))
{
auto const expiration = ctx_.tx.getFieldU32(sfExpiration);
if (expiration < currentTime)
{
JLOG(ctx_.journal.trace())
<< "SubscriptionSet: The expiration time is in the past.";
return temBAD_EXPIRATION;
}
if (expiration < nextClaimTime)
{
JLOG(ctx_.journal.trace())
<< "SubscriptionSet: The expiration time is "
"less than the next claim time.";
return temBAD_EXPIRATION;
}
sle->setFieldU32(sfExpiration, expiration);
}
{
auto page = sb.dirInsert(
keylet::ownerDir(account),
subKeylet,
describeOwnerDir(account));
if (!page)
return tecDIR_FULL;
(*sle)[sfOwnerNode] = *page;
}
{
auto page = sb.dirInsert(
keylet::ownerDir(dest), subKeylet, describeOwnerDir(dest));
if (!page)
return tecDIR_FULL;
(*sle)[sfDestinationNode] = *page;
}
adjustOwnerCount(sb, sleAccount, 1, ctx_.journal);
sb.insert(sle);
}
sb.apply(ctx_.rawView());
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -1,48 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
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.
*/
//==============================================================================
#ifndef RIPPLE_TX_SUBSCRIPTIONSET_H_INCLUDED
#define RIPPLE_TX_SUBSCRIPTIONSET_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class SubscriptionSet : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit SubscriptionSet(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
#endif // RIPPLE_TX_SUBSCRIPTIONSET_H_INCLUDED

View File

@@ -21,10 +21,8 @@
#include <xrpld/ledger/View.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/TER.h>
@@ -153,7 +151,7 @@ VaultClawback::doApply()
if (!vault)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const mptIssuanceID = *((*vault)[sfShareMPTID]);
auto const mptIssuanceID = (*vault)[sfShareMPTID];
auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
{
@@ -163,169 +161,68 @@ VaultClawback::doApply()
// LCOV_EXCL_STOP
}
Asset const vaultAsset = vault->at(sfAsset);
Asset const asset = vault->at(sfAsset);
STAmount const amount = [&]() -> STAmount {
auto const maybeAmount = tx[~sfAmount];
if (maybeAmount)
return *maybeAmount;
return {sfAmount, vaultAsset, 0};
return {sfAmount, asset, 0};
}();
XRPL_ASSERT(
amount.asset() == vaultAsset,
amount.asset() == asset,
"ripple::VaultClawback::doApply : matching asset");
auto assetsAvailable = vault->at(sfAssetsAvailable);
auto assetsTotal = vault->at(sfAssetsTotal);
[[maybe_unused]] auto const lossUnrealized = vault->at(sfLossUnrealized);
XRPL_ASSERT(
lossUnrealized <= (assetsTotal - assetsAvailable),
"ripple::VaultClawback::doApply : loss and assets do balance");
AccountID holder = tx[sfHolder];
MPTIssue const share{mptIssuanceID};
STAmount sharesDestroyed = {share};
STAmount assetsRecovered;
try
STAmount assets, shares;
if (amount == beast::zero)
{
if (amount == beast::zero)
{
sharesDestroyed = accountHolds(
view(),
holder,
share,
FreezeHandling::fhIGNORE_FREEZE,
AuthHandling::ahIGNORE_AUTH,
j_);
auto const maybeAssets =
sharesToAssetsWithdraw(vault, sleIssuance, sharesDestroyed);
if (!maybeAssets)
return tecINTERNAL; // LCOV_EXCL_LINE
assetsRecovered = *maybeAssets;
}
else
{
assetsRecovered = amount;
{
auto const maybeShares =
assetsToSharesWithdraw(vault, sleIssuance, assetsRecovered);
if (!maybeShares)
return tecINTERNAL; // LCOV_EXCL_LINE
sharesDestroyed = *maybeShares;
}
auto const maybeAssets =
sharesToAssetsWithdraw(vault, sleIssuance, sharesDestroyed);
if (!maybeAssets)
return tecINTERNAL; // LCOV_EXCL_LINE
assetsRecovered = *maybeAssets;
}
// Clamp to maximum.
if (assetsRecovered > *assetsAvailable)
{
assetsRecovered = *assetsAvailable;
// Note, it is important to truncate the number of shares, otherwise
// the corresponding assets might breach the AssetsAvailable
{
auto const maybeShares = assetsToSharesWithdraw(
vault, sleIssuance, assetsRecovered, TruncateShares::yes);
if (!maybeShares)
return tecINTERNAL; // LCOV_EXCL_LINE
sharesDestroyed = *maybeShares;
}
auto const maybeAssets =
sharesToAssetsWithdraw(vault, sleIssuance, sharesDestroyed);
if (!maybeAssets)
return tecINTERNAL; // LCOV_EXCL_LINE
assetsRecovered = *maybeAssets;
if (assetsRecovered > *assetsAvailable)
{
// LCOV_EXCL_START
JLOG(j_.error())
<< "VaultClawback: invalid rounding of shares.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
}
Asset share = *(*vault)[sfShareMPTID];
shares = accountHolds(
view(),
holder,
share,
FreezeHandling::fhIGNORE_FREEZE,
AuthHandling::ahIGNORE_AUTH,
j_);
assets = sharesToAssetsWithdraw(vault, sleIssuance, shares);
}
catch (std::overflow_error const&)
else
{
// It's easy to hit this exception from Number with large enough Scale
// so we avoid spamming the log and only use debug here.
JLOG(j_.debug()) //
<< "VaultClawback: overflow error with"
<< " scale=" << (int)vault->at(sfScale).value() //
<< ", assetsTotal=" << vault->at(sfAssetsTotal).value()
<< ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
<< ", amount=" << amount.value();
return tecPATH_DRY;
assets = amount;
shares = assetsToSharesWithdraw(vault, sleIssuance, assets);
}
if (sharesDestroyed == beast::zero)
return tecPRECISION_LOSS;
// Clamp to maximum.
Number maxAssets = *vault->at(sfAssetsAvailable);
if (assets > maxAssets)
{
assets = maxAssets;
shares = assetsToSharesWithdraw(vault, sleIssuance, assets);
}
assetsTotal -= assetsRecovered;
assetsAvailable -= assetsRecovered;
if (shares == beast::zero)
return tecINSUFFICIENT_FUNDS;
vault->at(sfAssetsTotal) -= assets;
vault->at(sfAssetsAvailable) -= assets;
view().update(vault);
auto const& vaultAccount = vault->at(sfAccount);
// Transfer shares from holder to vault.
if (auto const ter = accountSend(
view(),
holder,
vaultAccount,
sharesDestroyed,
j_,
WaiveTransferFee::Yes);
!isTesSuccess(ter))
if (auto ter = accountSend(
view(), holder, vaultAccount, shares, j_, WaiveTransferFee::Yes))
return ter;
// Try to remove MPToken for shares, if the holder balance is zero. Vault
// pseudo-account will never set lsfMPTAuthorized, so we ignore flags.
// Keep MPToken if holder is the vault owner.
if (holder != vault->at(sfOwner))
{
if (auto const ter =
removeEmptyHolding(view(), holder, sharesDestroyed.asset(), j_);
isTesSuccess(ter))
{
JLOG(j_.debug()) //
<< "VaultClawback: removed empty MPToken for vault shares"
<< " MPTID=" << to_string(mptIssuanceID) //
<< " account=" << toBase58(holder);
}
else if (ter != tecHAS_OBLIGATIONS)
{
// LCOV_EXCL_START
JLOG(j_.error()) //
<< "VaultClawback: failed to remove MPToken for vault shares"
<< " MPTID=" << to_string(mptIssuanceID) //
<< " account=" << toBase58(holder) //
<< " with result: " << transToken(ter);
return ter;
// LCOV_EXCL_STOP
}
// else quietly ignore, holder balance is not zero
}
// Transfer assets from vault to issuer.
if (auto const ter = accountSend(
view(),
vaultAccount,
account_,
assetsRecovered,
j_,
WaiveTransferFee::Yes);
!isTesSuccess(ter))
if (auto ter = accountSend(
view(), vaultAccount, account_, assets, j_, WaiveTransferFee::Yes))
return ter;
// Sanity check
if (accountHolds(
view(),
vaultAccount,
assetsRecovered.asset(),
assets.asset(),
FreezeHandling::fhIGNORE_FREEZE,
AuthHandling::ahIGNORE_AUTH,
j_) < beast::zero)

View File

@@ -25,10 +25,8 @@
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
@@ -86,16 +84,6 @@ VaultCreate::preflight(PreflightContext const& ctx)
return temMALFORMED;
}
if (auto const scale = ctx.tx[~sfScale])
{
auto const vaultAsset = ctx.tx[sfAsset];
if (vaultAsset.holds<MPTIssue>() || vaultAsset.native())
return temMALFORMED;
if (scale > vaultMaximumIOUScale)
return temMALFORMED;
}
return preflight2(ctx);
}
@@ -109,8 +97,8 @@ VaultCreate::calculateBaseFee(ReadView const& view, STTx const& tx)
TER
VaultCreate::preclaim(PreclaimContext const& ctx)
{
auto const vaultAsset = ctx.tx[sfAsset];
auto const account = ctx.tx[sfAccount];
auto vaultAsset = ctx.tx[sfAsset];
auto account = ctx.tx[sfAccount];
if (vaultAsset.native())
; // No special checks for XRP
@@ -160,7 +148,7 @@ VaultCreate::preclaim(PreclaimContext const& ctx)
return tecOBJECT_NOT_FOUND;
}
auto const sequence = ctx.tx.getSeqValue();
auto sequence = ctx.tx.getSeqValue();
if (auto const accountId = pseudoAccountAddress(
ctx.view, keylet::vault(account, sequence).key);
accountId == beast::zero)
@@ -177,8 +165,8 @@ VaultCreate::doApply()
// we can consider downgrading them to `tef` or `tem`.
auto const& tx = ctx_.tx;
auto const sequence = tx.getSeqValue();
auto const owner = view().peek(keylet::account(account_));
auto sequence = tx.getSeqValue();
auto owner = view().peek(keylet::account(account_));
if (owner == nullptr)
return tefINTERNAL; // LCOV_EXCL_LINE
@@ -202,10 +190,6 @@ VaultCreate::doApply()
!isTesSuccess(ter))
return ter;
std::uint8_t const scale = (asset.holds<MPTIssue>() || asset.native())
? 0
: ctx_.tx[~sfScale].value_or(vaultDefaultIOUScale);
auto txFlags = tx.getFlags();
std::uint32_t mptFlags = 0;
if ((txFlags & tfVaultShareNonTransferable) == 0)
@@ -225,13 +209,12 @@ VaultCreate::doApply()
.account = pseudoId->value(),
.sequence = 1,
.flags = mptFlags,
.assetScale = scale,
.metadata = tx[~sfMPTokenMetadata],
.domainId = tx[~sfDomainID],
});
if (!maybeShare)
return maybeShare.error(); // LCOV_EXCL_LINE
auto const& mptIssuanceID = *maybeShare;
auto& share = *maybeShare;
vault->setFieldIssue(sfAsset, STIssue{sfAsset, asset});
vault->at(sfFlags) = txFlags & tfVaultPrivate;
@@ -244,7 +227,7 @@ VaultCreate::doApply()
// Leave default values for AssetTotal and AssetAvailable, both zero.
if (auto value = tx[~sfAssetsMaximum])
vault->at(sfAssetsMaximum) = *value;
vault->at(sfShareMPTID) = mptIssuanceID;
vault->at(sfShareMPTID) = share;
if (auto value = tx[~sfData])
vault->at(sfData) = *value;
// Required field, default to vaultStrategyFirstComeFirstServe
@@ -252,31 +235,9 @@ VaultCreate::doApply()
vault->at(sfWithdrawalPolicy) = *value;
else
vault->at(sfWithdrawalPolicy) = vaultStrategyFirstComeFirstServe;
if (scale)
vault->at(sfScale) = scale;
// No `LossUnrealized`.
view().insert(vault);
// Explicitly create MPToken for the vault owner
if (auto const err = authorizeMPToken(
view(), mPriorBalance, mptIssuanceID, account_, ctx_.journal);
!isTesSuccess(err))
return err;
// If the vault is private, set the authorized flag for the vault owner
if (txFlags & tfVaultPrivate)
{
if (auto const err = authorizeMPToken(
view(),
mPriorBalance,
mptIssuanceID,
pseudoId,
ctx_.journal,
{},
account_);
!isTesSuccess(err))
return err;
}
return tesSUCCESS;
}

View File

@@ -21,7 +21,6 @@
#include <xrpld/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
@@ -129,8 +128,7 @@ VaultDelete::doApply()
// Destroy the share issuance. Do not use MPTokenIssuanceDestroy for this,
// no special logic needed. First run few checks, duplicated from preclaim.
auto const shareMPTID = *vault->at(sfShareMPTID);
auto const mpt = view().peek(keylet::mptIssuance(shareMPTID));
auto const mpt = view().peek(keylet::mptIssuance(vault->at(sfShareMPTID)));
if (!mpt)
{
// LCOV_EXCL_START
@@ -139,24 +137,6 @@ VaultDelete::doApply()
// LCOV_EXCL_STOP
}
// Try to remove MPToken for vault shares for the vault owner if it exists.
if (auto const mptoken = view().peek(keylet::mptoken(shareMPTID, account_)))
{
if (auto const ter =
removeEmptyHolding(view(), account_, MPTIssue(shareMPTID), j_);
!isTesSuccess(ter))
{
// LCOV_EXCL_START
JLOG(j_.error()) //
<< "VaultDelete: failed to remove vault owner's MPToken"
<< " MPTID=" << to_string(shareMPTID) //
<< " account=" << toBase58(account_) //
<< " with result: " << transToken(ter);
return ter;
// LCOV_EXCL_STOP
}
}
if (!view().dirRemove(
keylet::ownerDir(pseudoID), (*mpt)[sfOwnerNode], mpt->key(), false))
{

View File

@@ -26,7 +26,6 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
@@ -139,7 +138,7 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
if (isFrozen(ctx.view, account, vaultShare))
return tecLOCKED;
if (vault->isFlag(lsfVaultPrivate) && account != vault->at(sfOwner))
if (vault->isFlag(tfVaultPrivate) && account != vault->at(sfOwner))
{
auto const maybeDomainID = sleIssuance->at(~sfDomainID);
// Since this is a private vault and the account is not its owner, we
@@ -184,7 +183,7 @@ VaultDeposit::doApply()
if (!vault)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const amount = ctx_.tx[sfAmount];
auto const assets = ctx_.tx[sfAmount];
// Make sure the depositor can hold shares.
auto const mptIssuanceID = (*vault)[sfShareMPTID];
auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID));
@@ -198,14 +197,14 @@ VaultDeposit::doApply()
auto const& vaultAccount = vault->at(sfAccount);
// Note, vault owner is always authorized
if (vault->isFlag(lsfVaultPrivate) && account_ != vault->at(sfOwner))
if ((vault->getFlags() & tfVaultPrivate) && account_ != vault->at(sfOwner))
{
if (auto const err = enforceMPTokenAuthorization(
ctx_.view(), mptIssuanceID, account_, mPriorBalance, j_);
!isTesSuccess(err))
return err;
}
else // !vault->isFlag(lsfVaultPrivate) || account_ == vault->at(sfOwner)
else
{
// No authorization needed, but must ensure there is MPToken
auto sleMpt = view().read(keylet::mptoken(mptIssuanceID, account_));
@@ -222,12 +221,8 @@ VaultDeposit::doApply()
}
// If the vault is private, set the authorized flag for the vault owner
if (vault->isFlag(lsfVaultPrivate))
if (vault->isFlag(tfVaultPrivate))
{
// This follows from the reverse of the outer enclosing if condition
XRPL_ASSERT(
account_ == vault->at(sfOwner),
"ripple::VaultDeposit::doApply : account is owner");
if (auto const err = authorizeMPToken(
view(),
mPriorBalance, // priorBalance
@@ -242,52 +237,14 @@ VaultDeposit::doApply()
}
}
STAmount sharesCreated = {vault->at(sfShareMPTID)}, assetsDeposited;
try
{
// Compute exchange before transferring any amounts.
{
auto const maybeShares =
assetsToSharesDeposit(vault, sleIssuance, amount);
if (!maybeShares)
return tecINTERNAL; // LCOV_EXCL_LINE
sharesCreated = *maybeShares;
}
if (sharesCreated == beast::zero)
return tecPRECISION_LOSS;
auto const maybeAssets =
sharesToAssetsDeposit(vault, sleIssuance, sharesCreated);
if (!maybeAssets)
return tecINTERNAL; // LCOV_EXCL_LINE
else if (*maybeAssets > amount)
{
// LCOV_EXCL_START
JLOG(j_.error()) << "VaultDeposit: would take more than offered.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
assetsDeposited = *maybeAssets;
}
catch (std::overflow_error const&)
{
// It's easy to hit this exception from Number with large enough Scale
// so we avoid spamming the log and only use debug here.
JLOG(j_.debug()) //
<< "VaultDeposit: overflow error with"
<< " scale=" << (int)vault->at(sfScale).value() //
<< ", assetsTotal=" << vault->at(sfAssetsTotal).value()
<< ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
<< ", amount=" << amount;
return tecPATH_DRY;
}
// Compute exchange before transferring any amounts.
auto const shares = assetsToSharesDeposit(vault, sleIssuance, assets);
XRPL_ASSERT(
sharesCreated.asset() != assetsDeposited.asset(),
shares.asset() != assets.asset(),
"ripple::VaultDeposit::doApply : assets are not shares");
vault->at(sfAssetsTotal) += assetsDeposited;
vault->at(sfAssetsAvailable) += assetsDeposited;
vault->at(sfAssetsTotal) += assets;
vault->at(sfAssetsAvailable) += assets;
view().update(vault);
// A deposit must not push the vault over its limit.
@@ -296,21 +253,15 @@ VaultDeposit::doApply()
return tecLIMIT_EXCEEDED;
// Transfer assets from depositor to vault.
if (auto const ter = accountSend(
view(),
account_,
vaultAccount,
assetsDeposited,
j_,
WaiveTransferFee::Yes);
!isTesSuccess(ter))
if (auto ter = accountSend(
view(), account_, vaultAccount, assets, j_, WaiveTransferFee::Yes))
return ter;
// Sanity check
if (accountHolds(
view(),
account_,
assetsDeposited.asset(),
assets.asset(),
FreezeHandling::fhIGNORE_FREEZE,
AuthHandling::ahIGNORE_AUTH,
j_) < beast::zero)
@@ -322,14 +273,8 @@ VaultDeposit::doApply()
}
// Transfer shares from vault to depositor.
if (auto const ter = accountSend(
view(),
vaultAccount,
account_,
sharesCreated,
j_,
WaiveTransferFee::Yes);
!isTesSuccess(ter))
if (auto ter = accountSend(
view(), vaultAccount, account_, shares, j_, WaiveTransferFee::Yes))
return ter;
return tesSUCCESS;

View File

@@ -108,7 +108,7 @@ VaultSet::preclaim(PreclaimContext const& ctx)
if (auto const domain = ctx.tx[~sfDomainID])
{
// We can only set domain if private flag was originally set
if (!vault->isFlag(lsfVaultPrivate))
if ((vault->getFlags() & tfVaultPrivate) == 0)
{
JLOG(ctx.j.debug()) << "VaultSet: vault is not private";
return tecNO_PERMISSION;
@@ -175,9 +175,9 @@ VaultSet::doApply()
{
if (*domainId != beast::zero)
{
// In VaultSet::preclaim we enforce that lsfVaultPrivate must have
// In VaultSet::preclaim we enforce that tfVaultPrivate must have
// been set in the vault. We currently do not support making such a
// vault public (i.e. removal of lsfVaultPrivate flag). The
// vault public (i.e. removal of tfVaultPrivate flag). The
// sfDomainID flag must be set in the MPTokenIssuance object and can
// be freely updated.
sleIssuance->setFieldH256(sfDomainID, *domainId);

View File

@@ -177,7 +177,7 @@ VaultWithdraw::doApply()
if (!vault)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const mptIssuanceID = *((*vault)[sfShareMPTID]);
auto const mptIssuanceID = (*vault)[sfShareMPTID];
auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
{
@@ -192,57 +192,24 @@ VaultWithdraw::doApply()
// to deposit into it, and this means you are also indefinitely authorized
// to withdraw from it.
auto const amount = ctx_.tx[sfAmount];
Asset const vaultAsset = vault->at(sfAsset);
MPTIssue const share{mptIssuanceID};
STAmount sharesRedeemed = {share};
STAmount assetsWithdrawn;
try
auto amount = ctx_.tx[sfAmount];
auto const asset = vault->at(sfAsset);
auto const share = MPTIssue(mptIssuanceID);
STAmount shares, assets;
if (amount.asset() == asset)
{
if (amount.asset() == vaultAsset)
{
// Fixed assets, variable shares.
{
auto const maybeShares =
assetsToSharesWithdraw(vault, sleIssuance, amount);
if (!maybeShares)
return tecINTERNAL; // LCOV_EXCL_LINE
sharesRedeemed = *maybeShares;
}
if (sharesRedeemed == beast::zero)
return tecPRECISION_LOSS;
auto const maybeAssets =
sharesToAssetsWithdraw(vault, sleIssuance, sharesRedeemed);
if (!maybeAssets)
return tecINTERNAL; // LCOV_EXCL_LINE
assetsWithdrawn = *maybeAssets;
}
else if (amount.asset() == share)
{
// Fixed shares, variable assets.
sharesRedeemed = amount;
auto const maybeAssets =
sharesToAssetsWithdraw(vault, sleIssuance, sharesRedeemed);
if (!maybeAssets)
return tecINTERNAL; // LCOV_EXCL_LINE
assetsWithdrawn = *maybeAssets;
}
else
return tefINTERNAL; // LCOV_EXCL_LINE
// Fixed assets, variable shares.
assets = amount;
shares = assetsToSharesWithdraw(vault, sleIssuance, assets);
}
catch (std::overflow_error const&)
else if (amount.asset() == share)
{
// It's easy to hit this exception from Number with large enough Scale
// so we avoid spamming the log and only use debug here.
JLOG(j_.debug()) //
<< "VaultWithdraw: overflow error with"
<< " scale=" << (int)vault->at(sfScale).value() //
<< ", assetsTotal=" << vault->at(sfAssetsTotal).value()
<< ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
<< ", amount=" << amount.value();
return tecPATH_DRY;
// Fixed shares, variable assets.
shares = amount;
assets = sharesToAssetsWithdraw(vault, sleIssuance, shares);
}
else
return tefINTERNAL; // LCOV_EXCL_LINE
if (accountHolds(
view(),
@@ -250,72 +217,31 @@ VaultWithdraw::doApply()
share,
FreezeHandling::fhZERO_IF_FROZEN,
AuthHandling::ahIGNORE_AUTH,
j_) < sharesRedeemed)
j_) < shares)
{
JLOG(j_.debug()) << "VaultWithdraw: account doesn't hold enough shares";
return tecINSUFFICIENT_FUNDS;
}
auto assetsAvailable = vault->at(sfAssetsAvailable);
auto assetsTotal = vault->at(sfAssetsTotal);
[[maybe_unused]] auto const lossUnrealized = vault->at(sfLossUnrealized);
XRPL_ASSERT(
lossUnrealized <= (assetsTotal - assetsAvailable),
"ripple::VaultWithdraw::doApply : loss and assets do balance");
// The vault must have enough assets on hand. The vault may hold assets
// that it has already pledged. That is why we look at AssetAvailable
// instead of the pseudo-account balance.
if (*assetsAvailable < assetsWithdrawn)
// The vault must have enough assets on hand. The vault may hold assets that
// it has already pledged. That is why we look at AssetAvailable instead of
// the pseudo-account balance.
if (*vault->at(sfAssetsAvailable) < assets)
{
JLOG(j_.debug()) << "VaultWithdraw: vault doesn't hold enough assets";
return tecINSUFFICIENT_FUNDS;
}
assetsTotal -= assetsWithdrawn;
assetsAvailable -= assetsWithdrawn;
vault->at(sfAssetsTotal) -= assets;
vault->at(sfAssetsAvailable) -= assets;
view().update(vault);
auto const& vaultAccount = vault->at(sfAccount);
// Transfer shares from depositor to vault.
if (auto const ter = accountSend(
view(),
account_,
vaultAccount,
sharesRedeemed,
j_,
WaiveTransferFee::Yes);
!isTesSuccess(ter))
if (auto ter = accountSend(
view(), account_, vaultAccount, shares, j_, WaiveTransferFee::Yes))
return ter;
// Try to remove MPToken for shares, if the account balance is zero. Vault
// pseudo-account will never set lsfMPTAuthorized, so we ignore flags.
// Keep MPToken if holder is the vault owner.
if (account_ != vault->at(sfOwner))
{
if (auto const ter = removeEmptyHolding(
view(), account_, sharesRedeemed.asset(), j_);
isTesSuccess(ter))
{
JLOG(j_.debug()) //
<< "VaultWithdraw: removed empty MPToken for vault shares"
<< " MPTID=" << to_string(mptIssuanceID) //
<< " account=" << toBase58(account_);
}
else if (ter != tecHAS_OBLIGATIONS)
{
// LCOV_EXCL_START
JLOG(j_.error()) //
<< "VaultWithdraw: failed to remove MPToken for vault shares"
<< " MPTID=" << to_string(mptIssuanceID) //
<< " account=" << toBase58(account_) //
<< " with result: " << transToken(ter);
return ter;
// LCOV_EXCL_STOP
}
// else quietly ignore, account balance is not zero
}
auto const dstAcct = [&]() -> AccountID {
if (ctx_.tx.isFieldPresent(sfDestination))
return ctx_.tx.getAccountID(sfDestination);
@@ -323,21 +249,15 @@ VaultWithdraw::doApply()
}();
// Transfer assets from vault to depositor or destination account.
if (auto const ter = accountSend(
view(),
vaultAccount,
dstAcct,
assetsWithdrawn,
j_,
WaiveTransferFee::Yes);
!isTesSuccess(ter))
if (auto ter = accountSend(
view(), vaultAccount, dstAcct, assets, j_, WaiveTransferFee::Yes))
return ter;
// Sanity check
if (accountHolds(
view(),
vaultAccount,
assetsWithdrawn.asset(),
assets.asset(),
FreezeHandling::fhIGNORE_FREEZE,
AuthHandling::ahIGNORE_AUTH,
j_) < beast::zero)

View File

@@ -62,9 +62,6 @@
#include <xrpld/app/tx/detail/SetRegularKey.h>
#include <xrpld/app/tx/detail/SetSignerList.h>
#include <xrpld/app/tx/detail/SetTrust.h>
#include <xrpld/app/tx/detail/SubscriptionCancel.h>
#include <xrpld/app/tx/detail/SubscriptionClaim.h>
#include <xrpld/app/tx/detail/SubscriptionSet.h>
#include <xrpld/app/tx/detail/VaultClawback.h>
#include <xrpld/app/tx/detail/VaultCreate.h>
#include <xrpld/app/tx/detail/VaultDelete.h>

View File

@@ -912,41 +912,28 @@ deleteAMMTrustLine(
std::optional<AccountID> const& ammAccountID,
beast::Journal j);
// From the perspective of a vault, return the number of shares to give the
// depositor when they deposit a fixed amount of assets. Since shares are MPT
// this number is integral and always truncated in this calculation.
[[nodiscard]] std::optional<STAmount>
// From the perspective of a vault,
// return the number of shares to give the depositor
// when they deposit a fixed amount of assets.
[[nodiscard]] STAmount
assetsToSharesDeposit(
std::shared_ptr<SLE const> const& vault,
std::shared_ptr<SLE const> const& issuance,
STAmount const& assets);
// From the perspective of a vault, return the number of assets to take from
// depositor when they receive a fixed amount of shares. Note, since shares are
// MPT, they are always an integral number.
[[nodiscard]] std::optional<STAmount>
sharesToAssetsDeposit(
std::shared_ptr<SLE const> const& vault,
std::shared_ptr<SLE const> const& issuance,
STAmount const& shares);
enum class TruncateShares : bool { no = false, yes = true };
// From the perspective of a vault, return the number of shares to demand from
// the depositor when they ask to withdraw a fixed amount of assets. Since
// shares are MPT this number is integral, and it will be rounded to nearest
// unless explicitly requested to be truncated instead.
[[nodiscard]] std::optional<STAmount>
// From the perspective of a vault,
// return the number of shares to demand from the depositor
// when they ask to withdraw a fixed amount of assets.
[[nodiscard]] STAmount
assetsToSharesWithdraw(
std::shared_ptr<SLE const> const& vault,
std::shared_ptr<SLE const> const& issuance,
STAmount const& assets,
TruncateShares truncate = TruncateShares::no);
STAmount const& assets);
// From the perspective of a vault, return the number of assets to give the
// depositor when they redeem a fixed amount of shares. Note, since shares are
// MPT, they are always an integral number.
[[nodiscard]] std::optional<STAmount>
// From the perspective of a vault,
// return the number of assets to give the depositor
// when they redeem a fixed amount of shares.
[[nodiscard]] STAmount
sharesToAssetsWithdraw(
std::shared_ptr<SLE const> const& vault,
std::shared_ptr<SLE const> const& issuance,

View File

@@ -2793,113 +2793,58 @@ rippleCredit(
saAmount.asset().value());
}
[[nodiscard]] std::optional<STAmount>
[[nodiscard]] STAmount
assetsToSharesDeposit(
std::shared_ptr<SLE const> const& vault,
std::shared_ptr<SLE const> const& issuance,
STAmount const& assets)
{
XRPL_ASSERT(
!assets.negative(),
"ripple::assetsToSharesDeposit : non-negative assets");
XRPL_ASSERT(
assets.asset() == vault->at(sfAsset),
"ripple::assetsToSharesDeposit : assets and vault match");
if (assets.negative() || assets.asset() != vault->at(sfAsset))
return std::nullopt; // LCOV_EXCL_LINE
Number const assetTotal = vault->at(sfAssetsTotal);
STAmount shares{vault->at(sfShareMPTID)};
Number assetTotal = vault->at(sfAssetsTotal);
STAmount shares{vault->at(sfShareMPTID), static_cast<Number>(assets)};
if (assetTotal == 0)
return STAmount{
shares.asset(),
Number(assets.mantissa(), assets.exponent() + vault->at(sfScale))
.truncate()};
Number const shareTotal = issuance->at(sfOutstandingAmount);
shares = (shareTotal * (assets / assetTotal)).truncate();
return shares;
Number shareTotal = issuance->at(sfOutstandingAmount);
shares = shareTotal * (assets / assetTotal);
return shares;
}
[[nodiscard]] std::optional<STAmount>
sharesToAssetsDeposit(
std::shared_ptr<SLE const> const& vault,
std::shared_ptr<SLE const> const& issuance,
STAmount const& shares)
{
XRPL_ASSERT(
!shares.negative(),
"ripple::sharesToAssetsDeposit : non-negative shares");
XRPL_ASSERT(
shares.asset() == vault->at(sfShareMPTID),
"ripple::sharesToAssetsDeposit : shares and vault match");
if (shares.negative() || shares.asset() != vault->at(sfShareMPTID))
return std::nullopt; // LCOV_EXCL_LINE
Number const assetTotal = vault->at(sfAssetsTotal);
STAmount assets{vault->at(sfAsset)};
if (assetTotal == 0)
return STAmount{
assets.asset(),
shares.mantissa(),
shares.exponent() - vault->at(sfScale),
false};
Number const shareTotal = issuance->at(sfOutstandingAmount);
assets = assetTotal * (shares / shareTotal);
return assets;
}
[[nodiscard]] std::optional<STAmount>
[[nodiscard]] STAmount
assetsToSharesWithdraw(
std::shared_ptr<SLE const> const& vault,
std::shared_ptr<SLE const> const& issuance,
STAmount const& assets,
TruncateShares truncate)
STAmount const& assets)
{
XRPL_ASSERT(
!assets.negative(),
"ripple::assetsToSharesDeposit : non-negative assets");
XRPL_ASSERT(
assets.asset() == vault->at(sfAsset),
"ripple::assetsToSharesWithdraw : assets and vault match");
if (assets.negative() || assets.asset() != vault->at(sfAsset))
return std::nullopt; // LCOV_EXCL_LINE
Number assetTotal = vault->at(sfAssetsTotal);
assetTotal -= vault->at(sfLossUnrealized);
STAmount shares{vault->at(sfShareMPTID)};
if (assetTotal == 0)
return shares;
Number const shareTotal = issuance->at(sfOutstandingAmount);
Number result = shareTotal * (assets / assetTotal);
if (truncate == TruncateShares::yes)
result = result.truncate();
shares = result;
Number shareTotal = issuance->at(sfOutstandingAmount);
shares = shareTotal * (assets / assetTotal);
return shares;
}
[[nodiscard]] std::optional<STAmount>
[[nodiscard]] STAmount
sharesToAssetsWithdraw(
std::shared_ptr<SLE const> const& vault,
std::shared_ptr<SLE const> const& issuance,
STAmount const& shares)
{
XRPL_ASSERT(
!shares.negative(),
"ripple::sharesToAssetsDeposit : non-negative shares");
XRPL_ASSERT(
shares.asset() == vault->at(sfShareMPTID),
"ripple::sharesToAssetsWithdraw : shares and vault match");
if (shares.negative() || shares.asset() != vault->at(sfShareMPTID))
return std::nullopt; // LCOV_EXCL_LINE
Number assetTotal = vault->at(sfAssetsTotal);
assetTotal -= vault->at(sfLossUnrealized);
STAmount assets{vault->at(sfAsset)};
if (assetTotal == 0)
return assets;
Number const shareTotal = issuance->at(sfOutstandingAmount);
Number shareTotal = issuance->at(sfOutstandingAmount);
assets = assetTotal * (shares / shareTotal);
return assets;
}
@@ -3129,6 +3074,7 @@ rippleUnlockEscrowMPT(
auto const delta = amount.mpt().value();
// Underflow check for subtraction
// LCOV_EXCL_START
if (!canSubtract(STAmount(mptIssue, locked), STAmount(mptIssue, delta)))
{ // LCOV_EXCL_START
JLOG(j.error())

View File

@@ -53,6 +53,14 @@ public:
virtual std::string
getName() = 0;
/** Get the block size for backends that support it
*/
virtual std::optional<std::size_t>
getBlockSize() const
{
return std::nullopt;
}
/** Open the backend.
@param createIfMissing Create the database files if necessary.
This allows the caller to catch exceptions.

View File

@@ -24,6 +24,7 @@
#include <xrpld/nodestore/detail/codec.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/core/LexicalCast.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <boost/filesystem.hpp>
@@ -52,6 +53,7 @@ public:
size_t const keyBytes_;
std::size_t const burstSize_;
std::string const name_;
std::size_t const blockSize_;
nudb::store db_;
std::atomic<bool> deletePath_;
Scheduler& scheduler_;
@@ -66,6 +68,7 @@ public:
, keyBytes_(keyBytes)
, burstSize_(burstSize)
, name_(get(keyValues, "path"))
, blockSize_(parseBlockSize(name_, keyValues, journal))
, deletePath_(false)
, scheduler_(scheduler)
{
@@ -85,6 +88,7 @@ public:
, keyBytes_(keyBytes)
, burstSize_(burstSize)
, name_(get(keyValues, "path"))
, blockSize_(parseBlockSize(name_, keyValues, journal))
, db_(context)
, deletePath_(false)
, scheduler_(scheduler)
@@ -114,6 +118,12 @@ public:
return name_;
}
std::optional<std::size_t>
getBlockSize() const override
{
return blockSize_;
}
void
open(bool createIfMissing, uint64_t appType, uint64_t uid, uint64_t salt)
override
@@ -143,7 +153,7 @@ public:
uid,
salt,
keyBytes_,
nudb::block_size(kp),
blockSize_,
0.50,
ec);
if (ec == nudb::errc::file_exists)
@@ -359,6 +369,56 @@ public:
{
return 3;
}
private:
static std::size_t
parseBlockSize(
std::string const& name,
Section const& keyValues,
beast::Journal journal)
{
using namespace boost::filesystem;
auto const folder = path(name);
auto const kp = (folder / "nudb.key").string();
std::size_t const defaultSize =
nudb::block_size(kp); // Default 4K from NuDB
std::size_t blockSize = defaultSize;
std::string blockSizeStr;
if (!get_if_exists(keyValues, "nudb_block_size", blockSizeStr))
{
return blockSize; // Early return with default
}
try
{
std::size_t const parsedBlockSize =
beast::lexicalCastThrow<std::size_t>(blockSizeStr);
// Validate: must be power of 2 between 4K and 32K
if (parsedBlockSize < 4096 || parsedBlockSize > 32768 ||
(parsedBlockSize & (parsedBlockSize - 1)) != 0)
{
std::stringstream s;
s << "Invalid nudb_block_size: " << parsedBlockSize
<< ". Must be power of 2 between 4096 and 32768.";
Throw<std::runtime_error>(s.str());
}
JLOG(journal.info())
<< "Using custom NuDB block size: " << parsedBlockSize
<< " bytes";
return parsedBlockSize;
}
catch (std::exception const& e)
{
std::stringstream s;
s << "Invalid nudb_block_size value: " << blockSizeStr
<< ". Error: " << e.what();
Throw<std::runtime_error>(s.str());
}
}
};
//------------------------------------------------------------------------------

View File

@@ -195,16 +195,14 @@ OverlayImpl::onHandoff(
if (consumer.disconnect(journal))
return handoff;
auto const [slot, result] = m_peerFinder->new_inbound_slot(
auto const slot = m_peerFinder->new_inbound_slot(
beast::IPAddressConversion::from_asio(local_endpoint),
beast::IPAddressConversion::from_asio(remote_endpoint));
if (slot == nullptr)
{
// connection refused either IP limit exceeded or self-connect
// self-connect, close
handoff.moved = false;
JLOG(journal.debug())
<< "Peer " << remote_endpoint << " refused, " << to_string(result);
return handoff;
}
@@ -404,11 +402,10 @@ OverlayImpl::connect(beast::IP::Endpoint const& remote_endpoint)
return;
}
auto const [slot, result] = peerFinder().new_outbound_slot(remote_endpoint);
auto const slot = peerFinder().new_outbound_slot(remote_endpoint);
if (slot == nullptr)
{
JLOG(journal_.debug()) << "Connect: No slot for " << remote_endpoint
<< ": " << to_string(result);
JLOG(journal_.debug()) << "Connect: No slot for " << remote_endpoint;
return;
}

View File

@@ -109,9 +109,6 @@ struct Config
std::uint16_t port,
bool validationPublicKey,
int ipLimit);
friend bool
operator==(Config const& lhs, Config const& rhs);
};
//------------------------------------------------------------------------------
@@ -139,13 +136,7 @@ using Endpoints = std::vector<Endpoint>;
//------------------------------------------------------------------------------
/** Possible results from activating a slot. */
enum class Result {
inboundDisabled,
duplicatePeer,
ipLimitExceeded,
full,
success
};
enum class Result { duplicate, full, success };
/**
* @brief Converts a `Result` enum value to its string representation.
@@ -166,16 +157,12 @@ to_string(Result result) noexcept
{
switch (result)
{
case Result::inboundDisabled:
return "inbound disabled";
case Result::duplicatePeer:
return "peer already connected";
case Result::ipLimitExceeded:
return "ip limit exceeded";
case Result::full:
return "slots full";
case Result::success:
return "success";
case Result::duplicate:
return "duplicate connection";
case Result::full:
return "slots full";
}
return "unknown";
@@ -247,7 +234,7 @@ public:
If nullptr is returned, then the slot could not be assigned.
Usually this is because of a detected self-connection.
*/
virtual std::pair<std::shared_ptr<Slot>, Result>
virtual std::shared_ptr<Slot>
new_inbound_slot(
beast::IP::Endpoint const& local_endpoint,
beast::IP::Endpoint const& remote_endpoint) = 0;
@@ -256,7 +243,7 @@ public:
If nullptr is returned, then the slot could not be assigned.
Usually this is because of a duplicate connection.
*/
virtual std::pair<std::shared_ptr<Slot>, Result>
virtual std::shared_ptr<Slot>
new_outbound_slot(beast::IP::Endpoint const& remote_endpoint) = 0;
/** Called when mtENDPOINTS is received. */

View File

@@ -163,7 +163,7 @@ public:
/** Returns the total number of inbound slots. */
int
in_max() const
inboundSlots() const
{
return m_in_max;
}

View File

@@ -172,7 +172,9 @@ public:
void
addFixedPeer(std::string const& name, beast::IP::Endpoint const& ep)
{
addFixedPeer(name, std::vector<beast::IP::Endpoint>{ep});
std::vector<beast::IP::Endpoint> v;
v.push_back(ep);
addFixedPeer(name, v);
}
void
@@ -259,7 +261,7 @@ public:
//--------------------------------------------------------------------------
std::pair<SlotImp::ptr, Result>
SlotImp::ptr
new_inbound_slot(
beast::IP::Endpoint const& local_endpoint,
beast::IP::Endpoint const& remote_endpoint)
@@ -275,12 +277,12 @@ public:
{
auto const count =
connectedAddresses_.count(remote_endpoint.address());
if (count + 1 > config_.ipLimit)
if (count > config_.ipLimit)
{
JLOG(m_journal.debug())
<< beast::leftw(18) << "Logic dropping inbound "
<< remote_endpoint << " because of ip limits.";
return {SlotImp::ptr(), Result::ipLimitExceeded};
return SlotImp::ptr();
}
}
@@ -290,7 +292,7 @@ public:
JLOG(m_journal.debug())
<< beast::leftw(18) << "Logic dropping " << remote_endpoint
<< " as duplicate incoming";
return {SlotImp::ptr(), Result::duplicatePeer};
return SlotImp::ptr();
}
// Create the slot
@@ -312,11 +314,11 @@ public:
// Update counts
counts_.add(*slot);
return {result.first->second, Result::success};
return result.first->second;
}
// Can't check for self-connect because we don't know the local endpoint
std::pair<SlotImp::ptr, Result>
SlotImp::ptr
new_outbound_slot(beast::IP::Endpoint const& remote_endpoint)
{
JLOG(m_journal.debug())
@@ -330,7 +332,7 @@ public:
JLOG(m_journal.debug())
<< beast::leftw(18) << "Logic dropping " << remote_endpoint
<< " as duplicate connect";
return {SlotImp::ptr(), Result::duplicatePeer};
return SlotImp::ptr();
}
// Create the slot
@@ -351,7 +353,7 @@ public:
// Update counts
counts_.add(*slot);
return {result.first->second, Result::success};
return result.first->second;
}
bool
@@ -415,7 +417,7 @@ public:
// Check for duplicate connection by key
if (keys_.find(key) != keys_.end())
return Result::duplicatePeer;
return Result::duplicate;
// If the peer belongs to a cluster or is reserved,
// update the slot to reflect that.
@@ -428,8 +430,6 @@ public:
{
if (!slot->inbound())
bootcache_.on_success(slot->remote_endpoint());
if (slot->inbound() && counts_.in_max() == 0)
return Result::inboundDisabled;
return Result::full;
}
@@ -651,7 +651,7 @@ public:
// 2. We have slots
// 3. We haven't failed the firewalled test
//
if (config_.wantIncoming && counts_.in_max() > 0)
if (config_.wantIncoming && counts_.inboundSlots() > 0)
{
Endpoint ep;
ep.hops = 0;

View File

@@ -34,17 +34,6 @@ Config::Config()
{
}
bool
operator==(Config const& lhs, Config const& rhs)
{
return lhs.autoConnect == rhs.autoConnect &&
lhs.peerPrivate == rhs.peerPrivate &&
lhs.wantIncoming == rhs.wantIncoming && lhs.inPeers == rhs.inPeers &&
lhs.maxPeers == rhs.maxPeers && lhs.outPeers == rhs.outPeers &&
lhs.features == lhs.features && lhs.ipLimit == rhs.ipLimit &&
lhs.listeningPort == rhs.listeningPort;
}
std::size_t
Config::calcOutPeers() const
{

View File

@@ -125,7 +125,7 @@ public:
//--------------------------------------------------------------------------
std::pair<std::shared_ptr<Slot>, Result>
std::shared_ptr<Slot>
new_inbound_slot(
beast::IP::Endpoint const& local_endpoint,
beast::IP::Endpoint const& remote_endpoint) override
@@ -133,7 +133,7 @@ public:
return m_logic.new_inbound_slot(local_endpoint, remote_endpoint);
}
std::pair<std::shared_ptr<Slot>, Result>
std::shared_ptr<Slot>
new_outbound_slot(beast::IP::Endpoint const& remote_endpoint) override
{
return m_logic.new_outbound_slot(remote_endpoint);

View File

@@ -190,7 +190,7 @@ getAccountObjects(
auto& jvObjects = (jvResult[jss::account_objects] = Json::arrayValue);
// this is a mutable version of limit, used to seamlessly switch
// this is a mutable version of limit, used to seemlessly switch
// to iterating directory entries when nftokenpages are exhausted
uint32_t mlimit = limit;
@@ -373,7 +373,7 @@ ledgerFromRequest(T& ledger, JsonContext& context)
indexValue = legacyLedger;
}
if (!hashValue.isNull())
if (hashValue)
{
if (!hashValue.isString())
return {rpcINVALID_PARAMS, "ledgerHashNotString"};
@@ -384,9 +384,6 @@ ledgerFromRequest(T& ledger, JsonContext& context)
return getLedger(ledger, ledgerHash, context);
}
if (!indexValue.isConvertibleTo(Json::stringValue))
return {rpcINVALID_PARAMS, "ledgerIndexMalformed"};
auto const index = indexValue.asString();
if (index == "current" || index.empty())
@@ -398,11 +395,11 @@ ledgerFromRequest(T& ledger, JsonContext& context)
if (index == "closed")
return getLedger(ledger, LedgerShortcut::CLOSED, context);
std::uint32_t val;
if (!beast::lexicalCastChecked(val, index))
return {rpcINVALID_PARAMS, "ledgerIndexMalformed"};
std::uint32_t iVal;
if (beast::lexicalCastChecked(iVal, index))
return getLedger(ledger, iVal, context);
return getLedger(ledger, val, context);
return {rpcINVALID_PARAMS, "ledgerIndexMalformed"};
}
} // namespace
@@ -589,7 +586,7 @@ getLedger(T& ledger, LedgerShortcut shortcut, Context& context)
return Status::OK;
}
// Explicit instantiation of above three functions
// Explicit instantiaion of above three functions
template Status
getLedger<>(std::shared_ptr<ReadView const>&, uint32_t, Context&);

File diff suppressed because it is too large Load Diff

View File

@@ -1,299 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012-2025 Ripple Labs Inc.
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 <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/core/LexicalCast.h>
#include <xrpl/json/json_errors.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/STXChainBridge.h>
#include <xrpl/protocol/jss.h>
#include <functional>
namespace ripple {
namespace LedgerEntryHelpers {
Unexpected<Json::Value>
missingFieldError(
Json::StaticString const field,
std::optional<std::string> err = std::nullopt)
{
Json::Value json = Json::objectValue;
auto error = RPC::missing_field_message(std::string(field.c_str()));
json[jss::error] = err.value_or("malformedRequest");
json[jss::error_code] = rpcINVALID_PARAMS;
json[jss::error_message] = std::move(error);
return Unexpected(json);
}
Unexpected<Json::Value>
invalidFieldError(
std::string const& err,
Json::StaticString const field,
std::string const& type)
{
Json::Value json = Json::objectValue;
auto error = RPC::expected_field_message(field, type);
json[jss::error] = err;
json[jss::error_code] = rpcINVALID_PARAMS;
json[jss::error_message] = std::move(error);
return Unexpected(json);
}
Unexpected<Json::Value>
malformedError(std::string const& err, std::string const& message)
{
Json::Value json = Json::objectValue;
json[jss::error] = err;
json[jss::error_code] = rpcINVALID_PARAMS;
json[jss::error_message] = message;
return Unexpected(json);
}
Expected<bool, Json::Value>
hasRequired(
Json::Value const& params,
std::initializer_list<Json::StaticString> fields,
std::optional<std::string> err = std::nullopt)
{
for (auto const field : fields)
{
if (!params.isMember(field) || params[field].isNull())
{
return missingFieldError(field, err);
}
}
return true;
}
template <class T>
std::optional<T>
parse(Json::Value const& param);
template <class T>
Expected<T, Json::Value>
required(
Json::Value const& params,
Json::StaticString const fieldName,
std::string const& err,
std::string const& expectedType)
{
if (!params.isMember(fieldName) || params[fieldName].isNull())
{
return missingFieldError(fieldName);
}
if (auto obj = parse<T>(params[fieldName]))
{
return *obj;
}
return invalidFieldError(err, fieldName, expectedType);
}
template <>
std::optional<AccountID>
parse(Json::Value const& param)
{
if (!param.isString())
return std::nullopt;
auto const account = parseBase58<AccountID>(param.asString());
if (!account || account->isZero())
{
return std::nullopt;
}
return account;
}
Expected<AccountID, Json::Value>
requiredAccountID(
Json::Value const& params,
Json::StaticString const fieldName,
std::string const& err)
{
return required<AccountID>(params, fieldName, err, "AccountID");
}
std::optional<Blob>
parseHexBlob(Json::Value const& param, std::size_t maxLength)
{
if (!param.isString())
return std::nullopt;
auto const blob = strUnHex(param.asString());
if (!blob || blob->empty() || blob->size() > maxLength)
return std::nullopt;
return blob;
}
Expected<Blob, Json::Value>
requiredHexBlob(
Json::Value const& params,
Json::StaticString const fieldName,
std::size_t maxLength,
std::string const& err)
{
if (!params.isMember(fieldName) || params[fieldName].isNull())
{
return missingFieldError(fieldName);
}
if (auto blob = parseHexBlob(params[fieldName], maxLength))
{
return *blob;
}
return invalidFieldError(err, fieldName, "hex string");
}
template <>
std::optional<std::uint32_t>
parse(Json::Value const& param)
{
if (param.isUInt() || (param.isInt() && param.asInt() >= 0))
return param.asUInt();
if (param.isString())
{
std::uint32_t v;
if (beast::lexicalCastChecked(v, param.asString()))
return v;
}
return std::nullopt;
}
Expected<std::uint32_t, Json::Value>
requiredUInt32(
Json::Value const& params,
Json::StaticString const fieldName,
std::string const& err)
{
return required<std::uint32_t>(params, fieldName, err, "number");
}
template <>
std::optional<uint256>
parse(Json::Value const& param)
{
uint256 uNodeIndex;
if (!param.isString() || !uNodeIndex.parseHex(param.asString()))
{
return std::nullopt;
}
return uNodeIndex;
}
Expected<uint256, Json::Value>
requiredUInt256(
Json::Value const& params,
Json::StaticString const fieldName,
std::string const& err)
{
return required<uint256>(params, fieldName, err, "Hash256");
}
template <>
std::optional<uint192>
parse(Json::Value const& param)
{
uint192 field;
if (!param.isString() || !field.parseHex(param.asString()))
{
return std::nullopt;
}
return field;
}
Expected<uint192, Json::Value>
requiredUInt192(
Json::Value const& params,
Json::StaticString const fieldName,
std::string const& err)
{
return required<uint192>(params, fieldName, err, "Hash192");
}
Expected<STXChainBridge, Json::Value>
parseBridgeFields(Json::Value const& params)
{
if (auto const value = hasRequired(
params,
{jss::LockingChainDoor,
jss::LockingChainIssue,
jss::IssuingChainDoor,
jss::IssuingChainIssue});
!value)
{
return Unexpected(value.error());
}
auto const lockingChainDoor = requiredAccountID(
params, jss::LockingChainDoor, "malformedLockingChainDoor");
if (!lockingChainDoor)
{
return Unexpected(lockingChainDoor.error());
}
auto const issuingChainDoor = requiredAccountID(
params, jss::IssuingChainDoor, "malformedIssuingChainDoor");
if (!issuingChainDoor)
{
return Unexpected(issuingChainDoor.error());
}
Issue lockingChainIssue;
try
{
lockingChainIssue = issueFromJson(params[jss::LockingChainIssue]);
}
catch (std::runtime_error const& ex)
{
return invalidFieldError(
"malformedIssue", jss::LockingChainIssue, "Issue");
}
Issue issuingChainIssue;
try
{
issuingChainIssue = issueFromJson(params[jss::IssuingChainIssue]);
}
catch (std::runtime_error const& ex)
{
return invalidFieldError(
"malformedIssue", jss::IssuingChainIssue, "Issue");
}
return STXChainBridge(
*lockingChainDoor,
lockingChainIssue,
*issuingChainDoor,
issuingChainIssue);
}
} // namespace LedgerEntryHelpers
} // namespace ripple