Compare commits

..

3 Commits

Author SHA1 Message Date
Jingchen
058b38a488 Merge branch 'develop' into a1q123456/add-levelization-python-script 2026-03-06 11:40:39 +00:00
JCW
f638cbae3e Fixed errors
Signed-off-by: JCW <a1q123456@users.noreply.github.com>
2026-02-17 17:08:37 +00:00
JCW
94737f399a Replace levelization shell script with the python version to optimise the performance 2026-02-17 17:08:03 +00:00
45 changed files with 2860 additions and 2840 deletions

View File

@@ -1,98 +0,0 @@
# Custom CMake command definitions for gersemi formatting.
# These stubs teach gersemi the signatures of project-specific commands
# so it can format their invocations correctly.
function(git_branch branch_val)
endfunction()
function(isolate_headers target A B scope)
endfunction()
function(create_symbolic_link target link)
endfunction()
function(xrpl_add_test name)
endfunction()
macro(exclude_from_default target_)
endmacro()
macro(exclude_if_included target_)
endmacro()
function(target_protobuf_sources target prefix)
set(options APPEND_PATH DESCRIPTORS)
set(oneValueArgs
LANGUAGE
OUT_VAR
EXPORT_MACRO
TARGET
PROTOC_OUT_DIR
PLUGIN
PLUGIN_OPTIONS
PROTOC_EXE
)
set(multiValueArgs
PROTOS
IMPORT_DIRS
GENERATE_EXTENSIONS
PROTOC_OPTIONS
DEPENDENCIES
)
cmake_parse_arguments(
THIS_FUNCTION_PREFIX
"${options}"
"${oneValueArgs}"
"${multiValueArgs}"
${ARGN}
)
endfunction()
function(add_module parent name)
endfunction()
function(target_link_modules parent scope)
endfunction()
function(setup_target_for_coverage_gcovr)
set(options NONE)
set(oneValueArgs BASE_DIRECTORY NAME FORMAT)
set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES)
cmake_parse_arguments(
THIS_FUNCTION_PREFIX
"${options}"
"${oneValueArgs}"
"${multiValueArgs}"
${ARGN}
)
endfunction()
function(add_code_coverage_to_target name scope)
endfunction()
function(verbose_find_path variable name)
set(options
NO_CACHE
REQUIRED
OPTIONAL
NO_DEFAULT_PATH
NO_PACKAGE_ROOT_PATH
NO_CMAKE_PATH
NO_CMAKE_ENVIRONMENT_PATH
NO_SYSTEM_ENVIRONMENT_PATH
NO_CMAKE_SYSTEM_PATH
NO_CMAKE_INSTALL_PREFIX
CMAKE_FIND_ROOT_PATH_BOTH
ONLY_CMAKE_FIND_ROOT_PATH
NO_CMAKE_FIND_ROOT_PATH
)
set(oneValueArgs REGISTRY_VIEW VALIDATOR DOC)
set(multiValueArgs NAMES HINTS PATHS PATH_SUFFIXES)
cmake_parse_arguments(
THIS_FUNCTION_PREFIX
"${options}"
"${oneValueArgs}"
"${multiValueArgs}"
${ARGN}
)
endfunction()

View File

@@ -1 +0,0 @@
definitions: [.gersemi]

View File

@@ -70,7 +70,7 @@ that `test` code should _never_ be included in `xrpl` or `xrpld` code.)
## Validation
The [levelization](generate.sh) script takes no parameters,
The [levelization](generate.py) script takes no parameters,
reads no environment variables, and can be run from any directory,
as long as it is in the expected location in the rippled repo.
It can be run at any time from within a checked out repo, and will
@@ -104,7 +104,7 @@ It generates many files of [results](results):
Github Actions workflow to test that levelization loops haven't
changed. Unfortunately, if changes are detected, it can't tell if
they are improvements or not, so if you have resolved any issues or
done anything else to improve levelization, run `levelization.sh`,
done anything else to improve levelization, run `generate.py`,
and commit the updated results.
The `loops.txt` and `ordering.txt` files relate the modules
@@ -128,7 +128,7 @@ The committed files hide the detailed values intentionally, to
prevent false alarms and merging issues, and because it's easy to
get those details locally.
1. Run `levelization.sh`
1. Run `generate.py`
2. Grep the modules in `paths.txt`.
- For example, if a cycle is found `A ~= B`, simply `grep -w
A .github/scripts/levelization/results/paths.txt | grep -w B`

369
.github/scripts/levelization/generate.py vendored Normal file
View File

@@ -0,0 +1,369 @@
#!/usr/bin/env python3
"""
Usage: generate.py
This script takes no parameters, and can be run from any directory,
as long as it is in the expected.
location in the repo.
"""
import os
import re
import subprocess
import sys
from collections import defaultdict
from pathlib import Path
from typing import Dict, List, Tuple, Set, Optional
# Compile regex patterns once at module level
INCLUDE_PATTERN = re.compile(r"^\s*#include.*/.*\.h")
INCLUDE_PATH_PATTERN = re.compile(r'[<"]([^>"]+)[>"]')
def dictionary_sort_key(s: str) -> str:
"""
Create a sort key that mimics 'sort -d' (dictionary order).
Dictionary order only considers blanks and alphanumeric characters.
This means punctuation like '.' is ignored during sorting.
"""
# Keep only alphanumeric characters and spaces
return "".join(c for c in s if c.isalnum() or c.isspace())
def get_level(file_path: str) -> str:
"""
Extract the level from a file path (second and third directory components).
Equivalent to bash: cut -d/ -f 2,3
Examples:
src/xrpld/app/main.cpp -> xrpld.app
src/libxrpl/protocol/STObject.cpp -> libxrpl.protocol
include/xrpl/basics/base_uint.h -> xrpl.basics
"""
parts = file_path.split("/")
# Get fields 2 and 3 (indices 1 and 2 in 0-based indexing)
if len(parts) >= 3:
level = f"{parts[1]}/{parts[2]}"
elif len(parts) >= 2:
level = f"{parts[1]}/toplevel"
else:
level = file_path
# If the "level" indicates a file, cut off the filename
if "." in level.split("/")[-1]: # Avoid Path object creation
# Use the "toplevel" label as a workaround for `sort`
# inconsistencies between different utility versions
level = level.rsplit("/", 1)[0] + "/toplevel"
return level.replace("/", ".")
def extract_include_level(include_line: str) -> Optional[str]:
"""
Extract the include path from an #include directive.
Gets the first two directory components from the include path.
Equivalent to bash: cut -d/ -f 1,2
Examples:
#include <xrpl/basics/base_uint.h> -> xrpl.basics
#include "xrpld/app/main/Application.h" -> xrpld.app
"""
# Remove everything before the quote or angle bracket
match = INCLUDE_PATH_PATTERN.search(include_line)
if not match:
return None
include_path = match.group(1)
parts = include_path.split("/")
# Get first two fields (indices 0 and 1)
if len(parts) >= 2:
include_level = f"{parts[0]}/{parts[1]}"
else:
include_level = include_path
# If the "includelevel" indicates a file, cut off the filename
if "." in include_level.split("/")[-1]: # Avoid Path object creation
include_level = include_level.rsplit("/", 1)[0] + "/toplevel"
return include_level.replace("/", ".")
def find_repo_root(start_path: Path, depth_limit: int = 10) -> Path:
"""
Find the repository root by looking for .git directory or src/include folders.
Walks up the directory tree from the start path.
"""
current = start_path.resolve()
# Walk up the directory tree
for _ in range(depth_limit): # Limit search depth to prevent infinite loops
# Check if this directory has src or include folders
has_src = (current / "src").exists()
has_include = (current / "include").exists()
if has_src or has_include:
return current
# Check if this is a git repository root
if (current / ".git").exists():
# Check if it has src or include nearby
if has_src or has_include:
return current
# Move up one level
parent = current.parent
if parent == current: # Reached filesystem root
break
current = parent
# If we couldn't find it, raise an error
raise RuntimeError(
"Could not find repository root. "
"Expected to find a directory containing 'src' and/or 'include' folders."
)
def get_scan_directories(repo_root: Path) -> List[Path]:
"""
Get the list of directories to scan for include files.
Returns paths that actually exist.
"""
directories = []
for dir_name in ["include", "src"]:
dir_path = repo_root / dir_name
if dir_path.exists() and dir_path.is_dir():
directories.append(dir_path)
if not directories:
raise RuntimeError(f"No 'src' or 'include' directories found in {repo_root}")
return directories
def main():
# Change to the script's directory
script_dir = Path(__file__).parent.resolve()
os.chdir(script_dir)
# If the shell is interactive, clean up any flotsam before analyzing
# Match bash behavior: check if PS1 is set (indicates interactive shell)
# When running a script, PS1 is not set even if stdin/stdout are TTYs
if os.environ.get("PS1"):
try:
subprocess.run(["git", "clean", "-ix"], check=False, timeout=30)
except (subprocess.TimeoutExpired, KeyboardInterrupt):
print("Skipping git clean...")
except Exception:
# If git clean fails for any reason, just continue
pass
# Clean up and create results directory
results_dir = script_dir / "results"
if results_dir.exists():
import shutil
shutil.rmtree(results_dir)
results_dir.mkdir()
# Find the repository root by searching for src and include directories
try:
repo_root = find_repo_root(script_dir)
scan_dirs = get_scan_directories(repo_root)
print(f"Found repository root: {repo_root}")
print(f"Scanning directories:")
for scan_dir in scan_dirs:
print(f" - {scan_dir.relative_to(repo_root)}")
except RuntimeError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
print("\nScanning for raw includes...")
# Find all #include directives
raw_includes: List[Tuple[str, str]] = []
rawincludes_file = results_dir / "rawincludes.txt"
# Write to file as we go to avoid storing everything in memory
with open(rawincludes_file, "w", buffering=8192) as raw_f:
for dir_path in scan_dirs:
print(f" Scanning {dir_path.relative_to(repo_root)}...")
for file_path in dir_path.rglob("*"):
if not file_path.is_file():
continue
try:
rel_path_str = str(file_path.relative_to(repo_root))
# Read file with larger buffer for better performance
with open(
file_path,
"r",
encoding="utf-8",
errors="ignore",
buffering=8192,
) as f:
for line in f:
# Quick check before regex
if "#include" not in line or "boost" in line:
continue
if INCLUDE_PATTERN.match(line):
line_stripped = line.strip()
entry = f"{rel_path_str}:{line_stripped}\n"
print(entry, end="")
raw_f.write(entry)
raw_includes.append((rel_path_str, line_stripped))
except Exception as e:
print(f"Error reading {file_path}: {e}", file=sys.stderr)
# Build levelization paths and count directly (no need to sort first)
print("Build levelization paths")
path_counts: Dict[Tuple[str, str], int] = defaultdict(int)
for file_path, include_line in raw_includes:
level = get_level(file_path)
include_level = extract_include_level(include_line)
if include_level and level != include_level:
path_counts[(level, include_level)] += 1
# Sort and deduplicate paths (using dictionary order like bash 'sort -d')
print("Sort and deduplicate paths")
paths_file = results_dir / "paths.txt"
with open(paths_file, "w") as f:
# Sort using dictionary order: only alphanumeric and spaces matter
sorted_items = sorted(
path_counts.items(),
key=lambda x: (dictionary_sort_key(x[0][0]), dictionary_sort_key(x[0][1])),
)
for (level, include_level), count in sorted_items:
line = f"{count:7} {level} {include_level}\n"
print(line.rstrip())
f.write(line)
# Split into flat-file database
print("Split into flat-file database")
includes_dir = results_dir / "includes"
included_by_dir = results_dir / "included_by"
includes_dir.mkdir()
included_by_dir.mkdir()
# Batch writes by grouping data first to avoid repeated file opens
includes_data: Dict[str, List[Tuple[str, int]]] = defaultdict(list)
included_by_data: Dict[str, List[Tuple[str, int]]] = defaultdict(list)
# Process in sorted order to match bash script behavior (dictionary order)
sorted_items = sorted(
path_counts.items(),
key=lambda x: (dictionary_sort_key(x[0][0]), dictionary_sort_key(x[0][1])),
)
for (level, include_level), count in sorted_items:
includes_data[level].append((include_level, count))
included_by_data[include_level].append((level, count))
# Write all includes files in sorted order (dictionary order)
for level in sorted(includes_data.keys(), key=dictionary_sort_key):
entries = includes_data[level]
with open(includes_dir / level, "w") as f:
for include_level, count in entries:
line = f"{include_level} {count}\n"
print(line.rstrip())
f.write(line)
# Write all included_by files in sorted order (dictionary order)
for include_level in sorted(included_by_data.keys(), key=dictionary_sort_key):
entries = included_by_data[include_level]
with open(included_by_dir / include_level, "w") as f:
for level, count in entries:
line = f"{level} {count}\n"
print(line.rstrip())
f.write(line)
# Search for loops
print("Search for loops")
loops_file = results_dir / "loops.txt"
ordering_file = results_dir / "ordering.txt"
loops_found: Set[Tuple[str, str]] = set()
# Pre-load all include files into memory to avoid repeated I/O
# This is the biggest optimization - we were reading files repeatedly in nested loops
# Use list of tuples to preserve file order
includes_cache: Dict[str, List[Tuple[str, int]]] = {}
includes_lookup: Dict[str, Dict[str, int]] = {} # For fast lookup
# Note: bash script uses 'for source in *' which uses standard glob sorting,
# NOT dictionary order. So we use standard sorted() here, not dictionary_sort_key.
for include_file in sorted(includes_dir.iterdir(), key=lambda p: p.name):
if not include_file.is_file():
continue
includes_cache[include_file.name] = []
includes_lookup[include_file.name] = {}
with open(include_file, "r") as f:
for line in f:
parts = line.strip().split()
if len(parts) >= 2:
include_name = parts[0]
include_count = int(parts[1])
includes_cache[include_file.name].append(
(include_name, include_count)
)
includes_lookup[include_file.name][include_name] = include_count
with open(loops_file, "w", buffering=8192) as loops_f, open(
ordering_file, "w", buffering=8192
) as ordering_f:
# Use standard sorting to match bash glob expansion 'for source in *'
for source in sorted(includes_cache.keys()):
source_includes = includes_cache[source]
for include, include_freq in source_includes:
# Check if include file exists and references source
if include not in includes_lookup:
continue
source_freq = includes_lookup[include].get(source)
if source_freq is not None:
# Found a loop
loop_key = tuple(sorted([source, include]))
if loop_key in loops_found:
continue
loops_found.add(loop_key)
loops_f.write(f"Loop: {source} {include}\n")
# If the counts are close, indicate that the two modules are
# on the same level, though they shouldn't be
diff = include_freq - source_freq
if diff > 3:
loops_f.write(f" {source} > {include}\n\n")
elif diff < -3:
loops_f.write(f" {include} > {source}\n\n")
elif source_freq == include_freq:
loops_f.write(f" {include} == {source}\n\n")
else:
loops_f.write(f" {include} ~= {source}\n\n")
else:
ordering_f.write(f"{source} > {include}\n")
# Print results
print("\nOrdering:")
with open(ordering_file, "r") as f:
print(f.read(), end="")
print("\nLoops:")
with open(loops_file, "r") as f:
print(f.read(), end="")
if __name__ == "__main__":
main()

