mirror of
https://github.com/Xahau/xahaud.git
synced 2025-11-09 13:15:49 +00:00
Compare commits
27 Commits
add-clang-
...
cronjob
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccf3b38fad | ||
|
|
86a9ea999f | ||
|
|
0755fb186a | ||
|
|
526154b97a | ||
|
|
052135a800 | ||
|
|
793249d031 | ||
|
|
c1e011a16a | ||
|
|
caa486f19c | ||
|
|
106abfa9e0 | ||
|
|
88828bbf63 | ||
|
|
cf3db6eb42 | ||
|
|
9b1f3ebdd7 | ||
|
|
e64692fc8b | ||
|
|
ac1bf88596 | ||
|
|
0d7dd0597d | ||
|
|
9c8b005406 | ||
|
|
687ccf4203 | ||
|
|
83f09fd8ab | ||
|
|
3dddb907c2 | ||
|
|
9d6ea9ac60 | ||
|
|
e9a414cff2 | ||
|
|
fe1b424bea | ||
|
|
b1c366761f | ||
|
|
82af6d9eee | ||
|
|
20e6e62660 | ||
|
|
1d6066127c | ||
|
|
d3d5c757fe |
46
.github/actions/xahau-ga-build/action.yml
vendored
46
.github/actions/xahau-ga-build/action.yml
vendored
@@ -21,7 +21,7 @@ inputs:
|
||||
required: false
|
||||
default: ''
|
||||
compiler-id:
|
||||
description: 'Unique identifier for compiler/version combination used for cache keys'
|
||||
description: 'Unique identifier: compiler-version-stdlib[-gccversion] (e.g. clang-14-libstdcxx-gcc11, gcc-13-libstdcxx)'
|
||||
required: false
|
||||
default: ''
|
||||
cache_version:
|
||||
@@ -36,6 +36,17 @@ inputs:
|
||||
description: 'Main branch name for restore keys'
|
||||
required: false
|
||||
default: 'dev'
|
||||
stdlib:
|
||||
description: 'C++ standard library to use'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- libstdcxx
|
||||
- libcxx
|
||||
clang_gcc_toolchain:
|
||||
description: 'GCC version to use for Clang toolchain (e.g. 11, 13)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
@@ -93,6 +104,38 @@ runs:
|
||||
CCACHE_ARGS="-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache"
|
||||
fi
|
||||
|
||||
# Configure C++ standard library if specified
|
||||
# libstdcxx used for clang-14/16 to work around missing lexicographical_compare_three_way in libc++
|
||||
# libcxx can be used with clang-17+ which has full C++20 support
|
||||
# Note: -stdlib flag is Clang-specific, GCC always uses libstdc++
|
||||
CMAKE_CXX_FLAGS=""
|
||||
if [[ "${{ inputs.cxx }}" == clang* ]]; then
|
||||
# Only Clang needs the -stdlib flag
|
||||
if [ "${{ inputs.stdlib }}" = "libstdcxx" ]; then
|
||||
CMAKE_CXX_FLAGS="-stdlib=libstdc++"
|
||||
elif [ "${{ inputs.stdlib }}" = "libcxx" ]; then
|
||||
CMAKE_CXX_FLAGS="-stdlib=libc++"
|
||||
fi
|
||||
fi
|
||||
# GCC always uses libstdc++ and doesn't need/support the -stdlib flag
|
||||
|
||||
# Configure GCC toolchain for Clang if specified
|
||||
if [ -n "${{ inputs.clang_gcc_toolchain }}" ] && [[ "${{ inputs.cxx }}" == clang* ]]; then
|
||||
# Extract Clang version from compiler executable name (e.g., clang++-14 -> 14)
|
||||
clang_version=$(echo "${{ inputs.cxx }}" | grep -oE '[0-9]+$')
|
||||
|
||||
# Clang 16+ supports --gcc-install-dir (precise path specification)
|
||||
# Clang <16 only has --gcc-toolchain (uses discovery heuristics)
|
||||
if [ -n "$clang_version" ] && [ "$clang_version" -ge "16" ]; then
|
||||
# Clang 16+ uses --gcc-install-dir (canonical, precise)
|
||||
CMAKE_CXX_FLAGS="$CMAKE_CXX_FLAGS --gcc-install-dir=/usr/lib/gcc/x86_64-linux-gnu/${{ inputs.clang_gcc_toolchain }}"
|
||||
else
|
||||
# Clang 14-15 uses --gcc-toolchain (deprecated but necessary)
|
||||
# Note: This still uses discovery, so we hide newer GCC versions in the workflow
|
||||
CMAKE_CXX_FLAGS="$CMAKE_CXX_FLAGS --gcc-toolchain=/usr"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run CMake configure
|
||||
# Note: conanfile.py hardcodes 'build/generators' as the output path.
|
||||
# If we're in a 'build' folder, Conan detects this and uses just 'generators/'
|
||||
@@ -101,6 +144,7 @@ runs:
|
||||
cmake .. \
|
||||
-G "${{ inputs.generator }}" \
|
||||
$CCACHE_ARGS \
|
||||
${CMAKE_CXX_FLAGS:+-DCMAKE_CXX_FLAGS="$CMAKE_CXX_FLAGS"} \
|
||||
-DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake \
|
||||
-DCMAKE_BUILD_TYPE=${{ inputs.configuration }}
|
||||
|
||||
|
||||
10
.github/actions/xahau-ga-dependencies/action.yml
vendored
10
.github/actions/xahau-ga-dependencies/action.yml
vendored
@@ -10,7 +10,7 @@ inputs:
|
||||
required: false
|
||||
default: '.build'
|
||||
compiler-id:
|
||||
description: 'Unique identifier for compiler/version combination used for cache keys'
|
||||
description: 'Unique identifier: compiler-version-stdlib[-gccversion] (e.g. clang-14-libstdcxx-gcc11, gcc-13-libstdcxx)'
|
||||
required: false
|
||||
default: ''
|
||||
cache_version:
|
||||
@@ -25,6 +25,13 @@ inputs:
|
||||
description: 'Main branch name for restore keys'
|
||||
required: false
|
||||
default: 'dev'
|
||||
stdlib:
|
||||
description: 'C++ standard library for Conan configuration (note: also in compiler-id)'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- libstdcxx
|
||||
- libcxx
|
||||
|
||||
outputs:
|
||||
cache-hit:
|
||||
@@ -70,6 +77,7 @@ runs:
|
||||
path: |
|
||||
~/.conan
|
||||
~/.conan2
|
||||
# Note: compiler-id format is compiler-version-stdlib[-gccversion]
|
||||
key: ${{ runner.os }}-conan-v${{ inputs.cache_version }}-${{ inputs.compiler-id }}-${{ hashFiles('**/conanfile.txt', '**/conanfile.py') }}-${{ inputs.configuration }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-conan-v${{ inputs.cache_version }}-${{ inputs.compiler-id }}-${{ hashFiles('**/conanfile.txt', '**/conanfile.py') }}-
|
||||
|
||||
223
.github/workflows/xahau-ga-nix.yml
vendored
223
.github/workflows/xahau-ga-nix.yml
vendored
@@ -13,21 +13,146 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-job:
|
||||
matrix-setup:
|
||||
runs-on: ubuntu-latest
|
||||
container: python:3-slim
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Generate build matrix
|
||||
id: set-matrix
|
||||
shell: python
|
||||
run: |
|
||||
import json
|
||||
import os
|
||||
|
||||
# Full matrix with all 6 compiler configurations
|
||||
# Each configuration includes all parameters needed by the build job
|
||||
full_matrix = [
|
||||
{
|
||||
"compiler_id": "gcc-11-libstdcxx",
|
||||
"compiler": "gcc",
|
||||
"cc": "gcc-11",
|
||||
"cxx": "g++-11",
|
||||
"compiler_version": 11,
|
||||
"stdlib": "libstdcxx",
|
||||
"configuration": "Debug"
|
||||
},
|
||||
{
|
||||
"compiler_id": "gcc-13-libstdcxx",
|
||||
"compiler": "gcc",
|
||||
"cc": "gcc-13",
|
||||
"cxx": "g++-13",
|
||||
"compiler_version": 13,
|
||||
"stdlib": "libstdcxx",
|
||||
"configuration": "Debug"
|
||||
},
|
||||
{
|
||||
"compiler_id": "clang-14-libstdcxx-gcc11",
|
||||
"compiler": "clang",
|
||||
"cc": "clang-14",
|
||||
"cxx": "clang++-14",
|
||||
"compiler_version": 14,
|
||||
"stdlib": "libstdcxx",
|
||||
"clang_gcc_toolchain": 11,
|
||||
"configuration": "Debug"
|
||||
},
|
||||
{
|
||||
"compiler_id": "clang-16-libstdcxx-gcc13",
|
||||
"compiler": "clang",
|
||||
"cc": "clang-16",
|
||||
"cxx": "clang++-16",
|
||||
"compiler_version": 16,
|
||||
"stdlib": "libstdcxx",
|
||||
"clang_gcc_toolchain": 13,
|
||||
"configuration": "Debug"
|
||||
},
|
||||
{
|
||||
"compiler_id": "clang-17-libcxx",
|
||||
"compiler": "clang",
|
||||
"cc": "clang-17",
|
||||
"cxx": "clang++-17",
|
||||
"compiler_version": 17,
|
||||
"stdlib": "libcxx",
|
||||
"configuration": "Debug"
|
||||
},
|
||||
{
|
||||
# Clang 18 - testing if it's faster than Clang 17 with libc++
|
||||
# Requires patching Conan v1 settings.yml to add version 18
|
||||
"compiler_id": "clang-18-libcxx",
|
||||
"compiler": "clang",
|
||||
"cc": "clang-18",
|
||||
"cxx": "clang++-18",
|
||||
"compiler_version": 18,
|
||||
"stdlib": "libcxx",
|
||||
"configuration": "Debug"
|
||||
}
|
||||
]
|
||||
|
||||
# Minimal matrix for PRs and feature branches
|
||||
minimal_matrix = [
|
||||
full_matrix[1], # gcc-13 (middle-ground gcc)
|
||||
full_matrix[2] # clang-14 (mature, stable clang)
|
||||
]
|
||||
|
||||
# Determine which matrix to use based on the target branch
|
||||
ref = "${{ github.ref }}"
|
||||
base_ref = "${{ github.base_ref }}" # For PRs, this is the target branch
|
||||
event_name = "${{ github.event_name }}"
|
||||
commit_message = """${{ github.event.head_commit.message }}"""
|
||||
pr_title = """${{ github.event.pull_request.title }}"""
|
||||
|
||||
# Debug logging
|
||||
print(f"Event: {event_name}")
|
||||
print(f"Ref: {ref}")
|
||||
print(f"Base ref: {base_ref}")
|
||||
print(f"PR title: {pr_title}")
|
||||
print(f"Commit message: {commit_message}")
|
||||
|
||||
# Check for override tags in commit message or PR title
|
||||
force_full = "[ci-nix-full-matrix]" in commit_message or "[ci-nix-full-matrix]" in pr_title
|
||||
print(f"Force full matrix: {force_full}")
|
||||
|
||||
# Check if this is targeting a main branch
|
||||
# For PRs: check base_ref (target branch)
|
||||
# For pushes: check ref (current branch)
|
||||
main_branches = ["refs/heads/dev", "refs/heads/release", "refs/heads/candidate"]
|
||||
|
||||
if force_full:
|
||||
# Override: always use full matrix if tag is present
|
||||
use_full = True
|
||||
elif event_name == "pull_request":
|
||||
# For PRs, base_ref is just the branch name (e.g., "dev", not "refs/heads/dev")
|
||||
# Check if the PR targets release or candidate (more critical branches)
|
||||
use_full = base_ref in ["release", "candidate"]
|
||||
else:
|
||||
# For pushes, ref is the full reference (e.g., "refs/heads/dev")
|
||||
use_full = ref in main_branches
|
||||
|
||||
# Select the appropriate matrix
|
||||
if use_full:
|
||||
if force_full:
|
||||
print(f"Using FULL matrix (6 configs) - forced by [ci-nix-full-matrix] tag")
|
||||
else:
|
||||
print(f"Using FULL matrix (6 configs) - targeting main branch")
|
||||
matrix = full_matrix
|
||||
else:
|
||||
print(f"Using MINIMAL matrix (2 configs) - feature branch/PR")
|
||||
matrix = minimal_matrix
|
||||
|
||||
# Output the matrix as JSON
|
||||
output = json.dumps({"include": matrix})
|
||||
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
|
||||
f.write(f"matrix={output}\n")
|
||||
|
||||
build:
|
||||
needs: matrix-setup
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
artifact_name: ${{ steps.set-artifact-name.outputs.artifact_name }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
compiler: [gcc]
|
||||
configuration: [Debug]
|
||||
include:
|
||||
- compiler: gcc
|
||||
cc: gcc-13
|
||||
cxx: g++-13
|
||||
compiler_id: gcc-13
|
||||
compiler_version: 13
|
||||
matrix: ${{ fromJSON(needs.matrix-setup.outputs.matrix) }}
|
||||
env:
|
||||
build_dir: .build
|
||||
# Bump this number to invalidate all caches globally.
|
||||
@@ -41,8 +166,70 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ninja-build ${{ matrix.cc }} ${{ matrix.cxx }} ccache
|
||||
|
||||
# Install the specific GCC version needed for Clang
|
||||
if [ -n "${{ matrix.clang_gcc_toolchain }}" ]; then
|
||||
echo "=== Installing GCC ${{ matrix.clang_gcc_toolchain }} for Clang ==="
|
||||
sudo apt-get install -y gcc-${{ matrix.clang_gcc_toolchain }} g++-${{ matrix.clang_gcc_toolchain }} libstdc++-${{ matrix.clang_gcc_toolchain }}-dev
|
||||
|
||||
echo "=== GCC versions available after installation ==="
|
||||
ls -la /usr/lib/gcc/x86_64-linux-gnu/ | grep -E "^d"
|
||||
fi
|
||||
|
||||
# For Clang < 16 with --gcc-toolchain, hide newer GCC versions
|
||||
# This is needed because --gcc-toolchain still picks the highest version
|
||||
#
|
||||
# THE GREAT GCC HIDING TRICK (for Clang < 16):
|
||||
# Clang versions before 16 don't have --gcc-install-dir, only --gcc-toolchain
|
||||
# which is deprecated and still uses discovery heuristics that ALWAYS pick
|
||||
# the highest version number. So we play a sneaky game...
|
||||
#
|
||||
# We rename newer GCC versions to very low integers (1, 2, 3...) which makes
|
||||
# Clang think they're ancient GCC versions. Since 11 > 3 > 2 > 1, Clang will
|
||||
# pick GCC 11 over our renamed versions. It's dumb but it works!
|
||||
#
|
||||
# Example: GCC 12→1, GCC 13→2, GCC 14→3, so Clang picks 11 (highest number)
|
||||
if [ -n "${{ matrix.clang_gcc_toolchain }}" ] && [ "${{ matrix.compiler_version }}" -lt "16" ]; then
|
||||
echo "=== Hiding GCC versions newer than ${{ matrix.clang_gcc_toolchain }} for Clang < 16 ==="
|
||||
target_version=${{ matrix.clang_gcc_toolchain }}
|
||||
counter=1 # Start with 1 - these will be seen as "GCC version 1, 2, 3" etc
|
||||
for dir in /usr/lib/gcc/x86_64-linux-gnu/*/; do
|
||||
if [ -d "$dir" ]; then
|
||||
version=$(basename "$dir")
|
||||
# Check if version is numeric and greater than target
|
||||
if [[ "$version" =~ ^[0-9]+$ ]] && [ "$version" -gt "$target_version" ]; then
|
||||
echo "Hiding GCC $version -> renaming to $counter (will be seen as GCC version $counter)"
|
||||
# Safety check: ensure target doesn't already exist
|
||||
if [ ! -e "/usr/lib/gcc/x86_64-linux-gnu/$counter" ]; then
|
||||
sudo mv "$dir" "/usr/lib/gcc/x86_64-linux-gnu/$counter"
|
||||
else
|
||||
echo "ERROR: Cannot rename GCC $version - /usr/lib/gcc/x86_64-linux-gnu/$counter already exists"
|
||||
exit 1
|
||||
fi
|
||||
counter=$((counter + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Verify what Clang will use
|
||||
if [ -n "${{ matrix.clang_gcc_toolchain }}" ]; then
|
||||
echo "=== Verifying GCC toolchain selection ==="
|
||||
echo "Available GCC versions:"
|
||||
ls -la /usr/lib/gcc/x86_64-linux-gnu/ | grep -E "^d.*[0-9]+$" || true
|
||||
|
||||
echo ""
|
||||
echo "Clang's detected GCC installation:"
|
||||
${{ matrix.cxx }} -v -E -x c++ /dev/null -o /dev/null 2>&1 | grep "Found candidate GCC installation" || true
|
||||
fi
|
||||
|
||||
# Install libc++ dev packages if using libc++ (not needed for libstdc++)
|
||||
if [ "${{ matrix.stdlib }}" = "libcxx" ]; then
|
||||
sudo apt-get install -y libc++-${{ matrix.compiler_version }}-dev libc++abi-${{ matrix.compiler_version }}-dev
|
||||
fi
|
||||
|
||||
# Install Conan 2
|
||||
pip install --upgrade "conan>=2.0"
|
||||
pip install --upgrade "conan>=2.0,<3"
|
||||
|
||||
- name: Configure ccache
|
||||
uses: ./.github/actions/xahau-configure-ccache
|
||||
@@ -57,14 +244,21 @@ jobs:
|
||||
# Create the default profile directory if it doesn't exist
|
||||
mkdir -p ~/.conan2/profiles
|
||||
|
||||
# Determine the correct libcxx based on stdlib parameter
|
||||
if [ "${{ matrix.stdlib }}" = "libcxx" ]; then
|
||||
LIBCXX="libc++"
|
||||
else
|
||||
LIBCXX="libstdc++11"
|
||||
fi
|
||||
|
||||
# Create profile with our specific settings
|
||||
cat > ~/.conan2/profiles/default <<EOF
|
||||
[settings]
|
||||
arch=x86_64
|
||||
build_type=Release
|
||||
build_type=${{ matrix.configuration }}
|
||||
compiler=${{ matrix.compiler }}
|
||||
compiler.cppstd=20
|
||||
compiler.libcxx=libstdc++11
|
||||
compiler.libcxx=${LIBCXX}
|
||||
compiler.version=${{ matrix.compiler_version }}
|
||||
os=Linux
|
||||
|
||||
@@ -99,6 +293,7 @@ jobs:
|
||||
compiler-id: ${{ matrix.compiler_id }}
|
||||
cache_version: ${{ env.CACHE_VERSION }}
|
||||
main_branch: ${{ env.MAIN_BRANCH_NAME }}
|
||||
stdlib: ${{ matrix.stdlib }}
|
||||
|
||||
- name: Build
|
||||
uses: ./.github/actions/xahau-ga-build
|
||||
@@ -111,6 +306,8 @@ jobs:
|
||||
compiler-id: ${{ matrix.compiler_id }}
|
||||
cache_version: ${{ env.CACHE_VERSION }}
|
||||
main_branch: ${{ env.MAIN_BRANCH_NAME }}
|
||||
stdlib: ${{ matrix.stdlib }}
|
||||
clang_gcc_toolchain: ${{ matrix.clang_gcc_toolchain || '' }}
|
||||
|
||||
- name: Set artifact name
|
||||
id: set-artifact-name
|
||||
|
||||
@@ -457,6 +457,7 @@ target_sources (rippled PRIVATE
|
||||
src/ripple/app/tx/impl/CreateCheck.cpp
|
||||
src/ripple/app/tx/impl/CreateOffer.cpp
|
||||
src/ripple/app/tx/impl/CreateTicket.cpp
|
||||
src/ripple/app/tx/impl/Cron.cpp
|
||||
src/ripple/app/tx/impl/DeleteAccount.cpp
|
||||
src/ripple/app/tx/impl/DepositPreauth.cpp
|
||||
src/ripple/app/tx/impl/Escrow.cpp
|
||||
@@ -474,6 +475,7 @@ target_sources (rippled PRIVATE
|
||||
src/ripple/app/tx/impl/Payment.cpp
|
||||
src/ripple/app/tx/impl/Remit.cpp
|
||||
src/ripple/app/tx/impl/SetAccount.cpp
|
||||
src/ripple/app/tx/impl/SetCron.cpp
|
||||
src/ripple/app/tx/impl/SetHook.cpp
|
||||
src/ripple/app/tx/impl/SetRemarks.cpp
|
||||
src/ripple/app/tx/impl/SetRegularKey.cpp
|
||||
@@ -734,6 +736,7 @@ if (tests)
|
||||
src/test/app/BaseFee_test.cpp
|
||||
src/test/app/Check_test.cpp
|
||||
src/test/app/ClaimReward_test.cpp
|
||||
src/test/app/Cron_test.cpp
|
||||
src/test/app/Clawback_test.cpp
|
||||
src/test/app/CrossingLimits_test.cpp
|
||||
src/test/app/DeliverMin_test.cpp
|
||||
@@ -898,6 +901,7 @@ if (tests)
|
||||
src/test/jtx/impl/amount.cpp
|
||||
src/test/jtx/impl/balance.cpp
|
||||
src/test/jtx/impl/check.cpp
|
||||
src/test/jtx/impl/cron.cpp
|
||||
src/test/jtx/impl/delivermin.cpp
|
||||
src/test/jtx/impl/deposit.cpp
|
||||
src/test/jtx/impl/envconfig.cpp
|
||||
|
||||
@@ -75,6 +75,11 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
|
||||
|
||||
switch (tt)
|
||||
{
|
||||
case ttCRON: {
|
||||
ADD_TSH(tx.getAccountID(sfOwner), tshWEAK);
|
||||
break;
|
||||
}
|
||||
|
||||
case ttREMIT: {
|
||||
if (destAcc)
|
||||
ADD_TSH(*destAcc, tshSTRONG);
|
||||
@@ -3577,7 +3582,7 @@ DEFINE_HOOK_FUNCTION(
|
||||
{
|
||||
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
|
||||
<< "]: Transaction preflight failure: "
|
||||
<< preflightResult.ter;
|
||||
<< transHuman(preflightResult.ter);
|
||||
return EMISSION_FAILURE;
|
||||
}
|
||||
|
||||
|
||||
@@ -1477,6 +1477,64 @@ TxQ::accept(Application& app, OpenView& view)
|
||||
}
|
||||
}
|
||||
|
||||
// Inject cron transactions, if any
|
||||
if (view.rules().enabled(featureCron))
|
||||
{
|
||||
uint32_t currentTime =
|
||||
view.parentCloseTime().time_since_epoch().count();
|
||||
uint256 klStart = keylet::cron(0, AccountID(beast::zero)).key;
|
||||
uint256 const klEnd =
|
||||
keylet::cron(currentTime + 1, AccountID(beast::zero)).key;
|
||||
|
||||
std::set<AccountID> cronAccs;
|
||||
|
||||
auto counter = 0;
|
||||
// include max 128 cron txns in the ledger
|
||||
while (++counter < 129 && klStart < klEnd)
|
||||
{
|
||||
std::optional<uint256 const> next = view.succ(klStart, klEnd);
|
||||
if (!next.has_value())
|
||||
break;
|
||||
|
||||
Keylet kl{ltANY, *next};
|
||||
|
||||
if (view.exists(kl))
|
||||
{
|
||||
auto sle = view.read(kl);
|
||||
if (safe_cast<LedgerEntryType>(
|
||||
sle->getFieldU16(sfLedgerEntryType)) == ltCRON)
|
||||
{
|
||||
// valid cron object, add it to the list
|
||||
cronAccs.emplace(sle->getAccountID(sfOwner));
|
||||
}
|
||||
}
|
||||
|
||||
klStart = *next;
|
||||
}
|
||||
|
||||
auto const seq = view.info().seq;
|
||||
|
||||
// insert Cron pseudos for each of the accs we need to ping
|
||||
for (AccountID const& id : cronAccs)
|
||||
{
|
||||
STTx cronTx(ttCRON, [=](auto& obj) {
|
||||
obj[sfAccount] = AccountID();
|
||||
obj[sfLedgerSequence] = seq;
|
||||
obj[sfOwner] = id;
|
||||
});
|
||||
|
||||
uint256 txID = cronTx.getTransactionID();
|
||||
|
||||
auto s = std::make_shared<ripple::Serializer>();
|
||||
cronTx.add(*s);
|
||||
|
||||
app.getHashRouter().setFlags(txID, SF_PRIVATE2);
|
||||
app.getHashRouter().setFlags(txID, SF_EMITTED);
|
||||
view.rawTxInsert(txID, std::move(s), nullptr);
|
||||
ledgerChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Inject emitted transactions if any
|
||||
if (view.rules().enabled(featureHooks))
|
||||
do
|
||||
|
||||
188
src/ripple/app/tx/impl/Cron.cpp
Normal file
188
src/ripple/app/tx/impl/Cron.cpp
Normal file
@@ -0,0 +1,188 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2024 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/app/tx/impl/Cron.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/core/Config.h>
|
||||
#include <ripple/ledger/View.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/PublicKey.h>
|
||||
#include <ripple/protocol/Quality.h>
|
||||
#include <ripple/protocol/TxFlags.h>
|
||||
#include <ripple/protocol/st.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
TxConsequences
|
||||
Cron::makeTxConsequences(PreflightContext const& ctx)
|
||||
{
|
||||
return TxConsequences{ctx.tx, TxConsequences::normal};
|
||||
}
|
||||
|
||||
NotTEC
|
||||
Cron::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureCron))
|
||||
return temDISABLED;
|
||||
|
||||
auto const ret = preflight0(ctx);
|
||||
if (!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
auto account = ctx.tx.getAccountID(sfAccount);
|
||||
if (account != beast::zero)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Cron: Bad source id";
|
||||
return temBAD_SRC_ACCOUNT;
|
||||
}
|
||||
|
||||
// No point in going any further if the transaction fee is malformed.
|
||||
auto const fee = ctx.tx.getFieldAmount(sfFee);
|
||||
if (!fee.native() || fee != beast::zero)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Cron: invalid fee";
|
||||
return temBAD_FEE;
|
||||
}
|
||||
|
||||
if (!ctx.tx.getSigningPubKey().empty() || !ctx.tx.getSignature().empty() ||
|
||||
ctx.tx.isFieldPresent(sfSigners))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Cron: Bad signature";
|
||||
return temBAD_SIGNATURE;
|
||||
}
|
||||
|
||||
if (ctx.tx.getFieldU32(sfSequence) != 0 ||
|
||||
ctx.tx.isFieldPresent(sfPreviousTxnID))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Cron: Bad sequence";
|
||||
return temBAD_SEQUENCE;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
Cron::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
if (!ctx.view.rules().enabled(featureCron))
|
||||
return temDISABLED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
Cron::doApply()
|
||||
{
|
||||
auto& view = ctx_.view();
|
||||
auto const& tx = ctx_.tx;
|
||||
|
||||
AccountID const& id = tx.getAccountID(sfOwner);
|
||||
|
||||
auto sle = view.peek(keylet::account(id));
|
||||
if (!sle)
|
||||
{
|
||||
// should never happen... but might in a race condition with acc delete
|
||||
JLOG(j_.warn()) << "Cron: sfOwner account missing. " << id;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
if (!sle->isFieldPresent(sfCron))
|
||||
{
|
||||
JLOG(j_.warn()) << "Cron: sfCron missing from account " << id;
|
||||
return tefINTERNAL;
|
||||
}
|
||||
|
||||
uint256 ptr = sle->getFieldH256(sfCron);
|
||||
Keylet klOld{ltCRON, ptr};
|
||||
auto sleCron = view.peek(klOld);
|
||||
if (!sleCron)
|
||||
{
|
||||
JLOG(j_.warn()) << "Cron: Cron object missing for account " << id;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
uint32_t delay = sleCron->getFieldU32(sfDelaySeconds);
|
||||
uint32_t recur = sleCron->getFieldU32(sfRepeatCount);
|
||||
|
||||
uint32_t currentTime = view.parentCloseTime().time_since_epoch().count();
|
||||
|
||||
// do all this sanity checking before we modify the ledger...
|
||||
uint32_t afterTime = currentTime + delay;
|
||||
if (afterTime < currentTime)
|
||||
return tefINTERNAL;
|
||||
|
||||
// in all circumstances the Cron object is deleted...
|
||||
// if there are further crons to do then a new one is created at the next
|
||||
// time point
|
||||
|
||||
if (!view.dirRemove(
|
||||
keylet::ownerDir(id), (*sleCron)[sfOwnerNode], klOld, false))
|
||||
return tefBAD_LEDGER;
|
||||
|
||||
view.erase(sleCron);
|
||||
|
||||
if (recur == 0)
|
||||
{
|
||||
// already at last execution, stop here
|
||||
adjustOwnerCount(view, sle, -1, j_);
|
||||
sle->makeFieldAbsent(sfCron);
|
||||
view.update(sle);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// more executions remain, so create a new object
|
||||
|
||||
Keylet klCron = keylet::cron(afterTime, id);
|
||||
|
||||
// insert into owner dir, we don't need to check reserve because we've just
|
||||
// deleted an object
|
||||
auto const page =
|
||||
view.dirInsert(keylet::ownerDir(id), klCron, describeOwnerDir(id));
|
||||
if (!page)
|
||||
return tecDIR_FULL;
|
||||
|
||||
sleCron = std::make_shared<SLE>(klCron);
|
||||
|
||||
sleCron->setFieldU64(sfOwnerNode, *page);
|
||||
sleCron->setFieldU32(sfDelaySeconds, delay);
|
||||
sleCron->setFieldU32(sfRepeatCount, recur - 1);
|
||||
sleCron->setAccountID(sfOwner, id);
|
||||
|
||||
sle->setFieldH256(sfCron, klCron.key);
|
||||
|
||||
view.insert(sleCron);
|
||||
view.update(sle);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
Cron::preCompute()
|
||||
{
|
||||
assert(account_ == beast::zero);
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
Cron::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
return XRPAmount{0};
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
60
src/ripple/app/tx/impl/Cron.h
Normal file
60
src/ripple/app/tx/impl/Cron.h
Normal file
@@ -0,0 +1,60 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2024 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TX_CRON_H_INCLUDED
|
||||
#define RIPPLE_TX_CRON_H_INCLUDED
|
||||
|
||||
#include <ripple/app/tx/impl/Transactor.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/core/Config.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class Cron : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
|
||||
|
||||
explicit Cron(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static XRPAmount
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx);
|
||||
|
||||
static TxConsequences
|
||||
makeTxConsequences(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const&);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
|
||||
void
|
||||
preCompute() override;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -491,6 +491,7 @@ LedgerEntryTypesMatch::visitEntry(
|
||||
case ltNFTOKEN_PAGE:
|
||||
case ltNFTOKEN_OFFER:
|
||||
case ltURI_TOKEN:
|
||||
case ltCRON:
|
||||
case ltIMPORT_VLSEQ:
|
||||
case ltUNL_REPORT:
|
||||
break;
|
||||
|
||||
317
src/ripple/app/tx/impl/SetCron.cpp
Normal file
317
src/ripple/app/tx/impl/SetCron.cpp
Normal file
@@ -0,0 +1,317 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2024 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/app/tx/impl/SetCron.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/ledger/View.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/TxFlags.h>
|
||||
#include <ripple/protocol/st.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
TxConsequences
|
||||
SetCron::makeTxConsequences(PreflightContext const& ctx)
|
||||
{
|
||||
return TxConsequences{ctx.tx, TxConsequences::normal};
|
||||
}
|
||||
|
||||
NotTEC
|
||||
SetCron::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureCron))
|
||||
return temDISABLED;
|
||||
|
||||
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
auto& tx = ctx.tx;
|
||||
auto& j = ctx.j;
|
||||
|
||||
if (tx.getFlags() & tfCronSetMask)
|
||||
{
|
||||
JLOG(j.warn()) << "SetCron: Invalid flags set.";
|
||||
return temINVALID_FLAG;
|
||||
}
|
||||
|
||||
// StartAfter(s) DelaySeconds (D), RepeatCount (R)
|
||||
|
||||
// SDR - Set Cron with After, Delay and Repeat
|
||||
// SD- - Invalid, if repeat count isn't included then only start or delay
|
||||
// S-R - Invalid
|
||||
// S-- - Set Cron with After for a once off execution
|
||||
|
||||
// -DR - Set Cron with Delay and Repeat
|
||||
// -D- - Set Cron (once off) with Delay only (repat implicitly 0)
|
||||
// --R - Invalid
|
||||
// --- - Clear any existing cron (succeeds even if there isn't one) / with
|
||||
// tfCronUnset flag set
|
||||
|
||||
bool const hasStart = tx.isFieldPresent(sfStartAfter);
|
||||
bool const hasDelay = tx.isFieldPresent(sfDelaySeconds);
|
||||
bool const hasRepeat = tx.isFieldPresent(sfRepeatCount);
|
||||
|
||||
// unset is a special case, handle first
|
||||
if (tx.isFlag(tfCronUnset))
|
||||
{
|
||||
if (hasDelay || hasRepeat || hasStart)
|
||||
{
|
||||
JLOG(j.debug()) << "SetCron: tfCronUnset flag cannot be used with "
|
||||
"DelaySeconds or RepeatCount.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
return preflight2(ctx);
|
||||
}
|
||||
|
||||
if (hasStart)
|
||||
{
|
||||
if (hasRepeat && hasDelay)
|
||||
{
|
||||
// valid, this is a fully specified cron
|
||||
// fall through to validate other fields
|
||||
}
|
||||
else if (!hasRepeat && !hasDelay)
|
||||
{
|
||||
// valid this is a once off cron
|
||||
// no other fields to validate, done
|
||||
return preflight2(ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
// invalid, must specify both or neither repeat and delay count with
|
||||
// startafter
|
||||
JLOG(j.debug()) << "SetCron: StartAfter can only be used with "
|
||||
"either both or neither of "
|
||||
"DelaySeconds and RepeatCount.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasDelay)
|
||||
{
|
||||
JLOG(j.debug()) << "SetCron: DelaySeconds or StartAfter must be "
|
||||
"specified to create a cron.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// check delay is not too high
|
||||
auto delay = tx.getFieldU32(sfDelaySeconds);
|
||||
if (delay > 31536000UL /* 365 days in seconds */)
|
||||
{
|
||||
JLOG(j.debug()) << "SetCron: DelaySeconds was too high. (max 365 "
|
||||
"days in seconds).";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// check repeat is not too high
|
||||
if (hasRepeat)
|
||||
{
|
||||
auto recur = tx.getFieldU32(sfRepeatCount);
|
||||
if (recur > 256)
|
||||
{
|
||||
JLOG(j.debug())
|
||||
<< "SetCron: RepeatCount too high. Limit is 256. Issue "
|
||||
"new SetCron to increase.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
|
||||
return preflight2(ctx);
|
||||
}
|
||||
|
||||
TER
|
||||
SetCron::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
if (ctx.tx.isFieldPresent(sfStartAfter))
|
||||
{
|
||||
uint32_t currentTime =
|
||||
ctx.view.parentCloseTime().time_since_epoch().count();
|
||||
uint32_t afterTime = ctx.tx.getFieldU32(sfStartAfter);
|
||||
|
||||
if (afterTime <= currentTime)
|
||||
{
|
||||
// we'll pass this as though they meant execute asap, similar to a
|
||||
// delay of 0
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
uint32_t waitSeconds = afterTime - currentTime;
|
||||
|
||||
if (waitSeconds > afterTime)
|
||||
return tefINTERNAL;
|
||||
|
||||
if (waitSeconds >> 31536000UL /* 365 days in seconds */)
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
<< "SetCron: DelaySeconds was too high. (max 365 "
|
||||
"days in seconds).";
|
||||
return tecSTART_AFTER_TOO_HIGH;
|
||||
}
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
SetCron::doApply()
|
||||
{
|
||||
auto& view = ctx_.view();
|
||||
auto const& tx = ctx_.tx;
|
||||
|
||||
bool const isDelete = tx.isFlag(tfCronUnset);
|
||||
|
||||
// delay can be zero, in which case the cron will usually execute next
|
||||
// ledger.
|
||||
uint32_t delay{0};
|
||||
uint32_t recur{0};
|
||||
uint32_t after{0};
|
||||
|
||||
if (!isDelete)
|
||||
{
|
||||
if (tx.isFieldPresent(sfDelaySeconds))
|
||||
delay = tx.getFieldU32(sfDelaySeconds);
|
||||
if (tx.isFieldPresent(sfRepeatCount))
|
||||
recur = tx.getFieldU32(sfRepeatCount);
|
||||
}
|
||||
|
||||
uint32_t currentTime = view.parentCloseTime().time_since_epoch().count();
|
||||
|
||||
// do all this sanity checking before we modify the ledger...
|
||||
// even for a delete operation this will fall through without incident
|
||||
|
||||
uint32_t afterTime = tx.isFieldPresent(sfStartAfter)
|
||||
? tx.getFieldU32(sfStartAfter)
|
||||
: currentTime + delay;
|
||||
|
||||
if (afterTime < currentTime)
|
||||
return tefINTERNAL;
|
||||
|
||||
AccountID const& id = tx.getAccountID(sfAccount);
|
||||
auto sle = view.peek(keylet::account(id));
|
||||
if (!sle)
|
||||
return tefINTERNAL;
|
||||
|
||||
// in all cases whatsoever, this transaction will delete an existing
|
||||
// old cron object and return the owner reserve to the owner.
|
||||
|
||||
if (sle->isFieldPresent(sfCron))
|
||||
{
|
||||
Keylet klOld{ltCRON, sle->getFieldH256(sfCron)};
|
||||
|
||||
auto sleCron = view.peek(klOld);
|
||||
if (!sleCron)
|
||||
{
|
||||
JLOG(j_.warn()) << "SetCron: Cron object didn't exist.";
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
if (safe_cast<LedgerEntryType>(
|
||||
sleCron->getFieldU16(sfLedgerEntryType)) != ltCRON)
|
||||
{
|
||||
JLOG(j_.warn()) << "SetCron: sfCron pointed to non-cron object!!";
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
if (!view.dirRemove(
|
||||
keylet::ownerDir(id), (*sleCron)[sfOwnerNode], klOld, false))
|
||||
{
|
||||
JLOG(j_.warn()) << "SetCron: Ownerdir bad. " << id;
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
view.erase(sleCron);
|
||||
adjustOwnerCount(view, sle, -1, j_);
|
||||
sle->makeFieldAbsent(sfCron);
|
||||
}
|
||||
|
||||
// if the operation is a delete (no delay or recur specified then stop
|
||||
// here.)
|
||||
if (isDelete)
|
||||
{
|
||||
view.update(sle);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// execution to here means we're creating a new Cron object and adding it to
|
||||
// the user's owner dir
|
||||
|
||||
Keylet klCron = keylet::cron(afterTime, id);
|
||||
|
||||
std::shared_ptr<SLE> sleCron = std::make_shared<SLE>(klCron);
|
||||
|
||||
STAmount const reserve{
|
||||
view.fees().accountReserve(sle->getFieldU32(sfOwnerCount) + 1)};
|
||||
|
||||
STAmount const afterFee =
|
||||
mPriorBalance - ctx_.tx.getFieldAmount(sfFee).xrp();
|
||||
|
||||
if (afterFee > mPriorBalance || afterFee < reserve)
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
|
||||
// add to owner dir
|
||||
auto const page =
|
||||
view.dirInsert(keylet::ownerDir(id), klCron, describeOwnerDir(id));
|
||||
if (!page)
|
||||
return tecDIR_FULL;
|
||||
|
||||
sleCron->setFieldU64(sfOwnerNode, *page);
|
||||
|
||||
adjustOwnerCount(view, sle, 1, j_);
|
||||
|
||||
// set the fields
|
||||
sleCron->setFieldU32(sfDelaySeconds, delay);
|
||||
sleCron->setFieldU32(sfRepeatCount, recur);
|
||||
sleCron->setAccountID(sfOwner, id);
|
||||
|
||||
sle->setFieldH256(sfCron, klCron.key);
|
||||
|
||||
view.update(sle);
|
||||
|
||||
view.insert(sleCron);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
SetCron::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
auto const baseFee = Transactor::calculateBaseFee(view, tx);
|
||||
|
||||
auto const hasRepeat = tx.isFieldPresent(sfRepeatCount);
|
||||
|
||||
if (tx.isFlag(tfCronUnset))
|
||||
// delete cron
|
||||
return baseFee;
|
||||
|
||||
// factor a cost based on the total number of txns expected
|
||||
// for RepeatCount of 0 we have this txn (SetCron) and the
|
||||
// single Cron txn (2). For a RepeatCount of 1 we have this txn,
|
||||
// the first time the cron executes, and the second time (3).
|
||||
uint32_t const additionalExpectedExecutions =
|
||||
hasRepeat ? tx.getFieldU32(sfRepeatCount) + 1 : 1;
|
||||
auto const additionalFee = baseFee * additionalExpectedExecutions;
|
||||
|
||||
if (baseFee + additionalFee < baseFee)
|
||||
return baseFee;
|
||||
|
||||
return baseFee + additionalFee;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
56
src/ripple/app/tx/impl/SetCron.h
Normal file
56
src/ripple/app/tx/impl/SetCron.h
Normal file
@@ -0,0 +1,56 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2024 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TX_SETCRON_H_INCLUDED
|
||||
#define RIPPLE_TX_SETCRON_H_INCLUDED
|
||||
|
||||
#include <ripple/app/tx/impl/Transactor.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class SetCron : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
|
||||
|
||||
explicit SetCron(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static XRPAmount
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx);
|
||||
|
||||
static TxConsequences
|
||||
makeTxConsequences(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const&);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <ripple/app/tx/impl/CreateCheck.h>
|
||||
#include <ripple/app/tx/impl/CreateOffer.h>
|
||||
#include <ripple/app/tx/impl/CreateTicket.h>
|
||||
#include <ripple/app/tx/impl/Cron.h>
|
||||
#include <ripple/app/tx/impl/DeleteAccount.h>
|
||||
#include <ripple/app/tx/impl/DepositPreauth.h>
|
||||
#include <ripple/app/tx/impl/Escrow.h>
|
||||
@@ -43,6 +44,7 @@
|
||||
#include <ripple/app/tx/impl/Payment.h>
|
||||
#include <ripple/app/tx/impl/Remit.h>
|
||||
#include <ripple/app/tx/impl/SetAccount.h>
|
||||
#include <ripple/app/tx/impl/SetCron.h>
|
||||
#include <ripple/app/tx/impl/SetHook.h>
|
||||
#include <ripple/app/tx/impl/SetRegularKey.h>
|
||||
#include <ripple/app/tx/impl/SetRemarks.h>
|
||||
@@ -181,6 +183,10 @@ invoke_preflight(PreflightContext const& ctx)
|
||||
case ttURITOKEN_CREATE_SELL_OFFER:
|
||||
case ttURITOKEN_CANCEL_SELL_OFFER:
|
||||
return invoke_preflight_helper<URIToken>(ctx);
|
||||
case ttCRON_SET:
|
||||
return invoke_preflight_helper<SetCron>(ctx);
|
||||
case ttCRON:
|
||||
return invoke_preflight_helper<Cron>(ctx);
|
||||
default:
|
||||
assert(false);
|
||||
return {temUNKNOWN, TxConsequences{temUNKNOWN}};
|
||||
@@ -306,6 +312,10 @@ invoke_preclaim(PreclaimContext const& ctx)
|
||||
case ttURITOKEN_CREATE_SELL_OFFER:
|
||||
case ttURITOKEN_CANCEL_SELL_OFFER:
|
||||
return invoke_preclaim<URIToken>(ctx);
|
||||
case ttCRON_SET:
|
||||
return invoke_preclaim<SetCron>(ctx);
|
||||
case ttCRON:
|
||||
return invoke_preclaim<Cron>(ctx);
|
||||
default:
|
||||
assert(false);
|
||||
return temUNKNOWN;
|
||||
@@ -393,6 +403,10 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
case ttURITOKEN_CREATE_SELL_OFFER:
|
||||
case ttURITOKEN_CANCEL_SELL_OFFER:
|
||||
return URIToken::calculateBaseFee(view, tx);
|
||||
case ttCRON_SET:
|
||||
return SetCron::calculateBaseFee(view, tx);
|
||||
case ttCRON:
|
||||
return Cron::calculateBaseFee(view, tx);
|
||||
default:
|
||||
return XRPAmount{0};
|
||||
}
|
||||
@@ -586,6 +600,14 @@ invoke_apply(ApplyContext& ctx)
|
||||
URIToken p(ctx);
|
||||
return p();
|
||||
}
|
||||
case ttCRON_SET: {
|
||||
SetCron p(ctx);
|
||||
return p();
|
||||
}
|
||||
case ttCRON: {
|
||||
Cron p(ctx);
|
||||
return p();
|
||||
}
|
||||
default:
|
||||
assert(false);
|
||||
return {temUNKNOWN, false};
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace detail {
|
||||
// Feature.cpp. Because it's only used to reserve storage, and determine how
|
||||
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
|
||||
// the actual number of amendments. A LogicError on startup will verify this.
|
||||
static constexpr std::size_t numFeatures = 86;
|
||||
static constexpr std::size_t numFeatures = 87;
|
||||
|
||||
/** Amendments that this server supports and the default voting behavior.
|
||||
Whether they are enabled depends on the Rules defined in the validated
|
||||
@@ -373,6 +373,7 @@ extern uint256 const fixProvisionalDoubleThreading;
|
||||
extern uint256 const featureClawback;
|
||||
extern uint256 const featureDeepFreeze;
|
||||
extern uint256 const featureIOUIssuerWeakTSH;
|
||||
extern uint256 const featureCron;
|
||||
extern uint256 const fixInvalidTxFlags;
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -297,6 +297,9 @@ import_vlseq(PublicKey const& key) noexcept;
|
||||
Keylet
|
||||
uritoken(AccountID const& issuer, Blob const& uri);
|
||||
|
||||
Keylet
|
||||
cron(uint32_t timestamp, AccountID const& id);
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
// Everything below is deprecated and should be removed in favor of keylets:
|
||||
|
||||
@@ -58,6 +58,12 @@ enum LedgerEntryType : std::uint16_t
|
||||
*/
|
||||
ltACCOUNT_ROOT = 0x0061,
|
||||
|
||||
/** A ledger object representing a scheduled cron execution on an account.
|
||||
|
||||
\sa keylet::cron
|
||||
*/
|
||||
ltCRON = 0x0041,
|
||||
|
||||
/** A ledger object which contains a list of object identifiers.
|
||||
|
||||
\sa keylet::page, keylet::quality, keylet::book, keylet::next and
|
||||
|
||||
@@ -410,6 +410,9 @@ extern SF_UINT32 const sfRewardLgrLast;
|
||||
extern SF_UINT32 const sfFirstNFTokenSequence;
|
||||
extern SF_UINT32 const sfImportSequence;
|
||||
extern SF_UINT32 const sfXahauActivationLgrSeq;
|
||||
extern SF_UINT32 const sfDelaySeconds;
|
||||
extern SF_UINT32 const sfRepeatCount;
|
||||
extern SF_UINT32 const sfStartAfter;
|
||||
|
||||
// 64-bit integers (common)
|
||||
extern SF_UINT64 const sfIndexNext;
|
||||
@@ -486,6 +489,7 @@ extern SF_UINT256 const sfURITokenID;
|
||||
extern SF_UINT256 const sfGovernanceFlags;
|
||||
extern SF_UINT256 const sfGovernanceMarks;
|
||||
extern SF_UINT256 const sfEmittedTxnID;
|
||||
extern SF_UINT256 const sfCron;
|
||||
|
||||
// currency amount (common)
|
||||
extern SF_AMOUNT const sfAmount;
|
||||
|
||||
@@ -343,6 +343,7 @@ enum TECcodes : TERUnderlyingType {
|
||||
tecINSUF_RESERVE_SELLER = 187,
|
||||
tecIMMUTABLE = 188,
|
||||
tecTOO_MANY_REMARKS = 189,
|
||||
tecSTART_AFTER_TOO_HIGH = 190,
|
||||
tecLAST_POSSIBLE_ENTRY = 255,
|
||||
};
|
||||
|
||||
|
||||
@@ -200,6 +200,12 @@ constexpr std::uint32_t const tfImmutable = 1;
|
||||
// Clawback flags:
|
||||
constexpr std::uint32_t const tfClawbackMask = ~tfUniversal;
|
||||
|
||||
// CronSet Flags:
|
||||
enum CronSetFlags : uint32_t {
|
||||
tfCronUnset = 0x00000001,
|
||||
};
|
||||
constexpr std::uint32_t const tfCronSetMask = ~(tfUniversal | tfCronUnset);
|
||||
|
||||
// clang-format on
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -149,6 +149,12 @@ enum TxType : std::uint16_t
|
||||
ttURITOKEN_CREATE_SELL_OFFER = 48,
|
||||
ttURITOKEN_CANCEL_SELL_OFFER = 49,
|
||||
|
||||
/* A pseudo-txn alarm signal for invoking a hook, emitted by validators after alarm set conditions are met */
|
||||
ttCRON = 92,
|
||||
|
||||
/* Sechedule an alarm for later */
|
||||
ttCRON_SET = 93,
|
||||
|
||||
/* A note attaching transactor that allows the owner or issuer (on a object by object basis) to attach remarks */
|
||||
ttREMARKS_SET = 94,
|
||||
|
||||
|
||||
@@ -479,6 +479,7 @@ REGISTER_FEATURE(Clawback, Supported::yes, VoteBehavior::De
|
||||
REGISTER_FIX (fixProvisionalDoubleThreading, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FEATURE(DeepFreeze, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FEATURE(IOUIssuerWeakTSH, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FEATURE(Cron, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX (fixInvalidTxFlags, Supported::yes, VoteBehavior::DefaultYes);
|
||||
|
||||
// The following amendments are obsolete, but must remain supported
|
||||
|
||||
@@ -72,6 +72,7 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
URI_TOKEN = 'U',
|
||||
IMPORT_VLSEQ = 'I',
|
||||
UNL_REPORT = 'R',
|
||||
CRON = 'A',
|
||||
|
||||
// No longer used or supported. Left here to reserve the space
|
||||
// to avoid accidental reuse.
|
||||
@@ -443,6 +444,51 @@ uritoken(AccountID const& issuer, Blob const& uri)
|
||||
LedgerNameSpace::URI_TOKEN, issuer, Slice{uri.data(), uri.size()})};
|
||||
}
|
||||
|
||||
// Constructs an ordered CRON keylet (32 bytes):
|
||||
// [8-byte namespace][4-byte timestamp (big-endian, seconds)][20-byte
|
||||
// AccountID]
|
||||
//
|
||||
// Properties
|
||||
// - Namespacing: first 8 bytes are the most-significant bytes of
|
||||
// indexHash(LedgerNameSpace::CRON).
|
||||
// - Uniqueness (per ts,acct): exactly ONE cron per (timestamp, AccountID).
|
||||
// Insert is upsert (last-write-wins).
|
||||
// This is fine because we only ever allow one cron per account at a time—if
|
||||
// the same (ts,acct) is written, it’s simply an update to that single entry
|
||||
// (idempotent; no duplicate leaves).
|
||||
// - Iteration order: chronological by timestamp (BE), then by raw AccountID
|
||||
// bytes.
|
||||
// NOTE: raw AccountID ordering may bias priority; consider hashing AccountID
|
||||
// for uniform per-timestamp spread.
|
||||
// - Expected accidental prefix collisions (foreign objects sharing the 8-byte
|
||||
// namespace): n / 2^64,
|
||||
// assuming uniform high-64-bit distribution of other objects.
|
||||
// Examples: 100M → ~5.4e-12, 1B → ~5.4e-11, 10B → ~5.4e-10, 100B → ~5.4e-9
|
||||
// (negligible).
|
||||
Keylet
|
||||
cron(uint32_t timestamp, AccountID const& id)
|
||||
{
|
||||
static const uint256 ns = indexHash(LedgerNameSpace::CRON);
|
||||
|
||||
uint8_t h[32];
|
||||
|
||||
// first 8 bytes are the namespacing
|
||||
std::memcpy(h, ns.data(), 8);
|
||||
|
||||
// next 4 bytes are the timestamp in BE
|
||||
h[8] = static_cast<uint8_t>((timestamp >> 24) & 0xFFU);
|
||||
h[9] = static_cast<uint8_t>((timestamp >> 16) & 0xFFU);
|
||||
h[10] = static_cast<uint8_t>((timestamp >> 8) & 0xFFU);
|
||||
h[11] = static_cast<uint8_t>((timestamp >> 0) & 0xFFU);
|
||||
|
||||
const uint256 accHash = indexHash(LedgerNameSpace::CRON, timestamp, id);
|
||||
|
||||
// final 20 bytes are account ID
|
||||
std::memcpy(h + 12, accHash.cdata(), 20);
|
||||
|
||||
return {ltCRON, uint256::fromVoid(h)};
|
||||
}
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -68,6 +68,7 @@ LedgerFormats::LedgerFormats()
|
||||
{sfGovernanceMarks, soeOPTIONAL},
|
||||
{sfAccountIndex, soeOPTIONAL},
|
||||
{sfTouchCount, soeOPTIONAL},
|
||||
{sfCron, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
@@ -366,6 +367,16 @@ LedgerFormats::LedgerFormats()
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::Cron,
|
||||
ltCRON,
|
||||
{
|
||||
{sfOwner, soeREQUIRED},
|
||||
{sfDelaySeconds, soeREQUIRED},
|
||||
{sfRepeatCount, soeREQUIRED},
|
||||
{sfOwnerNode, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
|
||||
@@ -157,6 +157,9 @@ CONSTRUCT_TYPED_SFIELD(sfLockCount, "LockCount", UINT32,
|
||||
|
||||
CONSTRUCT_TYPED_SFIELD(sfFirstNFTokenSequence, "FirstNFTokenSequence", UINT32, 50);
|
||||
|
||||
CONSTRUCT_TYPED_SFIELD(sfStartAfter, "StartAfter", UINT32, 93);
|
||||
CONSTRUCT_TYPED_SFIELD(sfRepeatCount, "RepeatCount", UINT32, 94);
|
||||
CONSTRUCT_TYPED_SFIELD(sfDelaySeconds, "DelaySeconds", UINT32, 95);
|
||||
CONSTRUCT_TYPED_SFIELD(sfXahauActivationLgrSeq, "XahauActivationLgrSeq",UINT32, 96);
|
||||
CONSTRUCT_TYPED_SFIELD(sfImportSequence, "ImportSequence", UINT32, 97);
|
||||
CONSTRUCT_TYPED_SFIELD(sfRewardTime, "RewardTime", UINT32, 98);
|
||||
@@ -239,6 +242,7 @@ CONSTRUCT_TYPED_SFIELD(sfGovernanceFlags, "GovernanceFlags", UINT256,
|
||||
CONSTRUCT_TYPED_SFIELD(sfGovernanceMarks, "GovernanceMarks", UINT256, 98);
|
||||
CONSTRUCT_TYPED_SFIELD(sfEmittedTxnID, "EmittedTxnID", UINT256, 97);
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookCanEmit, "HookCanEmit", UINT256, 96);
|
||||
CONSTRUCT_TYPED_SFIELD(sfCron, "Cron", UINT256, 95);
|
||||
|
||||
// currency amount (common)
|
||||
CONSTRUCT_TYPED_SFIELD(sfAmount, "Amount", AMOUNT, 1);
|
||||
|
||||
@@ -615,7 +615,7 @@ isPseudoTx(STObject const& tx)
|
||||
|
||||
auto tt = safe_cast<TxType>(*t);
|
||||
return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY ||
|
||||
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT;
|
||||
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -94,6 +94,7 @@ transResults()
|
||||
MAKE_ERROR(tecINSUF_RESERVE_SELLER, "The seller of an object has insufficient reserves, and thus cannot complete the sale."),
|
||||
MAKE_ERROR(tecIMMUTABLE, "The remark is marked immutable on the object, and therefore cannot be updated."),
|
||||
MAKE_ERROR(tecTOO_MANY_REMARKS, "The number of remarks on the object would exceed the limit of 32."),
|
||||
MAKE_ERROR(tecSTART_AFTER_TOO_HIGH, "The proposed StartAfter time is greater than one year away."),
|
||||
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
|
||||
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
|
||||
MAKE_ERROR(tefBAD_AUTH, "Transaction's public key is not authorized."),
|
||||
|
||||
@@ -472,6 +472,23 @@ TxFormats::TxFormats()
|
||||
{sfTicketSequence, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::Cron,
|
||||
ttCRON,
|
||||
{
|
||||
{sfOwner, soeREQUIRED},
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::CronSet,
|
||||
ttCRON_SET,
|
||||
{
|
||||
{sfDelaySeconds, soeOPTIONAL},
|
||||
{sfRepeatCount, soeOPTIONAL},
|
||||
{sfStartAfter, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
}
|
||||
|
||||
TxFormats const&
|
||||
|
||||
@@ -51,14 +51,16 @@ JSS(Amendments); // ledger type.
|
||||
JSS(Amount); // in: TransactionSign; field.
|
||||
JSS(Authorize); // field
|
||||
JSS(Blob);
|
||||
JSS(Check); // ledger type.
|
||||
JSS(CheckCancel); // transaction type.
|
||||
JSS(CheckCash); // transaction type.
|
||||
JSS(CheckCreate); // transaction type.
|
||||
JSS(ClaimReward); // transaction type.
|
||||
JSS(Clawback); // transaction type.
|
||||
JSS(ClearFlag); // field.
|
||||
JSS(CreateCode); // field.
|
||||
JSS(Check); // ledger type.
|
||||
JSS(CheckCancel); // transaction type.
|
||||
JSS(CheckCash); // transaction type.
|
||||
JSS(CheckCreate); // transaction type.
|
||||
JSS(ClaimReward); // transaction type.
|
||||
JSS(Clawback); // transaction type.
|
||||
JSS(ClearFlag); // field.
|
||||
JSS(CreateCode); // field.
|
||||
JSS(Cron);
|
||||
JSS(CronSet);
|
||||
JSS(DeliverMin); // in: TransactionSign
|
||||
JSS(DepositPreauth); // transaction and ledger type.
|
||||
JSS(Destination); // in: TransactionSign; field.
|
||||
|
||||
@@ -423,6 +423,7 @@ private:
|
||||
addFlagsToJson<NFTokenMintFlags>(ret, "NFTokenMint");
|
||||
addFlagsToJson<NFTokenCreateOfferFlags>(ret, "NFTokenCreateOffer");
|
||||
addFlagsToJson<ClaimRewardFlags>(ret, "ClaimReward");
|
||||
addFlagsToJson<CronSetFlags>(ret, "CronSet");
|
||||
struct FlagData
|
||||
{
|
||||
std::string name;
|
||||
|
||||
461
src/test/app/Cron_test.cpp
Normal file
461
src/test/app/Cron_test.cpp
Normal file
@@ -0,0 +1,461 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/app/ledger/LedgerMaster.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <test/jtx.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
struct Cron_test : public beast::unit_test::suite
|
||||
{
|
||||
void
|
||||
testEnabled(FeatureBitset features)
|
||||
{
|
||||
testcase("enabled");
|
||||
using namespace jtx;
|
||||
using namespace std::literals::chrono_literals;
|
||||
|
||||
// setup env
|
||||
auto const alice = Account("alice");
|
||||
auto const issuer = Account("issuer");
|
||||
|
||||
for (bool const withCron : {false, true})
|
||||
{
|
||||
auto const amend = withCron ? features : features - featureCron;
|
||||
Env env{*this, amend};
|
||||
|
||||
env.fund(XRP(1000), alice, issuer);
|
||||
env.close();
|
||||
|
||||
auto const expectResult =
|
||||
withCron ? ter(tesSUCCESS) : ter(temDISABLED);
|
||||
|
||||
auto tx = cron::set(alice);
|
||||
// CLAIM
|
||||
env(cron::set(alice), cron::delay(100), fee(XRP(1)), expectResult);
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFee(FeatureBitset features)
|
||||
{
|
||||
testcase("fee");
|
||||
using namespace jtx;
|
||||
using namespace std::literals::chrono_literals;
|
||||
|
||||
auto const alice = Account("alice");
|
||||
Env env{*this, features | featureCron};
|
||||
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
// create with RepeatCount
|
||||
auto expected = baseFee * 2 + baseFee * 256;
|
||||
env(cron::set(alice),
|
||||
cron::delay(356 * 24 * 60 * 60),
|
||||
cron::repeat(256),
|
||||
fee(expected - 1),
|
||||
ter(telINSUF_FEE_P));
|
||||
env.close();
|
||||
|
||||
env(cron::set(alice),
|
||||
cron::delay(356 * 24 * 60 * 60),
|
||||
cron::repeat(256),
|
||||
fee(expected),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// create with no RepeatCount
|
||||
expected = baseFee * 2;
|
||||
env(cron::set(alice),
|
||||
cron::delay(356 * 24 * 60 * 60),
|
||||
fee(expected - 1),
|
||||
ter(telINSUF_FEE_P));
|
||||
env.close();
|
||||
|
||||
env(cron::set(alice),
|
||||
cron::delay(356 * 24 * 60 * 60),
|
||||
fee(expected),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// delete
|
||||
expected = baseFee;
|
||||
env(cron::set(alice),
|
||||
txflags(tfCronUnset),
|
||||
fee(expected - 1),
|
||||
ter(telINSUF_FEE_P));
|
||||
env.close();
|
||||
|
||||
env(cron::set(alice),
|
||||
txflags(tfCronUnset),
|
||||
fee(expected),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testInvalidPreflight(FeatureBitset features)
|
||||
{
|
||||
testcase("invalid preflight");
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals;
|
||||
|
||||
auto const alice = Account("alice");
|
||||
|
||||
test::jtx::Env env{
|
||||
*this, network::makeNetworkConfig(21337), features | featureCron};
|
||||
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// preflight
|
||||
|
||||
// temINVALID_FLAG
|
||||
{
|
||||
env(cron::set(alice), txflags(tfClose), ter(temINVALID_FLAG));
|
||||
env(cron::set(alice),
|
||||
txflags(tfUniversalMask),
|
||||
ter(temINVALID_FLAG));
|
||||
}
|
||||
|
||||
// temMALFORMED
|
||||
{
|
||||
// Invalid both DelaySeconds and RepeatCount are not specified
|
||||
env(cron::set(alice), ter(temMALFORMED));
|
||||
|
||||
// Invalid DelaySeconds and RepeatCount combination
|
||||
// (only RepeatCount specified)
|
||||
env(cron::set(alice), cron::repeat(256), ter(temMALFORMED));
|
||||
|
||||
// Invalid DelaySeconds
|
||||
env(cron::set(alice),
|
||||
cron::delay(365 * 24 * 60 * 60 + 1),
|
||||
cron::repeat(256),
|
||||
ter(temMALFORMED));
|
||||
|
||||
// Invalid RepeatCount
|
||||
env(cron::set(alice),
|
||||
cron::delay(365 * 24 * 60 * 60),
|
||||
cron::repeat(257),
|
||||
ter(temMALFORMED));
|
||||
|
||||
// Invalid tfCronUnset flag
|
||||
env(cron::set(alice),
|
||||
cron::delay(365 * 24 * 60 * 60),
|
||||
txflags(tfCronUnset),
|
||||
ter(temMALFORMED));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testInvalidPreclaim(FeatureBitset features)
|
||||
{
|
||||
testcase("invalid preclaim");
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals;
|
||||
|
||||
auto const alice = Account("alice");
|
||||
Env env{*this, features | featureCron};
|
||||
|
||||
// there is no check in preclaim
|
||||
BEAST_EXPECT(true);
|
||||
}
|
||||
|
||||
void
|
||||
testDoApply(FeatureBitset features)
|
||||
{
|
||||
testcase("doApply");
|
||||
using namespace jtx;
|
||||
using namespace std::literals::chrono_literals;
|
||||
auto const alice = Account("alice");
|
||||
Env env{*this, features | featureCron};
|
||||
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
auto const aliceOwnerCount = ownerCount(env, alice);
|
||||
|
||||
// create cron
|
||||
env(cron::set(alice),
|
||||
cron::delay(356 * 24 * 60 * 60),
|
||||
cron::repeat(256),
|
||||
fee(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// increment owner count
|
||||
BEAST_EXPECT(ownerCount(env, alice) == aliceOwnerCount + 1);
|
||||
|
||||
auto const accSle = env.le(keylet::account(alice.id()));
|
||||
BEAST_EXPECT(accSle);
|
||||
BEAST_EXPECT(accSle->isFieldPresent(sfCron));
|
||||
|
||||
auto const cronKey = keylet::child(accSle->getFieldH256(sfCron));
|
||||
auto const cronSle = env.le(cronKey);
|
||||
BEAST_EXPECT(cronSle);
|
||||
BEAST_EXPECT(
|
||||
cronSle->getFieldU32(sfDelaySeconds) == 356 * 24 * 60 * 60);
|
||||
BEAST_EXPECT(cronSle->getFieldU32(sfRepeatCount) == 256);
|
||||
|
||||
// update cron
|
||||
env(cron::set(alice),
|
||||
cron::delay(100),
|
||||
cron::repeat(10),
|
||||
fee(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// owner count does not change
|
||||
BEAST_EXPECT(ownerCount(env, alice) == aliceOwnerCount + 1);
|
||||
|
||||
auto const accSle2 = env.le(keylet::account(alice.id()));
|
||||
BEAST_EXPECT(accSle2);
|
||||
BEAST_EXPECT(accSle2->isFieldPresent(sfCron));
|
||||
|
||||
// old cron sle is deleted
|
||||
BEAST_EXPECT(!env.le(cronKey));
|
||||
|
||||
auto const cronKey2 = keylet::child(accSle2->getFieldH256(sfCron));
|
||||
auto const cronSle2 = env.le(cronKey2);
|
||||
BEAST_EXPECT(cronSle2);
|
||||
BEAST_EXPECT(cronSle2->getFieldU32(sfDelaySeconds) == 100);
|
||||
BEAST_EXPECT(cronSle2->getFieldU32(sfRepeatCount) == 10);
|
||||
|
||||
// delete cron
|
||||
env(cron::set(alice),
|
||||
fee(XRP(1)),
|
||||
txflags(tfCronUnset),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// owner count decremented
|
||||
BEAST_EXPECT(ownerCount(env, alice) == aliceOwnerCount);
|
||||
|
||||
auto const accSle3 = env.le(keylet::account(alice.id()));
|
||||
BEAST_EXPECT(accSle3);
|
||||
BEAST_EXPECT(!accSle3->isFieldPresent(sfCron));
|
||||
|
||||
// old cron sle is deleted
|
||||
BEAST_EXPECT(!env.le(cronKey2));
|
||||
|
||||
// delete cron without object will succeed
|
||||
env(cron::set(alice),
|
||||
fee(XRP(1)),
|
||||
txflags(tfCronUnset),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testCronExecution(FeatureBitset features)
|
||||
{
|
||||
testcase("cron execution");
|
||||
using namespace jtx;
|
||||
using namespace std::literals::chrono_literals;
|
||||
auto const alice = Account("alice");
|
||||
|
||||
{
|
||||
// test ttCron execution and repeatCount
|
||||
Env env{*this, features | featureCron};
|
||||
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
auto baseTime = env.timeKeeper().now().time_since_epoch().count();
|
||||
|
||||
auto repeatCount = 10;
|
||||
|
||||
env(cron::set(alice),
|
||||
cron::delay(100),
|
||||
cron::repeat(repeatCount),
|
||||
fee(XRP(1)));
|
||||
env.close(10s);
|
||||
|
||||
auto lastCronKeylet =
|
||||
keylet::child(env.le(alice)->getFieldH256(sfCron));
|
||||
|
||||
while (repeatCount >= 0)
|
||||
{
|
||||
// close ledger until 100 seconds has passed
|
||||
while (env.timeKeeper().now().time_since_epoch().count() -
|
||||
baseTime <
|
||||
100)
|
||||
{
|
||||
env.close(10s);
|
||||
auto txns = env.closed()->txs;
|
||||
auto size = std::distance(txns.begin(), txns.end());
|
||||
BEAST_EXPECT(size == 0);
|
||||
}
|
||||
|
||||
// close after 100 seconds passed
|
||||
env.close();
|
||||
|
||||
auto txns = env.closed()->txs;
|
||||
auto size = std::distance(txns.begin(), txns.end());
|
||||
BEAST_EXPECT(size == 1);
|
||||
for (auto it = txns.begin(); it != txns.end(); ++it)
|
||||
{
|
||||
auto const& tx = *it->first;
|
||||
// check pseudo txn format
|
||||
BEAST_EXPECT(tx.getTxnType() == ttCRON);
|
||||
BEAST_EXPECT(tx.getAccountID(sfAccount) == AccountID());
|
||||
BEAST_EXPECT(tx.getAccountID(sfOwner) == alice.id());
|
||||
BEAST_EXPECT(
|
||||
tx.getFieldU32(sfLedgerSequence) ==
|
||||
env.closed()->info().seq);
|
||||
BEAST_EXPECT(tx.getFieldAmount(sfFee) == XRP(0));
|
||||
BEAST_EXPECT(tx.getFieldVL(sfSigningPubKey).size() == 0);
|
||||
|
||||
// check old Cron object is deleted
|
||||
BEAST_EXPECT(!env.le(lastCronKeylet));
|
||||
|
||||
if (repeatCount > 0)
|
||||
{
|
||||
// check new Cron object
|
||||
auto const cronKeylet =
|
||||
keylet::child(env.le(alice)->getFieldH256(sfCron));
|
||||
auto const cronSle = env.le(cronKeylet);
|
||||
BEAST_EXPECT(cronSle);
|
||||
BEAST_EXPECT(
|
||||
cronSle->getFieldU32(sfDelaySeconds) == 100);
|
||||
BEAST_EXPECT(
|
||||
cronSle->getFieldU32(sfRepeatCount) ==
|
||||
--repeatCount);
|
||||
BEAST_EXPECT(
|
||||
cronSle->getAccountID(sfOwner) == alice.id());
|
||||
|
||||
// set new base time
|
||||
baseTime =
|
||||
env.timeKeeper().now().time_since_epoch().count();
|
||||
lastCronKeylet = cronKeylet;
|
||||
}
|
||||
else
|
||||
{
|
||||
// after all executions, the cron object should be
|
||||
// deleted
|
||||
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfCron));
|
||||
BEAST_EXPECT(!env.le(lastCronKeylet));
|
||||
--repeatCount; // decrement for break double loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// test ttCron limit in a ledger
|
||||
Env env{*this, features | featureCron};
|
||||
std::vector<Account> accounts;
|
||||
accounts.reserve(300);
|
||||
for (int i = 0; i < 300; ++i)
|
||||
{
|
||||
auto const& account = accounts.emplace_back(
|
||||
Account("account_" + std::to_string(i)));
|
||||
accounts.emplace_back(account);
|
||||
env.fund(XRP(10000), account);
|
||||
}
|
||||
env.close();
|
||||
|
||||
for (auto const& account : accounts)
|
||||
{
|
||||
env(cron::set(account), cron::delay(0), fee(XRP(1)));
|
||||
}
|
||||
env.close();
|
||||
|
||||
// proceed ledger
|
||||
env.close();
|
||||
{
|
||||
auto const txns = env.closed()->txs;
|
||||
auto size = std::distance(txns.begin(), txns.end());
|
||||
BEAST_EXPECT(size == 128);
|
||||
for (auto it = txns.begin(); it != txns.end(); ++it)
|
||||
{
|
||||
auto const& tx = *it->first;
|
||||
BEAST_EXPECT(tx.getTxnType() == ttCRON);
|
||||
}
|
||||
}
|
||||
|
||||
// proceed ledger
|
||||
env.close();
|
||||
{
|
||||
auto const txns = env.closed()->txs;
|
||||
auto size = std::distance(txns.begin(), txns.end());
|
||||
BEAST_EXPECT(size == 128);
|
||||
for (auto it = txns.begin(); it != txns.end(); ++it)
|
||||
{
|
||||
auto const& tx = *it->first;
|
||||
BEAST_EXPECT(tx.getTxnType() == ttCRON);
|
||||
}
|
||||
}
|
||||
|
||||
// proceed ledger
|
||||
env.close();
|
||||
{
|
||||
auto const txns = env.closed()->txs;
|
||||
auto size = std::distance(txns.begin(), txns.end());
|
||||
BEAST_EXPECT(size == 44);
|
||||
for (auto it = txns.begin(); it != txns.end(); ++it)
|
||||
{
|
||||
auto const& tx = *it->first;
|
||||
BEAST_EXPECT(tx.getTxnType() == ttCRON);
|
||||
}
|
||||
}
|
||||
|
||||
// proceed ledger
|
||||
env.close();
|
||||
{
|
||||
auto const txns = env.closed()->txs;
|
||||
auto size = std::distance(txns.begin(), txns.end());
|
||||
BEAST_EXPECT(size == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithFeats(FeatureBitset features)
|
||||
{
|
||||
testEnabled(features);
|
||||
testFee(features);
|
||||
testInvalidPreflight(features);
|
||||
testInvalidPreclaim(features);
|
||||
testDoApply(features);
|
||||
|
||||
testCronExecution(features);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
auto const sa = supported_amendments();
|
||||
testWithFeats(sa);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Cron, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -1675,7 +1675,6 @@ public:
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
bool const enabled = features[fixInvalidTxFlags];
|
||||
testcase(
|
||||
std::string("SignerListSet flag, fix ") +
|
||||
(withFixInvalidTxFlags ? "enabled" : "disabled"));
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <ripple/app/misc/HashRouter.h>
|
||||
#include <ripple/app/misc/TxQ.h>
|
||||
#include <ripple/app/tx/apply.h>
|
||||
#include <ripple/basics/StringUtilities.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/PayChan.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
@@ -748,10 +749,22 @@ private:
|
||||
jtx::Env& env,
|
||||
int const& expected,
|
||||
uint64_t const& lineno)
|
||||
{
|
||||
auto const hashStr =
|
||||
env.tx()->getJson(JsonOptions::none)[jss::hash].asString();
|
||||
uint256 const txHash = uint256::fromVoid(strUnHex(hashStr)->data());
|
||||
testTSHStrongWeak(env, txHash, expected, lineno);
|
||||
}
|
||||
|
||||
void
|
||||
testTSHStrongWeak(
|
||||
jtx::Env& env,
|
||||
uint256 const& txHash,
|
||||
int const& expected,
|
||||
uint64_t const& lineno)
|
||||
{
|
||||
Json::Value params;
|
||||
params[jss::transaction] =
|
||||
env.tx()->getJson(JsonOptions::none)[jss::hash];
|
||||
params[jss::transaction] = strHex(txHash);
|
||||
auto const jrr = env.rpc("json", "tx", to_string(params));
|
||||
auto const meta = jrr[jss::result][jss::meta];
|
||||
validateTSHStrongWeak(meta, expected, lineno);
|
||||
@@ -6251,6 +6264,103 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
// CronSet
|
||||
// | otxn | tsh | cset |
|
||||
// | A | A | S |
|
||||
void
|
||||
testCronSetTSH(FeatureBitset features)
|
||||
{
|
||||
testcase("cron set tsh");
|
||||
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals;
|
||||
|
||||
// otxn: account
|
||||
// tsh account
|
||||
// w/s: strong
|
||||
for (bool const testStrong : {true, false})
|
||||
{
|
||||
test::jtx::Env env{
|
||||
*this,
|
||||
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
|
||||
features};
|
||||
|
||||
auto const account = Account("alice");
|
||||
env.fund(XRP(1000), account);
|
||||
env.close();
|
||||
|
||||
if (!testStrong)
|
||||
addWeakTSH(env, account);
|
||||
|
||||
// set tsh hook
|
||||
setTSHHook(env, account, testStrong);
|
||||
|
||||
// cron set
|
||||
env(cron::set(account),
|
||||
cron::delay(100),
|
||||
cron::repeat(1),
|
||||
fee(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// verify tsh hook triggered
|
||||
testTSHStrongWeak(env, tshSTRONG, __LINE__);
|
||||
}
|
||||
}
|
||||
|
||||
// | otxn | tsh | cron |
|
||||
// | - | O | W |
|
||||
void
|
||||
testCronTSH(FeatureBitset features)
|
||||
{
|
||||
testcase("cron tsh");
|
||||
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals;
|
||||
|
||||
// otxn: -
|
||||
// tsh owner
|
||||
// w/s: weak
|
||||
for (bool const testStrong : {true, false})
|
||||
{
|
||||
test::jtx::Env env{
|
||||
*this,
|
||||
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
|
||||
features};
|
||||
|
||||
auto const account = Account("alice");
|
||||
env.fund(XRP(1000), account);
|
||||
env.close();
|
||||
|
||||
// cron set
|
||||
env(cron::set(account),
|
||||
cron::delay(100),
|
||||
cron::repeat(1),
|
||||
fee(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
if (!testStrong)
|
||||
addWeakTSH(env, account);
|
||||
|
||||
// set tsh hook
|
||||
setTSHHook(env, account, testStrong);
|
||||
|
||||
// proceed ledger
|
||||
env.close(100s);
|
||||
|
||||
// close ledger
|
||||
env.close();
|
||||
|
||||
// verify tsh hook triggered
|
||||
auto const expected = testStrong ? tshNONE : tshWEAK;
|
||||
auto const txs = env.closed()->txs;
|
||||
BEAST_EXPECT(std::distance(txs.begin(), txs.end()) == 1);
|
||||
auto const tx = txs.begin()->first;
|
||||
BEAST_EXPECT(tx->getTxnType() == ttCRON);
|
||||
testTSHStrongWeak(env, tx->getTransactionID(), expected, __LINE__);
|
||||
}
|
||||
}
|
||||
void
|
||||
testEmissionOrdering(FeatureBitset features)
|
||||
{
|
||||
@@ -6398,6 +6508,8 @@ private:
|
||||
testURITokenCancelSellOfferTSH(features);
|
||||
testURITokenCreateSellOfferTSH(features);
|
||||
testRemitTSH(features);
|
||||
testCronSetTSH(features);
|
||||
testCronTSH(features);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/balance.h>
|
||||
#include <test/jtx/check.h>
|
||||
#include <test/jtx/cron.h>
|
||||
#include <test/jtx/delivermin.h>
|
||||
#include <test/jtx/deposit.h>
|
||||
#include <test/jtx/escrow.h>
|
||||
|
||||
74
src/test/jtx/cron.h
Normal file
74
src/test/jtx/cron.h
Normal file
@@ -0,0 +1,74 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TEST_JTX_CRON_H_INCLUDED
|
||||
#define RIPPLE_TEST_JTX_CRON_H_INCLUDED
|
||||
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
|
||||
/** Cron operations. */
|
||||
namespace cron {
|
||||
|
||||
/** Set a cron. */
|
||||
Json::Value
|
||||
set(jtx::Account const& account);
|
||||
|
||||
/** Sets the optional DelaySeconds on a JTx. */
|
||||
class delay
|
||||
{
|
||||
private:
|
||||
uint32_t delay_;
|
||||
|
||||
public:
|
||||
explicit delay(uint32_t delay) : delay_(delay)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
operator()(Env&, JTx& jtx) const;
|
||||
};
|
||||
|
||||
/** Sets the optional RepeatCount on a JTx. */
|
||||
class repeat
|
||||
{
|
||||
private:
|
||||
uint32_t repeat_;
|
||||
|
||||
public:
|
||||
explicit repeat(uint32_t repeat) : repeat_(repeat)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
operator()(Env&, JTx& jtx) const;
|
||||
};
|
||||
|
||||
} // namespace cron
|
||||
|
||||
} // namespace jtx
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
|
||||
#endif // RIPPLE_TEST_JTX_CRON_H_INCLUDED
|
||||
56
src/test/jtx/impl/cron.cpp
Normal file
56
src/test/jtx/impl/cron.cpp
Normal file
@@ -0,0 +1,56 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <test/jtx/cron.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
|
||||
namespace cron {
|
||||
|
||||
// Set a cron.
|
||||
Json::Value
|
||||
set(jtx::Account const& account)
|
||||
{
|
||||
using namespace jtx;
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::CronSet;
|
||||
jv[jss::Account] = account.human();
|
||||
return jv;
|
||||
}
|
||||
|
||||
void
|
||||
delay::operator()(Env& env, JTx& jt) const
|
||||
{
|
||||
jt.jv[sfDelaySeconds.jsonName] = delay_;
|
||||
}
|
||||
|
||||
void
|
||||
repeat::operator()(Env& env, JTx& jt) const
|
||||
{
|
||||
jt.jv[sfRepeatCount.jsonName] = repeat_;
|
||||
}
|
||||
|
||||
} // namespace cron
|
||||
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
Reference in New Issue
Block a user