View File

@@ -1,130 +0,0 @@
#!/bin/bash
# Usage: generate.sh
# This script takes no parameters, reads no environment variables,
# and can be run from any directory, as long as it is in the expected
# location in the repo.
pushd $( dirname $0 )
if [ -v PS1 ]
then
# if the shell is interactive, clean up any flotsam before analyzing
git clean -ix
fi
# Ensure all sorting is ASCII-order consistently across platforms.
export LANG=C
rm -rfv results
mkdir results
includes="$( pwd )/results/rawincludes.txt"
pushd ../../..
echo Raw includes:
grep -r '^[ ]*#include.*/.*\.h' include src | \
grep -v boost | tee ${includes}
popd
pushd results
oldifs=${IFS}
IFS=:
mkdir includes
mkdir included_by
echo Build levelization paths
exec 3< ${includes} # open rawincludes.txt for input
while read -r -u 3 file include
do
level=$( echo ${file} | cut -d/ -f 2,3 )
# If the "level" indicates a file, cut off the filename
if [[ "${level##*.}" != "${level}" ]]
then
# Use the "toplevel" label as a workaround for `sort`
# inconsistencies between different utility versions
level="$( dirname ${level} )/toplevel"
fi
level=$( echo ${level} | tr '/' '.' )
includelevel=$( echo ${include} | sed 's/.*["<]//; s/[">].*//' | \
cut -d/ -f 1,2 )
if [[ "${includelevel##*.}" != "${includelevel}" ]]
then
# Use the "toplevel" label as a workaround for `sort`
# inconsistencies between different utility versions
includelevel="$( dirname ${includelevel} )/toplevel"
fi
includelevel=$( echo ${includelevel} | tr '/' '.' )
if [[ "$level" != "$includelevel" ]]
then
echo $level $includelevel | tee -a paths.txt
fi
done
echo Sort and deduplicate paths
sort -ds paths.txt | uniq -c | tee sortedpaths.txt
mv sortedpaths.txt paths.txt
exec 3>&- #close fd 3
IFS=${oldifs}
unset oldifs
echo Split into flat-file database
exec 4<paths.txt # open paths.txt for input
while read -r -u 4 count level include
do
echo ${include} ${count} | tee -a includes/${level}
echo ${level} ${count} | tee -a included_by/${include}
done
exec 4>&- #close fd 4
loops="$( pwd )/loops.txt"
ordering="$( pwd )/ordering.txt"
pushd includes
echo Search for loops
# Redirect stdout to a file
exec 4>&1
exec 1>"${loops}"
for source in *
do
if [[ -f "$source" ]]
then
exec 5<"${source}" # open for input
while read -r -u 5 include includefreq
do
if [[ -f $include ]]
then
if grep -q -w $source $include
then
if grep -q -w "Loop: $include $source" "${loops}"
then
continue
fi
sourcefreq=$( grep -w $source $include | cut -d\ -f2 )
echo "Loop: $source $include"
# If the counts are close, indicate that the two modules are
# on the same level, though they shouldn't be
if [[ $(( $includefreq - $sourcefreq )) -gt 3 ]]
then
echo -e " $source > $include\n"
elif [[ $(( $sourcefreq - $includefreq )) -gt 3 ]]
then
echo -e " $include > $source\n"
elif [[ $sourcefreq -eq $includefreq ]]
then
echo -e " $include == $source\n"
else
echo -e " $include ~= $source\n"
fi
else
echo "$source > $include" >> "${ordering}"
fi
fi
done
exec 5>&- #close fd 5
fi
done
exec 1>&4 #close fd 1
exec 4>&- #close fd 4
cat "${ordering}"
cat "${loops}"
popd
popd
popd

View File

@@ -20,7 +20,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check levelization
run: .github/scripts/levelization/generate.sh
run: python .github/scripts/levelization/generate.py
- name: Check for differences
env:
MESSAGE: |
@@ -32,7 +32,7 @@ jobs:
removed from loops.txt, it's probably an improvement, while if
something was added, it's probably a regression.
Run '.github/scripts/levelization/generate.sh' in your repo, commit
Run '.github/scripts/levelization/generate.py' in your repo, commit
and push the changes. See .github/scripts/levelization/README.md for
more info.
run: |

3
.gitignore vendored
View File

@@ -75,6 +75,9 @@ DerivedData
/.claude
/CLAUDE.md
# Python
__pycache__
# Direnv's directory
/.direnv

View File

@@ -11,12 +11,7 @@ function(xrpl_add_test name)
)
add_executable(${target} ${ARGN} ${sources})
isolate_headers(
${target}
"${CMAKE_SOURCE_DIR}"
"${CMAKE_SOURCE_DIR}/tests/${name}"
PRIVATE
)
isolate_headers(${target} "${CMAKE_SOURCE_DIR}" "${CMAKE_SOURCE_DIR}/tests/${name}" PRIVATE)
add_test(NAME ${target} COMMAND ${target})
endfunction()

View File

@@ -10,25 +10,16 @@ include(target_protobuf_sources)
# so we just build them as a separate library.
add_library(xrpl.libpb)
set_target_properties(xrpl.libpb PROPERTIES UNITY_BUILD OFF)
target_protobuf_sources(
xrpl.libpb
xrpl/proto
LANGUAGE cpp
IMPORT_DIRS include/xrpl/proto
PROTOS include/xrpl/proto/xrpl.proto
target_protobuf_sources(xrpl.libpb xrpl/proto LANGUAGE cpp IMPORT_DIRS include/xrpl/proto
PROTOS include/xrpl/proto/xrpl.proto
)
file(GLOB_RECURSE protos "include/xrpl/proto/org/*.proto")
target_protobuf_sources(
xrpl.libpb
xrpl/proto
LANGUAGE cpp
IMPORT_DIRS include/xrpl/proto
PROTOS "${protos}"
target_protobuf_sources(xrpl.libpb xrpl/proto LANGUAGE cpp IMPORT_DIRS include/xrpl/proto
PROTOS "${protos}"
)
target_protobuf_sources(
xrpl.libpb
xrpl/proto
xrpl.libpb xrpl/proto
LANGUAGE grpc
IMPORT_DIRS include/xrpl/proto
PROTOS "${protos}"

View File

@@ -39,15 +39,19 @@ list(
)
setup_target_for_coverage_gcovr(
NAME coverage
FORMAT ${coverage_format}
NAME
coverage
FORMAT
${coverage_format}
EXCLUDE
"src/test"
"src/tests"
"include/xrpl/beast/test"
"include/xrpl/beast/unit_test"
"${CMAKE_BINARY_DIR}/pb-xrpl.libpb"
DEPENDENCIES xrpld xrpl.tests
"src/test"
"src/tests"
"include/xrpl/beast/test"
"include/xrpl/beast/unit_test"
"${CMAKE_BINARY_DIR}/pb-xrpl.libpb"
DEPENDENCIES
xrpld
xrpl.tests
)
add_code_coverage_to_target(opts INTERFACE)

View File

@@ -42,11 +42,7 @@ function(verbose_find_path variable name)
endif()
endfunction()
verbose_find_path(
doxygen_plantuml_jar_path
plantuml.jar
PATH_SUFFIXES share/plantuml
)
verbose_find_path(doxygen_plantuml_jar_path plantuml.jar PATH_SUFFIXES share/plantuml)
verbose_find_path(doxygen_dot_path dot)
# https://en.cppreference.com/w/Cppreference:Archives

View File

@@ -25,16 +25,10 @@ function(add_module parent name)
${target}
PUBLIC "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
)
isolate_headers(
${target}
"${CMAKE_CURRENT_SOURCE_DIR}/include"
"${CMAKE_CURRENT_SOURCE_DIR}/include/${parent}/${name}"
PUBLIC
isolate_headers(${target} "${CMAKE_CURRENT_SOURCE_DIR}/include"
"${CMAKE_CURRENT_SOURCE_DIR}/include/${parent}/${name}" PUBLIC
)
isolate_headers(
${target}
"${CMAKE_CURRENT_SOURCE_DIR}/src"
"${CMAKE_CURRENT_SOURCE_DIR}/src/lib${parent}/${name}"
PRIVATE
isolate_headers(${target} "${CMAKE_CURRENT_SOURCE_DIR}/src"
"${CMAKE_CURRENT_SOURCE_DIR}/src/lib${parent}/${name}" PRIVATE
)
endfunction()

View File

@@ -42,7 +42,7 @@ TRANSACTION(ttPAYMENT, 0, Payment,
/** This transaction type creates an escrow object. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/escrow/EscrowCreate.h>
# include <xrpl/tx/transactors/escrow/Escrow.h>
#endif
TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate,
Delegation::delegable,
@@ -58,9 +58,6 @@ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate,
}))
/** This transaction type completes an existing escrow. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/escrow/EscrowFinish.h>
#endif
TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish,
Delegation::delegable,
uint256{},
@@ -97,7 +94,7 @@ TRANSACTION(ttACCOUNT_SET, 3, AccountSet,
/** This transaction type cancels an existing escrow. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/escrow/EscrowCancel.h>
# include <xrpl/tx/transactors/escrow/Escrow.h>
#endif
TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel,
Delegation::delegable,
@@ -183,7 +180,7 @@ TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet,
/** This transaction type creates a new unidirectional XRP payment channel. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/payment_channel/PayChanCreate.h>
# include <xrpl/tx/transactors/payment_channel/PayChan.h>
#endif
TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate,
Delegation::delegable,
@@ -199,9 +196,6 @@ TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate,
}))
/** This transaction type funds an existing unidirectional XRP payment channel. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/payment_channel/PayChanFund.h>
#endif
TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund,
Delegation::delegable,
uint256{},
@@ -213,9 +207,6 @@ TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund,
}))
/** This transaction type submits a claim against an existing unidirectional payment channel. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/payment_channel/PayChanClaim.h>
#endif
TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim,
Delegation::delegable,
uint256{},
@@ -626,7 +617,7 @@ TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 48, XChainCreateBridge,
/** This transaction type creates or updates a DID */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/did/DIDSet.h>
# include <xrpl/tx/transactors/did/DID.h>
#endif
TRANSACTION(ttDID_SET, 49, DIDSet,
Delegation::delegable,
@@ -639,9 +630,6 @@ TRANSACTION(ttDID_SET, 49, DIDSet,
}))
/** This transaction type deletes a DID */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/did/DIDDelete.h>
#endif
TRANSACTION(ttDID_DELETE, 50, DIDDelete,
Delegation::delegable,
featureDID,
@@ -751,7 +739,7 @@ TRANSACTION(ttMPTOKEN_AUTHORIZE, 57, MPTokenAuthorize,
/** This transaction type create an Credential instance */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/credentials/CredentialCreate.h>
# include <xrpl/tx/transactors/credentials/Credentials.h>
#endif
TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate,
Delegation::delegable,
@@ -765,9 +753,6 @@ TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate,
}))
/** This transaction type accept an Credential object */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/credentials/CredentialAccept.h>
#endif
TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept,
Delegation::delegable,
featureCredentials,
@@ -778,9 +763,6 @@ TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept,
}))
/** This transaction type delete an Credential object */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/credentials/CredentialDelete.h>
#endif
TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete,
Delegation::delegable,
featureCredentials,

View File

@@ -1,29 +0,0 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class CredentialAccept : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit CredentialAccept(ApplyContext& ctx) : Transactor(ctx)
{
}
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -1,29 +0,0 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class CredentialCreate : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit CredentialCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -1,29 +0,0 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class CredentialDelete : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit CredentialDelete(ApplyContext& ctx) : Transactor(ctx)
{
}
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -0,0 +1,77 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class CredentialCreate : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit CredentialCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
class CredentialDelete : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit CredentialDelete(ApplyContext& ctx) : Transactor(ctx)
{
}
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
class CredentialAccept : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit CredentialAccept(ApplyContext& ctx) : Transactor(ctx)
{
}
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -4,6 +4,24 @@
namespace xrpl {
class DIDSet : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit DIDSet(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
class DIDDelete : public Transactor
{
public:

View File

@@ -1,23 +0,0 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class DIDSet : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit DIDSet(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -0,0 +1,80 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class EscrowCreate : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit EscrowCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
class EscrowFinish : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit EscrowFinish(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static NotTEC
preflightSigValidated(PreflightContext const& ctx);
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
class EscrowCancel : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit EscrowCancel(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -1,26 +0,0 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class EscrowCancel : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit EscrowCancel(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -1,29 +0,0 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class EscrowCreate : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit EscrowCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -1,35 +0,0 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class EscrowFinish : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit EscrowFinish(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static NotTEC
preflightSigValidated(PreflightContext const& ctx);
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace xrpl

View File

@@ -1,229 +0,0 @@
#pragma once
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/tx/transactors/token/MPTokenAuthorize.h>
namespace xrpl {
template <ValidIssueType T>
TER
escrowUnlockApplyHelper(
ApplyView& view,
Rate lockedRate,
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 <>
inline TER
escrowUnlockApplyHelper<Issue>(
ApplyView& view,
Rate lockedRate,
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;
bool const senderIssuer = issuer == sender;
bool const receiverIssuer = issuer == receiver;
bool const issuerHigh = issuer > receiver;
if (senderIssuer)
return tecINTERNAL; // LCOV_EXCL_LINE
if (receiverIssuer)
return tesSUCCESS;
if (!view.exists(trustLineKey) && createAsset && !receiverIssuer)
{
// 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()) << "Trust line does not exist. "
"Insufficient 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))
{
return ter; // LCOV_EXCL_LINE
}
// clang-format on
view.update(sleDest);
}
if (!view.exists(trustLineKey) && !receiverIssuer)
return tecNO_LINE;
auto const xferRate = transferRate(view, amount);
// update if issuer rate is less than locked rate
if (xferRate < lockedRate)
lockedRate = xferRate;
// Transfer Rate only applies when:
// 1. Issuer is not involved in the transfer (senderIssuer or
// receiverIssuer)
// 2. The locked rate is different from the parity rate
// NOTE: Transfer fee in escrow works a bit differently from a normal
// payment. In escrow, the fee is deducted from the locked/sending amount,
// whereas in a normal payment, the transfer fee is taken on top of the
// sending amount.
auto finalAmt = amount;
if ((!senderIssuer && !receiverIssuer) && lockedRate != parityRate)
{
// compute transfer fee, if any
auto const xferFee = amount.value() - divideRound(amount, lockedRate, amount.issue(), true);
// compute balance to transfer
finalAmt = amount.value() - xferFee;
}
// validate the line limit if the account submitting txn is not the receiver
// of the funds
if (!createAsset)
{
auto const sleRippleState = view.peek(trustLineKey);
if (!sleRippleState)
return tecINTERNAL; // LCOV_EXCL_LINE
// if the issuer is the high, then we use the low limit
// otherwise we use the high limit
STAmount const lineLimit =
sleRippleState->getFieldAmount(issuerHigh ? sfLowLimit : sfHighLimit);
STAmount lineBalance = sleRippleState->getFieldAmount(sfBalance);
// flip the sign of the line balance if the issuer is not high
if (!issuerHigh)
lineBalance.negate();
// add the final amount to the line balance
lineBalance += finalAmt;
// if the transfer would exceed the line limit return tecLIMIT_EXCEEDED
if (lineLimit < lineBalance)
return tecLIMIT_EXCEEDED;
}
// if destination is not the issuer then transfer funds
if (!receiverIssuer)
{
auto const ter = rippleCredit(view, issuer, receiver, finalAmt, true, journal);
if (ter != tesSUCCESS)
return ter; // LCOV_EXCL_LINE
}
return tesSUCCESS;
}
template <>
inline TER
escrowUnlockApplyHelper<MPTIssue>(
ApplyView& view,
Rate lockedRate,
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)
{
bool const senderIssuer = issuer == sender;
bool const receiverIssuer = issuer == receiver;
auto const mptID = amount.get<MPTIssue>().getMptID();
auto const issuanceKey = keylet::mptIssuance(mptID);
if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && createAsset && !receiverIssuer)
{
if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)};
xrpBalance < view.fees().accountReserve(ownerCount + 1))
{
return tecINSUFFICIENT_RESERVE;
}
if (auto const ter = MPTokenAuthorize::createMPToken(view, mptID, receiver, 0);
!isTesSuccess(ter))
{
return ter; // LCOV_EXCL_LINE
}
// update owner count.
adjustOwnerCount(view, sleDest, 1, journal);
}
if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && !receiverIssuer)
return tecNO_PERMISSION;
auto const xferRate = transferRate(view, amount);
// update if issuer rate is less than locked rate
if (xferRate < lockedRate)
lockedRate = xferRate;
// Transfer Rate only applies when:
// 1. Issuer is not involved in the transfer (senderIssuer or
// receiverIssuer)
// 2. The locked rate is different from the parity rate
// NOTE: Transfer fee in escrow works a bit differently from a normal
// payment. In escrow, the fee is deducted from the locked/sending amount,
// whereas in a normal payment, the transfer fee is taken on top of the
// sending amount.
auto finalAmt = amount;
if ((!senderIssuer && !receiverIssuer) && lockedRate != parityRate)
{
// compute transfer fee, if any
auto const xferFee = amount.value() - divideRound(amount, lockedRate, amount.asset(), true);
// compute balance to transfer
finalAmt = amount.value() - xferFee;
}
return rippleUnlockEscrowMPT(
view,
sender,
receiver,
finalAmt,
view.rules().enabled(fixTokenEscrowV1) ? amount : finalAmt,
journal);
}
} // namespace xrpl

View File

@@ -0,0 +1,83 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class PayChanCreate : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit PayChanCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
using PaymentChannelCreate = PayChanCreate;
//------------------------------------------------------------------------------
class PayChanFund : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit PayChanFund(ApplyContext& ctx) : Transactor(ctx)
{
}
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
TER
doApply() override;
};
using PaymentChannelFund = PayChanFund;
//------------------------------------------------------------------------------
class PayChanClaim : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit PayChanClaim(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
using PaymentChannelClaim = PayChanClaim;
} // namespace xrpl

View File

@@ -1,34 +0,0 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class PayChanClaim : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit PayChanClaim(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
using PaymentChannelClaim = PayChanClaim;
} // namespace xrpl

View File

@@ -1,31 +0,0 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class PayChanCreate : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit PayChanCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
using PaymentChannelCreate = PayChanCreate;
} // namespace xrpl

View File

@@ -1,28 +0,0 @@
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class PayChanFund : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit PayChanFund(ApplyContext& ctx) : Transactor(ctx)
{
}
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
TER
doApply() override;
};
using PaymentChannelFund = PayChanFund;
} // namespace xrpl

View File

@@ -1,15 +0,0 @@
#pragma once
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/protocol/UintTypes.h>
namespace xrpl {
TER
closeChannel(
std::shared_ptr<SLE> const& slep,
ApplyView& view,
uint256 const& key,
beast::Journal j);
} // namespace xrpl

View File

@@ -11,7 +11,7 @@
#include <xrpl/tx/transactors/account/DeleteAccount.h>
#include <xrpl/tx/transactors/account/SetSignerList.h>
#include <xrpl/tx/transactors/delegate/DelegateSet.h>
#include <xrpl/tx/transactors/did/DIDDelete.h>
#include <xrpl/tx/transactors/did/DID.h>
#include <xrpl/tx/transactors/nft/NFTokenUtils.h>
#include <xrpl/tx/transactors/oracle/DeleteOracle.h>
#include <xrpl/tx/transactors/payment/DepositPreauth.h>

View File

@@ -1,113 +0,0 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/credentials/CredentialAccept.h>
#include <chrono>
namespace xrpl {
using namespace credentials;
std::uint32_t
CredentialAccept::getFlagsMask(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0;
}
NotTEC
CredentialAccept::preflight(PreflightContext const& ctx)
{
if (!ctx.tx[sfIssuer])
{
JLOG(ctx.j.trace()) << "Malformed transaction: Issuer field zeroed.";
return temINVALID_ACCOUNT_ID;
}
auto const credType = ctx.tx[sfCredentialType];
if (credType.empty() || (credType.size() > maxCredentialTypeLength))
{
JLOG(ctx.j.trace()) << "Malformed transaction: invalid size of CredentialType.";
return temMALFORMED;
}
return tesSUCCESS;
}
TER
CredentialAccept::preclaim(PreclaimContext const& ctx)
{
AccountID const subject = ctx.tx[sfAccount];
AccountID const issuer = ctx.tx[sfIssuer];
auto const credType(ctx.tx[sfCredentialType]);
if (!ctx.view.exists(keylet::account(issuer)))
{
JLOG(ctx.j.warn()) << "No issuer: " << to_string(issuer);
return tecNO_ISSUER;
}
auto const sleCred = ctx.view.read(keylet::credential(subject, issuer, credType));
if (!sleCred)
{
JLOG(ctx.j.warn()) << "No credential: " << to_string(subject) << ", " << to_string(issuer)
<< ", " << credType;
return tecNO_ENTRY;
}
if (sleCred->getFieldU32(sfFlags) & lsfAccepted)
{
JLOG(ctx.j.warn()) << "Credential already accepted: " << to_string(subject) << ", "
<< to_string(issuer) << ", " << credType;
return tecDUPLICATE;
}
return tesSUCCESS;
}
TER
CredentialAccept::doApply()
{
AccountID const issuer{ctx_.tx[sfIssuer]};
// Both exist as credential object exist itself (checked in preclaim)
auto const sleSubject = view().peek(keylet::account(account_));
auto const sleIssuer = view().peek(keylet::account(issuer));
if (!sleSubject || !sleIssuer)
return tefINTERNAL; // LCOV_EXCL_LINE
{
STAmount const reserve{
view().fees().accountReserve(sleSubject->getFieldU32(sfOwnerCount) + 1)};
if (mPriorBalance < reserve)
return tecINSUFFICIENT_RESERVE;
}
auto const credType(ctx_.tx[sfCredentialType]);
Keylet const credentialKey = keylet::credential(account_, issuer, credType);
auto const sleCred = view().peek(credentialKey); // Checked in preclaim()
if (checkExpired(sleCred, view().header().parentCloseTime))
{
JLOG(j_.trace()) << "Credential is expired: " << sleCred->getText();
// delete expired credentials even if the transaction failed
auto const err = credentials::deleteSLE(view(), sleCred, j_);
return isTesSuccess(err) ? tecEXPIRED : err;
}
sleCred->setFieldU32(sfFlags, lsfAccepted);
view().update(sleCred);
adjustOwnerCount(view(), sleIssuer, -1, j_);
adjustOwnerCount(view(), sleSubject, 1, j_);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -1,164 +0,0 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/credentials/CredentialCreate.h>
#include <chrono>
namespace xrpl {
/*
Credentials
======
A verifiable credentials (VC
https://en.wikipedia.org/wiki/Verifiable_credentials), as defined by the W3C
specification (https://www.w3.org/TR/vc-data-model-2.0/), is a
secure and tamper-evident way to represent information about a subject, such
as an individual, organization, or even an IoT device. These credentials are
issued by a trusted entity and can be verified by third parties without
directly involving the issuer at all.
*/
using namespace credentials;
std::uint32_t
CredentialCreate::getFlagsMask(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0;
}
NotTEC
CredentialCreate::preflight(PreflightContext const& ctx)
{
auto const& tx = ctx.tx;
auto& j = ctx.j;
if (!tx[sfSubject])
{
JLOG(j.trace()) << "Malformed transaction: Invalid Subject";
return temMALFORMED;
}
auto const uri = tx[~sfURI];
if (uri && (uri->empty() || (uri->size() > maxCredentialURILength)))
{
JLOG(j.trace()) << "Malformed transaction: invalid size of URI.";
return temMALFORMED;
}
auto const credType = tx[sfCredentialType];
if (credType.empty() || (credType.size() > maxCredentialTypeLength))
{
JLOG(j.trace()) << "Malformed transaction: invalid size of CredentialType.";
return temMALFORMED;
}
return tesSUCCESS;
}
TER
CredentialCreate::preclaim(PreclaimContext const& ctx)
{
auto const credType(ctx.tx[sfCredentialType]);
auto const subject = ctx.tx[sfSubject];
if (!ctx.view.exists(keylet::account(subject)))
{
JLOG(ctx.j.trace()) << "Subject doesn't exist.";
return tecNO_TARGET;
}
if (ctx.view.exists(keylet::credential(subject, ctx.tx[sfAccount], credType)))
{
JLOG(ctx.j.trace()) << "Credential already exists.";
return tecDUPLICATE;
}
return tesSUCCESS;
}
TER
CredentialCreate::doApply()
{
auto const subject = ctx_.tx[sfSubject];
auto const credType(ctx_.tx[sfCredentialType]);
Keylet const credentialKey = keylet::credential(subject, account_, credType);
auto const sleCred = std::make_shared<SLE>(credentialKey);
if (!sleCred)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const optExp = ctx_.tx[~sfExpiration];
if (optExp)
{
std::uint32_t const closeTime =
ctx_.view().header().parentCloseTime.time_since_epoch().count();
if (closeTime > *optExp)
{
JLOG(j_.trace()) << "Malformed transaction: "
"Expiration time is in the past.";
return tecEXPIRED;
}
sleCred->setFieldU32(sfExpiration, ctx_.tx.getFieldU32(sfExpiration));
}
auto const sleIssuer = view().peek(keylet::account(account_));
if (!sleIssuer)
return tefINTERNAL; // LCOV_EXCL_LINE
{
STAmount const reserve{
view().fees().accountReserve(sleIssuer->getFieldU32(sfOwnerCount) + 1)};
if (mPriorBalance < reserve)
return tecINSUFFICIENT_RESERVE;
}
sleCred->setAccountID(sfSubject, subject);
sleCred->setAccountID(sfIssuer, account_);
sleCred->setFieldVL(sfCredentialType, credType);
if (ctx_.tx.isFieldPresent(sfURI))
sleCred->setFieldVL(sfURI, ctx_.tx.getFieldVL(sfURI));
{
auto const page =
view().dirInsert(keylet::ownerDir(account_), credentialKey, describeOwnerDir(account_));
JLOG(j_.trace()) << "Adding Credential to owner directory " << to_string(credentialKey.key)
<< ": " << (page ? "success" : "failure");
if (!page)
return tecDIR_FULL;
sleCred->setFieldU64(sfIssuerNode, *page);
adjustOwnerCount(view(), sleIssuer, 1, j_);
}
if (subject == account_)
{
sleCred->setFieldU32(sfFlags, lsfAccepted);
}
else
{
auto const page =
view().dirInsert(keylet::ownerDir(subject), credentialKey, describeOwnerDir(subject));
JLOG(j_.trace()) << "Adding Credential to owner directory " << to_string(credentialKey.key)
<< ": " << (page ? "success" : "failure");
if (!page)
return tecDIR_FULL;
sleCred->setFieldU64(sfSubjectNode, *page);
view().update(view().peek(keylet::account(subject)));
}
view().insert(sleCred);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -1,90 +0,0 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/credentials/CredentialDelete.h>
#include <chrono>
namespace xrpl {
using namespace credentials;
std::uint32_t
CredentialDelete::getFlagsMask(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0;
}
NotTEC
CredentialDelete::preflight(PreflightContext const& ctx)
{
auto const subject = ctx.tx[~sfSubject];
auto const issuer = ctx.tx[~sfIssuer];
if (!subject && !issuer)
{
// Neither field is present, the transaction is malformed.
JLOG(ctx.j.trace()) << "Malformed transaction: "
"No Subject or Issuer fields.";
return temMALFORMED;
}
// Make sure that the passed account is valid.
if ((subject && subject->isZero()) || (issuer && issuer->isZero()))
{
JLOG(ctx.j.trace()) << "Malformed transaction: Subject or Issuer "
"field zeroed.";
return temINVALID_ACCOUNT_ID;
}
auto const credType = ctx.tx[sfCredentialType];
if (credType.empty() || (credType.size() > maxCredentialTypeLength))
{
JLOG(ctx.j.trace()) << "Malformed transaction: invalid size of CredentialType.";
return temMALFORMED;
}
return tesSUCCESS;
}
TER
CredentialDelete::preclaim(PreclaimContext const& ctx)
{
AccountID const account{ctx.tx[sfAccount]};
auto const subject = ctx.tx[~sfSubject].value_or(account);
auto const issuer = ctx.tx[~sfIssuer].value_or(account);
auto const credType(ctx.tx[sfCredentialType]);
if (!ctx.view.exists(keylet::credential(subject, issuer, credType)))
return tecNO_ENTRY;
return tesSUCCESS;
}
TER
CredentialDelete::doApply()
{
auto const subject = ctx_.tx[~sfSubject].value_or(account_);
auto const issuer = ctx_.tx[~sfIssuer].value_or(account_);
auto const credType(ctx_.tx[sfCredentialType]);
auto const sleCred = view().peek(keylet::credential(subject, issuer, credType));
if (!sleCred)
return tefINTERNAL; // LCOV_EXCL_LINE
if ((subject != account_) && (issuer != account_) &&
!checkExpired(sleCred, ctx_.view().header().parentCloseTime))
{
JLOG(j_.trace()) << "Can't delete non-expired credential.";
return tecNO_PERMISSION;
}
return deleteSLE(view(), sleCred, j_);
}
} // namespace xrpl

View File

@@ -0,0 +1,341 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/credentials/Credentials.h>
#include <chrono>
namespace xrpl {
/*
Credentials
======
A verifiable credentials (VC
https://en.wikipedia.org/wiki/Verifiable_credentials), as defined by the W3C
specification (https://www.w3.org/TR/vc-data-model-2.0/), is a
secure and tamper-evident way to represent information about a subject, such
as an individual, organization, or even an IoT device. These credentials are
issued by a trusted entity and can be verified by third parties without
directly involving the issuer at all.
*/
using namespace credentials;
// ------- CREATE --------------------------
std::uint32_t
CredentialCreate::getFlagsMask(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0;
}
NotTEC
CredentialCreate::preflight(PreflightContext const& ctx)
{
auto const& tx = ctx.tx;
auto& j = ctx.j;
if (!tx[sfSubject])
{
JLOG(j.trace()) << "Malformed transaction: Invalid Subject";
return temMALFORMED;
}
auto const uri = tx[~sfURI];
if (uri && (uri->empty() || (uri->size() > maxCredentialURILength)))
{
JLOG(j.trace()) << "Malformed transaction: invalid size of URI.";
return temMALFORMED;
}
auto const credType = tx[sfCredentialType];
if (credType.empty() || (credType.size() > maxCredentialTypeLength))
{
JLOG(j.trace()) << "Malformed transaction: invalid size of CredentialType.";
return temMALFORMED;
}
return tesSUCCESS;
}
TER
CredentialCreate::preclaim(PreclaimContext const& ctx)
{
auto const credType(ctx.tx[sfCredentialType]);
auto const subject = ctx.tx[sfSubject];
if (!ctx.view.exists(keylet::account(subject)))
{
JLOG(ctx.j.trace()) << "Subject doesn't exist.";
return tecNO_TARGET;
}
if (ctx.view.exists(keylet::credential(subject, ctx.tx[sfAccount], credType)))
{
JLOG(ctx.j.trace()) << "Credential already exists.";
return tecDUPLICATE;
}
return tesSUCCESS;
}
TER
CredentialCreate::doApply()
{
auto const subject = ctx_.tx[sfSubject];
auto const credType(ctx_.tx[sfCredentialType]);
Keylet const credentialKey = keylet::credential(subject, account_, credType);
auto const sleCred = std::make_shared<SLE>(credentialKey);
if (!sleCred)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const optExp = ctx_.tx[~sfExpiration];
if (optExp)
{
std::uint32_t const closeTime =
ctx_.view().header().parentCloseTime.time_since_epoch().count();
if (closeTime > *optExp)
{
JLOG(j_.trace()) << "Malformed transaction: "
"Expiration time is in the past.";
return tecEXPIRED;
}
sleCred->setFieldU32(sfExpiration, ctx_.tx.getFieldU32(sfExpiration));
}
auto const sleIssuer = view().peek(keylet::account(account_));
if (!sleIssuer)
return tefINTERNAL; // LCOV_EXCL_LINE
{
STAmount const reserve{
view().fees().accountReserve(sleIssuer->getFieldU32(sfOwnerCount) + 1)};
if (mPriorBalance < reserve)
return tecINSUFFICIENT_RESERVE;
}
sleCred->setAccountID(sfSubject, subject);
sleCred->setAccountID(sfIssuer, account_);
sleCred->setFieldVL(sfCredentialType, credType);
if (ctx_.tx.isFieldPresent(sfURI))
sleCred->setFieldVL(sfURI, ctx_.tx.getFieldVL(sfURI));
{
auto const page =
view().dirInsert(keylet::ownerDir(account_), credentialKey, describeOwnerDir(account_));
JLOG(j_.trace()) << "Adding Credential to owner directory " << to_string(credentialKey.key)
<< ": " << (page ? "success" : "failure");
if (!page)
return tecDIR_FULL;
sleCred->setFieldU64(sfIssuerNode, *page);
adjustOwnerCount(view(), sleIssuer, 1, j_);
}
if (subject == account_)
{
sleCred->setFieldU32(sfFlags, lsfAccepted);
}
else
{
auto const page =
view().dirInsert(keylet::ownerDir(subject), credentialKey, describeOwnerDir(subject));
JLOG(j_.trace()) << "Adding Credential to owner directory " << to_string(credentialKey.key)
<< ": " << (page ? "success" : "failure");
if (!page)
return tecDIR_FULL;
sleCred->setFieldU64(sfSubjectNode, *page);
view().update(view().peek(keylet::account(subject)));
}
view().insert(sleCred);
return tesSUCCESS;
}
// ------- DELETE --------------------------
std::uint32_t
CredentialDelete::getFlagsMask(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0;
}
NotTEC
CredentialDelete::preflight(PreflightContext const& ctx)
{
auto const subject = ctx.tx[~sfSubject];
auto const issuer = ctx.tx[~sfIssuer];
if (!subject && !issuer)
{
// Neither field is present, the transaction is malformed.
JLOG(ctx.j.trace()) << "Malformed transaction: "
"No Subject or Issuer fields.";
return temMALFORMED;
}
// Make sure that the passed account is valid.
if ((subject && subject->isZero()) || (issuer && issuer->isZero()))
{
JLOG(ctx.j.trace()) << "Malformed transaction: Subject or Issuer "
"field zeroed.";
return temINVALID_ACCOUNT_ID;
}
auto const credType = ctx.tx[sfCredentialType];
if (credType.empty() || (credType.size() > maxCredentialTypeLength))
{
JLOG(ctx.j.trace()) << "Malformed transaction: invalid size of CredentialType.";
return temMALFORMED;
}
return tesSUCCESS;
}
TER
CredentialDelete::preclaim(PreclaimContext const& ctx)
{
AccountID const account{ctx.tx[sfAccount]};
auto const subject = ctx.tx[~sfSubject].value_or(account);
auto const issuer = ctx.tx[~sfIssuer].value_or(account);
auto const credType(ctx.tx[sfCredentialType]);
if (!ctx.view.exists(keylet::credential(subject, issuer, credType)))
return tecNO_ENTRY;
return tesSUCCESS;
}
TER
CredentialDelete::doApply()
{
auto const subject = ctx_.tx[~sfSubject].value_or(account_);
auto const issuer = ctx_.tx[~sfIssuer].value_or(account_);
auto const credType(ctx_.tx[sfCredentialType]);
auto const sleCred = view().peek(keylet::credential(subject, issuer, credType));
if (!sleCred)
return tefINTERNAL; // LCOV_EXCL_LINE
if ((subject != account_) && (issuer != account_) &&
!checkExpired(sleCred, ctx_.view().header().parentCloseTime))
{
JLOG(j_.trace()) << "Can't delete non-expired credential.";
return tecNO_PERMISSION;
}
return deleteSLE(view(), sleCred, j_);
}
// ------- APPLY --------------------------
std::uint32_t
CredentialAccept::getFlagsMask(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0;
}
NotTEC
CredentialAccept::preflight(PreflightContext const& ctx)
{
if (!ctx.tx[sfIssuer])
{
JLOG(ctx.j.trace()) << "Malformed transaction: Issuer field zeroed.";
return temINVALID_ACCOUNT_ID;
}
auto const credType = ctx.tx[sfCredentialType];
if (credType.empty() || (credType.size() > maxCredentialTypeLength))
{
JLOG(ctx.j.trace()) << "Malformed transaction: invalid size of CredentialType.";
return temMALFORMED;
}
return tesSUCCESS;
}
TER
CredentialAccept::preclaim(PreclaimContext const& ctx)
{
AccountID const subject = ctx.tx[sfAccount];
AccountID const issuer = ctx.tx[sfIssuer];
auto const credType(ctx.tx[sfCredentialType]);
if (!ctx.view.exists(keylet::account(issuer)))
{
JLOG(ctx.j.warn()) << "No issuer: " << to_string(issuer);
return tecNO_ISSUER;
}
auto const sleCred = ctx.view.read(keylet::credential(subject, issuer, credType));
if (!sleCred)
{
JLOG(ctx.j.warn()) << "No credential: " << to_string(subject) << ", " << to_string(issuer)
<< ", " << credType;
return tecNO_ENTRY;
}
if (sleCred->getFieldU32(sfFlags) & lsfAccepted)
{
JLOG(ctx.j.warn()) << "Credential already accepted: " << to_string(subject) << ", "
<< to_string(issuer) << ", " << credType;
return tecDUPLICATE;
}
return tesSUCCESS;
}
TER
CredentialAccept::doApply()
{
AccountID const issuer{ctx_.tx[sfIssuer]};
// Both exist as credential object exist itself (checked in preclaim)
auto const sleSubject = view().peek(keylet::account(account_));
auto const sleIssuer = view().peek(keylet::account(issuer));
if (!sleSubject || !sleIssuer)
return tefINTERNAL; // LCOV_EXCL_LINE
{
STAmount const reserve{
view().fees().accountReserve(sleSubject->getFieldU32(sfOwnerCount) + 1)};
if (mPriorBalance < reserve)
return tecINSUFFICIENT_RESERVE;
}
auto const credType(ctx_.tx[sfCredentialType]);
Keylet const credentialKey = keylet::credential(account_, issuer, credType);
auto const sleCred = view().peek(credentialKey); // Checked in preclaim()
if (checkExpired(sleCred, view().header().parentCloseTime))
{
JLOG(j_.trace()) << "Credential is expired: " << sleCred->getText();
// delete expired credentials even if the transaction failed
auto const err = credentials::deleteSLE(view(), sleCred, j_);
return isTesSuccess(err) ? tecEXPIRED : err;
}
sleCred->setFieldU32(sfFlags, lsfAccepted);
view().update(sleCred);
adjustOwnerCount(view(), sleIssuer, -1, j_);
adjustOwnerCount(view(), sleSubject, 1, j_);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -4,7 +4,7 @@
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/did/DIDSet.h>
#include <xrpl/tx/transactors/did/DID.h>
namespace xrpl {
@@ -134,4 +134,54 @@ DIDSet::doApply()
return addSLE(ctx_, sleDID, account_);
}
NotTEC
DIDDelete::preflight(PreflightContext const& ctx)
{
return tesSUCCESS;
}
TER
DIDDelete::deleteSLE(ApplyContext& ctx, Keylet sleKeylet, AccountID const owner)
{
auto const sle = ctx.view().peek(sleKeylet);
if (!sle)
return tecNO_ENTRY;
return DIDDelete::deleteSLE(ctx.view(), sle, owner, ctx.journal);
}
TER
DIDDelete::deleteSLE(
ApplyView& view,
std::shared_ptr<SLE> sle,
AccountID const owner,
beast::Journal j)
{
// Remove object from owner directory
if (!view.dirRemove(keylet::ownerDir(owner), (*sle)[sfOwnerNode], sle->key(), true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete DID Token from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
auto const sleOwner = view.peek(keylet::account(owner));
if (!sleOwner)
return tecINTERNAL; // LCOV_EXCL_LINE
adjustOwnerCount(view, sleOwner, -1, j);
view.update(sleOwner);
// Remove object from ledger
view.erase(sle);
return tesSUCCESS;
}
TER
DIDDelete::doApply()
{
return deleteSLE(ctx_, keylet::did(account_), account_);
}
} // namespace xrpl

View File

@@ -1,59 +0,0 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/tx/transactors/did/DIDDelete.h>
namespace xrpl {
NotTEC
DIDDelete::preflight(PreflightContext const& ctx)
{
return tesSUCCESS;
}
TER
DIDDelete::deleteSLE(ApplyContext& ctx, Keylet sleKeylet, AccountID const owner)
{
auto const sle = ctx.view().peek(sleKeylet);
if (!sle)
return tecNO_ENTRY;
return DIDDelete::deleteSLE(ctx.view(), sle, owner, ctx.journal);
}
TER
DIDDelete::deleteSLE(
ApplyView& view,
std::shared_ptr<SLE> sle,
AccountID const owner,
beast::Journal j)
{
// Remove object from owner directory
if (!view.dirRemove(keylet::ownerDir(owner), (*sle)[sfOwnerNode], sle->key(), true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete DID Token from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
auto const sleOwner = view.peek(keylet::account(owner));
if (!sleOwner)
return tecINTERNAL; // LCOV_EXCL_LINE
adjustOwnerCount(view, sleOwner, -1, j);
view.update(sleOwner);
// Remove object from ledger
view.erase(sle);
return tesSUCCESS;
}
TER
DIDDelete::doApply()
{
return deleteSLE(ctx_, keylet::did(account_), account_);
}
} // namespace xrpl

File diff suppressed because it is too large Load Diff

View File

@@ -1,200 +0,0 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/tx/transactors/escrow/EscrowCancel.h>
#include <xrpl/tx/transactors/escrow/EscrowHelpers.h>
namespace xrpl {
NotTEC
EscrowCancel::preflight(PreflightContext const& ctx)
{
return tesSUCCESS;
}
template <ValidIssueType T>
static TER
escrowCancelPreclaimHelper(
PreclaimContext const& ctx,
AccountID const& account,
STAmount const& amount);
template <>
TER
escrowCancelPreclaimHelper<Issue>(
PreclaimContext const& ctx,
AccountID const& account,
STAmount const& amount)
{
AccountID issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecINTERNAL
if (issuer == account)
return tecINTERNAL; // LCOV_EXCL_LINE
// If the issuer has requireAuth set, check if the account is authorized
if (auto const ter = requireAuth(ctx.view, amount.issue(), account); ter != tesSUCCESS)
return ter;
return tesSUCCESS;
}
template <>
TER
escrowCancelPreclaimHelper<MPTIssue>(
PreclaimContext const& ctx,
AccountID const& account,
STAmount const& amount)
{
AccountID issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecINTERNAL
if (issuer == account)
return tecINTERNAL; // LCOV_EXCL_LINE
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = ctx.view.read(issuanceKey);
if (!sleIssuance)
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(ctx.view, mptIssue, account, AuthType::WeakAuth);
ter != tesSUCCESS)
return ter;
return tesSUCCESS;
}
TER
EscrowCancel::preclaim(PreclaimContext const& ctx)
{
if (ctx.view.rules().enabled(featureTokenEscrow))
{
auto const k = keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence]);
auto const slep = ctx.view.read(k);
if (!slep)
return tecNO_TARGET;
AccountID const account = (*slep)[sfAccount];
STAmount const amount = (*slep)[sfAmount];
if (!isXRP(amount))
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowCancelPreclaimHelper<T>(ctx, account, amount);
},
amount.asset().value());
!isTesSuccess(ret))
return ret;
}
}
return tesSUCCESS;
}
TER
EscrowCancel::doApply()
{
auto const k = keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]);
auto const slep = ctx_.view().peek(k);
if (!slep)
{
if (ctx_.view().rules().enabled(featureTokenEscrow))
return tecINTERNAL; // LCOV_EXCL_LINE
return tecNO_TARGET;
}
auto const now = ctx_.view().header().parentCloseTime;
// No cancel time specified: can't execute at all.
if (!(*slep)[~sfCancelAfter])
return tecNO_PERMISSION;
// Too soon: can't execute before the cancel time.
if (!after(now, (*slep)[sfCancelAfter]))
return tecNO_PERMISSION;
AccountID const account = (*slep)[sfAccount];
// Remove escrow from owner directory
{
auto const page = (*slep)[sfOwnerNode];
if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, k.key, true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
// Remove escrow from recipient's owner directory, if present.
if (auto const optPage = (*slep)[~sfDestinationNode]; optPage)
{
if (!ctx_.view().dirRemove(keylet::ownerDir((*slep)[sfDestination]), *optPage, k.key, true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
auto const sle = ctx_.view().peek(keylet::account(account));
STAmount const amount = slep->getFieldAmount(sfAmount);
// Transfer amount back to the owner
if (isXRP(amount))
(*sle)[sfBalance] = (*sle)[sfBalance] + amount;
else
{
if (!ctx_.view().rules().enabled(featureTokenEscrow))
return temDISABLED; // LCOV_EXCL_LINE
auto const issuer = amount.getIssuer();
bool const createAsset = account == account_;
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowUnlockApplyHelper<T>(
ctx_.view(),
parityRate,
slep,
mPriorBalance,
amount,
issuer,
account, // sender and receiver are the same
account,
createAsset,
j_);
},
amount.asset().value());
!isTesSuccess(ret))
return ret; // LCOV_EXCL_LINE
// Remove escrow from issuers owner directory, if present.
if (auto const optPage = (*slep)[~sfIssuerNode]; optPage)
{
if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, k.key, true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
}
adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal);
ctx_.view().update(sle);
// Remove escrow from ledger
ctx_.view().erase(slep);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -1,498 +0,0 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/conditions/Condition.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/transactors/escrow/EscrowCreate.h>
namespace xrpl {
/*
Escrow
======
Escrow is a feature of the XRP Ledger that allows you to send conditional
XRP payments. These conditional payments, called escrows, set aside XRP and
deliver it later when certain conditions are met. Conditions to successfully
finish an escrow include time-based unlocks and crypto-conditions. Escrows
can also be set to expire if not finished in time.
The XRP set aside in an escrow is locked up. No one can use or destroy the
XRP until the escrow has been successfully finished or canceled. Before the
expiration time, only the intended receiver can get the XRP. After the
expiration time, the XRP can only be returned to the sender.
For more details on escrow, including examples, diagrams and more please
visit https://xrpl.org/escrow.html
For details on specific transactions, including fields and validation rules
please see:
`EscrowCreate`
--------------
See: https://xrpl.org/escrowcreate.html
`EscrowFinish`
--------------
See: https://xrpl.org/escrowfinish.html
`EscrowCancel`
--------------
See: https://xrpl.org/escrowcancel.html
*/
//------------------------------------------------------------------------------
TxConsequences
EscrowCreate::makeTxConsequences(PreflightContext const& ctx)
{
auto const amount = ctx.tx[sfAmount];
return TxConsequences{ctx.tx, isXRP(amount) ? amount.xrp() : beast::zero};
}
template <ValidIssueType T>
static NotTEC
escrowCreatePreflightHelper(PreflightContext const& ctx);
template <>
NotTEC
escrowCreatePreflightHelper<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
escrowCreatePreflightHelper<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
EscrowCreate::preflight(PreflightContext const& ctx)
{
STAmount const amount{ctx.tx[sfAmount]};
if (!isXRP(amount))
{
if (!ctx.rules.enabled(featureTokenEscrow))
return temBAD_AMOUNT;
if (auto const ret = std::visit(
[&]<typename T>(T const&) { return escrowCreatePreflightHelper<T>(ctx); },
amount.asset().value());
!isTesSuccess(ret))
return ret;
}
else
{
if (amount <= beast::zero)
return temBAD_AMOUNT;
}
// We must specify at least one timeout value
if (!ctx.tx[~sfCancelAfter] && !ctx.tx[~sfFinishAfter])
return temBAD_EXPIRATION;
// If both finish and cancel times are specified then the cancel time must
// be strictly after the finish time.
if (ctx.tx[~sfCancelAfter] && ctx.tx[~sfFinishAfter] &&
ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter])
return temBAD_EXPIRATION;
// In the absence of a FinishAfter, the escrow can be finished
// immediately, which can be confusing. When creating an escrow,
// we want to ensure that either a FinishAfter time is explicitly
// specified or a completion condition is attached.
if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition])
return temMALFORMED;
if (auto const cb = ctx.tx[~sfCondition])
{
using namespace xrpl::cryptoconditions;
std::error_code ec;
auto condition = Condition::deserialize(*cb, ec);
if (!condition)
{
JLOG(ctx.j.debug()) << "Malformed condition during escrow creation: " << ec.message();
return temMALFORMED;
}
}
return tesSUCCESS;
}
template <ValidIssueType T>
static TER
escrowCreatePreclaimHelper(
PreclaimContext const& ctx,
AccountID const& account,
AccountID const& dest,
STAmount const& amount);
template <>
TER
escrowCreatePreclaimHelper<Issue>(
PreclaimContext const& ctx,
AccountID const& account,
AccountID const& dest,
STAmount const& amount)
{
AccountID issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecNO_PERMISSION
if (issuer == account)
return tecNO_PERMISSION;
// If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION
auto const sleIssuer = ctx.view.read(keylet::account(issuer));
if (!sleIssuer)
return tecNO_ISSUER;
if (!sleIssuer->isFlag(lsfAllowTrustLineLocking))
return tecNO_PERMISSION;
// If the account does not have a trustline to the issuer, return tecNO_LINE
auto const sleRippleState = ctx.view.read(keylet::line(account, issuer, amount.getCurrency()));
if (!sleRippleState)
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)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If balance is negative, issuer must have lower address than account
if (balance < beast::zero && issuer > account)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If the issuer has requireAuth set, check if the account is authorized
if (auto const ter = requireAuth(ctx.view, amount.issue(), account); ter != tesSUCCESS)
return ter;
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(ctx.view, amount.issue(), dest); ter != tesSUCCESS)
return ter;
// If the issuer has frozen the account, return tecFROZEN
if (isFrozen(ctx.view, account, amount.issue()))
return tecFROZEN;
// If the issuer has frozen the destination, return tecFROZEN
if (isFrozen(ctx.view, dest, amount.issue()))
return tecFROZEN;
STAmount const spendableAmount =
accountHolds(ctx.view, account, amount.getCurrency(), issuer, fhIGNORE_FREEZE, ctx.j);
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::zero)
return tecINSUFFICIENT_FUNDS;
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < 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
escrowCreatePreclaimHelper<MPTIssue>(
PreclaimContext const& ctx,
AccountID const& account,
AccountID const& dest,
STAmount const& amount)
{
AccountID issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecNO_PERMISSION
if (issuer == account)
return tecNO_PERMISSION;
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = ctx.view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION
if (!sleIssuance->isFlag(lsfMPTCanEscrow))
return tecNO_PERMISSION;
// If the issuer is not the same as the issuer of the mpt, return
// tecNO_PERMISSION
if (sleIssuance->getAccountID(sfIssuer) != issuer)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If the account does not have the mpt, return tecOBJECT_NOT_FOUND
if (!ctx.view.exists(keylet::mptoken(issuanceKey.key, account)))
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(ctx.view, mptIssue, account, AuthType::WeakAuth);
ter != tesSUCCESS)
return ter;
// If the issuer has requireAuth set, check if the destination is
// authorized
if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth);
ter != tesSUCCESS)
return ter;
// If the issuer has frozen the account, return tecLOCKED
if (isFrozen(ctx.view, account, mptIssue))
return tecLOCKED;
// If the issuer has frozen the destination, return tecLOCKED
if (isFrozen(ctx.view, dest, mptIssue))
return tecLOCKED;
// If the mpt cannot be transferred, return tecNO_AUTH
if (auto const ter = canTransfer(ctx.view, mptIssue, account, dest); ter != tesSUCCESS)
return ter;
STAmount const spendableAmount = accountHolds(
ctx.view, account, amount.get<MPTIssue>(), fhIGNORE_FREEZE, ahIGNORE_AUTH, ctx.j);
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::zero)
return tecINSUFFICIENT_FUNDS;
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
return tecINSUFFICIENT_FUNDS;
return tesSUCCESS;
}
TER
EscrowCreate::preclaim(PreclaimContext const& ctx)
{
STAmount const amount{ctx.tx[sfAmount]};
AccountID const account{ctx.tx[sfAccount]};
AccountID const dest{ctx.tx[sfDestination]};
auto const sled = ctx.view.read(keylet::account(dest));
if (!sled)
return tecNO_DST;
// Pseudo-accounts cannot receive escrow. Note, this is not amendment-gated
// because all writes to pseudo-account discriminator fields **are**
// amendment gated, hence the behaviour of this check will always match the
// currently active amendments.
if (isPseudoAccount(sled))
return tecNO_PERMISSION;
if (!isXRP(amount))
{
if (!ctx.view.rules().enabled(featureTokenEscrow))
return temDISABLED; // LCOV_EXCL_LINE
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowCreatePreclaimHelper<T>(ctx, account, dest, amount);
},
amount.asset().value());
!isTesSuccess(ret))
return ret;
}
return tesSUCCESS;
}
template <ValidIssueType T>
static TER
escrowLockApplyHelper(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal);
template <>
TER
escrowLockApplyHelper<Issue>(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal)
{
// Defensive: Issuer cannot create an escrow
if (issuer == sender)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const ter = rippleCredit(
view, sender, issuer, amount, amount.holds<MPTIssue>() ? false : true, journal);
if (ter != tesSUCCESS)
return ter; // LCOV_EXCL_LINE
return tesSUCCESS;
}
template <>
TER
escrowLockApplyHelper<MPTIssue>(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal)
{
// Defensive: Issuer cannot create an escrow
if (issuer == sender)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const ter = rippleLockEscrowMPT(view, sender, amount, journal);
if (ter != tesSUCCESS)
return ter; // LCOV_EXCL_LINE
return tesSUCCESS;
}
TER
EscrowCreate::doApply()
{
auto const closeTime = ctx_.view().header().parentCloseTime;
if (ctx_.tx[~sfCancelAfter] && after(closeTime, ctx_.tx[sfCancelAfter]))
return tecNO_PERMISSION;
if (ctx_.tx[~sfFinishAfter] && after(closeTime, ctx_.tx[sfFinishAfter]))
return tecNO_PERMISSION;
auto const sle = ctx_.view().peek(keylet::account(account_));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
// Check reserve and funds availability
STAmount const amount{ctx_.tx[sfAmount]};
auto const reserve = ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1);
if (mSourceBalance < reserve)
return tecINSUFFICIENT_RESERVE;
// Check reserve and funds availability
if (isXRP(amount))
{
if (mSourceBalance < reserve + STAmount(amount).xrp())
return tecUNFUNDED;
}
// Check destination account
{
auto const sled = ctx_.view().read(keylet::account(ctx_.tx[sfDestination]));
if (!sled)
return tecNO_DST; // LCOV_EXCL_LINE
if (((*sled)[sfFlags] & lsfRequireDestTag) && !ctx_.tx[~sfDestinationTag])
return tecDST_TAG_NEEDED;
}
// Create escrow in ledger. Note that we we use the value from the
// sequence or ticket. For more explanation see comments in SeqProxy.h.
Keylet const escrowKeylet = keylet::escrow(account_, ctx_.tx.getSeqValue());
auto const slep = std::make_shared<SLE>(escrowKeylet);
(*slep)[sfAmount] = amount;
(*slep)[sfAccount] = account_;
(*slep)[~sfCondition] = ctx_.tx[~sfCondition];
(*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag];
(*slep)[sfDestination] = ctx_.tx[sfDestination];
(*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
(*slep)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter];
(*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag];
if (ctx_.view().rules().enabled(fixIncludeKeyletFields))
{
(*slep)[sfSequence] = ctx_.tx.getSeqValue();
}
if (ctx_.view().rules().enabled(featureTokenEscrow) && !isXRP(amount))
{
auto const xferRate = transferRate(ctx_.view(), amount);
if (xferRate != parityRate)
(*slep)[sfTransferRate] = xferRate.value;
}
ctx_.view().insert(slep);
// Add escrow to sender's owner directory
{
auto page = ctx_.view().dirInsert(
keylet::ownerDir(account_), escrowKeylet, describeOwnerDir(account_));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfOwnerNode] = *page;
}
// If it's not a self-send, add escrow to recipient's owner directory.
AccountID const dest = ctx_.tx[sfDestination];
if (dest != account_)
{
auto page =
ctx_.view().dirInsert(keylet::ownerDir(dest), escrowKeylet, describeOwnerDir(dest));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfDestinationNode] = *page;
}
// IOU escrow objects are added to the issuer's owner directory to help
// track the total locked balance. For MPT, this isn't necessary because the
// locked balance is already stored directly in the MPTokenIssuance object.
AccountID const issuer = amount.getIssuer();
if (!isXRP(amount) && issuer != account_ && issuer != dest && !amount.holds<MPTIssue>())
{
auto page =
ctx_.view().dirInsert(keylet::ownerDir(issuer), escrowKeylet, describeOwnerDir(issuer));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfIssuerNode] = *page;
}
// Deduct owner's balance
if (isXRP(amount))
(*sle)[sfBalance] = (*sle)[sfBalance] - amount;
else
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowLockApplyHelper<T>(ctx_.view(), issuer, account_, amount, j_);
},
amount.asset().value());
!isTesSuccess(ret))
{
return ret; // LCOV_EXCL_LINE
}
}
// increment owner count
adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal);
ctx_.view().update(sle);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -1,371 +0,0 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/conditions/Condition.h>
#include <xrpl/conditions/Fulfillment.h>
#include <xrpl/core/HashRouter.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/transactors/escrow/EscrowFinish.h>
#include <xrpl/tx/transactors/escrow/EscrowHelpers.h>
namespace xrpl {
// During an EscrowFinish, the transaction must specify both
// a condition and a fulfillment. We track whether that
// fulfillment matches and validates the condition.
constexpr HashRouterFlags SF_CF_INVALID = HashRouterFlags::PRIVATE5;
constexpr HashRouterFlags SF_CF_VALID = HashRouterFlags::PRIVATE6;
//------------------------------------------------------------------------------
static bool
checkCondition(Slice f, Slice c)
{
using namespace xrpl::cryptoconditions;
std::error_code ec;
auto condition = Condition::deserialize(c, ec);
if (!condition)
return false;
auto fulfillment = Fulfillment::deserialize(f, ec);
if (!fulfillment)
return false;
return validate(*fulfillment, *condition);
}
bool
EscrowFinish::checkExtraFeatures(PreflightContext const& ctx)
{
return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials);
}
NotTEC
EscrowFinish::preflight(PreflightContext const& ctx)
{
auto const cb = ctx.tx[~sfCondition];
auto const fb = ctx.tx[~sfFulfillment];
// If you specify a condition, then you must also specify
// a fulfillment.
if (static_cast<bool>(cb) != static_cast<bool>(fb))
return temMALFORMED;
return tesSUCCESS;
}
NotTEC
EscrowFinish::preflightSigValidated(PreflightContext const& ctx)
{
auto const cb = ctx.tx[~sfCondition];
auto const fb = ctx.tx[~sfFulfillment];
if (cb && fb)
{
auto& router = ctx.registry.getHashRouter();
auto const id = ctx.tx.getTransactionID();
auto const flags = router.getFlags(id);
// If we haven't checked the condition, check it
// now. Whether it passes or not isn't important
// in preflight.
if (!any(flags & (SF_CF_INVALID | SF_CF_VALID)))
{
if (checkCondition(*fb, *cb))
router.setFlags(id, SF_CF_VALID);
else
router.setFlags(id, SF_CF_INVALID);
}
}
if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err))
return err;
return tesSUCCESS;
}
XRPAmount
EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx)
{
XRPAmount extraFee{0};
if (auto const fb = tx[~sfFulfillment])
{
extraFee += view.fees().base * (32 + (fb->size() / 16));
}
return Transactor::calculateBaseFee(view, tx) + extraFee;
}
template <ValidIssueType T>
static TER
escrowFinishPreclaimHelper(
PreclaimContext const& ctx,
AccountID const& dest,
STAmount const& amount);
template <>
TER
escrowFinishPreclaimHelper<Issue>(
PreclaimContext const& ctx,
AccountID const& dest,
STAmount const& amount)
{
AccountID issuer = amount.getIssuer();
// If the issuer is the same as the account, return tesSUCCESS
if (issuer == dest)
return tesSUCCESS;
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(ctx.view, amount.issue(), dest); ter != tesSUCCESS)
return ter;
// If the issuer has deep frozen the destination, return tecFROZEN
if (isDeepFrozen(ctx.view, dest, amount.getCurrency(), amount.getIssuer()))
return tecFROZEN;
return tesSUCCESS;
}
template <>
TER
escrowFinishPreclaimHelper<MPTIssue>(
PreclaimContext const& ctx,
AccountID const& dest,
STAmount const& amount)
{
AccountID issuer = amount.getIssuer();
// If the issuer is the same as the dest, return tesSUCCESS
if (issuer == dest)
return tesSUCCESS;
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = ctx.view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the destination is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth);
ter != tesSUCCESS)
return ter;
// If the issuer has frozen the destination, return tecLOCKED
if (isFrozen(ctx.view, dest, mptIssue))
return tecLOCKED;
return tesSUCCESS;
}
TER
EscrowFinish::preclaim(PreclaimContext const& ctx)
{
if (ctx.view.rules().enabled(featureCredentials))
{
if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j);
!isTesSuccess(err))
return err;
}
if (ctx.view.rules().enabled(featureTokenEscrow))
{
auto const k = keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence]);
auto const slep = ctx.view.read(k);
if (!slep)
return tecNO_TARGET;
AccountID const dest = (*slep)[sfDestination];
STAmount const amount = (*slep)[sfAmount];
if (!isXRP(amount))
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowFinishPreclaimHelper<T>(ctx, dest, amount);
},
amount.asset().value());
!isTesSuccess(ret))
return ret;
}
}
return tesSUCCESS;
}
TER
EscrowFinish::doApply()
{
auto const k = keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]);
auto const slep = ctx_.view().peek(k);
if (!slep)
{
if (ctx_.view().rules().enabled(featureTokenEscrow))
return tecINTERNAL; // LCOV_EXCL_LINE
return tecNO_TARGET;
}
// If a cancel time is present, a finish operation should only succeed prior
// to that time.
auto const now = ctx_.view().header().parentCloseTime;
// Too soon: can't execute before the finish time
if ((*slep)[~sfFinishAfter] && !after(now, (*slep)[sfFinishAfter]))
return tecNO_PERMISSION;
// Too late: can't execute after the cancel time
if ((*slep)[~sfCancelAfter] && after(now, (*slep)[sfCancelAfter]))
return tecNO_PERMISSION;
// Check cryptocondition fulfillment
{
auto const id = ctx_.tx.getTransactionID();
auto flags = ctx_.registry.getHashRouter().getFlags(id);
auto const cb = ctx_.tx[~sfCondition];
// It's unlikely that the results of the check will
// expire from the hash router, but if it happens,
// simply re-run the check.
if (cb && !any(flags & (SF_CF_INVALID | SF_CF_VALID)))
{
// LCOV_EXCL_START
auto const fb = ctx_.tx[~sfFulfillment];
if (!fb)
return tecINTERNAL;
if (checkCondition(*fb, *cb))
flags = SF_CF_VALID;
else
flags = SF_CF_INVALID;
ctx_.registry.getHashRouter().setFlags(id, flags);
// LCOV_EXCL_STOP
}
// If the check failed, then simply return an error
// and don't look at anything else.
if (any(flags & SF_CF_INVALID))
return tecCRYPTOCONDITION_ERROR;
// Check against condition in the ledger entry:
auto const cond = (*slep)[~sfCondition];
// If a condition wasn't specified during creation,
// one shouldn't be included now.
if (!cond && cb)
return tecCRYPTOCONDITION_ERROR;
// If a condition was specified during creation of
// the suspended payment, the identical condition
// must be presented again. We don't check if the
// fulfillment matches the condition since we did
// that in preflight.
if (cond && (cond != cb))
return tecCRYPTOCONDITION_ERROR;
}
// NOTE: Escrow payments cannot be used to fund accounts.
AccountID const destID = (*slep)[sfDestination];
auto const sled = ctx_.view().peek(keylet::account(destID));
if (!sled)
return tecNO_DST;
if (auto err = verifyDepositPreauth(ctx_.tx, ctx_.view(), account_, destID, sled, ctx_.journal);
!isTesSuccess(err))
return err;
AccountID const account = (*slep)[sfAccount];
// Remove escrow from owner directory
{
auto const page = (*slep)[sfOwnerNode];
if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, k.key, true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
// Remove escrow from recipient's owner directory, if present.
if (auto const optPage = (*slep)[~sfDestinationNode])
{
if (!ctx_.view().dirRemove(keylet::ownerDir(destID), *optPage, k.key, true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
STAmount const amount = slep->getFieldAmount(sfAmount);
// Transfer amount to destination
if (isXRP(amount))
(*sled)[sfBalance] = (*sled)[sfBalance] + amount;
else
{
if (!ctx_.view().rules().enabled(featureTokenEscrow))
return temDISABLED; // LCOV_EXCL_LINE
Rate lockedRate = slep->isFieldPresent(sfTransferRate)
? xrpl::Rate(slep->getFieldU32(sfTransferRate))
: parityRate;
auto const issuer = amount.getIssuer();
bool const createAsset = destID == account_;
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowUnlockApplyHelper<T>(
ctx_.view(),
lockedRate,
sled,
mPriorBalance,
amount,
issuer,
account,
destID,
createAsset,
j_);
},
amount.asset().value());
!isTesSuccess(ret))
return ret;
// Remove escrow from issuers owner directory, if present.
if (auto const optPage = (*slep)[~sfIssuerNode]; optPage)
{
if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, k.key, true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete Escrow from recipient.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
}
ctx_.view().update(sled);
// Adjust source owner count
auto const sle = ctx_.view().peek(keylet::account(account));
adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal);
ctx_.view().update(sle);
// Remove escrow from ledger
ctx_.view().erase(slep);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -0,0 +1,542 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PayChan.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/tx/transactors/payment_channel/PayChan.h>
namespace xrpl {
/*
PaymentChannel
Payment channels permit off-ledger checkpoints of XRP payments flowing
in a single direction. A channel sequesters the owner's XRP in its own
ledger entry. The owner can authorize the recipient to claim up to a
given balance by giving the receiver a signed message (off-ledger). The
recipient can use this signed message to claim any unpaid balance while
the channel remains open. The owner can top off the line as needed. If
the channel has not paid out all its funds, the owner must wait out a
delay to close the channel to give the recipient a chance to supply any
claims. The recipient can close the channel at any time. Any transaction
that touches the channel after the expiration time will close the
channel. The total amount paid increases monotonically as newer claims
are issued. When the channel is closed any remaining balance is returned
to the owner. Channels are intended to permit intermittent off-ledger
settlement of ILP trust lines as balances get substantial. For
bidirectional channels, a payment channel can be used in each direction.
PaymentChannelCreate
Create a unidirectional channel. The parameters are:
Destination
The recipient at the end of the channel.
Amount
The amount of XRP to deposit in the channel immediately.
SettleDelay
The amount of time everyone but the recipient must wait for a
superior claim.
PublicKey
The key that will sign claims against the channel.
CancelAfter (optional)
Any channel transaction that touches this channel after the
`CancelAfter` time will close it.
DestinationTag (optional)
Destination tags allow the different accounts inside of a Hosted
Wallet to be mapped back onto the Ripple ledger. The destination tag
tells the server to which account in the Hosted Wallet the funds are
intended to go to. Required if the destination has lsfRequireDestTag
set.
SourceTag (optional)
Source tags allow the different accounts inside of a Hosted Wallet
to be mapped back onto the Ripple ledger. Source tags are similar to
destination tags but are for the channel owner to identify their own
transactions.
PaymentChannelFund
Add additional funds to the payment channel. Only the channel owner may
use this transaction. The parameters are:
Channel
The 256-bit ID of the channel.
Amount
The amount of XRP to add.
Expiration (optional)
Time the channel closes. The transaction will fail if the expiration
times does not satisfy the SettleDelay constraints.
PaymentChannelClaim
Place a claim against an existing channel. The parameters are:
Channel
The 256-bit ID of the channel.
Balance (optional)
The total amount of XRP delivered after this claim is processed
(optional, not needed if just closing). Amount (optional) The amount of XRP
the signature is for (not needed if equal to Balance or just closing the
line). Signature (optional) Authorization for the balance above, signed by
the owner (optional, not needed if closing or owner is performing the
transaction). The signature if for the following message: CLM\0 followed by
the 256-bit channel ID, and a 64-bit integer drops. PublicKey (optional) The
public key that made the signature (optional, required if a signature is
present) Flags tfClose Request that the channel be closed tfRenew Request
that the channel's expiration be reset. Only the owner may renew a channel.
*/
//------------------------------------------------------------------------------
static TER
closeChannel(
std::shared_ptr<SLE> const& slep,
ApplyView& view,
uint256 const& key,
beast::Journal j)
{
AccountID const src = (*slep)[sfAccount];
// Remove PayChan from owner directory
{
auto const page = (*slep)[sfOwnerNode];
if (!view.dirRemove(keylet::ownerDir(src), page, key, true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not remove paychan from src owner directory";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
// Remove PayChan from recipient's owner directory, if present.
if (auto const page = (*slep)[~sfDestinationNode])
{
auto const dst = (*slep)[sfDestination];
if (!view.dirRemove(keylet::ownerDir(dst), *page, key, true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not remove paychan from dst owner directory";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
// Transfer amount back to owner, decrement owner count
auto const sle = view.peek(keylet::account(src));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
XRPL_ASSERT(
(*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount");
(*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance];
adjustOwnerCount(view, sle, -1, j);
view.update(sle);
// Remove PayChan from ledger
view.erase(slep);
return tesSUCCESS;
}
//------------------------------------------------------------------------------
TxConsequences
PayChanCreate::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()};
}
NotTEC
PayChanCreate::preflight(PreflightContext const& ctx)
{
if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero))
return temBAD_AMOUNT;
if (ctx.tx[sfAccount] == ctx.tx[sfDestination])
return temDST_IS_SRC;
if (!publicKeyType(ctx.tx[sfPublicKey]))
return temMALFORMED;
return tesSUCCESS;
}
TER
PayChanCreate::preclaim(PreclaimContext const& ctx)
{
auto const account = ctx.tx[sfAccount];
auto const sle = ctx.view.read(keylet::account(account));
if (!sle)
return terNO_ACCOUNT;
// Check reserve and funds availability
{
auto const balance = (*sle)[sfBalance];
auto const reserve = ctx.view.fees().accountReserve((*sle)[sfOwnerCount] + 1);
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
if (balance < reserve + ctx.tx[sfAmount])
return tecUNFUNDED;
}
auto const dst = ctx.tx[sfDestination];
{
// Check destination account
auto const sled = ctx.view.read(keylet::account(dst));
if (!sled)
return tecNO_DST;
auto const flags = sled->getFlags();
// Check if they have disallowed incoming payment channels
if (flags & lsfDisallowIncomingPayChan)
return tecNO_PERMISSION;
if ((flags & lsfRequireDestTag) && !ctx.tx[~sfDestinationTag])
return tecDST_TAG_NEEDED;
// Pseudo-accounts cannot receive payment channels, other than native
// to their underlying ledger object - implemented in their respective
// transaction types. Note, this is not amendment-gated because all
// writes to pseudo-account discriminator fields **are** amendment
// gated, hence the behaviour of this check will always match the
// currently active amendments.
if (isPseudoAccount(sled))
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
TER
PayChanCreate::doApply()
{
auto const account = ctx_.tx[sfAccount];
auto const sle = ctx_.view().peek(keylet::account(account));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
if (ctx_.view().rules().enabled(fixPayChanCancelAfter))
{
auto const closeTime = ctx_.view().header().parentCloseTime;
if (ctx_.tx[~sfCancelAfter] && after(closeTime, ctx_.tx[sfCancelAfter]))
return tecEXPIRED;
}
auto const dst = ctx_.tx[sfDestination];
// Create PayChan in ledger.
//
// Note that we we use the value from the sequence or ticket as the
// payChan sequence. For more explanation see comments in SeqProxy.h.
Keylet const payChanKeylet = keylet::payChan(account, dst, ctx_.tx.getSeqValue());
auto const slep = std::make_shared<SLE>(payChanKeylet);
// Funds held in this channel
(*slep)[sfAmount] = ctx_.tx[sfAmount];
// Amount channel has already paid
(*slep)[sfBalance] = ctx_.tx[sfAmount].zeroed();
(*slep)[sfAccount] = account;
(*slep)[sfDestination] = dst;
(*slep)[sfSettleDelay] = ctx_.tx[sfSettleDelay];
(*slep)[sfPublicKey] = ctx_.tx[sfPublicKey];
(*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
(*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag];
(*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag];
if (ctx_.view().rules().enabled(fixIncludeKeyletFields))
{
(*slep)[sfSequence] = ctx_.tx.getSeqValue();
}
ctx_.view().insert(slep);
// Add PayChan to owner directory
{
auto const page = ctx_.view().dirInsert(
keylet::ownerDir(account), payChanKeylet, describeOwnerDir(account));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfOwnerNode] = *page;
}
// Add PayChan to the recipient's owner directory
{
auto const page =
ctx_.view().dirInsert(keylet::ownerDir(dst), payChanKeylet, describeOwnerDir(dst));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfDestinationNode] = *page;
}
// Deduct owner's balance, increment owner count
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal);
ctx_.view().update(sle);
return tesSUCCESS;
}
//------------------------------------------------------------------------------
TxConsequences
PayChanFund::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()};
}
NotTEC
PayChanFund::preflight(PreflightContext const& ctx)
{
if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero))
return temBAD_AMOUNT;
return tesSUCCESS;
}
TER
PayChanFund::doApply()
{
Keylet const k(ltPAYCHAN, ctx_.tx[sfChannel]);
auto const slep = ctx_.view().peek(k);
if (!slep)
return tecNO_ENTRY;
AccountID const src = (*slep)[sfAccount];
auto const txAccount = ctx_.tx[sfAccount];
auto const expiration = (*slep)[~sfExpiration];
{
auto const cancelAfter = (*slep)[~sfCancelAfter];
auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count();
if ((cancelAfter && closeTime >= *cancelAfter) || (expiration && closeTime >= *expiration))
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.journal("View"));
}
if (src != txAccount)
// only the owner can add funds or extend
return tecNO_PERMISSION;
if (auto extend = ctx_.tx[~sfExpiration])
{
auto minExpiration = ctx_.view().header().parentCloseTime.time_since_epoch().count() +
(*slep)[sfSettleDelay];
if (expiration && *expiration < minExpiration)
minExpiration = *expiration;
if (*extend < minExpiration)
return temBAD_EXPIRATION;
(*slep)[~sfExpiration] = *extend;
ctx_.view().update(slep);
}
auto const sle = ctx_.view().peek(keylet::account(txAccount));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
{
// Check reserve and funds availability
auto const balance = (*sle)[sfBalance];
auto const reserve = ctx_.view().fees().accountReserve((*sle)[sfOwnerCount]);
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
if (balance < reserve + ctx_.tx[sfAmount])
return tecUNFUNDED;
}
// do not allow adding funds if dst does not exist
if (AccountID const dst = (*slep)[sfDestination]; !ctx_.view().read(keylet::account(dst)))
{
return tecNO_DST;
}
(*slep)[sfAmount] = (*slep)[sfAmount] + ctx_.tx[sfAmount];
ctx_.view().update(slep);
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
ctx_.view().update(sle);
return tesSUCCESS;
}
//------------------------------------------------------------------------------
bool
PayChanClaim::checkExtraFeatures(PreflightContext const& ctx)
{
return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials);
}
std::uint32_t
PayChanClaim::getFlagsMask(PreflightContext const&)
{
return tfPaymentChannelClaimMask;
}
NotTEC
PayChanClaim::preflight(PreflightContext const& ctx)
{
auto const bal = ctx.tx[~sfBalance];
if (bal && (!isXRP(*bal) || *bal <= beast::zero))
return temBAD_AMOUNT;
auto const amt = ctx.tx[~sfAmount];
if (amt && (!isXRP(*amt) || *amt <= beast::zero))
return temBAD_AMOUNT;
if (bal && amt && *bal > *amt)
return temBAD_AMOUNT;
{
auto const flags = ctx.tx.getFlags();
if ((flags & tfClose) && (flags & tfRenew))
return temMALFORMED;
}
if (auto const sig = ctx.tx[~sfSignature])
{
if (!(ctx.tx[~sfPublicKey] && bal))
return temMALFORMED;
// Check the signature
// The signature isn't needed if txAccount == src, but if it's
// present, check it
auto const reqBalance = bal->xrp();
auto const authAmt = amt ? amt->xrp() : reqBalance;
if (reqBalance > authAmt)
return temBAD_AMOUNT;
Keylet const k(ltPAYCHAN, ctx.tx[sfChannel]);
if (!publicKeyType(ctx.tx[sfPublicKey]))
return temMALFORMED;
PublicKey const pk(ctx.tx[sfPublicKey]);
Serializer msg;
serializePayChanAuthorization(msg, k.key, authAmt);
if (!verify(pk, msg.slice(), *sig))
return temBAD_SIGNATURE;
}
if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err))
return err;
return tesSUCCESS;
}
TER
PayChanClaim::preclaim(PreclaimContext const& ctx)
{
if (!ctx.view.rules().enabled(featureCredentials))
return Transactor::preclaim(ctx);
if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j);
!isTesSuccess(err))
return err;
return tesSUCCESS;
}
TER
PayChanClaim::doApply()
{
Keylet const k(ltPAYCHAN, ctx_.tx[sfChannel]);
auto const slep = ctx_.view().peek(k);
if (!slep)
return tecNO_TARGET;
AccountID const src = (*slep)[sfAccount];
AccountID const dst = (*slep)[sfDestination];
AccountID const txAccount = ctx_.tx[sfAccount];
auto const curExpiration = (*slep)[~sfExpiration];
{
auto const cancelAfter = (*slep)[~sfCancelAfter];
auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count();
if ((cancelAfter && closeTime >= *cancelAfter) ||
(curExpiration && closeTime >= *curExpiration))
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.journal("View"));
}
if (txAccount != src && txAccount != dst)
return tecNO_PERMISSION;
if (ctx_.tx[~sfBalance])
{
auto const chanBalance = slep->getFieldAmount(sfBalance).xrp();
auto const chanFunds = slep->getFieldAmount(sfAmount).xrp();
auto const reqBalance = ctx_.tx[sfBalance].xrp();
if (txAccount == dst && !ctx_.tx[~sfSignature])
return temBAD_SIGNATURE;
if (ctx_.tx[~sfSignature])
{
PublicKey const pk((*slep)[sfPublicKey]);
if (ctx_.tx[sfPublicKey] != pk)
return temBAD_SIGNER;
}
if (reqBalance > chanFunds)
return tecUNFUNDED_PAYMENT;
if (reqBalance <= chanBalance)
// nothing requested
return tecUNFUNDED_PAYMENT;
auto const sled = ctx_.view().peek(keylet::account(dst));
if (!sled)
return tecNO_DST;
if (auto err =
verifyDepositPreauth(ctx_.tx, ctx_.view(), txAccount, dst, sled, ctx_.journal);
!isTesSuccess(err))
return err;
(*slep)[sfBalance] = ctx_.tx[sfBalance];
XRPAmount const reqDelta = reqBalance - chanBalance;
XRPL_ASSERT(reqDelta >= beast::zero, "xrpl::PayChanClaim::doApply : minimum balance delta");
(*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta;
ctx_.view().update(sled);
ctx_.view().update(slep);
}
if (ctx_.tx.getFlags() & tfRenew)
{
if (src != txAccount)
return tecNO_PERMISSION;
(*slep)[~sfExpiration] = std::nullopt;
ctx_.view().update(slep);
}
if (ctx_.tx.getFlags() & tfClose)
{
// Channel will close immediately if dry or the receiver closes
if (dst == txAccount || (*slep)[sfBalance] == (*slep)[sfAmount])
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.journal("View"));
auto const settleExpiration =
ctx_.view().header().parentCloseTime.time_since_epoch().count() +
(*slep)[sfSettleDelay];
if (!curExpiration || *curExpiration > settleExpiration)
{
(*slep)[~sfExpiration] = settleExpiration;
ctx_.view().update(slep);
}
}
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -1,185 +0,0 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PayChan.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/payment_channel/PayChanClaim.h>
#include <xrpl/tx/transactors/payment_channel/PayChanHelpers.h>
namespace xrpl {
bool
PayChanClaim::checkExtraFeatures(PreflightContext const& ctx)
{
return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials);
}
std::uint32_t
PayChanClaim::getFlagsMask(PreflightContext const&)
{
return tfPaymentChannelClaimMask;
}
NotTEC
PayChanClaim::preflight(PreflightContext const& ctx)
{
auto const bal = ctx.tx[~sfBalance];
if (bal && (!isXRP(*bal) || *bal <= beast::zero))
return temBAD_AMOUNT;
auto const amt = ctx.tx[~sfAmount];
if (amt && (!isXRP(*amt) || *amt <= beast::zero))
return temBAD_AMOUNT;
if (bal && amt && *bal > *amt)
return temBAD_AMOUNT;
{
auto const flags = ctx.tx.getFlags();
if ((flags & tfClose) && (flags & tfRenew))
return temMALFORMED;
}
if (auto const sig = ctx.tx[~sfSignature])
{
if (!(ctx.tx[~sfPublicKey] && bal))
return temMALFORMED;
// Check the signature
// The signature isn't needed if txAccount == src, but if it's
// present, check it
auto const reqBalance = bal->xrp();
auto const authAmt = amt ? amt->xrp() : reqBalance;
if (reqBalance > authAmt)
return temBAD_AMOUNT;
Keylet const k(ltPAYCHAN, ctx.tx[sfChannel]);
if (!publicKeyType(ctx.tx[sfPublicKey]))
return temMALFORMED;
PublicKey const pk(ctx.tx[sfPublicKey]);
Serializer msg;
serializePayChanAuthorization(msg, k.key, authAmt);
if (!verify(pk, msg.slice(), *sig))
return temBAD_SIGNATURE;
}
if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err))
return err;
return tesSUCCESS;
}
TER
PayChanClaim::preclaim(PreclaimContext const& ctx)
{
if (!ctx.view.rules().enabled(featureCredentials))
return Transactor::preclaim(ctx);
if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j);
!isTesSuccess(err))
return err;
return tesSUCCESS;
}
TER
PayChanClaim::doApply()
{
Keylet const k(ltPAYCHAN, ctx_.tx[sfChannel]);
auto const slep = ctx_.view().peek(k);
if (!slep)
return tecNO_TARGET;
AccountID const src = (*slep)[sfAccount];
AccountID const dst = (*slep)[sfDestination];
AccountID const txAccount = ctx_.tx[sfAccount];
auto const curExpiration = (*slep)[~sfExpiration];
{
auto const cancelAfter = (*slep)[~sfCancelAfter];
auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count();
if ((cancelAfter && closeTime >= *cancelAfter) ||
(curExpiration && closeTime >= *curExpiration))
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.journal("View"));
}
if (txAccount != src && txAccount != dst)
return tecNO_PERMISSION;
if (ctx_.tx[~sfBalance])
{
auto const chanBalance = slep->getFieldAmount(sfBalance).xrp();
auto const chanFunds = slep->getFieldAmount(sfAmount).xrp();
auto const reqBalance = ctx_.tx[sfBalance].xrp();
if (txAccount == dst && !ctx_.tx[~sfSignature])
return temBAD_SIGNATURE;
if (ctx_.tx[~sfSignature])
{
PublicKey const pk((*slep)[sfPublicKey]);
if (ctx_.tx[sfPublicKey] != pk)
return temBAD_SIGNER;
}
if (reqBalance > chanFunds)
return tecUNFUNDED_PAYMENT;
if (reqBalance <= chanBalance)
// nothing requested
return tecUNFUNDED_PAYMENT;
auto const sled = ctx_.view().peek(keylet::account(dst));
if (!sled)
return tecNO_DST;
if (auto err =
verifyDepositPreauth(ctx_.tx, ctx_.view(), txAccount, dst, sled, ctx_.journal);
!isTesSuccess(err))
return err;
(*slep)[sfBalance] = ctx_.tx[sfBalance];
XRPAmount const reqDelta = reqBalance - chanBalance;
XRPL_ASSERT(reqDelta >= beast::zero, "xrpl::PayChanClaim::doApply : minimum balance delta");
(*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta;
ctx_.view().update(sled);
ctx_.view().update(slep);
}
if (ctx_.tx.getFlags() & tfRenew)
{
if (src != txAccount)
return tecNO_PERMISSION;
(*slep)[~sfExpiration] = std::nullopt;
ctx_.view().update(slep);
}
if (ctx_.tx.getFlags() & tfClose)
{
// Channel will close immediately if dry or the receiver closes
if (dst == txAccount || (*slep)[sfBalance] == (*slep)[sfAmount])
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.journal("View"));
auto const settleExpiration =
ctx_.view().header().parentCloseTime.time_since_epoch().count() +
(*slep)[sfSettleDelay];
if (!curExpiration || *curExpiration > settleExpiration)
{
(*slep)[~sfExpiration] = settleExpiration;
ctx_.view().update(slep);
}
}
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -1,173 +0,0 @@
#include <xrpl/basics/chrono.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/transactors/payment_channel/PayChanCreate.h>
namespace xrpl {
/*
PaymentChannel
Payment channels permit off-ledger checkpoints of XRP payments flowing
in a single direction. A channel sequesters the owner's XRP in its own
ledger entry. The owner can authorize the recipient to claim up to a
given balance by giving the receiver a signed message (off-ledger). The
recipient can use this signed message to claim any unpaid balance while
the channel remains open. The owner can top off the line as needed. If
the channel has not paid out all its funds, the owner must wait out a
delay to close the channel to give the recipient a chance to supply any
claims. The recipient can close the channel at any time. Any transaction
that touches the channel after the expiration time will close the
channel. The total amount paid increases monotonically as newer claims
are issued. When the channel is closed any remaining balance is returned
to the owner. Channels are intended to permit intermittent off-ledger
settlement of ILP trust lines as balances get substantial. For
bidirectional channels, a payment channel can be used in each direction.
*/
//------------------------------------------------------------------------------
TxConsequences
PayChanCreate::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()};
}
NotTEC
PayChanCreate::preflight(PreflightContext const& ctx)
{
if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero))
return temBAD_AMOUNT;
if (ctx.tx[sfAccount] == ctx.tx[sfDestination])
return temDST_IS_SRC;
if (!publicKeyType(ctx.tx[sfPublicKey]))
return temMALFORMED;
return tesSUCCESS;
}
TER
PayChanCreate::preclaim(PreclaimContext const& ctx)
{
auto const account = ctx.tx[sfAccount];
auto const sle = ctx.view.read(keylet::account(account));
if (!sle)
return terNO_ACCOUNT;
// Check reserve and funds availability
{
auto const balance = (*sle)[sfBalance];
auto const reserve = ctx.view.fees().accountReserve((*sle)[sfOwnerCount] + 1);
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
if (balance < reserve + ctx.tx[sfAmount])
return tecUNFUNDED;
}
auto const dst = ctx.tx[sfDestination];
{
// Check destination account
auto const sled = ctx.view.read(keylet::account(dst));
if (!sled)
return tecNO_DST;
auto const flags = sled->getFlags();
// Check if they have disallowed incoming payment channels
if (flags & lsfDisallowIncomingPayChan)
return tecNO_PERMISSION;
if ((flags & lsfRequireDestTag) && !ctx.tx[~sfDestinationTag])
return tecDST_TAG_NEEDED;
// Pseudo-accounts cannot receive payment channels, other than native
// to their underlying ledger object - implemented in their respective
// transaction types. Note, this is not amendment-gated because all
// writes to pseudo-account discriminator fields **are** amendment
// gated, hence the behaviour of this check will always match the
// currently active amendments.
if (isPseudoAccount(sled))
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
TER
PayChanCreate::doApply()
{
auto const account = ctx_.tx[sfAccount];
auto const sle = ctx_.view().peek(keylet::account(account));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
if (ctx_.view().rules().enabled(fixPayChanCancelAfter))
{
auto const closeTime = ctx_.view().header().parentCloseTime;
if (ctx_.tx[~sfCancelAfter] && after(closeTime, ctx_.tx[sfCancelAfter]))
return tecEXPIRED;
}
auto const dst = ctx_.tx[sfDestination];
// Create PayChan in ledger.
//
// Note that we we use the value from the sequence or ticket as the
// payChan sequence. For more explanation see comments in SeqProxy.h.
Keylet const payChanKeylet = keylet::payChan(account, dst, ctx_.tx.getSeqValue());
auto const slep = std::make_shared<SLE>(payChanKeylet);
// Funds held in this channel
(*slep)[sfAmount] = ctx_.tx[sfAmount];
// Amount channel has already paid
(*slep)[sfBalance] = ctx_.tx[sfAmount].zeroed();
(*slep)[sfAccount] = account;
(*slep)[sfDestination] = dst;
(*slep)[sfSettleDelay] = ctx_.tx[sfSettleDelay];
(*slep)[sfPublicKey] = ctx_.tx[sfPublicKey];
(*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
(*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag];
(*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag];
if (ctx_.view().rules().enabled(fixIncludeKeyletFields))
{
(*slep)[sfSequence] = ctx_.tx.getSeqValue();
}
ctx_.view().insert(slep);
// Add PayChan to owner directory
{
auto const page = ctx_.view().dirInsert(
keylet::ownerDir(account), payChanKeylet, describeOwnerDir(account));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfOwnerNode] = *page;
}
// Add PayChan to the recipient's owner directory
{
auto const page =
ctx_.view().dirInsert(keylet::ownerDir(dst), payChanKeylet, describeOwnerDir(dst));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfDestinationNode] = *page;
}
// Deduct owner's balance, increment owner count
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal);
ctx_.view().update(sle);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -1,91 +0,0 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/tx/transactors/payment_channel/PayChanFund.h>
#include <xrpl/tx/transactors/payment_channel/PayChanHelpers.h>
namespace xrpl {
TxConsequences
PayChanFund::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()};
}
NotTEC
PayChanFund::preflight(PreflightContext const& ctx)
{
if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero))
return temBAD_AMOUNT;
return tesSUCCESS;
}
TER
PayChanFund::doApply()
{
Keylet const k(ltPAYCHAN, ctx_.tx[sfChannel]);
auto const slep = ctx_.view().peek(k);
if (!slep)
return tecNO_ENTRY;
AccountID const src = (*slep)[sfAccount];
auto const txAccount = ctx_.tx[sfAccount];
auto const expiration = (*slep)[~sfExpiration];
{
auto const cancelAfter = (*slep)[~sfCancelAfter];
auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count();
if ((cancelAfter && closeTime >= *cancelAfter) || (expiration && closeTime >= *expiration))
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.journal("View"));
}
if (src != txAccount)
// only the owner can add funds or extend
return tecNO_PERMISSION;
if (auto extend = ctx_.tx[~sfExpiration])
{
auto minExpiration = ctx_.view().header().parentCloseTime.time_since_epoch().count() +
(*slep)[sfSettleDelay];
if (expiration && *expiration < minExpiration)
minExpiration = *expiration;
if (*extend < minExpiration)
return temBAD_EXPIRATION;
(*slep)[~sfExpiration] = *extend;
ctx_.view().update(slep);
}
auto const sle = ctx_.view().peek(keylet::account(txAccount));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
{
// Check reserve and funds availability
auto const balance = (*sle)[sfBalance];
auto const reserve = ctx_.view().fees().accountReserve((*sle)[sfOwnerCount]);
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
if (balance < reserve + ctx_.tx[sfAmount])
return tecUNFUNDED;
}
// do not allow adding funds if dst does not exist
if (AccountID const dst = (*slep)[sfDestination]; !ctx_.view().read(keylet::account(dst)))
{
return tecNO_DST;
}
(*slep)[sfAmount] = (*slep)[sfAmount] + ctx_.tx[sfAmount];
ctx_.view().update(slep);
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
ctx_.view().update(sle);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -1,57 +0,0 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/tx/transactors/payment_channel/PayChanHelpers.h>
namespace xrpl {
TER
closeChannel(
std::shared_ptr<SLE> const& slep,
ApplyView& view,
uint256 const& key,
beast::Journal j)
{
AccountID const src = (*slep)[sfAccount];
// Remove PayChan from owner directory
{
auto const page = (*slep)[sfOwnerNode];
if (!view.dirRemove(keylet::ownerDir(src), page, key, true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not remove paychan from src owner directory";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
// Remove PayChan from recipient's owner directory, if present.
if (auto const page = (*slep)[~sfDestinationNode])
{
auto const dst = (*slep)[sfDestination];
if (!view.dirRemove(keylet::ownerDir(dst), *page, key, true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not remove paychan from dst owner directory";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
// Transfer amount back to owner, decrement owner count
auto const sle = view.peek(keylet::account(src));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
XRPL_ASSERT(
(*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount");
(*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance];
adjustOwnerCount(view, sle, -1, j);
view.update(sle);
// Remove PayChan from ledger
view.erase(slep);
return tesSUCCESS;
}
} // namespace xrpl