mirror of
https://github.com/XRPLF/clio.git
synced 2026-08-28 16:10:51 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ee9bb92ae | ||
|
|
4afc3fdb76 | ||
|
|
8a91ba85c4 | ||
|
|
7d33ecac0f | ||
|
|
f41d533cf7 | ||
|
|
b0a3f85f08 | ||
|
|
96f56edb01 | ||
|
|
2e7f303b7a | ||
|
|
292d55aa1c | ||
|
|
3ad2ccddaa | ||
|
|
bdfbb78115 | ||
|
|
74f5dcc95e | ||
|
|
10b3eee511 | ||
|
|
e6c91a1572 | ||
|
|
4f884e3b4b | ||
|
|
8e091871cb | ||
|
|
e8e0478333 | ||
|
|
62428a2d91 | ||
|
|
dbdbe4ea1f | ||
|
|
f490117cfb | ||
|
|
466b60f8ea | ||
|
|
6ce668f493 | ||
|
|
d2d0f39614 | ||
|
|
c29c4f0a62 |
@@ -165,6 +165,8 @@ CheckOptions:
|
||||
|
||||
readability-braces-around-statements.ShortStatementLines: 2
|
||||
readability-identifier-naming.MacroDefinitionCase: UPPER_CASE
|
||||
readability-identifier-naming.NamespaceCase: lower_case
|
||||
readability-identifier-naming.InlineNamespaceCase: lower_case
|
||||
readability-identifier-naming.ClassCase: CamelCase
|
||||
readability-identifier-naming.StructCase: CamelCase
|
||||
readability-identifier-naming.UnionCase: CamelCase
|
||||
|
||||
@@ -34,14 +34,14 @@ runs:
|
||||
steps:
|
||||
- name: Login to DockerHub
|
||||
if: ${{ inputs.push_image == 'true' && inputs.dockerhub_repo != '' }}
|
||||
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
username: ${{ env.DOCKERHUB_USER }}
|
||||
password: ${{ env.DOCKERHUB_PW }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ inputs.push_image == 'true' }}
|
||||
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -50,7 +50,7 @@ runs:
|
||||
- uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||
with:
|
||||
cache-image: false
|
||||
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
id: meta
|
||||
|
||||
2
.github/workflows/check-libxrpl.yml
vendored
2
.github/workflows/check-libxrpl.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
2
.github/workflows/check-pr-title.yml
vendored
2
.github/workflows/check-pr-title.yml
vendored
@@ -7,4 +7,4 @@ on:
|
||||
|
||||
jobs:
|
||||
check_title:
|
||||
uses: XRPLF/actions/.github/workflows/check-pr-title.yml@cba1f0891650baf1a9c88624dc2d72573be2eb81
|
||||
uses: XRPLF/actions/.github/workflows/check-pr-title.yml@d7c65e49225a38f6d8010eacf017bb5a98d7476c
|
||||
|
||||
6
.github/workflows/clang-tidy.yml
vendored
6
.github/workflows/clang-tidy.yml
vendored
@@ -17,7 +17,7 @@ on:
|
||||
- "tests/**"
|
||||
- "benchmarks/**"
|
||||
|
||||
- .clang_tidy
|
||||
- .clang-tidy
|
||||
|
||||
concurrency:
|
||||
# Only cancel in-progress jobs or runs for the current workflow - matches against branch & tags
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
permissions:
|
||||
contents: read
|
||||
uses: XRPLF/actions/.github/workflows/determine-tidy-files.yml@d041ac9f1fa9f07a4ba335eb4c1c82233fb3fef6
|
||||
uses: XRPLF/actions/.github/workflows/determine-tidy-files.yml@70145243b905dc3e040a61d39c00e178cfb96f71
|
||||
|
||||
run-clang-tidy:
|
||||
name: Run clang tidy
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
lfs: true
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
2
.github/workflows/pre-commit-autoupdate.yml
vendored
2
.github/workflows/pre-commit-autoupdate.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
|
||||
jobs:
|
||||
auto-update:
|
||||
uses: XRPLF/actions/.github/workflows/pre-commit-autoupdate.yml@b929ad64a95f908dee04aa9f53d584cbe893595b
|
||||
uses: XRPLF/actions/.github/workflows/pre-commit-autoupdate.yml@70145243b905dc3e040a61d39c00e178cfb96f71
|
||||
with:
|
||||
sign_commit: true
|
||||
committer: "Clio CI <skuznetsov@ripple.com>"
|
||||
|
||||
2
.github/workflows/pre-commit.yml
vendored
2
.github/workflows/pre-commit.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
run-hooks:
|
||||
uses: XRPLF/actions/.github/workflows/pre-commit.yml@3ba08d6ddf114092891d48491fc2e26c3ba15552
|
||||
uses: XRPLF/actions/.github/workflows/pre-commit.yml@f1952595d212e86169935135efc66294b4574131
|
||||
with:
|
||||
runs_on: heavy
|
||||
container: '{ "image": "ghcr.io/xrplf/xrpld/pre-commit:sha-f56b79f" }'
|
||||
|
||||
22
.github/workflows/release.yml
vendored
22
.github/workflows/release.yml
vendored
@@ -14,6 +14,18 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
release-info:
|
||||
name: Determine release info
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
channel: ${{ steps.release_info.outputs.channel }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- id: release_info
|
||||
uses: XRPLF/actions/release-info@7cc0e4a8d9d0b838f92c48d312856b190341bbba
|
||||
|
||||
build-and-test:
|
||||
name: Build and Test
|
||||
|
||||
@@ -60,6 +72,16 @@ jobs:
|
||||
targets: package
|
||||
analyze_build_time: false
|
||||
|
||||
publish_package:
|
||||
name: Publish debian package
|
||||
needs: [release-info, package]
|
||||
uses: ./.github/workflows/reusable-publish-package.yml
|
||||
with:
|
||||
channel: ${{ needs.release-info.outputs.channel }}
|
||||
secrets:
|
||||
nexus_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
|
||||
nexus_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
|
||||
|
||||
release:
|
||||
needs: [build-and-test, package]
|
||||
uses: ./.github/workflows/reusable-release.yml
|
||||
|
||||
2
.github/workflows/reusable-build.yml
vendored
2
.github/workflows/reusable-build.yml
vendored
@@ -96,7 +96,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
with:
|
||||
enable_ccache: ${{ inputs.download_ccache }}
|
||||
|
||||
|
||||
61
.github/workflows/reusable-publish-package.yml
vendored
Normal file
61
.github/workflows/reusable-publish-package.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
# See docs/install-clio.md, "Publishing".
|
||||
name: Reusable publish package
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
channel:
|
||||
description: "Release channel to publish to, selecting the deb-<channel> repository"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
secrets:
|
||||
nexus_username:
|
||||
description: Username of a Nexus account with write access to the repositories
|
||||
required: false
|
||||
nexus_password:
|
||||
description: Password or token for that Nexus account
|
||||
required: false
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
PACKAGE_DIR: packages
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish debian package
|
||||
runs-on: heavy
|
||||
container:
|
||||
image: ghcr.io/xrplf/xrpld/packaging-debian:sha-b6a8995
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: ${{ env.PACKAGE_DIR }}
|
||||
pattern: clio_deb_package_*
|
||||
|
||||
# dpkg-deb refuses to read a version CPack should not have produced.
|
||||
- name: Verify packages
|
||||
run: |
|
||||
shopt -s globstar nullglob
|
||||
for package in "${PACKAGE_DIR}"/**/*.deb; do
|
||||
dpkg-deb --field "${package}" Package Version Architecture
|
||||
done
|
||||
|
||||
- name: Publish packages
|
||||
env:
|
||||
CHANNEL: ${{ inputs.channel }}
|
||||
NEXUS_USERNAME: ${{ secrets.nexus_username }}
|
||||
NEXUS_PASSWORD: ${{ secrets.nexus_password }}
|
||||
DRY_RUN_OPTION: ${{ (github.repository == 'XRPLF/clio' && github.event_name != 'pull_request') && '' || '--dry-run' }}
|
||||
run: |
|
||||
publish_pkg.py \
|
||||
--channel "${CHANNEL}" \
|
||||
--package-dir "${PACKAGE_DIR}" \
|
||||
${DRY_RUN_OPTION}
|
||||
2
.github/workflows/reusable-release.yml
vendored
2
.github/workflows/reusable-release.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
2
.github/workflows/upload-conan-deps.yml
vendored
2
.github/workflows/upload-conan-deps.yml
vendored
@@ -75,7 +75,7 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
@@ -4,3 +4,4 @@ ignored:
|
||||
- DL3007
|
||||
- DL3008
|
||||
- DL3013
|
||||
- DL3066
|
||||
|
||||
@@ -40,13 +40,13 @@ repos:
|
||||
exclude: LICENSE.md
|
||||
|
||||
- repo: https://github.com/hadolint/hadolint
|
||||
rev: 57e1618d78fd469a92c1e584e8c9313024656623 # frozen: v2.14.0
|
||||
rev: 2eece55955ced00200be9729e9728cb7dacca505 # frozen: v2.15.1
|
||||
hooks:
|
||||
- id: hadolint-docker
|
||||
# hadolint-docker is a special hook that runs hadolint in a Docker container
|
||||
# Docker is not installed in the environment where pre-commit is run
|
||||
stages: [manual]
|
||||
entry: hadolint/hadolint:v2.14.0 hadolint
|
||||
entry: hadolint/hadolint:v2.15.1 hadolint
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: 57b21406f092110c18776e39b0bda50d37c945c8 # frozen: v2.4.3
|
||||
@@ -107,7 +107,7 @@ repos:
|
||||
types: [c++]
|
||||
|
||||
- repo: https://github.com/BlankSpruce/gersemi-pre-commit
|
||||
rev: e98930bdc210d3387007f9252d8c1694ea7e410f # frozen: 0.27.7
|
||||
rev: b9d6d13dc9b753c5ff24ed5f8470671189c3fcf0 # frozen: 0.28.0
|
||||
hooks:
|
||||
- id: gersemi
|
||||
|
||||
|
||||
@@ -75,14 +75,15 @@ include(Settings)
|
||||
include(PatchNixBinary)
|
||||
|
||||
# Clio deps
|
||||
include(deps/libxrpl)
|
||||
include(deps/Boost)
|
||||
include(deps/OpenSSL)
|
||||
include(deps/Threads)
|
||||
include(deps/libfmt)
|
||||
include(deps/cassandra)
|
||||
include(deps/libbacktrace)
|
||||
include(deps/libfmt)
|
||||
include(deps/libxrpl)
|
||||
include(deps/OpenSSL)
|
||||
include(deps/rpcspec)
|
||||
include(deps/spdlog)
|
||||
include(deps/Threads)
|
||||
|
||||
add_subdirectory(src)
|
||||
add_subdirectory(tests)
|
||||
|
||||
@@ -32,7 +32,8 @@ Below are some useful docs to learn more about Clio.
|
||||
|
||||
**For Operators**:
|
||||
|
||||
- [How to configure Clio and rippled](./docs/configure-clio.md)
|
||||
- [How to install Clio](./docs/install-clio.md)
|
||||
- [How to configure Clio and xrpld](./docs/configure-clio.md)
|
||||
- [How to run Clio](./docs/run-clio.md)
|
||||
- [Troubleshooting guide](./docs/trouble_shooting.md)
|
||||
|
||||
|
||||
@@ -54,8 +54,16 @@ else()
|
||||
|
||||
string(SUBSTRING ${GIT_COMMIT_HASH} 0 7 GIT_COMMIT_HASH_SHORT)
|
||||
|
||||
# Debian package versions may only contain alphanumerics and '.+~-'
|
||||
string(
|
||||
REGEX REPLACE "[^a-zA-Z0-9.+~-]"
|
||||
"."
|
||||
SANITIZED_BUILD_BRANCH
|
||||
"${GIT_BUILD_BRANCH}"
|
||||
)
|
||||
|
||||
set(CLIO_VERSION
|
||||
"${BUILD_DATE}-${GIT_BUILD_BRANCH}-${GIT_COMMIT_HASH_SHORT}"
|
||||
"${BUILD_DATE}-${SANITIZED_BUILD_BRANCH}-${GIT_COMMIT_HASH_SHORT}"
|
||||
)
|
||||
set(DOC_CLIO_VERSION "develop")
|
||||
endif()
|
||||
|
||||
1
cmake/deps/rpcspec.cmake
Normal file
1
cmake/deps/rpcspec.cmake
Normal file
@@ -0,0 +1 @@
|
||||
find_package(xrpl-rpc-spec REQUIRED CONFIG)
|
||||
@@ -9,4 +9,4 @@ set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
||||
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA ${CMAKE_SOURCE_DIR}/cmake/pkg/postinst)
|
||||
|
||||
# We must replace "-" with "~" otherwise dpkg will sort "X.Y.Z-b1" as greater than "X.Y.Z"
|
||||
string(REPLACE "-" "~" git "${CPACK_PACKAGE_VERSION}")
|
||||
string(REPLACE "-" "~" CPACK_DEBIAN_PACKAGE_VERSION "${CPACK_PACKAGE_VERSION}")
|
||||
|
||||
13
conan.lock
13
conan.lock
@@ -3,7 +3,8 @@
|
||||
"requires": [
|
||||
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
|
||||
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1782392402.420688",
|
||||
"xrpl/3.3.0-rc1-custom#cdfb12fc2671eaea72d5920bea800fd2%1785426802.141277",
|
||||
"xrpl-rpc-spec/0.1.6#0147dce06088874791dca62dc8503b6c%1787751048.16847",
|
||||
"xrpl/3.3.0#5e356a24ae1f0d6da6bd617b926f92e6%1786467262.262007",
|
||||
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1782392403.185447",
|
||||
"spdlog/1.17.0#bcbaaf7147bda6ad24ffbd1ac3d7142c%1782736610.443882",
|
||||
"soci/4.0.3#e726491a03468795453f7c83fc924a96%1782392402.679521",
|
||||
@@ -15,7 +16,7 @@
|
||||
"protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933",
|
||||
"openssl/3.6.3#f806de8933e3bf6f01016c6a888cee2e%1783945160.863288",
|
||||
"nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1782392402.297166",
|
||||
"mpt-crypto/0.4.0-rc4#ffdba12f2332357f0d8b0ae944cfff52%1784138702.932355",
|
||||
"mpt-crypto/1.0.2#b313cef0c1a493eb970ad185b2e9bab7%1784285108.866483",
|
||||
"minizip/1.2.13#64dfec2ee447ab6c0c7eab967815a762%1782736605.272739",
|
||||
"lz4/1.10.0#982d9b673900f665a1da109e09c17cab%1782392402.164188",
|
||||
"libuv/1.46.0#e1a592bf7c0f37802889ca2c795fb26c%1782736605.776567",
|
||||
@@ -54,13 +55,13 @@
|
||||
"zlib/[>=1.2.11 <2]": [
|
||||
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb"
|
||||
],
|
||||
"boost/1.91.0": [
|
||||
"boost/1.91.0"
|
||||
],
|
||||
"zlib/1.3.2": [
|
||||
null,
|
||||
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb"
|
||||
],
|
||||
"boost/1.91.0": [
|
||||
"boost/1.91.0#ea540ca2133d831b560036aa24dece3c"
|
||||
],
|
||||
"protobuf/[>=5.27.0 <7]": [
|
||||
"protobuf/6.33.5"
|
||||
],
|
||||
@@ -74,7 +75,7 @@
|
||||
"sqlite3/3.53.0"
|
||||
],
|
||||
"boost/1.83.0": [
|
||||
"boost/1.91.0"
|
||||
"boost/1.91.0#ea540ca2133d831b560036aa24dece3c"
|
||||
],
|
||||
"openssl/[>=3.5 <4]": [
|
||||
"openssl/3.6.3#f806de8933e3bf6f01016c6a888cee2e"
|
||||
|
||||
@@ -17,7 +17,8 @@ class ClioConan(ConanFile):
|
||||
"fmt/12.1.0",
|
||||
"libbacktrace/cci.20210118",
|
||||
"spdlog/1.17.0",
|
||||
"xrpl/3.3.0-rc1-custom",
|
||||
"xrpl-rpc-spec/0.1.6",
|
||||
"xrpl/3.3.0",
|
||||
]
|
||||
|
||||
default_options = {
|
||||
@@ -32,6 +33,7 @@ class ClioConan(ConanFile):
|
||||
"protobuf/*:shared": False,
|
||||
"protobuf/*:with_zlib": True,
|
||||
"snappy/*:shared": False,
|
||||
"xrpl-rpc-spec/*:server": "clio",
|
||||
"xrpl/*:rocksdb": True, # TODO: revert to false when includes are fixed in libxrpl
|
||||
"xrpl/*:tests": False,
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
ARG GHCR_REPO=invalid
|
||||
FROM ${GHCR_REPO}/clio-tools:latest AS clio-tools
|
||||
|
||||
# We're using Ubuntu 24.04 to have a more recent version of Python
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
# hadolint ignore=DL3002
|
||||
USER root
|
||||
WORKDIR /root
|
||||
|
||||
# Install common tools and dependencies
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends --no-install-suggests \
|
||||
curl \
|
||||
git \
|
||||
libatomic1 \
|
||||
software-properties-common \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python tools
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends --no-install-suggests \
|
||||
python3 \
|
||||
python3-pip \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip install -q --no-cache-dir --break-system-packages \
|
||||
pre-commit
|
||||
|
||||
COPY --from=clio-tools \
|
||||
/usr/local/bin/doxygen \
|
||||
/usr/local/bin/
|
||||
@@ -132,10 +132,26 @@ This document provides a list of all available Clio configuration properties in
|
||||
### database.cassandra.request_timeout
|
||||
|
||||
- **Required**: False
|
||||
- **Type**: int
|
||||
- **Type**: double
|
||||
- **Default value**: None
|
||||
- **Constraints**: The minimum value is `1`. The maximum value is `4294967295`.
|
||||
- **Description**: The maximum amount of time in seconds that the system waits for a request to be fetched from the database.
|
||||
- **Constraints**: The value must be a positive double number.
|
||||
- **Description**: The maximum amount of time in seconds that the system waits for a request to be fetched from the database. Should be set higher than the server side read timeout. If omitted, no request timeout is applied.
|
||||
|
||||
### database.cassandra.initial_request_retry_delay
|
||||
|
||||
- **Required**: True
|
||||
- **Type**: double
|
||||
- **Default value**: `0.5`
|
||||
- **Constraints**: The value must be a positive double number.
|
||||
- **Description**: How long in seconds to wait before the first retry of a database request that failed with a transient error.
|
||||
|
||||
### database.cassandra.max_request_retry_delay
|
||||
|
||||
- **Required**: True
|
||||
- **Type**: double
|
||||
- **Default value**: `5`
|
||||
- **Constraints**: The value must be a positive double number.
|
||||
- **Description**: Upper bound in seconds for the exponential backoff between retries of a database request.
|
||||
|
||||
### database.cassandra.username
|
||||
|
||||
|
||||
35
docs/install-clio.md
Normal file
35
docs/install-clio.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# How to install Clio
|
||||
|
||||
Clio is published as a Debian package for 64-bit x86 Linux, in the XRPL Foundation repositories at <https://packages.xrplf.org>. To build from source instead, see [How to build Clio](./build-clio.md).
|
||||
|
||||
## Install
|
||||
|
||||
Clio publishes to the same repositories as [`xrpld`](https://github.com/XRPLF/rippled), so configure APT by following [its instructions](https://github.com/XRPLF/rippled/blob/develop/docs/install.md#with-the-apt-package-manager), which also document the [release channels](https://github.com/XRPLF/rippled/blob/develop/docs/install.md#release-channels), and install `clio` in place of `xrpld`:
|
||||
|
||||
```bash
|
||||
sudo apt -y install clio
|
||||
```
|
||||
|
||||
## The clio service
|
||||
|
||||
The package installs `/opt/clio/bin/clio_server` (symlinked into `/usr/bin`), a config at `/opt/clio/etc/config.json`, a log directory at `/var/log/clio`, and a systemd unit, all owned by the `clio` system user it creates.
|
||||
|
||||
The unit is not enabled, as Clio needs a configured database and `xrpld` node before it can start. Edit the config — see [How to configure Clio and xrpld](./configure-clio.md) — then:
|
||||
|
||||
```bash
|
||||
sudo systemctl enable --now clio
|
||||
```
|
||||
|
||||
Upgrades do not restart a running server; `systemctl restart clio` picks up the new binary.
|
||||
|
||||
## Publishing
|
||||
|
||||
Only tags are published. CPack builds the package in the `package` job of [`release.yml`](../.github/workflows/release.yml), and [`reusable-publish-package.yml`](../.github/workflows/reusable-publish-package.yml) uploads it with `publish_pkg.py` from the [`xrpld` packaging image](https://github.com/XRPLF/rippled/blob/develop/package/README.md#publishing-packages), so that Clio and `xrpld` agree on what a channel means. [`XRPLF/actions/release-info`](https://github.com/XRPLF/actions/blob/main/release-info/action.yml) picks the channel:
|
||||
|
||||
| Tag | Package version | Channel |
|
||||
| ----------- | --------------- | -------- |
|
||||
| `X.Y.Z` | `X.Y.Z` | `stable` |
|
||||
| `X.Y.Z-rcN` | `X.Y.Z~rcN` | `rc` |
|
||||
| `X.Y.Z-bN` | `X.Y.Z~bN` | `beta` |
|
||||
|
||||
[`cmake/pkg/deb.cmake`](../cmake/pkg/deb.cmake) converts the `-` to `~`, which Debian sorts below everything, so `X.Y.Z~rc1` ranks below the `X.Y.Z` it precedes.
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "data/LedgerCacheInterface.hpp"
|
||||
#include "data/Types.hpp"
|
||||
#include "etl/CorruptionDetector.hpp"
|
||||
#include "util/Retry.hpp"
|
||||
#include "util/Spawn.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
|
||||
@@ -35,41 +36,123 @@
|
||||
namespace data {
|
||||
|
||||
/**
|
||||
* @brief Represents a database timeout error.
|
||||
* @brief Represents a transient database error that the caller should retry.
|
||||
*/
|
||||
class DatabaseTimeout : public std::exception {
|
||||
class DatabaseError : public std::exception {
|
||||
std::string message_{"Transient database error. Please retry the request"};
|
||||
|
||||
public:
|
||||
DatabaseError() = default;
|
||||
|
||||
/**
|
||||
* @brief Construct with a description of the underlying failure.
|
||||
*
|
||||
* @param message What actually went wrong.
|
||||
*/
|
||||
explicit DatabaseError(std::string message) : message_{std::move(message)}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The error message as a C string
|
||||
*/
|
||||
[[nodiscard]] char const*
|
||||
what() const throw() override
|
||||
what() const noexcept override
|
||||
{
|
||||
return "Database read timed out. Please retry the request";
|
||||
return message_.c_str();
|
||||
}
|
||||
};
|
||||
|
||||
static constexpr std::size_t kDefaultWaitBetweenRetry = 500;
|
||||
/**
|
||||
* @brief A helper function that catches DatabaseTimeout exceptions and retries indefinitely.
|
||||
* @brief Delay before the first retry in @ref retryOnTimeout().
|
||||
*/
|
||||
static constexpr std::chrono::milliseconds kDefaultWaitBetweenRetry{500};
|
||||
|
||||
/**
|
||||
* @brief Default upper bound for the exponential backoff in @ref retryOnTimeout().
|
||||
*/
|
||||
static constexpr std::chrono::milliseconds kMaxWaitBetweenRetry{5'000};
|
||||
|
||||
/**
|
||||
* @brief Default delays for @ref retryOnTimeout().
|
||||
*/
|
||||
static constexpr util::Retry::Delays kDefaultRetryDelays{
|
||||
.initial = kDefaultWaitBetweenRetry,
|
||||
.max = kDefaultWaitBetweenRetry
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Retry `func` while it throws DatabaseError, suspending the calling coroutine in between.
|
||||
*
|
||||
* @tparam FnType The type of function object to execute
|
||||
* @param func The function object to execute
|
||||
* @param waitMs Delay between retry attempts
|
||||
* @param yield The coroutine to suspend between attempts
|
||||
* @param delays The delays to use between attempts
|
||||
* @return The same as the return type of func
|
||||
*/
|
||||
template <typename FnType>
|
||||
auto
|
||||
retryOnTimeout(FnType func, size_t waitMs = kDefaultWaitBetweenRetry)
|
||||
retryOnTimeout(
|
||||
FnType func,
|
||||
boost::asio::yield_context yield,
|
||||
util::Retry::Delays delays = kDefaultRetryDelays
|
||||
)
|
||||
{
|
||||
static util::Logger const log{"Backend"}; // NOLINT(readability-identifier-naming)
|
||||
|
||||
auto retry = util::makeRetryExponentialBackoff(delays, yield.get_executor());
|
||||
|
||||
while (true) {
|
||||
// Suspending a coroutine from inside a catch block is not safe so we copy out the failure.
|
||||
std::string failure;
|
||||
|
||||
try {
|
||||
return func();
|
||||
} catch (DatabaseError const& e) {
|
||||
failure = e.what();
|
||||
}
|
||||
|
||||
auto const delayMs =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(retry.delayValue()).count();
|
||||
LOG(log.error()) << failure << " (attempt " << retry.attemptNumber() + 1
|
||||
<< "). Retrying in " << delayMs << "ms ...";
|
||||
|
||||
retry.wait(yield);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Retry `func` while it throws DatabaseError, blocking the calling thread in between.
|
||||
*
|
||||
* @warning Blocks the calling thread; from a coroutine use the `yield_context` overload instead.
|
||||
*
|
||||
* @tparam FnType The type of function object to execute
|
||||
* @param func The function object to execute
|
||||
* @param delays The delays to use between attempts
|
||||
* @return The same as the return type of func
|
||||
*/
|
||||
template <typename FnType>
|
||||
auto
|
||||
retryOnTimeout(FnType func, util::Retry::Delays delays = kDefaultRetryDelays)
|
||||
{
|
||||
static util::Logger const log{"Backend"}; // NOLINT(readability-identifier-naming)
|
||||
|
||||
util::ExponentialBackoffStrategy backoff{delays};
|
||||
std::size_t attempt = 1;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
return func();
|
||||
} catch (DatabaseTimeout const&) {
|
||||
LOG(log.error()) << "Database request timed out. Sleeping and retrying ... ";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(waitMs));
|
||||
} catch (DatabaseError const& e) {
|
||||
auto const delay = backoff.getDelay();
|
||||
LOG(log.error()) << e.what() << " (attempt " << attempt << "). Retrying in "
|
||||
<< std::chrono::duration_cast<std::chrono::milliseconds>(delay).count()
|
||||
<< "ms ...";
|
||||
|
||||
++attempt;
|
||||
|
||||
std::this_thread::sleep_for(delay);
|
||||
backoff.increaseDelay();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,18 +188,21 @@ synchronous(FnType&& func)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Synchronously execute the given function object and retry until no DatabaseTimeout is
|
||||
* @brief Synchronously execute the given function object and retry until no DatabaseError is
|
||||
* thrown.
|
||||
*
|
||||
* @warning Blocks the calling thread while backing off.
|
||||
*
|
||||
* @tparam FnType The type of function object to execute
|
||||
* @param func The function object to execute
|
||||
* @param delays The delays to use between attempts
|
||||
* @return The same as the return type of func
|
||||
*/
|
||||
template <typename FnType>
|
||||
auto
|
||||
synchronousAndRetryOnTimeout(FnType&& func)
|
||||
synchronousAndRetryOnTimeout(FnType&& func, util::Retry::Delays delays = kDefaultRetryDelays)
|
||||
{
|
||||
return retryOnTimeout([&]() { return synchronous(func); });
|
||||
return retryOnTimeout([&]() { return synchronous(func); }, delays);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,8 +225,27 @@ public:
|
||||
BackendInterface(LedgerCacheInterface& cache) : cache_{cache}
|
||||
{
|
||||
}
|
||||
|
||||
virtual ~BackendInterface() = default;
|
||||
|
||||
/**
|
||||
* @return Delay before the first retry of a request against this backend
|
||||
*/
|
||||
[[nodiscard]] virtual std::chrono::milliseconds
|
||||
initialRetryDelay() const
|
||||
{
|
||||
return kDefaultWaitBetweenRetry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Upper bound for the retry backoff; equal to @ref initialRetryDelay() means flat
|
||||
*/
|
||||
[[nodiscard]] virtual std::chrono::milliseconds
|
||||
maxRetryDelay() const
|
||||
{
|
||||
return kMaxWaitBetweenRetry;
|
||||
}
|
||||
|
||||
// TODO https://github.com/XRPLF/clio/issues/1956: Remove this hack once old ETL is removed.
|
||||
// Cache should not be exposed thru BackendInterface
|
||||
|
||||
@@ -705,7 +810,7 @@ public:
|
||||
hardFetchLedgerRange(boost::asio::yield_context yield) const = 0;
|
||||
|
||||
/**
|
||||
* @brief Fetches the ledger range from DB retrying until no DatabaseTimeout is thrown.
|
||||
* @brief Fetches the ledger range from DB retrying until no DatabaseError is thrown.
|
||||
*
|
||||
* @return The ledger range if available; nullopt otherwise
|
||||
*/
|
||||
|
||||
@@ -139,6 +139,24 @@ public:
|
||||
*/
|
||||
CassandraBackendFamily(CassandraBackendFamily&&) = delete;
|
||||
|
||||
/**
|
||||
* @return The configured delay before the first retry
|
||||
*/
|
||||
[[nodiscard]] std::chrono::milliseconds
|
||||
initialRetryDelay() const override
|
||||
{
|
||||
return settingsProvider_.getInitialRetryDelay();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The configured upper bound for the retry backoff
|
||||
*/
|
||||
[[nodiscard]] std::chrono::milliseconds
|
||||
maxRetryDelay() const override
|
||||
{
|
||||
return settingsProvider_.getMaxRetryDelay();
|
||||
}
|
||||
|
||||
TransactionsAndCursor
|
||||
fetchAccountTransactions(
|
||||
xrpl::AccountID const& account,
|
||||
|
||||
@@ -80,17 +80,6 @@ public:
|
||||
return code_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the wrapped error is considered a timeout; false otherwise
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isTimeout() const
|
||||
{
|
||||
return code_ == CASS_ERROR_LIB_NO_HOSTS_AVAILABLE or
|
||||
code_ == CASS_ERROR_LIB_REQUEST_TIMED_OUT or code_ == CASS_ERROR_SERVER_UNAVAILABLE or
|
||||
code_ == CASS_ERROR_SERVER_OVERLOADED or code_ == CASS_ERROR_SERVER_READ_TIMEOUT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the wrapped error is an invalid query; false otherwise
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "data/cassandra/Types.hpp"
|
||||
#include "data/cassandra/impl/Cluster.hpp"
|
||||
#include "util/Constants.hpp"
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "util/config/ObjectView.hpp"
|
||||
|
||||
#include <cerrno>
|
||||
@@ -25,6 +26,12 @@ SettingsProvider::SettingsProvider(util::config::ObjectView const& cfg)
|
||||
, keyspace_{cfg.get<std::string>("keyspace")}
|
||||
, tablePrefix_{cfg.maybeValue<std::string>("table_prefix")}
|
||||
, replicationFactor_{cfg.get<uint16_t>("replication_factor")}
|
||||
, initialRetryDelay_{util::config::ClioConfigDefinition::toMilliseconds(
|
||||
cfg.get<float>("initial_request_retry_delay")
|
||||
)}
|
||||
, maxRetryDelay_{util::config::ClioConfigDefinition::toMilliseconds(
|
||||
cfg.get<float>("max_request_retry_delay")
|
||||
)}
|
||||
, settings_{parseSettings()}
|
||||
{
|
||||
}
|
||||
@@ -94,9 +101,9 @@ SettingsProvider::parseSettings() const
|
||||
}
|
||||
|
||||
if (config_.getValueView("request_timeout").hasValue()) {
|
||||
auto const requestTimeoutSecond = config_.get<uint32_t>("request_timeout");
|
||||
settings.requestTimeout =
|
||||
std::chrono::milliseconds{requestTimeoutSecond * util::kMillisecondsPerSecond};
|
||||
settings.requestTimeout = util::config::ClioConfigDefinition::toMilliseconds(
|
||||
config_.get<float>("request_timeout")
|
||||
);
|
||||
}
|
||||
|
||||
settings.certificate = parseOptionalCertificate();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "data/cassandra/impl/Cluster.hpp"
|
||||
#include "util/config/ObjectView.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -19,6 +20,8 @@ class SettingsProvider {
|
||||
std::string keyspace_;
|
||||
std::optional<std::string> tablePrefix_;
|
||||
uint16_t replicationFactor_;
|
||||
std::chrono::milliseconds initialRetryDelay_;
|
||||
std::chrono::milliseconds maxRetryDelay_;
|
||||
Settings settings_;
|
||||
|
||||
public:
|
||||
@@ -62,6 +65,24 @@ public:
|
||||
return replicationFactor_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Delay before the first retry of a failed request
|
||||
*/
|
||||
[[nodiscard]] std::chrono::milliseconds
|
||||
getInitialRetryDelay() const
|
||||
{
|
||||
return initialRetryDelay_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Upper bound for the retry backoff
|
||||
*/
|
||||
[[nodiscard]] std::chrono::milliseconds
|
||||
getMaxRetryDelay() const
|
||||
{
|
||||
return maxRetryDelay_;
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]] std::optional<std::string>
|
||||
parseOptionalCertificate() const;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
@@ -27,6 +28,7 @@
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
@@ -169,7 +171,7 @@ public:
|
||||
*
|
||||
* @param preparedStatement Statement to prepare and execute
|
||||
* @param args Args to bind to the prepared statement
|
||||
* @throws DatabaseTimeout on timeout
|
||||
* @throws DatabaseError on a database error
|
||||
*/
|
||||
template <typename... Args>
|
||||
void
|
||||
@@ -185,7 +187,7 @@ public:
|
||||
* Retries forever with retry policy specified by @ref AsyncExecutor
|
||||
*
|
||||
* @param statement Statement to execute
|
||||
* @throws DatabaseTimeout on timeout
|
||||
* @throws DatabaseError on a database error
|
||||
*/
|
||||
void
|
||||
write(StatementType&& statement)
|
||||
@@ -215,7 +217,7 @@ public:
|
||||
* Retries forever with retry policy specified by @ref AsyncExecutor.
|
||||
*
|
||||
* @param statements Vector of statements to execute as a batch
|
||||
* @throws DatabaseTimeout on timeout
|
||||
* @throws DatabaseError on a database error
|
||||
*/
|
||||
void
|
||||
write(std::vector<StatementType>&& statements)
|
||||
@@ -254,7 +256,7 @@ public:
|
||||
* Retries forever with retry policy specified by @ref AsyncExecutor.
|
||||
*
|
||||
* @param statements Vector of statements to execute
|
||||
* @throws DatabaseTimeout on timeout
|
||||
* @throws DatabaseError on a database error
|
||||
*/
|
||||
void
|
||||
writeEach(std::vector<StatementType>&& statements)
|
||||
@@ -272,7 +274,7 @@ public:
|
||||
* @param token Completion token (yield_context)
|
||||
* @param preparedStatement Statement to prepare and execute
|
||||
* @param args Args to bind to the prepared statement
|
||||
* @throws DatabaseTimeout on timeout
|
||||
* @throws DatabaseError on a database error
|
||||
* @return ResultType or error wrapped in Expected
|
||||
*/
|
||||
template <typename... Args>
|
||||
@@ -289,7 +291,7 @@ public:
|
||||
*
|
||||
* @param token Completion token (yield_context)
|
||||
* @param statements Statements to execute in a batch
|
||||
* @throws DatabaseTimeout on timeout
|
||||
* @throws DatabaseError on a database error
|
||||
* @return ResultType or error wrapped in Expected
|
||||
*/
|
||||
[[maybe_unused]] ResultOrErrorType
|
||||
@@ -346,7 +348,7 @@ public:
|
||||
*
|
||||
* @param token Completion token (yield_context)
|
||||
* @param statement Statement to execute
|
||||
* @throws DatabaseTimeout on timeout
|
||||
* @throws DatabaseError on a database error
|
||||
* @return ResultType or error wrapped in Expected
|
||||
*/
|
||||
[[maybe_unused]] ResultOrErrorType
|
||||
@@ -402,7 +404,7 @@ public:
|
||||
*
|
||||
* @param token Completion token (yield_context)
|
||||
* @param statements Statements to execute
|
||||
* @throws DatabaseTimeout on db error
|
||||
* @throws DatabaseError on a database error
|
||||
* @return Vector of results
|
||||
*/
|
||||
std::vector<ResultType>
|
||||
@@ -457,7 +459,7 @@ public:
|
||||
);
|
||||
counters_->registerReadError(errorsCount);
|
||||
counters_->registerReadFinished(startTime, statements.size() - errorsCount);
|
||||
throw DatabaseTimeout{};
|
||||
throw DatabaseError{};
|
||||
}
|
||||
counters_->registerReadFinished(startTime, statements.size());
|
||||
|
||||
@@ -551,11 +553,13 @@ private:
|
||||
void
|
||||
throwErrorIfNeeded(CassandraError err) const
|
||||
{
|
||||
if (err.isTimeout())
|
||||
throw DatabaseTimeout();
|
||||
|
||||
// NOTE: etl::impl::Loader and etl::impl::Extractor treat std::runtime_error as
|
||||
// "amendment blocked", so only genuinely permanent failures may be thrown as one.
|
||||
if (err.isInvalidQuery())
|
||||
throw std::runtime_error("Invalid query");
|
||||
|
||||
// anything else, including unclassified codes, is transient and gets retried
|
||||
throw DatabaseError{fmt::format("Database error [{}]: {}", err.code(), err.message())};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -28,8 +28,7 @@ public:
|
||||
ExponentialBackoffRetryPolicy(boost::asio::io_context& ioc)
|
||||
: retry_(
|
||||
util::makeRetryExponentialBackoff(
|
||||
std::chrono::milliseconds(1),
|
||||
std::chrono::seconds(1),
|
||||
{.initial = std::chrono::milliseconds(1), .max = std::chrono::seconds(1)},
|
||||
boost::asio::make_strand(ioc)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -29,4 +29,4 @@ target_sources(
|
||||
impl/ext/Successor.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(clio_etl PUBLIC clio_data)
|
||||
target_link_libraries(clio_etl PUBLIC clio_data clio_util)
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
#include "etl/MPTHelpers.hpp"
|
||||
|
||||
#include "data/DBHelpers.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/MPTIssuanceUtils.hpp"
|
||||
|
||||
#include <boost/container/flat_set.hpp>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STBase.h>
|
||||
#include <xrpl/protocol/STIssue.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
@@ -49,90 +46,23 @@ getMPTHolderFromTx(xrpl::TxMeta const& txMeta, xrpl::STTx const&)
|
||||
return holders;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using MPTokenIssuanceIDs = boost::container::flat_set<xrpl::uint192>;
|
||||
|
||||
/**
|
||||
* @brief Derive the MPTokenIssuanceID from an affected node in transaction metadata.
|
||||
*
|
||||
* @param node An entry of the metadata's AffectedNodes array.
|
||||
* @return The 192-bit issuance ID if the node is an MPTokenIssuance or MPToken object.
|
||||
*/
|
||||
std::optional<xrpl::uint192>
|
||||
getMPTokenIssuanceIDFromNode(xrpl::STObject const& node)
|
||||
{
|
||||
auto const entryType = node.getFieldU16(xrpl::sfLedgerEntryType);
|
||||
if (entryType != xrpl::ltMPTOKEN && entryType != xrpl::ltMPTOKEN_ISSUANCE)
|
||||
return std::nullopt;
|
||||
|
||||
auto const& fieldsName =
|
||||
node.getFName() == xrpl::sfCreatedNode ? xrpl::sfNewFields : xrpl::sfFinalFields;
|
||||
if (not node.isFieldPresent(fieldsName))
|
||||
return std::nullopt;
|
||||
|
||||
auto const& fields = node.peekAtField(fieldsName).downcast<xrpl::STObject>();
|
||||
|
||||
if (entryType == xrpl::ltMPTOKEN) {
|
||||
if (not fields.isFieldPresent(xrpl::sfMPTokenIssuanceID))
|
||||
return std::nullopt;
|
||||
|
||||
return fields[xrpl::sfMPTokenIssuanceID];
|
||||
}
|
||||
|
||||
// MPTokenIssuance objects carry no sfMPTokenIssuanceID, and the node's ledger key is a
|
||||
// one-way hash that does not embed the ID, so reconstruct it from sfSequence and sfIssuer
|
||||
if (not fields.isFieldPresent(xrpl::sfSequence) || not fields.isFieldPresent(xrpl::sfIssuer))
|
||||
return std::nullopt;
|
||||
|
||||
return xrpl::makeMptID(
|
||||
fields.getFieldU32(xrpl::sfSequence), fields.getAccountID(xrpl::sfIssuer)
|
||||
);
|
||||
}
|
||||
|
||||
void
|
||||
addMPTokenIssuanceIDsFromTx(MPTokenIssuanceIDs& issuanceIDs, xrpl::STTx const& sttx)
|
||||
{
|
||||
if (sttx.isFieldPresent(xrpl::sfMPTokenIssuanceID))
|
||||
issuanceIDs.insert(sttx.getFieldH192(xrpl::sfMPTokenIssuanceID));
|
||||
|
||||
for (xrpl::STBase const& field : sttx) {
|
||||
switch (field.getSType()) {
|
||||
case xrpl::STI_AMOUNT: {
|
||||
auto const& amount = field.downcast<xrpl::STAmount>();
|
||||
if (amount.holds<xrpl::MPTIssue>())
|
||||
issuanceIDs.insert(amount.get<xrpl::MPTIssue>().getMptID());
|
||||
break;
|
||||
}
|
||||
case xrpl::STI_ISSUE: {
|
||||
auto const& issue = field.downcast<xrpl::STIssue>();
|
||||
if (issue.holds<xrpl::MPTIssue>())
|
||||
issuanceIDs.insert(issue.value().get<xrpl::MPTIssue>().getMptID());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<MPTokenIssuanceTransactionsData>
|
||||
getMPTokenIssuanceTxsFromTx(xrpl::TxMeta const& txMeta, xrpl::STTx const& sttx)
|
||||
{
|
||||
// Collect each distinct issuance only once per transaction; the same set of affected accounts
|
||||
// is attached to every record produced below.
|
||||
MPTokenIssuanceIDs issuanceIDs;
|
||||
util::MPTokenIssuanceIDs issuanceIDs;
|
||||
|
||||
if (txMeta.getResultTER() == xrpl::tesSUCCESS) {
|
||||
for (auto const& node : txMeta.getNodes()) {
|
||||
if (auto const issuanceID = getMPTokenIssuanceIDFromNode(node); issuanceID.has_value())
|
||||
if (auto const issuanceID = util::getMPTokenIssuanceIDFromNode(node);
|
||||
issuanceID.has_value()) {
|
||||
issuanceIDs.insert(*issuanceID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addMPTokenIssuanceIDsFromTx(issuanceIDs, sttx);
|
||||
util::addMPTokenIssuanceIDsFromTx(issuanceIDs, sttx);
|
||||
|
||||
if (issuanceIDs.empty())
|
||||
return {};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "data/DBHelpers.hpp"
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TxMeta.h>
|
||||
|
||||
|
||||
@@ -145,9 +145,17 @@ private:
|
||||
LOG(log_.debug()) << "Starting a cursor: " << xrpl::strHex(start);
|
||||
|
||||
while (not token.isStopRequested() and not cache_.get().isDisabled()) {
|
||||
auto res = data::retryOnTimeout([this, seq, cachePageFetchSize, &start, token]() {
|
||||
return backend_->fetchLedgerPage(start, seq, cachePageFetchSize, false, token);
|
||||
});
|
||||
auto res = data::retryOnTimeout(
|
||||
[this, seq, cachePageFetchSize, &start, token]() {
|
||||
return backend_->fetchLedgerPage(
|
||||
start, seq, cachePageFetchSize, false, token
|
||||
);
|
||||
},
|
||||
token,
|
||||
util::Retry::Delays{
|
||||
.initial = backend_->initialRetryDelay(), .max = backend_->maxRetryDelay()
|
||||
}
|
||||
);
|
||||
|
||||
cache_.get().update(res.objects, seq, true);
|
||||
|
||||
|
||||
@@ -58,7 +58,9 @@ SubscriptionSource::SubscriptionSource(
|
||||
, subscriptions_(std::move(subscriptions))
|
||||
, strand_(boost::asio::make_strand(ioContext))
|
||||
, wsTimeout_(wsTimeout)
|
||||
, retry_(util::makeRetryExponentialBackoff(retryDelay, kRetryMaxDelay, strand_))
|
||||
, retry_(
|
||||
util::makeRetryExponentialBackoff({.initial = retryDelay, .max = kMaxRetryDelay}, strand_)
|
||||
)
|
||||
, onConnect_(std::move(onConnect))
|
||||
, onDisconnect_(std::move(onDisconnect))
|
||||
, onLedgerClosed_(std::move(onLedgerClosed))
|
||||
|
||||
@@ -75,7 +75,7 @@ private:
|
||||
util::StopHelper stopHelper_;
|
||||
|
||||
static constexpr std::chrono::seconds kWsTimeout{30};
|
||||
static constexpr std::chrono::seconds kRetryMaxDelay{30};
|
||||
static constexpr std::chrono::seconds kMaxRetryDelay{30};
|
||||
static constexpr std::chrono::seconds kRetryDelay{1};
|
||||
|
||||
public:
|
||||
|
||||
@@ -30,8 +30,10 @@ template <Hashable Key, typename Session, typename... Args>
|
||||
class TrackableSignalMap {
|
||||
using ConnectionPtr = Session*;
|
||||
using ConnectionSharedPtr = std::shared_ptr<Session>;
|
||||
using SignalType = TrackableSignal<Session, Args...>;
|
||||
using SignalPtr = std::shared_ptr<SignalType>;
|
||||
using SignalsMap = std::unordered_map<Key, SignalPtr>;
|
||||
|
||||
using SignalsMap = std::unordered_map<Key, TrackableSignal<Session, Args...>>;
|
||||
util::Mutex<SignalsMap> signalsMap_;
|
||||
|
||||
public:
|
||||
@@ -55,7 +57,13 @@ public:
|
||||
)
|
||||
{
|
||||
auto map = signalsMap_.template lock<std::scoped_lock>();
|
||||
return map->operator[](key).connectTrackableSlot(trackable, slot);
|
||||
auto it = map->find(key);
|
||||
if (it == map->end()) {
|
||||
auto signal = std::make_shared<SignalType>();
|
||||
it = map->emplace(key, std::move(signal)).first;
|
||||
}
|
||||
|
||||
return it->second->connectTrackableSlot(trackable, slot);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,13 +78,14 @@ public:
|
||||
disconnect(ConnectionPtr trackablePtr, Key const& key)
|
||||
{
|
||||
auto map = signalsMap_.template lock<std::scoped_lock>();
|
||||
if (!map->contains(key))
|
||||
auto const it = map->find(key);
|
||||
if (it == map->end())
|
||||
return false;
|
||||
|
||||
auto const disconnected = map->operator[](key).disconnect(trackablePtr);
|
||||
auto const disconnected = it->second->disconnect(trackablePtr);
|
||||
// clean the map if there is no connection left.
|
||||
if (disconnected && map->operator[](key).count() == 0)
|
||||
map->erase(key);
|
||||
if (disconnected && it->second->count() == 0)
|
||||
map->erase(it);
|
||||
|
||||
return disconnected;
|
||||
}
|
||||
@@ -90,9 +99,14 @@ public:
|
||||
void
|
||||
emit(Key const& key, Args const&... args)
|
||||
{
|
||||
auto map = signalsMap_.template lock<std::scoped_lock>();
|
||||
if (map->contains(key))
|
||||
map->operator[](key).emit(args...);
|
||||
auto const signal = [this, &key]() -> SignalPtr {
|
||||
auto map = signalsMap_.template lock<std::scoped_lock>();
|
||||
auto const it = map->find(key);
|
||||
return it == map->end() ? nullptr : it->second;
|
||||
}();
|
||||
|
||||
if (signal != nullptr)
|
||||
signal->emit(args...);
|
||||
}
|
||||
};
|
||||
} // namespace feed::impl
|
||||
|
||||
@@ -21,6 +21,8 @@ target_sources(
|
||||
common/MetaProcessors.cpp
|
||||
common/impl/APIVersionParser.cpp
|
||||
common/impl/HandlerProvider.cpp
|
||||
common/impl/HandlerRegistry.cpp
|
||||
filters/impl/DelegateTransactionsFilter.cpp
|
||||
handlers/AccountChannels.cpp
|
||||
handlers/AccountCurrencies.cpp
|
||||
handlers/AccountInfo.cpp
|
||||
@@ -44,6 +46,7 @@ target_sources(
|
||||
handlers/LedgerIndex.cpp
|
||||
handlers/LedgerRange.cpp
|
||||
handlers/MPTHolders.cpp
|
||||
handlers/MPTokenIssuanceHistory.cpp
|
||||
handlers/NFTsByIssuer.cpp
|
||||
handlers/NFTBuyOffers.cpp
|
||||
handlers/NFTHistory.cpp
|
||||
@@ -58,4 +61,4 @@ target_sources(
|
||||
handlers/VaultInfo.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(clio_rpc PUBLIC clio_util clio_data)
|
||||
target_link_libraries(clio_rpc PUBLIC clio_util clio_data rpcspec::rpcspec)
|
||||
|
||||
@@ -239,7 +239,7 @@ public:
|
||||
* @return The error message
|
||||
*/
|
||||
[[nodiscard]] char const*
|
||||
what() const throw() override
|
||||
what() const noexcept override
|
||||
{
|
||||
return msg_.c_str();
|
||||
}
|
||||
@@ -267,7 +267,7 @@ public:
|
||||
* @return The error message
|
||||
*/
|
||||
[[nodiscard]] char const*
|
||||
what() const throw() override
|
||||
what() const noexcept override
|
||||
{
|
||||
return account_.c_str();
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ handledRpcs()
|
||||
"ledger_index",
|
||||
"ledger_range",
|
||||
"mpt_holders",
|
||||
"mptoken_issuance_history",
|
||||
"nfts_by_issuer",
|
||||
"nft_history",
|
||||
"nft_buy_offers",
|
||||
|
||||
@@ -178,8 +178,8 @@ public:
|
||||
}
|
||||
|
||||
return Result{std::move(v)};
|
||||
} catch (data::DatabaseTimeout const& t) {
|
||||
LOG(log_.error()) << "Database timeout";
|
||||
} catch (data::DatabaseError const& t) {
|
||||
LOG(log_.error()) << "Database error: " << t.what();
|
||||
notifyTooBusy();
|
||||
|
||||
return Result{Status{RippledError::RpcTooBusy}};
|
||||
@@ -361,8 +361,8 @@ private:
|
||||
}
|
||||
|
||||
return Result{std::move(v)};
|
||||
} catch (data::DatabaseTimeout const& t) {
|
||||
LOG(log_.error()) << "Database timeout";
|
||||
} catch (data::DatabaseError const& t) {
|
||||
LOG(log_.error()) << "Database error: " << t.what();
|
||||
notifyTooBusy();
|
||||
|
||||
return Result{Status{RippledError::RpcTooBusy}};
|
||||
|
||||
@@ -43,7 +43,9 @@
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/LedgerHeader.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/NFTSyntheticSerializer.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -1240,6 +1242,66 @@ transferRate(
|
||||
return xrpl::kParityRate;
|
||||
}
|
||||
|
||||
xrpl::STAmount
|
||||
accountHoldsMPT(
|
||||
BackendInterface const& backend,
|
||||
std::uint32_t sequence,
|
||||
xrpl::AccountID const& account,
|
||||
xrpl::MPTIssue const& mptIssue,
|
||||
bool zeroIfFrozen,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
xrpl::STAmount zero{mptIssue, 0};
|
||||
|
||||
if (account == mptIssue.getIssuer()) {
|
||||
// Issuer: available = MaximumAmount - OutstandingAmount
|
||||
auto const issuanceKey = xrpl::keylet::mptokenIssuance(mptIssue.getMptID()).key;
|
||||
auto const issuanceBlob = backend.fetchLedgerObject(issuanceKey, sequence, yield);
|
||||
if (!issuanceBlob)
|
||||
return zero;
|
||||
xrpl::SerialIter it{issuanceBlob->data(), issuanceBlob->size()};
|
||||
xrpl::SLE const sle{it, issuanceKey};
|
||||
auto const maxAmount = sle.isFieldPresent(xrpl::sfMaximumAmount)
|
||||
? sle.getFieldU64(xrpl::sfMaximumAmount)
|
||||
: xrpl::kMaxMpTokenAmount;
|
||||
auto const outstanding = sle.isFieldPresent(xrpl::sfOutstandingAmount)
|
||||
? sle.getFieldU64(xrpl::sfOutstandingAmount)
|
||||
: 0u;
|
||||
auto const available = (outstanding > maxAmount) ? 0u : (maxAmount - outstanding);
|
||||
return xrpl::STAmount{mptIssue, available};
|
||||
}
|
||||
|
||||
auto const key = xrpl::keylet::mptoken(mptIssue.getMptID(), account).key;
|
||||
auto const blob = backend.fetchLedgerObject(key, sequence, yield);
|
||||
if (!blob)
|
||||
return zero;
|
||||
|
||||
xrpl::SerialIter it{blob->data(), blob->size()};
|
||||
xrpl::SLE const sle{it, key};
|
||||
|
||||
auto const issuanceKey = xrpl::keylet::mptokenIssuance(mptIssue.getMptID()).key;
|
||||
auto const issuanceBlob = backend.fetchLedgerObject(issuanceKey, sequence, yield);
|
||||
std::optional<xrpl::SLE> issuanceSle;
|
||||
if (issuanceBlob) {
|
||||
xrpl::SerialIter issuanceIt{issuanceBlob->data(), issuanceBlob->size()};
|
||||
issuanceSle.emplace(issuanceIt, issuanceKey);
|
||||
}
|
||||
|
||||
if (zeroIfFrozen) {
|
||||
if (issuanceSle && issuanceSle->isFlag(xrpl::lsfMPTLocked))
|
||||
return zero;
|
||||
if (sle.isFlag(xrpl::lsfMPTLocked))
|
||||
return zero;
|
||||
}
|
||||
|
||||
if (issuanceSle && issuanceSle->isFlag(xrpl::lsfMPTRequireAuth) &&
|
||||
!sle.isFlag(xrpl::lsfMPTAuthorized))
|
||||
return zero;
|
||||
|
||||
return xrpl::STAmount{mptIssue, sle.getFieldU64(xrpl::sfMPTAmount)};
|
||||
}
|
||||
|
||||
boost::json::array
|
||||
postProcessOrderBook(
|
||||
std::vector<data::LedgerObject> const& offers,
|
||||
@@ -1274,8 +1336,13 @@ postProcessOrderBook(
|
||||
bool firstOwnerOffer = true;
|
||||
|
||||
if (book.out.getIssuer() == uOfferOwnerID) {
|
||||
// If an offer is selling issuer's own IOUs, it is fully
|
||||
// funded.
|
||||
// If an offer is selling the issuer's own asset, it is treated as fully funded.
|
||||
// This mirrors xrpld's getBookPage for both IOU and MPT.
|
||||
//
|
||||
// NOTE: xrpld has a separate fix (issuerFundsToSelfIssue) that bounds an MPT
|
||||
// issuer's self-issued offers by the remaining issuance capacity
|
||||
// (MaximumAmount - OutstandingAmount). It is not part of the getBookPage logic
|
||||
// mirrored here; port it if/when clio mirrors that updated getBookPage.
|
||||
saOwnerFunds = saTakerGets;
|
||||
} else if (globalFreeze) {
|
||||
// If either asset is globally frozen, consider all offers
|
||||
@@ -1289,16 +1356,27 @@ postProcessOrderBook(
|
||||
saOwnerFunds = umBalanceEntry->second;
|
||||
firstOwnerOffer = false;
|
||||
} else {
|
||||
saOwnerFunds = accountHolds(
|
||||
backend,
|
||||
amendmentCenter,
|
||||
ledgerSequence,
|
||||
uOfferOwnerID,
|
||||
book.out.get<xrpl::Issue>().currency,
|
||||
book.out.getIssuer(),
|
||||
true,
|
||||
yield
|
||||
);
|
||||
if (book.out.holds<xrpl::MPTIssue>()) {
|
||||
saOwnerFunds = accountHoldsMPT(
|
||||
backend,
|
||||
ledgerSequence,
|
||||
uOfferOwnerID,
|
||||
book.out.get<xrpl::MPTIssue>(),
|
||||
true,
|
||||
yield
|
||||
);
|
||||
} else {
|
||||
saOwnerFunds = accountHolds(
|
||||
backend,
|
||||
amendmentCenter,
|
||||
ledgerSequence,
|
||||
uOfferOwnerID,
|
||||
book.out.get<xrpl::Issue>().currency,
|
||||
book.out.getIssuer(),
|
||||
true,
|
||||
yield
|
||||
);
|
||||
}
|
||||
|
||||
if (saOwnerFunds < beast::kZero)
|
||||
saOwnerFunds.clear();
|
||||
@@ -1333,8 +1411,7 @@ postProcessOrderBook(
|
||||
toBoostJson(saTakerGetsFunded.getJson(xrpl::JsonOptions::Values::None));
|
||||
offerJson["taker_pays_funded"] = toBoostJson(
|
||||
std::min(
|
||||
saTakerPays,
|
||||
xrpl::multiply(saTakerGetsFunded, dirRate, saTakerPays.get<xrpl::Issue>())
|
||||
saTakerPays, xrpl::multiply(saTakerGetsFunded, dirRate, saTakerPays.asset())
|
||||
)
|
||||
.getJson(xrpl::JsonOptions::Values::None)
|
||||
);
|
||||
@@ -1360,7 +1437,6 @@ postProcessOrderBook(
|
||||
return jsonOffers;
|
||||
}
|
||||
|
||||
// get book via currency type
|
||||
std::expected<xrpl::Book, Status>
|
||||
parseBook(
|
||||
xrpl::Currency pays,
|
||||
@@ -1370,46 +1446,58 @@ parseBook(
|
||||
std::optional<std::string> const& domain
|
||||
)
|
||||
{
|
||||
if (isXRP(pays) && !isXRP(payIssuer)) {
|
||||
return std::unexpected{Status{
|
||||
RippledError::RpcSrcIsrMalformed,
|
||||
"Unneeded field 'taker_pays.issuer' for XRP currency specification."
|
||||
}};
|
||||
}
|
||||
return parseBook(xrpl::Issue{pays, payIssuer}, xrpl::Issue{gets, getIssuer}, domain);
|
||||
}
|
||||
|
||||
if (!isXRP(pays) && isXRP(payIssuer)) {
|
||||
return std::unexpected{Status{
|
||||
RippledError::RpcSrcIsrMalformed,
|
||||
"Invalid field 'taker_pays.issuer', expected non-XRP issuer."
|
||||
}};
|
||||
}
|
||||
std::expected<xrpl::Book, Status>
|
||||
parseBook(
|
||||
xrpl::Asset const& pays,
|
||||
xrpl::Asset const& gets,
|
||||
std::optional<std::string> const& domain
|
||||
)
|
||||
{
|
||||
auto const checkIssuer = [](xrpl::Asset const& asset,
|
||||
std::string_view field,
|
||||
RippledError error) -> std::optional<Status> {
|
||||
if (!asset.holds<xrpl::Issue>())
|
||||
return std::nullopt;
|
||||
|
||||
if (xrpl::isXRP(gets) && !xrpl::isXRP(getIssuer)) {
|
||||
return std::unexpected{Status{
|
||||
RippledError::RpcDstIsrMalformed,
|
||||
"Unneeded field 'taker_gets.issuer' for XRP currency specification."
|
||||
}};
|
||||
}
|
||||
auto const& issue = asset.get<xrpl::Issue>();
|
||||
if (xrpl::isXRP(issue.currency) && !xrpl::isXRP(issue.account)) {
|
||||
return Status{
|
||||
error,
|
||||
fmt::format("Unneeded field '{}.issuer' for XRP currency specification.", field)
|
||||
};
|
||||
}
|
||||
if (!xrpl::isXRP(issue.currency) && xrpl::isXRP(issue.account)) {
|
||||
return Status{
|
||||
error, fmt::format("Invalid field '{}.issuer', expected non-XRP issuer.", field)
|
||||
};
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
if (!xrpl::isXRP(gets) && xrpl::isXRP(getIssuer)) {
|
||||
return std::unexpected{Status{
|
||||
RippledError::RpcDstIsrMalformed,
|
||||
"Invalid field 'taker_gets.issuer', expected non-XRP issuer."
|
||||
}};
|
||||
}
|
||||
if (auto const err = checkIssuer(pays, JS(taker_pays), RippledError::RpcSrcIsrMalformed))
|
||||
return std::unexpected{*err};
|
||||
|
||||
if (pays == gets && payIssuer == getIssuer)
|
||||
return std::unexpected{Status{RippledError::RpcBadMarket, "badMarket"}};
|
||||
if (auto const err = checkIssuer(gets, JS(taker_gets), RippledError::RpcDstIsrMalformed))
|
||||
return std::unexpected{*err};
|
||||
|
||||
std::optional<xrpl::uint256> domainID = std::nullopt;
|
||||
if (domain.has_value()) {
|
||||
xrpl::uint256 dom;
|
||||
if (!dom.parseHex(*domain))
|
||||
return std::unexpected{Status{RippledError::RpcDomainMalformed}};
|
||||
if (!dom.parseHex(*domain)) {
|
||||
return std::unexpected{
|
||||
Status{RippledError::RpcDomainMalformed, "Unable to parse domain."}
|
||||
};
|
||||
}
|
||||
domainID = dom;
|
||||
}
|
||||
|
||||
return xrpl::Book{xrpl::Issue{pays, payIssuer}, xrpl::Issue{gets, getIssuer}, domainID};
|
||||
if (pays == gets)
|
||||
return std::unexpected{Status{RippledError::RpcBadMarket}};
|
||||
|
||||
return xrpl::Book{pays, gets, domainID};
|
||||
}
|
||||
|
||||
std::expected<xrpl::Book, Status>
|
||||
@@ -1653,4 +1741,47 @@ toJsonWithBinaryTx(data::TransactionAndMetadata const& txnPlusMeta, std::uint32_
|
||||
return obj;
|
||||
}
|
||||
|
||||
std::optional<DelegateFilter::Role>
|
||||
parseDelegateType(boost::json::value const& delegateType)
|
||||
{
|
||||
if (not delegateType.is_string())
|
||||
return {};
|
||||
|
||||
auto const& type = delegateType.as_string();
|
||||
|
||||
if (type == JS(authorizer))
|
||||
return DelegateFilter::Role::Authorizer;
|
||||
if (type == JS(actor))
|
||||
return DelegateFilter::Role::Actor;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::optional<DelegateFilter>
|
||||
parseDelegateFilter(boost::json::object const& delegateObject)
|
||||
{
|
||||
if (!delegateObject.contains(JS(delegate_filter)))
|
||||
return {};
|
||||
|
||||
auto const& filterVal = delegateObject.at(JS(delegate_filter));
|
||||
if (!filterVal.is_string())
|
||||
return {};
|
||||
|
||||
auto const delegateTypeOpt = parseDelegateType(filterVal.as_string());
|
||||
if (!delegateTypeOpt.has_value())
|
||||
return {};
|
||||
|
||||
std::optional<std::string> counterParty;
|
||||
if (delegateObject.contains(JS(counter_party))) {
|
||||
auto const& counterpartyVal = delegateObject.at(JS(counter_party));
|
||||
|
||||
if (!counterpartyVal.is_string())
|
||||
return {};
|
||||
|
||||
counterParty = counterpartyVal.as_string();
|
||||
}
|
||||
|
||||
return DelegateFilter{*delegateTypeOpt, std::move(counterParty)};
|
||||
}
|
||||
|
||||
} // namespace rpc
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "data/BackendInterface.hpp"
|
||||
#include "data/Types.hpp"
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "util/JsonUtils.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
@@ -27,6 +26,7 @@
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -575,6 +575,37 @@ transferRate(
|
||||
boost::asio::yield_context yield
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Get the amount that an account holds in MPT
|
||||
*
|
||||
* Mirrors `xrpld`'s `accountHolds(MPTIssue)` as used by `getBookPage`. For the issuer the
|
||||
* spendable amount is the issuance capacity (`MaximumAmount - OutstandingAmount`). For a holder it
|
||||
* is the token balance, returned as zero if the holder's token is unauthorized while the issuance
|
||||
* requires authorization (xrpld's `AuthHandling::ZeroIfUnauthorized`), and, when @p zeroIfFrozen
|
||||
* is set, zero if the issuance or the token is locked.
|
||||
*
|
||||
* @note xrpld also zeroes the balance when the holder is a frozen vault pseudo-account
|
||||
* (`isVaultPseudoAccountFrozen`) and supports an amendment-gated StrongAuth variant; neither is
|
||||
* modelled here.
|
||||
*
|
||||
* @param backend The backend to use
|
||||
* @param sequence The sequence
|
||||
* @param account The account
|
||||
* @param mptIssue The MPT issue
|
||||
* @param zeroIfFrozen Whether to return zero if frozen
|
||||
* @param yield The coroutine context
|
||||
* @return The amount account holds
|
||||
*/
|
||||
xrpl::STAmount
|
||||
accountHoldsMPT(
|
||||
BackendInterface const& backend,
|
||||
std::uint32_t sequence,
|
||||
xrpl::AccountID const& account,
|
||||
xrpl::MPTIssue const& mptIssue,
|
||||
bool zeroIfFrozen,
|
||||
boost::asio::yield_context yield
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Get the XRP liquidity
|
||||
*
|
||||
@@ -634,6 +665,21 @@ parseBook(
|
||||
std::optional<std::string> const& domain
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Parse the book from assets (supports both IOU and MPT)
|
||||
*
|
||||
* @param pays The asset to pay
|
||||
* @param gets The asset to get
|
||||
* @param domain The domain
|
||||
* @return The book or an error status
|
||||
*/
|
||||
std::expected<xrpl::Book, Status>
|
||||
parseBook(
|
||||
xrpl::Asset const& pays,
|
||||
xrpl::Asset const& gets,
|
||||
std::optional<std::string> const& domain
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Parse the book from the request
|
||||
*
|
||||
@@ -853,4 +899,22 @@ getDeliveredAmount(
|
||||
uint32_t date
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Parse the delegate type from a JSON value
|
||||
*
|
||||
* @param delegateType The JSON value containing the delegate type string
|
||||
* @return The parsed delegate type or std::nullopt if the input is invalid or not a string
|
||||
*/
|
||||
std::optional<DelegateFilter::Role>
|
||||
parseDelegateType(boost::json::value const& delegateType);
|
||||
|
||||
/**
|
||||
* @brief Parse a delegate filter object from JSON
|
||||
*
|
||||
* @param delegateObject The JSON object containing the delegate filter input from user
|
||||
* @return The constructed DelegateFilter or std::nullopt if parsing fails
|
||||
*/
|
||||
std::optional<DelegateFilter>
|
||||
parseDelegateFilter(boost::json::object const& delegateObject);
|
||||
|
||||
} // namespace rpc
|
||||
|
||||
@@ -1,58 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <boost/json/conversion.hpp>
|
||||
#include <boost/json/kind.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <rpcspec/JsonBool.hpp>
|
||||
|
||||
namespace rpc {
|
||||
|
||||
/**
|
||||
* @brief A wrapper around bool that allows to convert from any JSON value
|
||||
*/
|
||||
struct JsonBool {
|
||||
bool value = false;
|
||||
|
||||
/** @cond */
|
||||
operator bool() const
|
||||
{
|
||||
return value;
|
||||
}
|
||||
/** @endcond */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Convert a JSON value to a JsonBool
|
||||
* @brief A wrapper around bool that allows conversion from any JSON value.
|
||||
*
|
||||
* @param jsonValue The JSON value to convert
|
||||
* @return The converted JsonBool
|
||||
*/
|
||||
inline JsonBool
|
||||
tag_invoke(boost::json::value_to_tag<JsonBool> const&, boost::json::value const& jsonValue)
|
||||
{
|
||||
switch (jsonValue.kind()) {
|
||||
case boost::json::kind::null:
|
||||
return JsonBool{false};
|
||||
case boost::json::kind::bool_:
|
||||
return JsonBool{jsonValue.as_bool()};
|
||||
case boost::json::kind::uint64:
|
||||
[[fallthrough]];
|
||||
case boost::json::kind::int64:
|
||||
return JsonBool{jsonValue.as_int64() != 0};
|
||||
case boost::json::kind::double_:
|
||||
return JsonBool{jsonValue.as_double() != 0.0};
|
||||
case boost::json::kind::string:
|
||||
// Also should be `jsonValue.as_string() != "false"` but rippled doesn't do
|
||||
// that. Anyway for v2 api we have bool validation
|
||||
return JsonBool{!jsonValue.as_string().empty() && jsonValue.as_string()[0] != 0};
|
||||
case boost::json::kind::array:
|
||||
return JsonBool{!jsonValue.as_array().empty()};
|
||||
case boost::json::kind::object:
|
||||
return JsonBool{!jsonValue.as_object().empty()};
|
||||
}
|
||||
throw std::runtime_error("Invalid json value");
|
||||
}
|
||||
using JsonBool = spec::JsonBool;
|
||||
|
||||
} // namespace rpc
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
namespace etl {
|
||||
class LoadBalancer;
|
||||
@@ -175,6 +175,35 @@ struct AccountCursor {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A delegate object used filter account_tx by specific delegate accounts
|
||||
*/
|
||||
struct DelegateFilter {
|
||||
/**
|
||||
* @brief A delegate type used in delegate filter
|
||||
*/
|
||||
enum class Role : std::uint8_t {
|
||||
Actor, /**< This account is the *active* sender, acting on behalf of another party.
|
||||
* e.g., Account A in "A sends payment to B on behalf of C." */
|
||||
|
||||
Authorizer /**< This account is the *passive* party whose funds are being moved from.
|
||||
* e.g., Account C in "A sends payment to B on behalf of C." */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Construct a delegate filter
|
||||
* @param role The delegate type to filter by
|
||||
* @param counterParty The optional counterparty to further narrow the filter by
|
||||
*/
|
||||
explicit DelegateFilter(Role role, std::optional<std::string> counterParty = std::nullopt)
|
||||
: delegateType(role), counterParty(std::move(counterParty))
|
||||
{
|
||||
}
|
||||
|
||||
Role delegateType;
|
||||
std::optional<std::string> counterParty;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Convert an empty output to a JSON object
|
||||
*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "rpc/common/Validators.hpp"
|
||||
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/RPCHelpers.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "util/AccountUtils.hpp"
|
||||
@@ -17,6 +18,7 @@
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
@@ -226,6 +228,40 @@ CustomValidator CustomValidators::issuerValidator =
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::bookTakerValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_object())
|
||||
return {}; // type is validated separately by Type<object>
|
||||
|
||||
auto const& obj = value.as_object();
|
||||
bool const hasCurrency = obj.contains("currency");
|
||||
bool const hasMptId = obj.contains("mpt_issuance_id");
|
||||
|
||||
if (!hasCurrency && !hasMptId) {
|
||||
return Error{Status{
|
||||
RippledError::RpcInvalidParams, fmt::format("Missing field '{}.currency'.", key)
|
||||
}};
|
||||
}
|
||||
|
||||
if (hasMptId && (hasCurrency || obj.contains("issuer"))) {
|
||||
return Error{
|
||||
Status{RippledError::RpcInvalidParams, fmt::format("Invalid field '{}'.", key)}
|
||||
};
|
||||
}
|
||||
|
||||
// Wrong type -> invalidParams (xrpld's validateTakerJSON), checked here before the
|
||||
// per-field validators so they can own bad *values* -> dst/srcAmtMalformed.
|
||||
if ((hasCurrency && !obj.at(JS(currency)).is_string()) ||
|
||||
(hasMptId && !obj.at(JS(mpt_issuance_id)).is_string())) {
|
||||
return Error{Status{
|
||||
RippledError::RpcInvalidParams,
|
||||
fmt::format("Invalid field '{}.currency', not string.", key)
|
||||
}};
|
||||
}
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::subscribeStreamValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_array())
|
||||
@@ -363,7 +399,7 @@ CustomValidator CustomValidators::authorizeCredentialValidator =
|
||||
}
|
||||
|
||||
// don't want to change issuer error message to be about credentials
|
||||
if (!issuerValidator.verify(credObj, "issuer")) {
|
||||
if (!accountBase58Validator.verify(credObj, "issuer")) {
|
||||
return Error{
|
||||
Status{ClioError::RpcMalformedAuthorizedCredentials, "issuer NotString"}
|
||||
};
|
||||
@@ -383,4 +419,34 @@ CustomValidator CustomValidators::authorizeCredentialValidator =
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::delegateValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (not value.is_object())
|
||||
return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotObject"}};
|
||||
|
||||
auto const& delegate = value.as_object();
|
||||
if (!delegate.contains(JS(delegate_filter))) {
|
||||
return Error{Status{
|
||||
RippledError::RpcInvalidParams, "Field 'delegate_filter' is required but missing."
|
||||
}};
|
||||
}
|
||||
|
||||
if (!parseDelegateType(delegate.at(JS(delegate_filter))).has_value()) {
|
||||
return Error{Status{
|
||||
RippledError::RpcInvalidParams,
|
||||
"Field 'delegate_filter' value must be 'actor' or 'authorizer'."
|
||||
}};
|
||||
}
|
||||
|
||||
if (delegate.contains(JS(counter_party)) &&
|
||||
!accountValidator.verify(delegate, JS(counter_party))) {
|
||||
return Error{Status{
|
||||
RippledError::RpcActMalformed,
|
||||
"Field 'counter_party' value must be a valid account."
|
||||
}};
|
||||
}
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
} // namespace rpc::validation
|
||||
|
||||
@@ -580,6 +580,20 @@ struct CustomValidators final {
|
||||
*/
|
||||
static CustomValidator currencyIssueValidator;
|
||||
|
||||
/**
|
||||
* @brief Validates a book taker object (`taker_gets`/`taker_pays`).
|
||||
*
|
||||
* The object must specify an asset as either a `currency` (optionally with an `issuer`) or an
|
||||
* `mpt_issuance_id`, but not both, and `mpt_issuance_id` must not be combined with `issuer`.
|
||||
* Mirrors `xrpld`'s `validateTakerJSON`: a missing asset yields `Missing field
|
||||
* '<field>.currency'.`, conflicting fields yield `Invalid field '<field>'.`, and a present but
|
||||
* non-string `currency`/`mpt_issuance_id` yields `Invalid field '<field>.currency', not
|
||||
* string.` (all `invalidParams`). The field name is taken from the validated key.
|
||||
*
|
||||
* Used by book_offers.
|
||||
*/
|
||||
static CustomValidator bookTakerValidator;
|
||||
|
||||
/**
|
||||
* @brief Provides a validator for validating authorized_credentials json array.
|
||||
*
|
||||
@@ -590,9 +604,17 @@ struct CustomValidators final {
|
||||
/**
|
||||
* @brief Provides a validator for validating credential_type.
|
||||
*
|
||||
* Used by AuthorizeCredentialValidator in deposit_preauth.
|
||||
* Used by AuthorizeCredentialValidator in deposit_preauth and by the credential
|
||||
* object lookup in ledger_entry.
|
||||
*/
|
||||
static CustomValidator credentialTypeValidator;
|
||||
|
||||
/**
|
||||
* @brief Provides a validator for validating filtering by delegation.
|
||||
*
|
||||
* Used by account_tx if user wants to filter by delegation.
|
||||
*/
|
||||
static CustomValidator delegateValidator;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,46 +5,8 @@
|
||||
#include "etl/ETLServiceInterface.hpp"
|
||||
#include "etl/LoadBalancerInterface.hpp"
|
||||
#include "feed/SubscriptionManagerInterface.hpp"
|
||||
#include "rpc/Counters.hpp"
|
||||
#include "rpc/common/AnyHandler.hpp"
|
||||
#include "rpc/handlers/AMMInfo.hpp"
|
||||
#include "rpc/handlers/AccountChannels.hpp"
|
||||
#include "rpc/handlers/AccountCurrencies.hpp"
|
||||
#include "rpc/handlers/AccountInfo.hpp"
|
||||
#include "rpc/handlers/AccountLines.hpp"
|
||||
#include "rpc/handlers/AccountMPTokenIssuances.hpp"
|
||||
#include "rpc/handlers/AccountMPTokens.hpp"
|
||||
#include "rpc/handlers/AccountNFTs.hpp"
|
||||
#include "rpc/handlers/AccountObjects.hpp"
|
||||
#include "rpc/handlers/AccountOffers.hpp"
|
||||
#include "rpc/handlers/AccountTx.hpp"
|
||||
#include "rpc/handlers/BookChanges.hpp"
|
||||
#include "rpc/handlers/BookOffers.hpp"
|
||||
#include "rpc/handlers/DepositAuthorized.hpp"
|
||||
#include "rpc/handlers/Feature.hpp"
|
||||
#include "rpc/handlers/GatewayBalances.hpp"
|
||||
#include "rpc/handlers/GetAggregatePrice.hpp"
|
||||
#include "rpc/handlers/Ledger.hpp"
|
||||
#include "rpc/handlers/LedgerData.hpp"
|
||||
#include "rpc/handlers/LedgerEntry.hpp"
|
||||
#include "rpc/handlers/LedgerIndex.hpp"
|
||||
#include "rpc/handlers/LedgerRange.hpp"
|
||||
#include "rpc/handlers/MPTHolders.hpp"
|
||||
#include "rpc/handlers/NFTBuyOffers.hpp"
|
||||
#include "rpc/handlers/NFTHistory.hpp"
|
||||
#include "rpc/handlers/NFTInfo.hpp"
|
||||
#include "rpc/handlers/NFTSellOffers.hpp"
|
||||
#include "rpc/handlers/NFTsByIssuer.hpp"
|
||||
#include "rpc/handlers/NoRippleCheck.hpp"
|
||||
#include "rpc/handlers/Ping.hpp"
|
||||
#include "rpc/handlers/Random.hpp"
|
||||
#include "rpc/handlers/ServerInfo.hpp"
|
||||
#include "rpc/handlers/Subscribe.hpp"
|
||||
#include "rpc/handlers/TransactionEntry.hpp"
|
||||
#include "rpc/handlers/Tx.hpp"
|
||||
#include "rpc/handlers/Unsubscribe.hpp"
|
||||
#include "rpc/handlers/VaultInfo.hpp"
|
||||
#include "rpc/handlers/VersionHandler.hpp"
|
||||
#include "rpc/common/impl/HandlerRegistry.hpp"
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
|
||||
#include <memory>
|
||||
@@ -63,55 +25,29 @@ ProductionHandlerProvider::ProductionHandlerProvider(
|
||||
std::shared_ptr<data::AmendmentCenterInterface const> const& amendmentCenter,
|
||||
Counters const& counters
|
||||
)
|
||||
: handlerMap_{
|
||||
{"account_channels", {.handler = AccountChannelsHandler{backend}}},
|
||||
{"account_currencies", {.handler = AccountCurrenciesHandler{backend}}},
|
||||
{"account_info", {.handler = AccountInfoHandler{backend, amendmentCenter}}},
|
||||
{"account_lines", {.handler = AccountLinesHandler{backend}}},
|
||||
{"account_mptoken_issuances",
|
||||
{.handler = AccountMPTokenIssuancesHandler{backend}, .isClioOnly = true}}, // clio only
|
||||
{"account_mptokens",
|
||||
{.handler = AccountMPTokensHandler{backend}, .isClioOnly = true}}, // clio only
|
||||
{"account_nfts", {.handler = AccountNFTsHandler{backend}}},
|
||||
{"account_objects", {.handler = AccountObjectsHandler{backend}}},
|
||||
{"account_offers", {.handler = AccountOffersHandler{backend}}},
|
||||
{"account_tx", {.handler = AccountTxHandler{backend, etl}}},
|
||||
{"amm_info", {.handler = AMMInfoHandler{backend, amendmentCenter}}},
|
||||
{"book_changes", {.handler = BookChangesHandler{backend}}},
|
||||
{"book_offers", {.handler = BookOffersHandler{backend, amendmentCenter}}},
|
||||
{"deposit_authorized", {.handler = DepositAuthorizedHandler{backend}}},
|
||||
{"feature", {.handler = FeatureHandler{backend, amendmentCenter}}},
|
||||
{"gateway_balances", {.handler = GatewayBalancesHandler{backend}}},
|
||||
{"get_aggregate_price", {.handler = GetAggregatePriceHandler{backend}}},
|
||||
{"ledger", {.handler = LedgerHandler{backend, amendmentCenter}}},
|
||||
{"ledger_data", {.handler = LedgerDataHandler{backend}}},
|
||||
{"ledger_entry", {.handler = LedgerEntryHandler{backend}}},
|
||||
{"ledger_index",
|
||||
{.handler = LedgerIndexHandler{backend}, .isClioOnly = true}}, // clio only
|
||||
{"ledger_range", {.handler = LedgerRangeHandler{backend}}},
|
||||
{"mpt_holders",
|
||||
{.handler = MPTHoldersHandler{backend}, .isClioOnly = true}}, // clio only
|
||||
{"nfts_by_issuer",
|
||||
{.handler = NFTsByIssuerHandler{backend}, .isClioOnly = true}}, // clio only
|
||||
{"nft_history",
|
||||
{.handler = NFTHistoryHandler{backend}, .isClioOnly = true}}, // clio only
|
||||
{"nft_buy_offers", {.handler = NFTBuyOffersHandler{backend}}},
|
||||
{"nft_info", {.handler = NFTInfoHandler{backend}, .isClioOnly = true}}, // clio only
|
||||
{"nft_sell_offers", {.handler = NFTSellOffersHandler{backend}}},
|
||||
{"noripple_check", {.handler = NoRippleCheckHandler{backend}}},
|
||||
{"ping", {.handler = PingHandler{}}},
|
||||
{"random", {.handler = RandomHandler{}}},
|
||||
{"server_info",
|
||||
{.handler = ServerInfoHandler{backend, subscriptionManager, balancer, etl, counters}}},
|
||||
{"transaction_entry", {.handler = TransactionEntryHandler{backend}}},
|
||||
{"tx", {.handler = TxHandler{backend, etl}}},
|
||||
{"subscribe",
|
||||
{.handler = SubscribeHandler{backend, amendmentCenter, subscriptionManager}}},
|
||||
{"unsubscribe", {.handler = UnsubscribeHandler{subscriptionManager}}},
|
||||
{"vault_info", {.handler = VaultInfoHandler{backend}}},
|
||||
{"version", {.handler = VersionHandler{config}}},
|
||||
}
|
||||
{
|
||||
HandlerDeps const deps{
|
||||
.config = config,
|
||||
.backend = backend,
|
||||
.subscriptionManager = subscriptionManager,
|
||||
.balancer = balancer,
|
||||
.etl = etl,
|
||||
.amendmentCenter = amendmentCenter,
|
||||
.counters = counters
|
||||
};
|
||||
|
||||
auto const registry = handlerRegistry();
|
||||
handlerMap_.reserve(registry.size());
|
||||
|
||||
for (auto const& entry : registry) {
|
||||
handlerMap_.emplace(
|
||||
entry.name,
|
||||
Handler{
|
||||
.handler = entry.factory(deps),
|
||||
.isClioOnly = entry.isClioOnly,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
|
||||
328
src/rpc/common/impl/HandlerRegistry.cpp
Normal file
328
src/rpc/common/impl/HandlerRegistry.cpp
Normal file
@@ -0,0 +1,328 @@
|
||||
#include "rpc/common/impl/HandlerRegistry.hpp"
|
||||
|
||||
#include "rpc/Counters.hpp" // IWYU pragma: keep
|
||||
#include "rpc/common/AnyHandler.hpp"
|
||||
#include "rpc/handlers/AMMInfo.hpp"
|
||||
#include "rpc/handlers/AccountChannels.hpp"
|
||||
#include "rpc/handlers/AccountCurrencies.hpp"
|
||||
#include "rpc/handlers/AccountInfo.hpp"
|
||||
#include "rpc/handlers/AccountLines.hpp"
|
||||
#include "rpc/handlers/AccountMPTokenIssuances.hpp"
|
||||
#include "rpc/handlers/AccountMPTokens.hpp"
|
||||
#include "rpc/handlers/AccountNFTs.hpp"
|
||||
#include "rpc/handlers/AccountObjects.hpp"
|
||||
#include "rpc/handlers/AccountOffers.hpp"
|
||||
#include "rpc/handlers/AccountTx.hpp"
|
||||
#include "rpc/handlers/BookChanges.hpp"
|
||||
#include "rpc/handlers/BookOffers.hpp"
|
||||
#include "rpc/handlers/DepositAuthorized.hpp"
|
||||
#include "rpc/handlers/Feature.hpp"
|
||||
#include "rpc/handlers/GatewayBalances.hpp"
|
||||
#include "rpc/handlers/GetAggregatePrice.hpp"
|
||||
#include "rpc/handlers/Ledger.hpp"
|
||||
#include "rpc/handlers/LedgerData.hpp"
|
||||
#include "rpc/handlers/LedgerEntry.hpp"
|
||||
#include "rpc/handlers/LedgerIndex.hpp"
|
||||
#include "rpc/handlers/LedgerRange.hpp"
|
||||
#include "rpc/handlers/MPTHolders.hpp"
|
||||
#include "rpc/handlers/MPTokenIssuanceHistory.hpp"
|
||||
#include "rpc/handlers/NFTBuyOffers.hpp"
|
||||
#include "rpc/handlers/NFTHistory.hpp"
|
||||
#include "rpc/handlers/NFTInfo.hpp"
|
||||
#include "rpc/handlers/NFTSellOffers.hpp"
|
||||
#include "rpc/handlers/NFTsByIssuer.hpp"
|
||||
#include "rpc/handlers/NoRippleCheck.hpp"
|
||||
#include "rpc/handlers/Ping.hpp"
|
||||
#include "rpc/handlers/Random.hpp"
|
||||
#include "rpc/handlers/ServerInfo.hpp"
|
||||
#include "rpc/handlers/Subscribe.hpp"
|
||||
#include "rpc/handlers/TransactionEntry.hpp"
|
||||
#include "rpc/handlers/Tx.hpp"
|
||||
#include "rpc/handlers/Unsubscribe.hpp"
|
||||
#include "rpc/handlers/VaultInfo.hpp"
|
||||
#include "rpc/handlers/VersionHandler.hpp"
|
||||
#include "util/Concepts.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <span>
|
||||
#include <tuple>
|
||||
|
||||
namespace rpc::impl {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kHandlers = std::to_array<HandlerEntry>({
|
||||
{
|
||||
.name = "account_channels",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return AccountChannelsHandler{d.backend};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "account_currencies",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return AccountCurrenciesHandler{d.backend};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "account_info",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return AccountInfoHandler{d.backend, d.amendmentCenter};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "account_lines",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return AccountLinesHandler{d.backend};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "account_mptoken_issuances",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return AccountMPTokenIssuancesHandler{d.backend};
|
||||
},
|
||||
.isClioOnly = true,
|
||||
},
|
||||
|
||||
{
|
||||
.name = "account_mptokens",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return AccountMPTokensHandler{d.backend};
|
||||
},
|
||||
.isClioOnly = true,
|
||||
},
|
||||
|
||||
{
|
||||
.name = "account_nfts",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler { return AccountNFTsHandler{d.backend}; },
|
||||
},
|
||||
|
||||
{
|
||||
.name = "account_objects",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return AccountObjectsHandler{d.backend};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "account_offers",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return AccountOffersHandler{d.backend};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "account_tx",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return AccountTxHandler{d.backend, d.etl};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "amm_info",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return AMMInfoHandler{d.backend, d.amendmentCenter};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "book_changes",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler { return BookChangesHandler{d.backend}; },
|
||||
},
|
||||
|
||||
{
|
||||
.name = "book_offers",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return BookOffersHandler{d.backend, d.amendmentCenter};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "deposit_authorized",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return DepositAuthorizedHandler{d.backend};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "feature",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return FeatureHandler{d.backend, d.amendmentCenter};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "gateway_balances",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return GatewayBalancesHandler{d.backend};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "get_aggregate_price",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return GetAggregatePriceHandler{d.backend};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "ledger",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return LedgerHandler{d.backend, d.amendmentCenter};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "ledger_data",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler { return LedgerDataHandler{d.backend}; },
|
||||
},
|
||||
|
||||
{
|
||||
.name = "ledger_entry",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler { return LedgerEntryHandler{d.backend}; },
|
||||
},
|
||||
|
||||
{
|
||||
.name = "ledger_index",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler { return LedgerIndexHandler{d.backend}; },
|
||||
.isClioOnly = true,
|
||||
},
|
||||
|
||||
{
|
||||
.name = "ledger_range",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler { return LedgerRangeHandler{d.backend}; },
|
||||
},
|
||||
|
||||
{
|
||||
.name = "mpt_holders",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler { return MPTHoldersHandler{d.backend}; },
|
||||
.isClioOnly = true,
|
||||
},
|
||||
|
||||
{
|
||||
.name = "mptoken_issuance_history",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return MPTokenIssuanceHistoryHandler{d.backend};
|
||||
},
|
||||
.isClioOnly = true,
|
||||
},
|
||||
|
||||
{
|
||||
.name = "nfts_by_issuer",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return NFTsByIssuerHandler{d.backend};
|
||||
},
|
||||
.isClioOnly = true,
|
||||
},
|
||||
|
||||
{
|
||||
.name = "nft_history",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler { return NFTHistoryHandler{d.backend}; },
|
||||
.isClioOnly = true,
|
||||
},
|
||||
|
||||
{
|
||||
.name = "nft_buy_offers",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return NFTBuyOffersHandler{d.backend};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "nft_info",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler { return NFTInfoHandler{d.backend}; },
|
||||
.isClioOnly = true,
|
||||
},
|
||||
|
||||
{
|
||||
.name = "nft_sell_offers",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return NFTSellOffersHandler{d.backend};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "noripple_check",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return NoRippleCheckHandler{d.backend};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "ping",
|
||||
.factory = [](HandlerDeps const&) -> AnyHandler { return PingHandler{}; },
|
||||
},
|
||||
|
||||
{
|
||||
.name = "random",
|
||||
.factory = [](HandlerDeps const&) -> AnyHandler { return RandomHandler{}; },
|
||||
},
|
||||
|
||||
{
|
||||
.name = "server_info",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return ServerInfoHandler{
|
||||
d.backend, d.subscriptionManager, d.balancer, d.etl, d.counters
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "transaction_entry",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return TransactionEntryHandler{d.backend};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "tx",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler { return TxHandler{d.backend, d.etl}; },
|
||||
},
|
||||
|
||||
{
|
||||
.name = "subscribe",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return SubscribeHandler{d.backend, d.amendmentCenter, d.subscriptionManager};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "unsubscribe",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler {
|
||||
return UnsubscribeHandler{d.subscriptionManager};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
.name = "vault_info",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler { return VaultInfoHandler{d.backend}; },
|
||||
},
|
||||
|
||||
{
|
||||
.name = "version",
|
||||
.factory = [](HandlerDeps const& d) -> AnyHandler { return VersionHandler{d.config}; },
|
||||
},
|
||||
});
|
||||
|
||||
// A duplicate name would silently shadow a handler so we check at compile time.
|
||||
static_assert(
|
||||
std::apply(
|
||||
[](auto const&... entry) { return util::hasNoDuplicates(entry.name...); },
|
||||
kHandlers
|
||||
),
|
||||
"RPC handler names must be unique"
|
||||
);
|
||||
|
||||
} // namespace
|
||||
|
||||
std::span<HandlerEntry const>
|
||||
handlerRegistry() noexcept
|
||||
{
|
||||
return kHandlers;
|
||||
}
|
||||
|
||||
} // namespace rpc::impl
|
||||
79
src/rpc/common/impl/HandlerRegistry.hpp
Normal file
79
src/rpc/common/impl/HandlerRegistry.hpp
Normal file
@@ -0,0 +1,79 @@
|
||||
/** @file */
|
||||
#pragma once
|
||||
|
||||
#include "rpc/common/AnyHandler.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
|
||||
namespace data {
|
||||
class AmendmentCenterInterface;
|
||||
class BackendInterface;
|
||||
} // namespace data
|
||||
|
||||
namespace etl {
|
||||
struct ETLServiceInterface;
|
||||
class LoadBalancerInterface;
|
||||
} // namespace etl
|
||||
|
||||
namespace feed {
|
||||
class SubscriptionManagerInterface;
|
||||
} // namespace feed
|
||||
|
||||
namespace util::config {
|
||||
class ClioConfigDefinition;
|
||||
} // namespace util::config
|
||||
|
||||
namespace rpc {
|
||||
class Counters;
|
||||
} // namespace rpc
|
||||
|
||||
namespace rpc::impl {
|
||||
|
||||
/**
|
||||
* @brief The bundle of runtime dependencies a handler factory may consume.
|
||||
*
|
||||
* A short-lived view over the caller's objects: it holds references only, so it must
|
||||
* not outlive the call it is passed to.
|
||||
*/
|
||||
struct HandlerDeps {
|
||||
util::config::ClioConfigDefinition const& config;
|
||||
std::shared_ptr<data::BackendInterface> const& backend;
|
||||
std::shared_ptr<feed::SubscriptionManagerInterface> const& subscriptionManager;
|
||||
std::shared_ptr<etl::LoadBalancerInterface> const& balancer;
|
||||
std::shared_ptr<etl::ETLServiceInterface const> const& etl;
|
||||
std::shared_ptr<data::AmendmentCenterInterface const> const& amendmentCenter;
|
||||
Counters const& counters;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Constructs one type-erased handler from the available dependencies.
|
||||
*
|
||||
* A plain function pointer rather than @c std::function so that the registry stays a
|
||||
* literal type and is constant-initialised.
|
||||
*/
|
||||
using HandlerFactory = AnyHandler (*)(HandlerDeps const&);
|
||||
|
||||
/**
|
||||
* @brief One row of the handler registry: the method name, how to construct the
|
||||
* handler, and its static metadata.
|
||||
*/
|
||||
struct HandlerEntry {
|
||||
std::string_view name;
|
||||
HandlerFactory factory;
|
||||
bool isClioOnly = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The full set of registered RPC handlers.
|
||||
*
|
||||
* Static data: reading the registry constructs no handler and requires no runtime
|
||||
* dependencies.
|
||||
*
|
||||
* @return A span over the registry, valid for the lifetime of the program
|
||||
*/
|
||||
[[nodiscard]] std::span<HandlerEntry const>
|
||||
handlerRegistry() noexcept;
|
||||
|
||||
} // namespace rpc::impl
|
||||
49
src/rpc/filters/TransactionFilter.hpp
Normal file
49
src/rpc/filters/TransactionFilter.hpp
Normal file
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "data/Types.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace rpc {
|
||||
|
||||
/**
|
||||
* @brief Interface for filtering transactions.
|
||||
*/
|
||||
class TransactionFilter {
|
||||
public:
|
||||
/**
|
||||
* @brief The result of a filter check that matched a transaction.
|
||||
*/
|
||||
struct CheckResult {
|
||||
/**
|
||||
* @brief Construct a check result
|
||||
*
|
||||
* @param account The relevant account to report for this txn
|
||||
* @param role The role `account` played in this txn
|
||||
*/
|
||||
CheckResult(xrpl::AccountID account, DelegateFilter::Role role)
|
||||
: account{account}, role{role}
|
||||
{
|
||||
}
|
||||
|
||||
xrpl::AccountID account;
|
||||
DelegateFilter::Role role;
|
||||
};
|
||||
|
||||
virtual ~TransactionFilter() = default;
|
||||
|
||||
/**
|
||||
* @brief Check if a transaction blob matches the filter criteria.
|
||||
*
|
||||
* @param txnPlusMeta The transaction and metadata blob from the backend.
|
||||
* @return The relevant account and role to report for this txn if it should be included in
|
||||
* the output Json, or std::nullopt if the txn should be excluded
|
||||
*/
|
||||
[[nodiscard]] virtual std::optional<CheckResult>
|
||||
check(data::TransactionAndMetadata const& txnPlusMeta) const = 0;
|
||||
};
|
||||
|
||||
} // namespace rpc
|
||||
64
src/rpc/filters/impl/DelegateTransactionsFilter.cpp
Normal file
64
src/rpc/filters/impl/DelegateTransactionsFilter.cpp
Normal file
@@ -0,0 +1,64 @@
|
||||
#include "rpc/filters/impl/DelegateTransactionsFilter.hpp"
|
||||
|
||||
#include "data/Types.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "rpc/filters/TransactionFilter.hpp"
|
||||
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
namespace rpc {
|
||||
|
||||
DelegateTransactionFilter::DelegateTransactionFilter(
|
||||
rpc::DelegateFilter filter,
|
||||
xrpl::AccountID queriedAccount
|
||||
)
|
||||
: delegateFilter_(std::move(filter)), queriedAccount_(queriedAccount)
|
||||
{
|
||||
if (delegateFilter_.counterParty)
|
||||
counterparty_ = xrpl::parseBase58<xrpl::AccountID>(*delegateFilter_.counterParty);
|
||||
}
|
||||
|
||||
std::optional<TransactionFilter::CheckResult>
|
||||
DelegateTransactionFilter::check(data::TransactionAndMetadata const& txnPlusMeta) const
|
||||
{
|
||||
xrpl::SerialIter sit{txnPlusMeta.transaction.data(), txnPlusMeta.transaction.size()};
|
||||
xrpl::STTx const sttx{sit};
|
||||
|
||||
// The account is always the owner whose funds move; when sfDelegate is present, sfAccount is
|
||||
// the "authorizer" and sfDelegate is the "actor" that signed on its behalf.
|
||||
auto const txAccount = sttx.getAccountID(xrpl::sfAccount);
|
||||
|
||||
std::optional<xrpl::AccountID> txDelegate;
|
||||
if (sttx.isFieldPresent(xrpl::sfDelegate))
|
||||
txDelegate = sttx.getAccountID(xrpl::sfDelegate);
|
||||
|
||||
// Transactions without an sfDelegate field are not delegated; exclude them immediately.
|
||||
if (not txDelegate.has_value())
|
||||
return std::nullopt;
|
||||
|
||||
switch (delegateFilter_.delegateType) {
|
||||
case DelegateFilter::Role::Authorizer:
|
||||
// The queried account is the actor (signer) and the user wants to find the
|
||||
// authorizer (owner) it acted for.
|
||||
if (*txDelegate == queriedAccount_ && (!counterparty_ || *counterparty_ == txAccount))
|
||||
return CheckResult{txAccount, DelegateFilter::Role::Authorizer};
|
||||
break;
|
||||
|
||||
case DelegateFilter::Role::Actor:
|
||||
// The queried account is the authorizer (owner) and the user wants to find the
|
||||
// actor (signer) that acted on its behalf.
|
||||
if (txAccount == queriedAccount_ && (!counterparty_ || *counterparty_ == *txDelegate))
|
||||
return CheckResult{*txDelegate, DelegateFilter::Role::Actor};
|
||||
break;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace rpc
|
||||
35
src/rpc/filters/impl/DelegateTransactionsFilter.hpp
Normal file
35
src/rpc/filters/impl/DelegateTransactionsFilter.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "data/Types.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "rpc/filters/TransactionFilter.hpp"
|
||||
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace rpc {
|
||||
|
||||
/**
|
||||
* @brief Delegate transaction filter to filter txn based on permission delegate
|
||||
*/
|
||||
class DelegateTransactionFilter : public TransactionFilter {
|
||||
rpc::DelegateFilter delegateFilter_;
|
||||
xrpl::AccountID queriedAccount_;
|
||||
std::optional<xrpl::AccountID> counterparty_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new delegate transaction filter
|
||||
*
|
||||
* @param filter The filter parameters from the JSON request (role, counterparty string)
|
||||
* @param queriedAccount The account currently being queried in account_tx (input from
|
||||
* account_tx handler)
|
||||
*/
|
||||
DelegateTransactionFilter(rpc::DelegateFilter filter, xrpl::AccountID queriedAccount);
|
||||
|
||||
[[nodiscard]] std::optional<TransactionFilter::CheckResult>
|
||||
check(data::TransactionAndMetadata const& txnPlusMeta) const override;
|
||||
};
|
||||
|
||||
} // namespace rpc
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -38,11 +39,13 @@ AccountMPTokenIssuancesHandler::addMPTokenIssuance(
|
||||
{
|
||||
MPTokenIssuanceResponse issuance;
|
||||
|
||||
issuance.mpTokenIssuanceId = xrpl::strHex(sle.key());
|
||||
issuance.issuer = xrpl::to_string(account);
|
||||
issuance.sequence = sle.getFieldU32(xrpl::sfSequence);
|
||||
auto const sequence = sle.getFieldU32(xrpl::sfSequence);
|
||||
auto const flags = sle.getFieldU32(xrpl::sfFlags);
|
||||
|
||||
issuance.mpTokenIssuanceId = xrpl::to_string(xrpl::makeMptID(sequence, account));
|
||||
issuance.issuer = xrpl::to_string(account);
|
||||
issuance.sequence = sequence;
|
||||
|
||||
auto const setFlag = [&](std::optional<bool>& field, std::uint32_t mask) {
|
||||
if ((flags & mask) != 0u)
|
||||
field = true;
|
||||
@@ -57,22 +60,25 @@ AccountMPTokenIssuancesHandler::addMPTokenIssuance(
|
||||
setFlag(issuance.mptCanClawback, xrpl::lsfMPTCanClawback);
|
||||
setFlag(issuance.mptCanHoldConfidentialBalance, xrpl::lsfMPTCanHoldConfidentialBalance);
|
||||
|
||||
if (sle.isFieldPresent(xrpl::sfMutableFlags)) {
|
||||
auto const mutableFlags = sle.getFieldU32(xrpl::sfMutableFlags);
|
||||
if (sle.isFieldPresent(xrpl::sfImmutableFlags)) {
|
||||
auto const immutableFlags = sle.getFieldU32(xrpl::sfImmutableFlags);
|
||||
|
||||
auto const setMutableFlag = [&](std::optional<bool>& field, std::uint32_t mask) {
|
||||
if ((mutableFlags & mask) != 0u)
|
||||
auto const setImmutableFlag = [&](std::optional<bool>& field, std::uint32_t mask) {
|
||||
if ((immutableFlags & mask) != 0u)
|
||||
field = true;
|
||||
};
|
||||
|
||||
setMutableFlag(issuance.mptCanMutateCanLock, xrpl::lsmfMPTCanEnableCanLock);
|
||||
setMutableFlag(issuance.mptCanMutateRequireAuth, xrpl::lsmfMPTCanEnableRequireAuth);
|
||||
setMutableFlag(issuance.mptCanMutateCanEscrow, xrpl::lsmfMPTCanEnableCanEscrow);
|
||||
setMutableFlag(issuance.mptCanMutateCanTrade, xrpl::lsmfMPTCanEnableCanTrade);
|
||||
setMutableFlag(issuance.mptCanMutateCanTransfer, xrpl::lsmfMPTCanEnableCanTransfer);
|
||||
setMutableFlag(issuance.mptCanMutateCanClawback, xrpl::lsmfMPTCanEnableCanClawback);
|
||||
setMutableFlag(issuance.mptCanMutateMetadata, xrpl::lsmfMPTCanMutateMetadata);
|
||||
setMutableFlag(issuance.mptCanMutateTransferFee, xrpl::lsmfMPTCanMutateTransferFee);
|
||||
setImmutableFlag(issuance.mptImmutableCanLock, xrpl::lsifMPTCanLock);
|
||||
setImmutableFlag(issuance.mptImmutableRequireAuth, xrpl::lsifMPTRequireAuth);
|
||||
setImmutableFlag(issuance.mptImmutableCanEscrow, xrpl::lsifMPTCanEscrow);
|
||||
setImmutableFlag(issuance.mptImmutableCanTrade, xrpl::lsifMPTCanTrade);
|
||||
setImmutableFlag(issuance.mptImmutableCanTransfer, xrpl::lsifMPTCanTransfer);
|
||||
setImmutableFlag(issuance.mptImmutableCanClawback, xrpl::lsifMPTCanClawback);
|
||||
setImmutableFlag(
|
||||
issuance.mptImmutableCanHoldConfidentialBalance, xrpl::lsifMPTCanHoldConfidentialBalance
|
||||
);
|
||||
setImmutableFlag(issuance.mptImmutableMetadata, xrpl::lsifMPTMetadata);
|
||||
setImmutableFlag(issuance.mptImmutableTransferFee, xrpl::lsifMPTTransferFee);
|
||||
}
|
||||
|
||||
if (sle.isFieldPresent(xrpl::sfTransferFee))
|
||||
@@ -277,14 +283,18 @@ tag_invoke(
|
||||
setIfPresent("mpt_can_transfer", issuance.mptCanTransfer);
|
||||
setIfPresent("mpt_can_clawback", issuance.mptCanClawback);
|
||||
|
||||
setIfPresent("mpt_can_mutate_can_lock", issuance.mptCanMutateCanLock);
|
||||
setIfPresent("mpt_can_mutate_require_auth", issuance.mptCanMutateRequireAuth);
|
||||
setIfPresent("mpt_can_mutate_can_escrow", issuance.mptCanMutateCanEscrow);
|
||||
setIfPresent("mpt_can_mutate_can_trade", issuance.mptCanMutateCanTrade);
|
||||
setIfPresent("mpt_can_mutate_can_transfer", issuance.mptCanMutateCanTransfer);
|
||||
setIfPresent("mpt_can_mutate_can_clawback", issuance.mptCanMutateCanClawback);
|
||||
setIfPresent("mpt_can_mutate_metadata", issuance.mptCanMutateMetadata);
|
||||
setIfPresent("mpt_can_mutate_transfer_fee", issuance.mptCanMutateTransferFee);
|
||||
setIfPresent("mpt_immutable_can_lock", issuance.mptImmutableCanLock);
|
||||
setIfPresent("mpt_immutable_require_auth", issuance.mptImmutableRequireAuth);
|
||||
setIfPresent("mpt_immutable_can_escrow", issuance.mptImmutableCanEscrow);
|
||||
setIfPresent("mpt_immutable_can_trade", issuance.mptImmutableCanTrade);
|
||||
setIfPresent("mpt_immutable_can_transfer", issuance.mptImmutableCanTransfer);
|
||||
setIfPresent("mpt_immutable_can_clawback", issuance.mptImmutableCanClawback);
|
||||
setIfPresent(
|
||||
"mpt_immutable_can_hold_confidential_balance",
|
||||
issuance.mptImmutableCanHoldConfidentialBalance
|
||||
);
|
||||
setIfPresent("mpt_immutable_metadata", issuance.mptImmutableMetadata);
|
||||
setIfPresent("mpt_immutable_transfer_fee", issuance.mptImmutableTransferFee);
|
||||
|
||||
setIfPresent("mpt_can_hold_confidential_balance", issuance.mptCanHoldConfidentialBalance);
|
||||
setUint64IfPresent(
|
||||
|
||||
@@ -63,14 +63,15 @@ public:
|
||||
std::optional<bool> mptCanTransfer;
|
||||
std::optional<bool> mptCanClawback;
|
||||
|
||||
std::optional<bool> mptCanMutateCanLock;
|
||||
std::optional<bool> mptCanMutateRequireAuth;
|
||||
std::optional<bool> mptCanMutateCanEscrow;
|
||||
std::optional<bool> mptCanMutateCanTrade;
|
||||
std::optional<bool> mptCanMutateCanTransfer;
|
||||
std::optional<bool> mptCanMutateCanClawback;
|
||||
std::optional<bool> mptCanMutateMetadata;
|
||||
std::optional<bool> mptCanMutateTransferFee;
|
||||
std::optional<bool> mptImmutableCanLock;
|
||||
std::optional<bool> mptImmutableRequireAuth;
|
||||
std::optional<bool> mptImmutableCanEscrow;
|
||||
std::optional<bool> mptImmutableCanTrade;
|
||||
std::optional<bool> mptImmutableCanTransfer;
|
||||
std::optional<bool> mptImmutableCanClawback;
|
||||
std::optional<bool> mptImmutableCanHoldConfidentialBalance;
|
||||
std::optional<bool> mptImmutableMetadata;
|
||||
std::optional<bool> mptImmutableTransferFee;
|
||||
|
||||
std::optional<bool> mptCanHoldConfidentialBalance;
|
||||
std::optional<std::uint64_t> confidentialOutstandingAmount;
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
#include "rpc/RPCHelpers.hpp"
|
||||
#include "rpc/common/JsonBool.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "rpc/filters/TransactionFilter.hpp"
|
||||
#include "rpc/filters/impl/DelegateTransactionsFilter.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/JsonUtils.hpp"
|
||||
#include "util/MPTIssuanceUtils.hpp"
|
||||
#include "util/Profiler.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
|
||||
@@ -16,6 +19,7 @@
|
||||
#include <boost/json/value.hpp>
|
||||
#include <boost/json/value_from.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -111,8 +115,17 @@ AccountTxHandler::process(AccountTxHandler::Input const& input, Context const& c
|
||||
}
|
||||
}
|
||||
|
||||
auto const limit = input.limit.value_or(kLimitDefault);
|
||||
auto const accountID = accountFromStringStrict(input.account);
|
||||
|
||||
std::optional<rpc::DelegateTransactionFilter> txFilter;
|
||||
if (input.delegateFilter) {
|
||||
txFilter.emplace(
|
||||
*input.delegateFilter,
|
||||
*accountID // NOLINT(bugprone-unchecked-optional-access)
|
||||
);
|
||||
}
|
||||
|
||||
auto const limit = input.limit.value_or(kLimitDefault);
|
||||
auto const [txnsAndCursor, timeDiff] = util::timed([&]() {
|
||||
return sharedPtrBackend_->fetchAccountTransactions(
|
||||
*accountID, limit, input.forward, cursor, ctx.yield
|
||||
@@ -128,6 +141,10 @@ AccountTxHandler::process(AccountTxHandler::Input const& input, Context const& c
|
||||
if (retCursor)
|
||||
response.marker = {.ledger = retCursor->ledgerSequence, .seq = retCursor->transactionIndex};
|
||||
|
||||
std::optional<xrpl::uint192> mptIssuanceFilter;
|
||||
if (input.mptIssuanceId)
|
||||
mptIssuanceFilter = xrpl::uint192{input.mptIssuanceId->c_str()};
|
||||
|
||||
for (auto const& txnPlusMeta : blobs) {
|
||||
// over the range
|
||||
if ((txnPlusMeta.ledgerSequence < minIndex && !input.forward) ||
|
||||
@@ -140,8 +157,23 @@ AccountTxHandler::process(AccountTxHandler::Input const& input, Context const& c
|
||||
continue;
|
||||
}
|
||||
|
||||
std::optional<rpc::TransactionFilter::CheckResult> filterResult;
|
||||
if (txFilter) {
|
||||
filterResult = txFilter->check(txnPlusMeta);
|
||||
if (not filterResult.has_value())
|
||||
continue;
|
||||
}
|
||||
|
||||
boost::json::object obj;
|
||||
|
||||
// Skip all Txns where the specified filter mpt_id doesn't match the query
|
||||
if (mptIssuanceFilter) {
|
||||
auto const [sttx, txMeta] =
|
||||
deserializeTxPlusMeta(txnPlusMeta, txnPlusMeta.ledgerSequence);
|
||||
if (!util::referencesMptIssuance(*txMeta, *sttx, *mptIssuanceFilter))
|
||||
continue;
|
||||
}
|
||||
|
||||
// if binary is false or transactionType is specified, we need to expand the transaction
|
||||
if (!input.binary || input.transactionTypeInLowercase.has_value()) {
|
||||
auto [txn, meta] = toExpandedJson(txnPlusMeta, ctx.apiVersion, NFTokenjson::ENABLE);
|
||||
@@ -190,6 +222,15 @@ AccountTxHandler::process(AccountTxHandler::Input const& input, Context const& c
|
||||
obj[JS(close_time_iso)] = xrpl::toStringIso(ledgerHeader->closeTime);
|
||||
}
|
||||
}
|
||||
|
||||
if (filterResult) {
|
||||
if (filterResult->role == rpc::DelegateFilter::Role::Authorizer) {
|
||||
obj[JS(authorizer)] = xrpl::to_string(filterResult->account);
|
||||
} else {
|
||||
obj[JS(actor)] = xrpl::to_string(filterResult->account);
|
||||
}
|
||||
}
|
||||
|
||||
obj[JS(validated)] = true;
|
||||
response.transactions.push_back(std::move(obj));
|
||||
continue;
|
||||
@@ -298,6 +339,14 @@ tag_invoke(boost::json::value_to_tag<AccountTxHandler::Input>, boost::json::valu
|
||||
boost::json::value_to<std::string>(jsonObject.at("tx_type"));
|
||||
}
|
||||
|
||||
if (jsonObject.contains(JS(delegate)))
|
||||
input.delegateFilter = parseDelegateFilter(jsonObject.at(JS(delegate)).as_object());
|
||||
|
||||
if (jsonObject.contains(JS(mpt_issuance_id))) {
|
||||
input.mptIssuanceId =
|
||||
boost::json::value_to<std::string>(jsonObject.at(JS(mpt_issuance_id)));
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,8 @@ public:
|
||||
std::optional<uint32_t> limit;
|
||||
std::optional<Marker> marker;
|
||||
std::optional<std::string> transactionTypeInLowercase;
|
||||
std::optional<DelegateFilter> delegateFilter;
|
||||
std::optional<std::string> mptIssuanceId;
|
||||
};
|
||||
|
||||
using Result = HandlerReturnType<Output>;
|
||||
@@ -141,6 +143,8 @@ public:
|
||||
typesKeysInLowercase.cbegin(), typesKeysInLowercase.cend()
|
||||
),
|
||||
},
|
||||
{JS(delegate), validation::CustomValidators::delegateValidator},
|
||||
{JS(mpt_issuance_id), validation::CustomValidators::uint192HexStringValidator},
|
||||
};
|
||||
|
||||
static auto const kRpcSpec = RpcSpec{
|
||||
|
||||
@@ -13,9 +13,12 @@
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/LedgerHeader.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
@@ -27,8 +30,7 @@ namespace rpc {
|
||||
BookOffersHandler::Result
|
||||
BookOffersHandler::process(Input const& input, Context const& ctx) const
|
||||
{
|
||||
auto bookMaybe =
|
||||
parseBook(input.paysCurrency, input.paysID, input.getsCurrency, input.getsID, input.domain);
|
||||
auto bookMaybe = parseBook(input.paysAsset, input.getsAsset, input.domain);
|
||||
if (!bookMaybe.has_value())
|
||||
return Error{bookMaybe.error()};
|
||||
|
||||
@@ -91,28 +93,30 @@ tag_invoke(boost::json::value_to_tag<BookOffersHandler::Input>, boost::json::val
|
||||
auto input = BookOffersHandler::Input{};
|
||||
auto const& jsonObject = jv.as_object();
|
||||
|
||||
xrpl::toCurrency(
|
||||
input.getsCurrency,
|
||||
boost::json::value_to<std::string>(jv.at(JS(taker_gets)).as_object().at(JS(currency)))
|
||||
);
|
||||
xrpl::toCurrency(
|
||||
input.paysCurrency,
|
||||
boost::json::value_to<std::string>(jv.at(JS(taker_pays)).as_object().at(JS(currency)))
|
||||
);
|
||||
|
||||
if (jv.at(JS(taker_gets)).as_object().contains(JS(issuer))) {
|
||||
xrpl::toIssuer(
|
||||
input.getsID,
|
||||
boost::json::value_to<std::string>(jv.at(JS(taker_gets)).as_object().at(JS(issuer)))
|
||||
auto const parseAsset = [](boost::json::object const& obj) -> xrpl::Asset {
|
||||
if (obj.contains(JS(mpt_issuance_id))) {
|
||||
xrpl::MPTID mptid;
|
||||
auto const parsed =
|
||||
mptid.parseHex(boost::json::value_to<std::string>(obj.at(JS(mpt_issuance_id))));
|
||||
ASSERT(parsed, "mpt_issuance_id is guaranteed to be valid hex by the spec validator");
|
||||
return xrpl::MPTIssue{mptid};
|
||||
}
|
||||
xrpl::Issue issue;
|
||||
auto const currencyParsed = xrpl::toCurrency(
|
||||
issue.currency, boost::json::value_to<std::string>(obj.at(JS(currency)))
|
||||
);
|
||||
}
|
||||
ASSERT(currencyParsed, "currency is guaranteed to be valid by the spec validator");
|
||||
if (obj.contains(JS(issuer))) {
|
||||
auto const issuerParsed = xrpl::toIssuer(
|
||||
issue.account, boost::json::value_to<std::string>(obj.at(JS(issuer)))
|
||||
);
|
||||
ASSERT(issuerParsed, "issuer is guaranteed to be valid by the spec validator");
|
||||
}
|
||||
return issue;
|
||||
};
|
||||
|
||||
if (jv.at(JS(taker_pays)).as_object().contains(JS(issuer))) {
|
||||
xrpl::toIssuer(
|
||||
input.paysID,
|
||||
boost::json::value_to<std::string>(jv.at(JS(taker_pays)).as_object().at(JS(issuer)))
|
||||
);
|
||||
}
|
||||
input.getsAsset = parseAsset(jv.at(JS(taker_gets)).as_object());
|
||||
input.paysAsset = parseAsset(jv.at(JS(taker_pays)).as_object());
|
||||
|
||||
if (jsonObject.contains(JS(ledger_hash)))
|
||||
input.ledgerHash = boost::json::value_to<std::string>(jv.at(JS(ledger_hash)));
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
@@ -23,6 +25,7 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace rpc {
|
||||
|
||||
@@ -62,11 +65,8 @@ public:
|
||||
std::optional<uint32_t> ledgerIndex;
|
||||
uint32_t limit = kLimitDefault;
|
||||
std::optional<xrpl::AccountID> taker;
|
||||
xrpl::Currency paysCurrency;
|
||||
xrpl::Currency getsCurrency;
|
||||
// accountID will be filled by input converter, if no issuer is given, will use XRP issuer
|
||||
xrpl::AccountID paysID = xrpl::xrpAccount();
|
||||
xrpl::AccountID getsID = xrpl::xrpAccount();
|
||||
xrpl::Asset paysAsset = xrpl::xrpIssue();
|
||||
xrpl::Asset getsAsset = xrpl::xrpIssue();
|
||||
std::optional<std::string> domain;
|
||||
};
|
||||
|
||||
@@ -99,13 +99,18 @@ public:
|
||||
{JS(taker_gets),
|
||||
validation::Required{},
|
||||
validation::Type<boost::json::object>{},
|
||||
validation::CustomValidators::bookTakerValidator,
|
||||
meta::Section{
|
||||
{JS(currency),
|
||||
validation::Required{},
|
||||
meta::WithCustomError{
|
||||
validation::CustomValidators::currencyValidator,
|
||||
Status(RippledError::RpcDstAmtMalformed)
|
||||
}},
|
||||
{JS(mpt_issuance_id),
|
||||
meta::WithCustomError{
|
||||
validation::CustomValidators::uint192HexStringValidator,
|
||||
Status(RippledError::RpcDstAmtMalformed)
|
||||
}},
|
||||
{JS(issuer),
|
||||
meta::WithCustomError{
|
||||
validation::CustomValidators::issuerValidator,
|
||||
@@ -115,13 +120,18 @@ public:
|
||||
{JS(taker_pays),
|
||||
validation::Required{},
|
||||
validation::Type<boost::json::object>{},
|
||||
validation::CustomValidators::bookTakerValidator,
|
||||
meta::Section{
|
||||
{JS(currency),
|
||||
validation::Required{},
|
||||
meta::WithCustomError{
|
||||
validation::CustomValidators::currencyValidator,
|
||||
Status(RippledError::RpcSrcCurMalformed)
|
||||
}},
|
||||
{JS(mpt_issuance_id),
|
||||
meta::WithCustomError{
|
||||
validation::CustomValidators::uint192HexStringValidator,
|
||||
Status(RippledError::RpcSrcCurMalformed)
|
||||
}},
|
||||
{JS(issuer),
|
||||
meta::WithCustomError{
|
||||
validation::CustomValidators::issuerValidator,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
@@ -32,6 +33,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
@@ -422,20 +424,23 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
|
||||
return xrpl::keylet::oracle(*account, documentId).key;
|
||||
};
|
||||
|
||||
auto const parseCredentialFromJson = [](boost::json::value const& json) {
|
||||
auto const parseCredentialFromJson =
|
||||
[](boost::json::value const& json) -> std::optional<xrpl::uint256> {
|
||||
auto const subject = util::parseBase58Wrapper<xrpl::AccountID>(
|
||||
boost::json::value_to<std::string>(json.at(JS(subject)))
|
||||
);
|
||||
auto const issuer = util::parseBase58Wrapper<xrpl::AccountID>(
|
||||
boost::json::value_to<std::string>(json.at(JS(issuer)))
|
||||
);
|
||||
auto const credType =
|
||||
auto const credTypeOpt =
|
||||
xrpl::strUnHex(boost::json::value_to<std::string>(json.at(JS(credential_type))));
|
||||
|
||||
return xrpl::keylet::credential(
|
||||
*subject, *issuer, xrpl::Slice(credType->data(), credType->size())
|
||||
)
|
||||
.key;
|
||||
return credTypeOpt.transform([&](xrpl::Blob const& credType) -> xrpl::uint256 {
|
||||
return xrpl::keylet::credential(
|
||||
*subject, *issuer, xrpl::Slice(credType.data(), credType.size())
|
||||
)
|
||||
.key;
|
||||
});
|
||||
};
|
||||
|
||||
auto const indexFieldType =
|
||||
|
||||
@@ -380,6 +380,7 @@ public:
|
||||
meta::WithCustomError{
|
||||
validation::Type<std::string>{}, Status(ClioError::RpcMalformedRequest)
|
||||
},
|
||||
validation::CustomValidators::credentialTypeValidator,
|
||||
},
|
||||
}}},
|
||||
{JS(mpt_issuance),
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerHeader.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STBase.h>
|
||||
#include <xrpl/protocol/STInteger.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
378
src/rpc/handlers/MPTokenIssuanceHistory.cpp
Normal file
378
src/rpc/handlers/MPTokenIssuanceHistory.cpp
Normal file
@@ -0,0 +1,378 @@
|
||||
#include "rpc/handlers/MPTokenIssuanceHistory.hpp"
|
||||
|
||||
#include "data/Types.hpp"
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/RPCHelpers.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/JsonUtils.hpp"
|
||||
#include "util/Profiler.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
|
||||
#include <boost/json/conversion.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <boost/json/value_from.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/protocol/LedgerHeader.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace rpc {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief The migrator status that reports a completed backfill.
|
||||
*
|
||||
* This is a literal to keep migration headers out of RPC; it must match the string form of
|
||||
* `migration::MigratorStatus::Status::Migrated`.
|
||||
*/
|
||||
constexpr auto kMigratedStatus = "Migrated";
|
||||
|
||||
} // namespace
|
||||
|
||||
MPTokenIssuanceHistoryHandler::Result
|
||||
MPTokenIssuanceHistoryHandler::process(
|
||||
MPTokenIssuanceHistoryHandler::Input const& input,
|
||||
Context const& ctx
|
||||
) const
|
||||
{
|
||||
if (auto const available = verifyHistoryAvailable(ctx); not available.has_value())
|
||||
return Error{available.error()};
|
||||
|
||||
auto const range = resolveSequenceRange(input, ctx);
|
||||
if (not range.has_value())
|
||||
return Error{range.error()};
|
||||
|
||||
auto const mptIssuanceID = xrpl::uint192{input.mptIssuanceID.c_str()};
|
||||
|
||||
auto const [page, timeDiff] =
|
||||
util::timed([&] { return fetchTransactions(input, ctx, mptIssuanceID, *range); });
|
||||
LOG(log_.info()) << "db fetch took " << timeDiff
|
||||
<< " milliseconds - num blobs = " << page.txns.size();
|
||||
|
||||
Output response;
|
||||
response.mptIssuanceID = xrpl::to_string(mptIssuanceID);
|
||||
response.ledgerIndexMin = range->min;
|
||||
response.ledgerIndexMax = range->max;
|
||||
response.limit = input.limit;
|
||||
|
||||
processTransactionsPage(input, ctx, *range, page, response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
MaybeError
|
||||
MPTokenIssuanceHistoryHandler::verifyHistoryAvailable(Context const& ctx) const
|
||||
{
|
||||
if (migrated_->load(std::memory_order_relaxed))
|
||||
return {};
|
||||
|
||||
auto const statusString = sharedPtrBackend_->fetchMigratorStatus(kMigratorName, ctx.yield);
|
||||
if (statusString.has_value() and *statusString == kMigratedStatus) {
|
||||
migrated_->store(true, std::memory_order_relaxed);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Fail closed: partial history must never be served.
|
||||
return Error{Status{
|
||||
RippledError::RpcNotReady,
|
||||
"mptoken_issuance_history is not available on this server because the required "
|
||||
"transaction-history backfill has not completed."
|
||||
}};
|
||||
}
|
||||
|
||||
std::expected<MPTokenIssuanceHistoryHandler::SequenceRange, Status>
|
||||
MPTokenIssuanceHistoryHandler::resolveSequenceRange(Input const& input, Context const& ctx) const
|
||||
{
|
||||
auto const ledgerRange = sharedPtrBackend_->fetchLedgerRange();
|
||||
ASSERT(ledgerRange.has_value(), "MPTokenIssuanceHistory's ledger range must be available");
|
||||
|
||||
auto const [dbMinSeq, dbMaxSeq] = *ledgerRange; // NOLINT(bugprone-unchecked-optional-access)
|
||||
auto resolved = SequenceRange{.min = dbMinSeq, .max = dbMaxSeq};
|
||||
|
||||
if (input.ledgerIndexMin.has_value()) {
|
||||
if (dbMaxSeq < input.ledgerIndexMin || dbMinSeq > input.ledgerIndexMin)
|
||||
return Error{Status{RippledError::RpcLgrIdxMalformed, "ledgerSeqMinOutOfRange"}};
|
||||
|
||||
resolved.min = *input.ledgerIndexMin;
|
||||
}
|
||||
|
||||
if (input.ledgerIndexMax.has_value()) {
|
||||
if (dbMaxSeq < input.ledgerIndexMax || dbMinSeq > input.ledgerIndexMax)
|
||||
return Error{Status{RippledError::RpcLgrIdxMalformed, "ledgerSeqMaxOutOfRange"}};
|
||||
|
||||
resolved.max = *input.ledgerIndexMax;
|
||||
}
|
||||
|
||||
if (resolved.min > resolved.max)
|
||||
return Error{Status{RippledError::RpcLgrIdxsInvalid}};
|
||||
|
||||
if (input.ledgerHash.has_value() || input.ledgerIndex.has_value()) {
|
||||
// rippled does not have this check
|
||||
if (input.ledgerIndexMax.has_value() || input.ledgerIndexMin.has_value())
|
||||
return Error{Status{RippledError::RpcInvalidParams, "containsLedgerSpecifierAndRange"}};
|
||||
|
||||
auto const expectedLgrInfo = getLedgerHeaderFromHashOrSeq(
|
||||
*sharedPtrBackend_, ctx.yield, input.ledgerHash, input.ledgerIndex, dbMaxSeq
|
||||
);
|
||||
|
||||
if (not expectedLgrInfo.has_value())
|
||||
return Error{expectedLgrInfo.error()};
|
||||
|
||||
resolved.max = resolved.min = expectedLgrInfo->seq;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
data::TransactionsAndCursor
|
||||
MPTokenIssuanceHistoryHandler::fetchTransactions(
|
||||
Input const& input,
|
||||
Context const& ctx,
|
||||
xrpl::uint192 const& mptIssuanceID,
|
||||
SequenceRange range
|
||||
) const
|
||||
{
|
||||
// Construct the database cursor as {ledgerSequence, transactionIndex}.
|
||||
auto const startCursor = [&]() -> data::TransactionsCursor {
|
||||
if (input.marker.has_value())
|
||||
return {input.marker->ledger, input.marker->seq};
|
||||
|
||||
// Forward iteration starts at the first possible transaction in the lowest ledger.
|
||||
if (input.forward)
|
||||
return {range.min, 0};
|
||||
|
||||
// Reverse iteration starts after all possible transactions in the highest ledger.
|
||||
return {range.max, std::numeric_limits<int32_t>::max()};
|
||||
}();
|
||||
|
||||
auto const limit = input.limit.value_or(kLimitDefault);
|
||||
|
||||
// tx_type is applied post-fetch, as account_tx does.
|
||||
if (input.account.has_value()) {
|
||||
auto const account = accountFromStringStrict(*input.account);
|
||||
if (not account.has_value()) {
|
||||
ASSERT(false, "Account must be decodable after spec validation");
|
||||
std::unreachable();
|
||||
}
|
||||
return sharedPtrBackend_->fetchAccountMPTokenIssuanceTransactions(
|
||||
mptIssuanceID, *account, limit, input.forward, startCursor, ctx.yield
|
||||
);
|
||||
}
|
||||
|
||||
return sharedPtrBackend_->fetchMPTokenIssuanceTransactions(
|
||||
mptIssuanceID, limit, input.forward, startCursor, ctx.yield
|
||||
);
|
||||
}
|
||||
|
||||
void
|
||||
MPTokenIssuanceHistoryHandler::processTransactionsPage(
|
||||
Input const& input,
|
||||
Context const& ctx,
|
||||
SequenceRange range,
|
||||
data::TransactionsAndCursor const& page,
|
||||
Output& response
|
||||
) const
|
||||
{
|
||||
if (page.cursor.has_value()) {
|
||||
response.marker = {
|
||||
.ledger = page.cursor->ledgerSequence, .seq = page.cursor->transactionIndex
|
||||
};
|
||||
}
|
||||
|
||||
for (auto const& txnPlusMeta : page.txns) {
|
||||
// A hash with no matching Transactions row yields a default-constructed record in-position.
|
||||
// Skip it before the range check so it neither shortens the page nor disturbs the marker.
|
||||
if (txnPlusMeta.transaction.empty() || txnPlusMeta.metadata.empty()) {
|
||||
LOG(log_.warn()) << "Skipping index entry with no matching transaction record; "
|
||||
"mpt_issuance_id = "
|
||||
<< input.mptIssuanceID;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Stop once iteration passes the far edge of the requested range.
|
||||
if ((txnPlusMeta.ledgerSequence < range.min && !input.forward) ||
|
||||
(txnPlusMeta.ledgerSequence > range.max && input.forward)) {
|
||||
response.marker = std::nullopt;
|
||||
break;
|
||||
}
|
||||
if (txnPlusMeta.ledgerSequence > range.max && !input.forward) {
|
||||
LOG(log_.debug()) << "Skipping over transactions from incomplete ledger";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (auto obj = transactionToJsonIfTypeMatches(txnPlusMeta, input, ctx); obj.has_value())
|
||||
response.transactions.push_back(std::move(*obj));
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<boost::json::object>
|
||||
MPTokenIssuanceHistoryHandler::transactionToJsonIfTypeMatches(
|
||||
data::TransactionAndMetadata const& txnPlusMeta,
|
||||
Input const& input,
|
||||
Context const& ctx
|
||||
) const
|
||||
{
|
||||
// The type filter needs the expanded form to read TransactionType, even for binary output.
|
||||
if (!input.binary || input.transactionTypeInLowercase.has_value()) {
|
||||
auto [txn, meta] = toExpandedJson(txnPlusMeta, ctx.apiVersion);
|
||||
|
||||
if (txn.contains(JS(TransactionType)) && input.transactionTypeInLowercase.has_value() &&
|
||||
util::toLower(boost::json::value_to<std::string>(txn[JS(TransactionType)])) !=
|
||||
*input.transactionTypeInLowercase)
|
||||
return std::nullopt;
|
||||
|
||||
if (!input.binary)
|
||||
return expandedTransactionToJson(std::move(txn), std::move(meta), txnPlusMeta, ctx);
|
||||
}
|
||||
|
||||
auto obj = toJsonWithBinaryTx(txnPlusMeta, ctx.apiVersion);
|
||||
obj[JS(ledger_index)] = txnPlusMeta.ledgerSequence;
|
||||
obj[JS(date)] = txnPlusMeta.date;
|
||||
obj[JS(validated)] = true;
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
MPTokenIssuanceHistoryHandler::expandedTransactionToJson(
|
||||
boost::json::object txn,
|
||||
boost::json::object meta,
|
||||
data::TransactionAndMetadata const& txnPlusMeta,
|
||||
Context const& ctx
|
||||
) const
|
||||
{
|
||||
auto const txKey = ctx.apiVersion > 1u ? JS(tx_json) : JS(tx);
|
||||
|
||||
boost::json::object obj;
|
||||
obj[JS(meta)] = std::move(meta);
|
||||
obj[txKey] = std::move(txn);
|
||||
obj[txKey].as_object()[JS(ledger_index)] = txnPlusMeta.ledgerSequence;
|
||||
obj[txKey].as_object()[JS(date)] = txnPlusMeta.date;
|
||||
|
||||
if (ctx.apiVersion > 1u) {
|
||||
obj[JS(ledger_index)] = txnPlusMeta.ledgerSequence;
|
||||
if (obj[txKey].as_object().contains(JS(hash))) {
|
||||
obj[JS(hash)] = obj[txKey].at(JS(hash));
|
||||
obj[txKey].as_object().erase(JS(hash));
|
||||
}
|
||||
if (auto const lgrInfo =
|
||||
sharedPtrBackend_->fetchLedgerBySequence(txnPlusMeta.ledgerSequence, ctx.yield);
|
||||
lgrInfo.has_value()) {
|
||||
obj[JS(close_time_iso)] = xrpl::toStringIso(lgrInfo->closeTime);
|
||||
obj[JS(ledger_hash)] = xrpl::strHex(lgrInfo->hash);
|
||||
}
|
||||
}
|
||||
|
||||
obj[JS(validated)] = true;
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
void
|
||||
tag_invoke(
|
||||
boost::json::value_from_tag,
|
||||
boost::json::value& jv,
|
||||
MPTokenIssuanceHistoryHandler::Output const& output
|
||||
)
|
||||
{
|
||||
jv = {
|
||||
{JS(mpt_issuance_id), output.mptIssuanceID},
|
||||
{JS(ledger_index_min), output.ledgerIndexMin},
|
||||
{JS(ledger_index_max), output.ledgerIndexMax},
|
||||
{JS(transactions), output.transactions},
|
||||
{JS(validated), output.validated},
|
||||
};
|
||||
|
||||
if (output.marker.has_value())
|
||||
jv.as_object()[JS(marker)] = boost::json::value_from(*(output.marker));
|
||||
|
||||
if (output.limit.has_value())
|
||||
jv.as_object()[JS(limit)] = *(output.limit);
|
||||
}
|
||||
|
||||
void
|
||||
tag_invoke(
|
||||
boost::json::value_from_tag,
|
||||
boost::json::value& jv,
|
||||
MPTokenIssuanceHistoryHandler::Marker const& marker
|
||||
)
|
||||
{
|
||||
jv = {
|
||||
{JS(ledger), marker.ledger},
|
||||
{JS(seq), marker.seq},
|
||||
};
|
||||
}
|
||||
|
||||
MPTokenIssuanceHistoryHandler::Input
|
||||
tag_invoke(
|
||||
boost::json::value_to_tag<MPTokenIssuanceHistoryHandler::Input>,
|
||||
boost::json::value const& jv
|
||||
)
|
||||
{
|
||||
auto const& jsonObject = jv.as_object();
|
||||
auto input = MPTokenIssuanceHistoryHandler::Input{};
|
||||
|
||||
input.mptIssuanceID = boost::json::value_to<std::string>(jsonObject.at(JS(mpt_issuance_id)));
|
||||
|
||||
if (jsonObject.contains(JS(account)))
|
||||
input.account = boost::json::value_to<std::string>(jsonObject.at(JS(account)));
|
||||
|
||||
if (jsonObject.contains("tx_type")) {
|
||||
input.transactionTypeInLowercase =
|
||||
boost::json::value_to<std::string>(jsonObject.at("tx_type"));
|
||||
}
|
||||
|
||||
if (jsonObject.contains(JS(ledger_index_min)) &&
|
||||
util::integralValueAs<int32_t>(jsonObject.at(JS(ledger_index_min))) != -1)
|
||||
input.ledgerIndexMin = util::integralValueAs<uint32_t>(jsonObject.at(JS(ledger_index_min)));
|
||||
|
||||
if (jsonObject.contains(JS(ledger_index_max)) &&
|
||||
util::integralValueAs<int32_t>(jsonObject.at(JS(ledger_index_max))) != -1)
|
||||
input.ledgerIndexMax = util::integralValueAs<uint32_t>(jsonObject.at(JS(ledger_index_max)));
|
||||
|
||||
if (jsonObject.contains(JS(ledger_hash)))
|
||||
input.ledgerHash = boost::json::value_to<std::string>(jsonObject.at(JS(ledger_hash)));
|
||||
|
||||
if (jsonObject.contains(JS(ledger_index))) {
|
||||
auto const expectedLedgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
|
||||
if (expectedLedgerIndex.has_value())
|
||||
input.ledgerIndex = *expectedLedgerIndex;
|
||||
}
|
||||
|
||||
if (jsonObject.contains(JS(binary)))
|
||||
input.binary = jsonObject.at(JS(binary)).as_bool();
|
||||
|
||||
if (jsonObject.contains(JS(forward)))
|
||||
input.forward = jsonObject.at(JS(forward)).as_bool();
|
||||
|
||||
if (jsonObject.contains(JS(limit)))
|
||||
input.limit = util::integralValueAs<uint32_t>(jsonObject.at(JS(limit)));
|
||||
|
||||
if (jsonObject.contains(JS(marker))) {
|
||||
input.marker = MPTokenIssuanceHistoryHandler::Marker{
|
||||
.ledger = util::integralValueAs<uint32_t>(
|
||||
jsonObject.at(JS(marker)).as_object().at(JS(ledger))
|
||||
),
|
||||
.seq =
|
||||
util::integralValueAs<uint32_t>(jsonObject.at(JS(marker)).as_object().at(JS(seq)))
|
||||
};
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
} // namespace rpc
|
||||
316
src/rpc/handlers/MPTokenIssuanceHistory.hpp
Normal file
316
src/rpc/handlers/MPTokenIssuanceHistory.hpp
Normal file
@@ -0,0 +1,316 @@
|
||||
#pragma once
|
||||
|
||||
#include "data/BackendInterface.hpp"
|
||||
#include "data/Types.hpp"
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/common/MetaProcessors.hpp"
|
||||
#include "rpc/common/Modifiers.hpp"
|
||||
#include "rpc/common/Specs.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "rpc/common/Validators.hpp"
|
||||
#include "util/TxUtils.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/conversion.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace rpc {
|
||||
|
||||
/**
|
||||
* @brief The mptoken_issuance_history command returns past transactions associated with the queried
|
||||
* MPTokenIssuance, optionally filtered by an affected account and/or transaction type.
|
||||
*
|
||||
* @note This is a Clio-only method. Requests fail with `notReady` until the issuance-history
|
||||
* backfill reports `Migrated`, so partial history is never served.
|
||||
*/
|
||||
class MPTokenIssuanceHistoryHandler {
|
||||
util::Logger log_{"RPC"};
|
||||
std::shared_ptr<BackendInterface> sharedPtrBackend_;
|
||||
|
||||
/**
|
||||
* @brief Whether the issuance-history backfill has completed.
|
||||
*
|
||||
* The status is monotonic, so the terminal result is cached across handler copies.
|
||||
*/
|
||||
std::shared_ptr<std::atomic_bool> migrated_ = std::make_shared<std::atomic_bool>(false);
|
||||
|
||||
public:
|
||||
static constexpr auto kLimitMin = 1;
|
||||
static constexpr auto kLimitMax = 100;
|
||||
static constexpr auto kLimitDefault = 50;
|
||||
|
||||
/**
|
||||
* @brief The name used to query the issuance-history migrator's status.
|
||||
*
|
||||
* This is a literal to keep Cassandra migration headers out of RPC.
|
||||
*/
|
||||
static constexpr char const* kMigratorName = "MPTokenIssuanceHistoryMigrator";
|
||||
|
||||
/**
|
||||
* @brief A struct to hold the marker data.
|
||||
*/
|
||||
struct Marker {
|
||||
uint32_t ledger;
|
||||
uint32_t seq;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A struct to hold the output data of the command.
|
||||
*/
|
||||
struct Output {
|
||||
std::string mptIssuanceID;
|
||||
uint32_t ledgerIndexMin{0};
|
||||
uint32_t ledgerIndexMax{0};
|
||||
std::optional<uint32_t> limit;
|
||||
std::optional<Marker> marker;
|
||||
/** @todo Use a domain-specific type instead of JSON. */
|
||||
boost::json::array transactions;
|
||||
/** @todo Send validated through the RPC framework. */
|
||||
bool validated = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A struct to hold the input data for the command.
|
||||
*
|
||||
* @note When no ledger selector is provided, the request uses the backend's full available
|
||||
* ledger range.
|
||||
*/
|
||||
struct Input {
|
||||
std::string mptIssuanceID;
|
||||
std::optional<std::string> account;
|
||||
std::optional<std::string> transactionTypeInLowercase;
|
||||
std::optional<std::string> ledgerHash;
|
||||
std::optional<uint32_t> ledgerIndex;
|
||||
std::optional<int32_t> ledgerIndexMin;
|
||||
std::optional<int32_t> ledgerIndexMax;
|
||||
bool binary = false;
|
||||
bool forward = false;
|
||||
std::optional<uint32_t> limit;
|
||||
std::optional<Marker> marker;
|
||||
};
|
||||
|
||||
using Result = HandlerReturnType<Output>;
|
||||
|
||||
/**
|
||||
* @brief Construct a new MPTokenIssuanceHistoryHandler object.
|
||||
*
|
||||
* @param sharedPtrBackend The backend to use.
|
||||
*/
|
||||
explicit MPTokenIssuanceHistoryHandler(std::shared_ptr<BackendInterface> sharedPtrBackend)
|
||||
: sharedPtrBackend_(std::move(sharedPtrBackend))
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the API specification for the command.
|
||||
*
|
||||
* @param apiVersion The api version to return the spec for.
|
||||
* @return The spec for the given apiVersion.
|
||||
*/
|
||||
static RpcSpecConstRef
|
||||
spec([[maybe_unused]] uint32_t apiVersion)
|
||||
{
|
||||
auto const& typesKeysInLowercase = util::getTxTypesInLowercase();
|
||||
static auto const kRpcSpec = RpcSpec{
|
||||
{JS(mpt_issuance_id),
|
||||
validation::Required{},
|
||||
validation::CustomValidators::uint192HexStringValidator},
|
||||
{JS(account), validation::CustomValidators::accountValidator},
|
||||
{
|
||||
"tx_type",
|
||||
validation::Type<std::string>{},
|
||||
modifiers::ToLower{},
|
||||
validation::OneOf<std::string>(
|
||||
typesKeysInLowercase.cbegin(), typesKeysInLowercase.cend()
|
||||
),
|
||||
},
|
||||
{JS(ledger_hash), validation::CustomValidators::uint256HexStringValidator},
|
||||
{JS(ledger_index), validation::CustomValidators::ledgerIndexValidator},
|
||||
{JS(ledger_index_min), validation::Type<int32_t>{}},
|
||||
{JS(ledger_index_max), validation::Type<int32_t>{}},
|
||||
{JS(binary), validation::Type<bool>{}},
|
||||
{JS(forward), validation::Type<bool>{}},
|
||||
{JS(limit),
|
||||
validation::Type<uint32_t>{},
|
||||
validation::Min(1u),
|
||||
modifiers::Clamp<int32_t>{kLimitMin, kLimitMax}},
|
||||
{JS(marker),
|
||||
meta::WithCustomError{
|
||||
validation::Type<boost::json::object>{},
|
||||
Status{RippledError::RpcInvalidParams, "invalidMarker"}
|
||||
},
|
||||
meta::Section{
|
||||
{JS(ledger), validation::Required{}, validation::Type<uint32_t>{}},
|
||||
{JS(seq), validation::Required{}, validation::Type<uint32_t>{}},
|
||||
}},
|
||||
};
|
||||
|
||||
return kRpcSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Process the MPTokenIssuanceHistory command.
|
||||
*
|
||||
* @param input The input data for the command.
|
||||
* @param ctx The context of the request.
|
||||
* @return The result of the operation.
|
||||
*/
|
||||
[[nodiscard]] Result
|
||||
process(Input const& input, Context const& ctx) const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief The inclusive range of ledger sequences a request is restricted to.
|
||||
*/
|
||||
struct SequenceRange {
|
||||
uint32_t min;
|
||||
uint32_t max;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Check that the transaction-history backfill has completed.
|
||||
*
|
||||
* The terminal Migrated status is cached, so the backend is only consulted until the backfill
|
||||
* reports completion.
|
||||
*
|
||||
* @param ctx The context of the request.
|
||||
* @return An empty result if the history is complete; an error otherwise.
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verifyHistoryAvailable(Context const& ctx) const;
|
||||
|
||||
/**
|
||||
* @brief Resolve the ledger sequence range to search from the request's ledger specifiers.
|
||||
*
|
||||
* Starts from the server's full ledger range, then narrows it by ledger_index_min /
|
||||
* ledger_index_max, or collapses it to a single sequence if ledger_hash / ledger_index is
|
||||
* given.
|
||||
*
|
||||
* @param input The input data for the command.
|
||||
* @param ctx The context of the request.
|
||||
* @return The resolved range if the specifiers are valid; an error otherwise.
|
||||
*/
|
||||
[[nodiscard]] std::expected<SequenceRange, Status>
|
||||
resolveSequenceRange(Input const& input, Context const& ctx) const;
|
||||
|
||||
/**
|
||||
* @brief Fetch one page of transactions for the issuance, optionally restricted to an account.
|
||||
*
|
||||
* When the request has no marker, a forward page starts at the lower bound of @p range and a
|
||||
* reverse page starts after the highest possible transaction at its upper bound.
|
||||
*
|
||||
* @param input The input data for the command.
|
||||
* @param ctx The context of the request.
|
||||
* @param mptIssuanceID The MPTokenIssuance ID to fetch transactions for.
|
||||
* @param range The resolved ledger sequence range.
|
||||
* @return The fetched transactions and the cursor to resume from.
|
||||
*/
|
||||
[[nodiscard]] data::TransactionsAndCursor
|
||||
fetchTransactions(
|
||||
Input const& input,
|
||||
Context const& ctx,
|
||||
xrpl::uint192 const& mptIssuanceID,
|
||||
SequenceRange range
|
||||
) const;
|
||||
|
||||
/**
|
||||
* @brief Process a fetched transaction page into the response.
|
||||
*
|
||||
* Appends transactions that pass the range and type filters. The response marker is taken from
|
||||
* the fetched page, then cleared if the page runs past the requested range because there is
|
||||
* nothing left to page through.
|
||||
*
|
||||
* @param input The input data for the command.
|
||||
* @param ctx The context of the request.
|
||||
* @param range The resolved ledger sequence range.
|
||||
* @param page The transactions fetched from the database and the cursor to resume from.
|
||||
* @param [out] response The response to append to.
|
||||
*/
|
||||
void
|
||||
processTransactionsPage(
|
||||
Input const& input,
|
||||
Context const& ctx,
|
||||
SequenceRange range,
|
||||
data::TransactionsAndCursor const& page,
|
||||
Output& response
|
||||
) const;
|
||||
|
||||
/**
|
||||
* @brief Convert one transaction to its JSON representation, unless its type is filtered out.
|
||||
*
|
||||
* @param txnPlusMeta The transaction and its metadata.
|
||||
* @param input The input data for the command.
|
||||
* @param ctx The context of the request.
|
||||
* @return The JSON representation, or nullopt if the transaction's type does not match the
|
||||
* requested one.
|
||||
*/
|
||||
[[nodiscard]] std::optional<boost::json::object>
|
||||
transactionToJsonIfTypeMatches(
|
||||
data::TransactionAndMetadata const& txnPlusMeta,
|
||||
Input const& input,
|
||||
Context const& ctx
|
||||
) const;
|
||||
|
||||
/**
|
||||
* @brief Assemble the API-specific JSON of an expanded transaction and its metadata.
|
||||
*
|
||||
* For API v2 and later, the transaction is enriched with ledger information when the
|
||||
* corresponding ledger header is available.
|
||||
*
|
||||
* @param txn The expanded transaction JSON.
|
||||
* @param meta The expanded metadata JSON.
|
||||
* @param txnPlusMeta The transaction and its metadata the JSON was expanded from.
|
||||
* @param ctx The context of the request.
|
||||
* @return The JSON representation of the transaction.
|
||||
*/
|
||||
[[nodiscard]] boost::json::object
|
||||
expandedTransactionToJson(
|
||||
boost::json::object txn,
|
||||
boost::json::object meta,
|
||||
data::TransactionAndMetadata const& txnPlusMeta,
|
||||
Context const& ctx
|
||||
) const;
|
||||
|
||||
/**
|
||||
* @brief Convert the Output to a JSON object.
|
||||
*
|
||||
* @param [out] jv The JSON object to convert to.
|
||||
* @param output The output to convert.
|
||||
*/
|
||||
friend void
|
||||
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Output const& output);
|
||||
|
||||
/**
|
||||
* @brief Convert a JSON object to Input type.
|
||||
*
|
||||
* @param jv The JSON object to convert.
|
||||
* @return Input parsed from the JSON object.
|
||||
*/
|
||||
friend Input
|
||||
tag_invoke(boost::json::value_to_tag<Input>, boost::json::value const& jv);
|
||||
|
||||
/**
|
||||
* @brief Convert the Marker to a JSON object.
|
||||
*
|
||||
* @param [out] jv The JSON object to convert to.
|
||||
* @param marker The marker to convert.
|
||||
*/
|
||||
friend void
|
||||
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Marker const& marker);
|
||||
};
|
||||
|
||||
} // namespace rpc
|
||||
@@ -35,6 +35,7 @@ target_sources(
|
||||
TimeUtils.cpp
|
||||
TxUtils.cpp
|
||||
LedgerUtils.cpp
|
||||
MPTIssuanceUtils.cpp
|
||||
config/Array.cpp
|
||||
config/ArrayView.cpp
|
||||
config/ConfigConstraints.cpp
|
||||
|
||||
96
src/util/MPTIssuanceUtils.cpp
Normal file
96
src/util/MPTIssuanceUtils.cpp
Normal file
@@ -0,0 +1,96 @@
|
||||
#include "util/MPTIssuanceUtils.hpp"
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STBase.h>
|
||||
#include <xrpl/protocol/STIssue.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxMeta.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace util {
|
||||
|
||||
std::optional<xrpl::uint192>
|
||||
getMPTokenIssuanceIDFromNode(xrpl::STObject const& node)
|
||||
{
|
||||
auto const entryType = node.getFieldU16(xrpl::sfLedgerEntryType);
|
||||
if (entryType != xrpl::ltMPTOKEN && entryType != xrpl::ltMPTOKEN_ISSUANCE)
|
||||
return std::nullopt;
|
||||
|
||||
auto const& fieldsName =
|
||||
node.getFName() == xrpl::sfCreatedNode ? xrpl::sfNewFields : xrpl::sfFinalFields;
|
||||
if (not node.isFieldPresent(fieldsName))
|
||||
return std::nullopt;
|
||||
|
||||
auto const& fields = node.peekAtField(fieldsName).downcast<xrpl::STObject>();
|
||||
|
||||
if (entryType == xrpl::ltMPTOKEN) {
|
||||
if (not fields.isFieldPresent(xrpl::sfMPTokenIssuanceID))
|
||||
return std::nullopt;
|
||||
|
||||
return fields[xrpl::sfMPTokenIssuanceID];
|
||||
}
|
||||
|
||||
// MPTokenIssuance objects carry no sfMPTokenIssuanceID, and the node's ledger key is a
|
||||
// one-way hash that does not embed the ID, so reconstruct it from sfSequence and sfIssuer
|
||||
if (not fields.isFieldPresent(xrpl::sfSequence) || not fields.isFieldPresent(xrpl::sfIssuer))
|
||||
return std::nullopt;
|
||||
|
||||
return xrpl::makeMptID(
|
||||
fields.getFieldU32(xrpl::sfSequence), fields.getAccountID(xrpl::sfIssuer)
|
||||
);
|
||||
}
|
||||
|
||||
void
|
||||
addMPTokenIssuanceIDsFromTx(MPTokenIssuanceIDs& issuanceIDs, xrpl::STTx const& sttx)
|
||||
{
|
||||
if (sttx.isFieldPresent(xrpl::sfMPTokenIssuanceID))
|
||||
issuanceIDs.insert(sttx.getFieldH192(xrpl::sfMPTokenIssuanceID));
|
||||
|
||||
for (xrpl::STBase const& field : sttx) {
|
||||
switch (field.getSType()) {
|
||||
case xrpl::STI_AMOUNT: {
|
||||
auto const& amount = field.downcast<xrpl::STAmount>();
|
||||
if (amount.holds<xrpl::MPTIssue>())
|
||||
issuanceIDs.insert(amount.get<xrpl::MPTIssue>().getMptID());
|
||||
break;
|
||||
}
|
||||
case xrpl::STI_ISSUE: {
|
||||
auto const& issue = field.downcast<xrpl::STIssue>();
|
||||
if (issue.holds<xrpl::MPTIssue>())
|
||||
issuanceIDs.insert(issue.value().get<xrpl::MPTIssue>().getMptID());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
referencesMptIssuance(
|
||||
xrpl::TxMeta const& txMeta,
|
||||
xrpl::STTx const& sttx,
|
||||
xrpl::uint192 const& mptIssuanceID
|
||||
)
|
||||
{
|
||||
if (txMeta.getResultTER() == xrpl::tesSUCCESS) {
|
||||
for (auto const& node : txMeta.getNodes()) {
|
||||
if (getMPTokenIssuanceIDFromNode(node) == mptIssuanceID)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
MPTokenIssuanceIDs issuanceIDs;
|
||||
addMPTokenIssuanceIDsFromTx(issuanceIDs, sttx);
|
||||
return issuanceIDs.contains(mptIssuanceID);
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
57
src/util/MPTIssuanceUtils.hpp
Normal file
57
src/util/MPTIssuanceUtils.hpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <boost/container/flat_set.hpp>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TxMeta.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace util {
|
||||
|
||||
/**
|
||||
* @brief A set of distinct MPTokenIssuanceIDs.
|
||||
*/
|
||||
using MPTokenIssuanceIDs = boost::container::flat_set<xrpl::uint192>;
|
||||
|
||||
/**
|
||||
* @brief Derive the MPTokenIssuanceID from an affected node in transaction metadata.
|
||||
*
|
||||
* @param node An entry of the metadata's AffectedNodes array.
|
||||
* @return The 192-bit issuance ID if the node is an MPTokenIssuance or MPToken object.
|
||||
*/
|
||||
std::optional<xrpl::uint192>
|
||||
getMPTokenIssuanceIDFromNode(xrpl::STObject const& node);
|
||||
|
||||
/**
|
||||
* @brief Collect every MPTokenIssuanceID referenced by a transaction's own fields.
|
||||
*
|
||||
* @note Checks the top-level sfMPTokenIssuanceID field, plus any STI_AMOUNT/STI_ISSUE field holding
|
||||
* an xrpl::MPTIssue (e.g. Payment's sfAmount, AMM's sfAsset/sfAsset2).
|
||||
*
|
||||
* @param [out] issuanceIDs Set to insert each found issuance ID into.
|
||||
* @param sttx The transaction.
|
||||
*/
|
||||
void
|
||||
addMPTokenIssuanceIDsFromTx(MPTokenIssuanceIDs& issuanceIDs, xrpl::STTx const& sttx);
|
||||
|
||||
/**
|
||||
* @brief Check whether a transaction references a specific MPT issuance.
|
||||
*
|
||||
* @note Scans the transaction's metadata for affected MPTokenIssuance/MPToken nodes, and the
|
||||
* transaction's own MPTokenIssuanceID/MPT issue fields, exiting as soon as a match is found.
|
||||
*
|
||||
* @param txMeta Transaction metadata.
|
||||
* @param sttx The transaction.
|
||||
* @param mptIssuanceID The MPT issuance to check for.
|
||||
* @return true if the transaction references mptIssuanceID.
|
||||
*/
|
||||
bool
|
||||
referencesMptIssuance(
|
||||
xrpl::TxMeta const& txMeta,
|
||||
xrpl::STTx const& sttx,
|
||||
xrpl::uint192 const& mptIssuanceID
|
||||
);
|
||||
|
||||
} // namespace util
|
||||
@@ -1,11 +1,16 @@
|
||||
#include "util/Retry.hpp"
|
||||
|
||||
#include "util/Assert.hpp"
|
||||
|
||||
#include <boost/asio/any_io_executor.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
@@ -42,6 +47,11 @@ Retry::Retry(
|
||||
{
|
||||
}
|
||||
|
||||
Retry::Retry(RetryStrategyPtr strategy, boost::asio::any_io_executor executor)
|
||||
: strategy_(std::move(strategy)), timer_(executor)
|
||||
{
|
||||
}
|
||||
|
||||
Retry::~Retry()
|
||||
{
|
||||
*canceled_ = true;
|
||||
@@ -73,11 +83,8 @@ Retry::reset()
|
||||
(*strategy_).reset();
|
||||
}
|
||||
|
||||
ExponentialBackoffStrategy::ExponentialBackoffStrategy(
|
||||
std::chrono::steady_clock::duration delay,
|
||||
std::chrono::steady_clock::duration maxDelay
|
||||
)
|
||||
: RetryStrategy(delay), maxDelay_(maxDelay)
|
||||
ExponentialBackoffStrategy::ExponentialBackoffStrategy(Retry::Delays delays)
|
||||
: RetryStrategy(delays.initial), maxDelay_(delays.max)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -88,14 +95,40 @@ ExponentialBackoffStrategy::nextDelay() const
|
||||
return std::min(next, maxDelay_);
|
||||
}
|
||||
|
||||
void
|
||||
Retry::wait(boost::asio::yield_context yield)
|
||||
{
|
||||
// Suspending a coroutine while an exception is being handled corrupts the caught-exception
|
||||
// state, which is per-thread rather than per-coroutine. Copy whatever is needed out of the
|
||||
// handler and let it exit before waiting.
|
||||
ASSERT(
|
||||
std::current_exception() == nullptr,
|
||||
"Retry::wait must not be called while an exception is being handled"
|
||||
);
|
||||
|
||||
*canceled_ = false;
|
||||
timer_.expires_after(strategy_->getDelay());
|
||||
strategy_->increaseDelay();
|
||||
++attemptNumber_;
|
||||
|
||||
// error ignored on purpose: a cancelled timer just means the caller retries sooner
|
||||
boost::system::error_code ec;
|
||||
timer_.async_wait(yield[ec]);
|
||||
}
|
||||
|
||||
Retry
|
||||
makeRetryExponentialBackoff(
|
||||
std::chrono::steady_clock::duration delay,
|
||||
std::chrono::steady_clock::duration maxDelay,
|
||||
Retry::Delays delays,
|
||||
boost::asio::strand<boost::asio::io_context::executor_type> strand
|
||||
)
|
||||
{
|
||||
return Retry(std::make_unique<ExponentialBackoffStrategy>(delay, maxDelay), std::move(strand));
|
||||
return Retry(std::make_unique<ExponentialBackoffStrategy>(delays), std::move(strand));
|
||||
}
|
||||
|
||||
Retry
|
||||
makeRetryExponentialBackoff(Retry::Delays delays, boost::asio::any_io_executor executor)
|
||||
{
|
||||
return Retry(std::make_unique<ExponentialBackoffStrategy>(delays), std::move(executor));
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <boost/asio/any_io_executor.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
|
||||
@@ -65,6 +67,16 @@ class Retry {
|
||||
std::shared_ptr<std::atomic_bool> canceled_{std::make_shared<std::atomic_bool>(false)};
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief The delays to use between retry attempts
|
||||
*
|
||||
* Equal `initial` and `max` mean a flat delay with no backoff.
|
||||
*/
|
||||
struct Delays {
|
||||
std::chrono::steady_clock::duration initial;
|
||||
std::chrono::steady_clock::duration max;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Construct a new Retry object
|
||||
*
|
||||
@@ -76,6 +88,16 @@ public:
|
||||
boost::asio::strand<boost::asio::io_context::executor_type> strand
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Construct a new Retry object from any I/O executor
|
||||
*
|
||||
* For coroutines, pass `yield.get_executor()` and drive it with @ref wait().
|
||||
*
|
||||
* @param strategy The retry strategy to use
|
||||
* @param executor The executor to run the retry timer on
|
||||
*/
|
||||
Retry(RetryStrategyPtr strategy, boost::asio::any_io_executor executor);
|
||||
|
||||
/**
|
||||
* @brief Destroy the Retry object
|
||||
*/
|
||||
@@ -105,6 +127,17 @@ public:
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Wait out the current delay by suspending the calling coroutine, then back off.
|
||||
*
|
||||
* Unlike @ref retry() this returns once the delay elapsed instead of scheduling a callback, so
|
||||
* the caller can keep its own loop. Advances the delay and attempt number like @ref retry().
|
||||
*
|
||||
* @param yield The coroutine to suspend
|
||||
*/
|
||||
void
|
||||
wait(boost::asio::yield_context yield);
|
||||
|
||||
/**
|
||||
* @brief Cancel scheduled retry if any
|
||||
*/
|
||||
@@ -140,13 +173,9 @@ public:
|
||||
/**
|
||||
* @brief Construct a new Exponential Backoff Strategy object
|
||||
*
|
||||
* @param delay The initial delay value
|
||||
* @param maxDelay The maximum delay value
|
||||
* @param delays The delays to use between attempts
|
||||
*/
|
||||
ExponentialBackoffStrategy(
|
||||
std::chrono::steady_clock::duration delay,
|
||||
std::chrono::steady_clock::duration maxDelay
|
||||
);
|
||||
explicit ExponentialBackoffStrategy(Retry::Delays delays);
|
||||
|
||||
private:
|
||||
[[nodiscard]] std::chrono::steady_clock::duration
|
||||
@@ -156,16 +185,24 @@ private:
|
||||
/**
|
||||
* @brief Create a retry mechanism with exponential backoff strategy
|
||||
*
|
||||
* @param delay The initial delay value
|
||||
* @param maxDelay The maximum delay value
|
||||
* @param delays The delays to use between attempts
|
||||
* @param strand The strand to use for async operations
|
||||
* @return The retry object
|
||||
*/
|
||||
Retry
|
||||
makeRetryExponentialBackoff(
|
||||
std::chrono::steady_clock::duration delay,
|
||||
std::chrono::steady_clock::duration maxDelay,
|
||||
Retry::Delays delays,
|
||||
boost::asio::strand<boost::asio::io_context::executor_type> strand
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Create a retry mechanism with exponential backoff strategy on any I/O executor
|
||||
*
|
||||
* @param delays The delays to use between attempts
|
||||
* @param executor The executor to run the retry timer on
|
||||
* @return The retry object
|
||||
*/
|
||||
Retry
|
||||
makeRetryExponentialBackoff(Retry::Delays delays, boost::asio::any_io_executor executor);
|
||||
|
||||
} // namespace util
|
||||
|
||||
@@ -278,7 +278,15 @@ getClioConfig()
|
||||
{"database.cassandra.connect_timeout",
|
||||
ConfigValue{ConfigType::Integer}.optional().withConstraint(gValidateUint32)},
|
||||
{"database.cassandra.request_timeout",
|
||||
ConfigValue{ConfigType::Integer}.optional().withConstraint(gValidateUint32)},
|
||||
ConfigValue{ConfigType::Double}.optional().withConstraint(gValidatePositiveDouble)},
|
||||
{"database.cassandra.initial_request_retry_delay",
|
||||
ConfigValue{ConfigType::Double}.defaultValue(0.5).withConstraint(
|
||||
gValidatePositiveDouble
|
||||
)},
|
||||
{"database.cassandra.max_request_retry_delay",
|
||||
ConfigValue{ConfigType::Double}.defaultValue(5.0).withConstraint(
|
||||
gValidatePositiveDouble
|
||||
)},
|
||||
{"database.cassandra.username", ConfigValue{ConfigType::String}.optional()},
|
||||
{"database.cassandra.password", ConfigValue{ConfigType::String}.optional()},
|
||||
{"database.cassandra.certfile", ConfigValue{ConfigType::String}.optional()},
|
||||
|
||||
@@ -182,8 +182,14 @@ This document provides a list of all available Clio configuration properties in
|
||||
"established."},
|
||||
KV{.key = "database.cassandra.request_timeout",
|
||||
.value = "The maximum amount of time in seconds that the system waits for a request to "
|
||||
"be fetched from the "
|
||||
"database."},
|
||||
"be fetched from the database. Should be set higher than the server side read "
|
||||
"timeout. If omitted, no request timeout is applied."},
|
||||
KV{.key = "database.cassandra.initial_request_retry_delay",
|
||||
.value = "How long in seconds to wait before the first retry of a database request "
|
||||
"that failed with a transient error."},
|
||||
KV{.key = "database.cassandra.max_request_retry_delay",
|
||||
.value = "Upper bound in seconds for the exponential backoff between retries of a "
|
||||
"database request."},
|
||||
KV{.key = "database.cassandra.username",
|
||||
.value = "The username used for authenticating with the database."},
|
||||
KV{.key = "database.cassandra.password",
|
||||
|
||||
54
src/web/LoadWarning.hpp
Normal file
54
src/web/LoadWarning.hpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include "rpc/Errors.hpp"
|
||||
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
namespace web {
|
||||
|
||||
/**
|
||||
* @brief Parse a serialized response body and attach the DOSGuard "load" warning to it.
|
||||
*
|
||||
* Sets `warning` to "load" and appends a rpc::WarningCode::WarnRpcRateLimit entry to the `warnings`
|
||||
* array, creating that array if the body has none - or replacing it if `warnings` is present but is
|
||||
* not an array.
|
||||
*
|
||||
* @note Not every response body is JSON. Over plain HTTP the error paths return text/html bodies
|
||||
* (e.g. "Null method" or "Unable to parse JSON from the request"), which cannot carry the warning.
|
||||
* For those this returns std::nullopt and the caller must send the body unchanged - parsing them as
|
||||
* JSON would throw.
|
||||
*
|
||||
* @param message The serialized response body
|
||||
* @return The response as a JSON object with the warning attached; callers that need a string body
|
||||
* must serialize it again. std::nullopt if @p message does not parse as a JSON object.
|
||||
*/
|
||||
inline std::optional<boost::json::object>
|
||||
withLoadWarning(std::string_view message)
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
auto const parsed = boost::json::parse(message, ec);
|
||||
if (ec.failed() or not parsed.is_object())
|
||||
return std::nullopt;
|
||||
|
||||
auto jsonResponse = parsed.as_object();
|
||||
jsonResponse["warning"] = "load";
|
||||
|
||||
if (jsonResponse.contains("warnings") and jsonResponse["warnings"].is_array()) {
|
||||
jsonResponse["warnings"].as_array().push_back(
|
||||
rpc::makeWarning(rpc::WarningCode::WarnRpcRateLimit)
|
||||
);
|
||||
} else {
|
||||
jsonResponse["warnings"] =
|
||||
boost::json::array{rpc::makeWarning(rpc::WarningCode::WarnRpcRateLimit)};
|
||||
}
|
||||
|
||||
return jsonResponse;
|
||||
}
|
||||
|
||||
} // namespace web
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "util/prometheus/Http.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
#include "web/LoadWarning.hpp"
|
||||
#include "web/ProxyIpResolver.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
@@ -309,27 +310,20 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send a response to the client
|
||||
* The message length will be added to the DOSGuard, if the limit is reached, a warning will be
|
||||
* added to the response
|
||||
* @copydoc ConnectionBase::send
|
||||
*
|
||||
* @note The message length is added to the DOSGuard. If that puts the client over its limit
|
||||
* and the body parses as a JSON object, a "load" warning is attached to it; bodies that are
|
||||
* not JSON objects - the text/html error paths - are sent unchanged.
|
||||
*/
|
||||
void
|
||||
send(std::string&& msg, http::status status = http::status::ok) override
|
||||
{
|
||||
if (!dosGuard_.get().add(clientIp_, msg.size())) {
|
||||
auto jsonResponse = boost::json::parse(msg).as_object();
|
||||
jsonResponse["warning"] = "load";
|
||||
if (jsonResponse.contains("warnings") && jsonResponse["warnings"].is_array()) {
|
||||
jsonResponse["warnings"].as_array().push_back(
|
||||
rpc::makeWarning(rpc::WarningCode::WarnRpcRateLimit)
|
||||
);
|
||||
} else {
|
||||
jsonResponse["warnings"] =
|
||||
boost::json::array{rpc::makeWarning(rpc::WarningCode::WarnRpcRateLimit)};
|
||||
if (auto const warned = withLoadWarning(msg); warned.has_value()) {
|
||||
// Reserialize when we need to include this warning
|
||||
msg = boost::json::serialize(*warned);
|
||||
}
|
||||
|
||||
// Reserialize when we need to include this warning
|
||||
msg = boost::json::serialize(jsonResponse);
|
||||
}
|
||||
sender_(httpResponse(status, "application/json", std::move(msg)));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/LoadWarning.hpp"
|
||||
#include "web/SubscriptionContext.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
@@ -196,29 +197,20 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send a message to the client
|
||||
* @param msg The message to send
|
||||
* Send this message to the client. The message length will be added to the DOSGuard
|
||||
* If the DOSGuard is triggered, the message will be modified to include a warning
|
||||
* @copydoc ConnectionBase::send
|
||||
*
|
||||
* @note The message length is added to the DOSGuard. If that puts the client over its limit
|
||||
* and the message parses as a JSON object, a "load" warning is attached to it; messages that
|
||||
* are not JSON objects are sent unchanged.
|
||||
*/
|
||||
void
|
||||
send(std::string&& msg, http::status) override
|
||||
{
|
||||
if (!dosGuard_.get().add(clientIp_, msg.size())) {
|
||||
auto jsonResponse = boost::json::parse(msg).as_object();
|
||||
jsonResponse["warning"] = "load";
|
||||
|
||||
if (jsonResponse.contains("warnings") && jsonResponse["warnings"].is_array()) {
|
||||
jsonResponse["warnings"].as_array().push_back(
|
||||
rpc::makeWarning(rpc::WarningCode::WarnRpcRateLimit)
|
||||
);
|
||||
} else {
|
||||
jsonResponse["warnings"] =
|
||||
boost::json::array{rpc::makeWarning(rpc::WarningCode::WarnRpcRateLimit)};
|
||||
if (auto const warned = withLoadWarning(msg); warned.has_value()) {
|
||||
// Reserialize when we need to include this warning
|
||||
msg = boost::json::serialize(*warned);
|
||||
}
|
||||
|
||||
// Reserialize when we need to include this warning
|
||||
msg = boost::json::serialize(jsonResponse);
|
||||
}
|
||||
auto sharedMsg = std::make_shared<std::string>(std::move(msg));
|
||||
send(std::move(sharedMsg));
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "util/Profiler.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/LoadWarning.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
@@ -182,7 +183,13 @@ public:
|
||||
|
||||
// NOLINTBEGIN(bugprone-unchecked-optional-access)
|
||||
if (not dosguard_.get().add(connectionMetadata.ip(), response->message().size())) {
|
||||
response->setMessage(makeLoadWarning(*response));
|
||||
if (auto const warned = withLoadWarning(response->message()); warned.has_value()) {
|
||||
response->setMessage(*warned);
|
||||
} else {
|
||||
LOG(log_.debug()) << connectionMetadata.tag()
|
||||
<< "Rate limit reached but the response body is not a JSON "
|
||||
"object; sending it without a load warning";
|
||||
}
|
||||
}
|
||||
|
||||
return *std::move(response);
|
||||
@@ -357,22 +364,6 @@ private:
|
||||
return web::ng::Response{boost::beast::http::status::service_unavailable, error, request};
|
||||
}
|
||||
|
||||
static boost::json::object
|
||||
makeLoadWarning(Response const& response)
|
||||
{
|
||||
auto jsonResponse = boost::json::parse(response.message()).as_object();
|
||||
jsonResponse["warning"] = "load";
|
||||
if (jsonResponse.contains("warnings") && jsonResponse["warnings"].is_array()) {
|
||||
jsonResponse["warnings"].as_array().push_back(
|
||||
rpc::makeWarning(rpc::WarningCode::WarnRpcRateLimit)
|
||||
);
|
||||
} else {
|
||||
jsonResponse["warnings"] =
|
||||
boost::json::array{rpc::makeWarning(rpc::WarningCode::WarnRpcRateLimit)};
|
||||
}
|
||||
return jsonResponse;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
shouldReplaceParams(boost::json::object const& req) const
|
||||
{
|
||||
|
||||
@@ -114,6 +114,7 @@ makeConnection(
|
||||
std::string ip,
|
||||
util::TagDecoratorFactory& tagDecoratorFactory,
|
||||
Server::OnConnectCheck onConnectCheck,
|
||||
size_t maxSendingQueueSize,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
@@ -130,7 +131,8 @@ makeConnection(
|
||||
std::move(ip),
|
||||
std::move(sslDetectionResult.buffer),
|
||||
*sslContext,
|
||||
tagDecoratorFactory
|
||||
tagDecoratorFactory,
|
||||
maxSendingQueueSize
|
||||
);
|
||||
sslConnection->setTimeout(std::chrono::seconds{10});
|
||||
auto const expectedSuccess = sslConnection->sslHandshake(yield);
|
||||
@@ -146,7 +148,8 @@ makeConnection(
|
||||
std::move(sslDetectionResult.socket),
|
||||
std::move(ip),
|
||||
std::move(sslDetectionResult.buffer),
|
||||
tagDecoratorFactory
|
||||
tagDecoratorFactory,
|
||||
maxSendingQueueSize
|
||||
);
|
||||
}
|
||||
|
||||
@@ -163,6 +166,7 @@ std::expected<ConnectionPtr, std::string>
|
||||
tryUpgradeConnection(
|
||||
impl::UpgradableConnectionPtr connection,
|
||||
util::TagDecoratorFactory& tagDecoratorFactory,
|
||||
size_t maxSendingQueueSize,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
@@ -174,7 +178,8 @@ tryUpgradeConnection(
|
||||
}
|
||||
|
||||
if (*expectedIsUpgrade) {
|
||||
auto expectedUpgradedConnection = connection->upgrade(tagDecoratorFactory, yield);
|
||||
auto expectedUpgradedConnection =
|
||||
connection->upgrade(tagDecoratorFactory, maxSendingQueueSize, yield);
|
||||
if (expectedUpgradedConnection.has_value())
|
||||
return std::move(expectedUpgradedConnection).value();
|
||||
|
||||
@@ -196,13 +201,14 @@ Server::Server(
|
||||
std::optional<size_t> parallelRequestLimit,
|
||||
util::TagDecoratorFactory tagDecoratorFactory,
|
||||
ProxyIpResolver proxyIpResolver,
|
||||
std::optional<size_t> maxSubscriptionSendQueueSize,
|
||||
size_t maxSubscriptionSendQueueSize,
|
||||
Hooks hooks
|
||||
)
|
||||
: ctx_{ctx}
|
||||
, sslContext_{std::move(sslContext)}
|
||||
, tagDecoratorFactory_{tagDecoratorFactory}
|
||||
, connectionHandler_{processingPolicy, parallelRequestLimit, tagDecoratorFactory_, maxSubscriptionSendQueueSize, std::move(proxyIpResolver), std::move(hooks.onDisconnectHook), std::move(hooks.onIpChangeHook)}
|
||||
, maxSubscriptionSendQueueSize_{maxSubscriptionSendQueueSize}
|
||||
, connectionHandler_{processingPolicy, parallelRequestLimit, tagDecoratorFactory_, std::move(proxyIpResolver), std::move(hooks.onDisconnectHook), std::move(hooks.onIpChangeHook)}
|
||||
, endpoint_{std::move(endpoint)}
|
||||
, onConnectCheck_{std::move(hooks.onConnectCheck)}
|
||||
{
|
||||
@@ -296,6 +302,7 @@ Server::handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield
|
||||
std::move(ip).value(),
|
||||
tagDecoratorFactory_,
|
||||
onConnectCheck_,
|
||||
maxSubscriptionSendQueueSize_,
|
||||
yield
|
||||
);
|
||||
if (not connectionExpected.has_value()) {
|
||||
@@ -318,8 +325,12 @@ Server::handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield
|
||||
return;
|
||||
}
|
||||
|
||||
auto connection =
|
||||
tryUpgradeConnection(std::move(connectionExpected).value(), tagDecoratorFactory_, yield);
|
||||
auto connection = tryUpgradeConnection(
|
||||
std::move(connectionExpected).value(),
|
||||
tagDecoratorFactory_,
|
||||
maxSubscriptionSendQueueSize_,
|
||||
yield
|
||||
);
|
||||
if (not connection.has_value()) {
|
||||
LOG(log_.info()) << connection.error();
|
||||
return;
|
||||
|
||||
@@ -59,6 +59,7 @@ private:
|
||||
std::optional<boost::asio::ssl::context> sslContext_;
|
||||
|
||||
util::TagDecoratorFactory tagDecoratorFactory_;
|
||||
size_t maxSubscriptionSendQueueSize_;
|
||||
|
||||
impl::ConnectionHandler connectionHandler_;
|
||||
boost::asio::ip::tcp::endpoint endpoint_;
|
||||
@@ -90,7 +91,7 @@ public:
|
||||
std::optional<size_t> parallelRequestLimit,
|
||||
util::TagDecoratorFactory tagDecoratorFactory,
|
||||
ProxyIpResolver proxyIpResolver,
|
||||
std::optional<size_t> maxSubscriptionSendQueueSize,
|
||||
size_t maxSubscriptionSendQueueSize,
|
||||
Hooks hooks
|
||||
);
|
||||
|
||||
|
||||
@@ -6,10 +6,8 @@
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
@@ -18,13 +16,11 @@ namespace web::ng {
|
||||
SubscriptionContext::SubscriptionContext(
|
||||
util::TagDecoratorFactory const& factory,
|
||||
impl::WsConnectionBase& connection,
|
||||
std::optional<size_t> maxSendQueueSize,
|
||||
boost::asio::yield_context yield,
|
||||
ErrorHandler errorHandler
|
||||
)
|
||||
: web::SubscriptionContextInterface(factory)
|
||||
, connection_(connection)
|
||||
, maxSendQueueSize_(maxSendQueueSize)
|
||||
, tasksGroup_(yield)
|
||||
, yield_(yield)
|
||||
, errorHandler_(std::move(errorHandler))
|
||||
@@ -42,14 +38,6 @@ SubscriptionContext::send(std::shared_ptr<std::string> message)
|
||||
if (disconnected_ or gotError_)
|
||||
return;
|
||||
|
||||
if (maxSendQueueSize_.has_value() and tasksGroup_.size() >= *maxSendQueueSize_) {
|
||||
tasksGroup_.spawn(yield_, [this](boost::asio::yield_context innerYield) {
|
||||
connection_.get().close(innerYield);
|
||||
});
|
||||
gotError_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
tasksGroup_.spawn(
|
||||
yield_,
|
||||
[this, message = std::move(message)](boost::asio::yield_context innerYield) mutable {
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
#include <boost/signals2/variadic_signal.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace web::ng {
|
||||
@@ -36,7 +34,6 @@ public:
|
||||
|
||||
private:
|
||||
std::reference_wrapper<impl::WsConnectionBase> connection_;
|
||||
std::optional<size_t> maxSendQueueSize_;
|
||||
util::CoroutineGroup tasksGroup_;
|
||||
boost::asio::yield_context yield_;
|
||||
ErrorHandler errorHandler_;
|
||||
@@ -59,15 +56,12 @@ public:
|
||||
*
|
||||
* @param factory The tag decorator factory to use to init taggable.
|
||||
* @param connection The connection for which the context is created.
|
||||
* @param maxSendQueueSize The maximum size of the send queue. If the queue is full, the
|
||||
* connection will be closed.
|
||||
* @param yield The yield context to spawn sending coroutines.
|
||||
* @param errorHandler The error handler.
|
||||
*/
|
||||
SubscriptionContext(
|
||||
util::TagDecoratorFactory const& factory,
|
||||
impl::WsConnectionBase& connection,
|
||||
std::optional<size_t> maxSendQueueSize,
|
||||
boost::asio::yield_context yield,
|
||||
ErrorHandler errorHandler
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -75,7 +76,6 @@ ConnectionHandler::ConnectionHandler(
|
||||
ProcessingPolicy processingPolicy,
|
||||
std::optional<size_t> maxParallelRequests,
|
||||
util::TagDecoratorFactory& tagFactory,
|
||||
std::optional<size_t> maxSubscriptionSendQueueSize,
|
||||
ProxyIpResolver proxyIpResolver,
|
||||
OnDisconnectHook onDisconnectHook,
|
||||
OnIpChangeHook onIpChangeHook
|
||||
@@ -83,7 +83,6 @@ ConnectionHandler::ConnectionHandler(
|
||||
: processingPolicy_{processingPolicy}
|
||||
, maxParallelRequests_{maxParallelRequests}
|
||||
, tagFactory_{tagFactory}
|
||||
, maxSubscriptionSendQueueSize_{maxSubscriptionSendQueueSize}
|
||||
, proxyIpResolver_(std::move(proxyIpResolver))
|
||||
, onDisconnectHook_{std::move(onDisconnectHook)}
|
||||
, onIpChangeHook_(std::move(onIpChangeHook))
|
||||
@@ -136,11 +135,7 @@ ConnectionHandler::processConnection(ConnectionPtr connectionPtr, boost::asio::y
|
||||
auto* ptr = dynamic_cast<impl::WsConnectionBase*>(connectionPtr.get());
|
||||
ASSERT(ptr != nullptr, "Casted not websocket connection");
|
||||
subscriptionContext = std::make_shared<SubscriptionContext>(
|
||||
tagFactory_,
|
||||
*ptr,
|
||||
maxSubscriptionSendQueueSize_,
|
||||
yield,
|
||||
[this](Error const& e, Connection const& c) { return handleError(e, c); }
|
||||
tagFactory_, *ptr, yield, std::bind_front(&ConnectionHandler::handleError, this)
|
||||
);
|
||||
LOG(log_.trace()) << connectionRef.tag()
|
||||
<< "Created SubscriptionContext for the connection";
|
||||
|
||||
@@ -46,7 +46,6 @@ private:
|
||||
std::optional<size_t> maxParallelRequests_;
|
||||
|
||||
std::reference_wrapper<util::TagDecoratorFactory> tagFactory_;
|
||||
std::optional<size_t> maxSubscriptionSendQueueSize_;
|
||||
|
||||
ProxyIpResolver proxyIpResolver_;
|
||||
|
||||
@@ -73,7 +72,6 @@ public:
|
||||
ProcessingPolicy processingPolicy,
|
||||
std::optional<size_t> maxParallelRequests,
|
||||
util::TagDecoratorFactory& tagFactory,
|
||||
std::optional<size_t> maxSubscriptionSendQueueSize,
|
||||
ProxyIpResolver proxyIpResolver,
|
||||
OnDisconnectHook onDisconnectHook,
|
||||
OnIpChangeHook onIpChangeHook
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <boost/beast/websocket.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -43,6 +44,7 @@ public:
|
||||
virtual std::expected<ConnectionPtr, Error>
|
||||
upgrade(
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory,
|
||||
size_t maxSendingQueueSize,
|
||||
boost::asio::yield_context yield
|
||||
) = 0;
|
||||
|
||||
@@ -71,15 +73,19 @@ public:
|
||||
boost::asio::ip::tcp::socket socket,
|
||||
std::string ip,
|
||||
boost::beast::flat_buffer buffer,
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory,
|
||||
size_t maxSendingQueueSize
|
||||
)
|
||||
requires IsTcpStream<StreamType>
|
||||
: UpgradableConnection(std::move(ip), std::move(buffer), tagDecoratorFactory)
|
||||
, stream_{std::move(socket)}
|
||||
, sendingQueue_([this](MessageType const& message, auto&& yield) {
|
||||
boost::beast::get_lowest_layer(stream_).expires_after(timeout_);
|
||||
boost::beast::http::async_write(stream_, message, yield);
|
||||
})
|
||||
, sendingQueue_(
|
||||
[this](MessageType const& message, auto&& yield) {
|
||||
boost::beast::get_lowest_layer(stream_).expires_after(timeout_);
|
||||
boost::beast::http::async_write(stream_, message, yield);
|
||||
},
|
||||
maxSendingQueueSize
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -88,15 +94,19 @@ public:
|
||||
std::string ip,
|
||||
boost::beast::flat_buffer buffer,
|
||||
boost::asio::ssl::context& sslCtx,
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory,
|
||||
size_t maxSendingQueueSize
|
||||
)
|
||||
requires IsSslTcpStream<StreamType>
|
||||
: UpgradableConnection(std::move(ip), std::move(buffer), tagDecoratorFactory)
|
||||
, stream_{std::move(socket), sslCtx}
|
||||
, sendingQueue_([this](MessageType const& message, auto&& yield) {
|
||||
boost::beast::get_lowest_layer(stream_).expires_after(timeout_);
|
||||
boost::beast::http::async_write(stream_, message, yield);
|
||||
})
|
||||
, sendingQueue_(
|
||||
[this](MessageType const& message, auto&& yield) {
|
||||
boost::beast::get_lowest_layer(stream_).expires_after(timeout_);
|
||||
boost::beast::http::async_write(stream_, message, yield);
|
||||
},
|
||||
maxSendingQueueSize
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -202,6 +212,7 @@ public:
|
||||
std::expected<ConnectionPtr, Error>
|
||||
upgrade(
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory,
|
||||
size_t maxSendingQueueSize,
|
||||
boost::asio::yield_context yield
|
||||
) override
|
||||
{
|
||||
@@ -213,6 +224,7 @@ public:
|
||||
std::move(buffer_),
|
||||
std::move(*request_), // NOLINT(bugprone-unchecked-optional-access)
|
||||
tagDecoratorFactory,
|
||||
maxSendingQueueSize,
|
||||
yield
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
#include "web/ng/Error.hpp"
|
||||
|
||||
#include <boost/asio/any_io_executor.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/system/detail/error_code.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <utility>
|
||||
|
||||
namespace web::ng::impl {
|
||||
|
||||
@@ -23,9 +26,10 @@ private:
|
||||
Sender sender_;
|
||||
Error error_;
|
||||
bool isSending_{false};
|
||||
size_t maxSize_;
|
||||
|
||||
public:
|
||||
SendingQueue(Sender sender) : sender_{std::move(sender)}
|
||||
SendingQueue(Sender sender, size_t maxSize) : sender_{std::move(sender)}, maxSize_{maxSize}
|
||||
{
|
||||
}
|
||||
|
||||
@@ -35,6 +39,11 @@ public:
|
||||
if (error_)
|
||||
return std::unexpected{error_};
|
||||
|
||||
if (queue_.size() >= maxSize_) {
|
||||
error_ = boost::asio::error::timed_out;
|
||||
return std::unexpected{error_};
|
||||
}
|
||||
|
||||
queue_.push(std::move(message));
|
||||
if (isSending_)
|
||||
return {};
|
||||
@@ -43,7 +52,11 @@ public:
|
||||
while (not queue_.empty() and not error_) {
|
||||
auto const responseToSend = std::move(queue_.front());
|
||||
queue_.pop();
|
||||
sender_(responseToSend, yield[error_]);
|
||||
|
||||
Error writeError;
|
||||
sender_(responseToSend, yield[writeError]);
|
||||
if (writeError)
|
||||
error_ = writeError;
|
||||
}
|
||||
isSending_ = false;
|
||||
if (error_)
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
#include <boost/beast/websocket/stream_base.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
@@ -60,23 +60,29 @@ public:
|
||||
std::string ip,
|
||||
boost::beast::flat_buffer buffer,
|
||||
boost::beast::http::request<boost::beast::http::string_body> initialRequest,
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory,
|
||||
size_t maxSendingQueueSize
|
||||
)
|
||||
: WsConnectionBase(std::move(ip), std::move(buffer), tagDecoratorFactory)
|
||||
, stream_(std::move(stream))
|
||||
, initialRequest_(std::move(initialRequest))
|
||||
, sendingQueue_{[this](MessageType const& message, auto&& yield) {
|
||||
boost::asio::const_buffer const buffer = std::visit(
|
||||
util::OverloadSet{
|
||||
[](Response const& r) -> boost::asio::const_buffer { return r.asWsResponse(); },
|
||||
[](std::shared_ptr<std::string> const& m) -> boost::asio::const_buffer {
|
||||
return boost::asio::buffer(*m);
|
||||
}
|
||||
},
|
||||
message
|
||||
);
|
||||
stream_.async_write(buffer, yield);
|
||||
}}
|
||||
, sendingQueue_{
|
||||
[this](MessageType const& message, auto&& yield) {
|
||||
boost::asio::const_buffer const buffer = std::visit(
|
||||
util::OverloadSet{
|
||||
[](Response const& r) -> boost::asio::const_buffer {
|
||||
return r.asWsResponse();
|
||||
},
|
||||
[](std::shared_ptr<std::string> const& m) -> boost::asio::const_buffer {
|
||||
return boost::asio::buffer(*m);
|
||||
}
|
||||
},
|
||||
message
|
||||
);
|
||||
stream_.async_write(buffer, yield);
|
||||
},
|
||||
maxSendingQueueSize
|
||||
}
|
||||
{
|
||||
setupWsStream();
|
||||
}
|
||||
@@ -188,6 +194,7 @@ makeWsConnection(
|
||||
boost::beast::flat_buffer buffer,
|
||||
boost::beast::http::request<boost::beast::http::string_body> request,
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory,
|
||||
size_t maxSendingQueueSize,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
@@ -196,7 +203,8 @@ makeWsConnection(
|
||||
std::move(ip),
|
||||
std::move(buffer),
|
||||
std::move(request),
|
||||
tagDecoratorFactory
|
||||
tagDecoratorFactory,
|
||||
maxSendingQueueSize
|
||||
);
|
||||
auto const expectedSuccess = connection->performHandshake(yield);
|
||||
if (not expectedSuccess.has_value())
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "data/BackendInterface.hpp"
|
||||
#include "util/LoggerFixtures.hpp"
|
||||
#include "util/MockBackend.hpp"
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
#include <xrpl/protocol/STIssue.h>
|
||||
#include <xrpl/protocol/STNumber.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/STVector256.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
@@ -1493,7 +1495,7 @@ createMptIssuanceObject(
|
||||
std::optional<std::uint64_t> maxAmount,
|
||||
std::optional<std::uint64_t> lockedAmount,
|
||||
std::optional<std::string_view> domainId,
|
||||
std::optional<std::uint32_t> mutableFlags,
|
||||
std::optional<std::uint32_t> immutableFlags,
|
||||
std::optional<std::string_view> issuerEncryptionKey,
|
||||
std::optional<std::string_view> auditorEncryptionKey,
|
||||
std::optional<std::uint64_t> confidentialOutstandingAmount
|
||||
@@ -1523,8 +1525,8 @@ createMptIssuanceObject(
|
||||
}
|
||||
if (domainId.has_value())
|
||||
mptIssuance.setFieldH256(xrpl::sfDomainID, xrpl::uint256{*domainId});
|
||||
if (mutableFlags.has_value())
|
||||
mptIssuance.setFieldU32(xrpl::sfMutableFlags, *mutableFlags);
|
||||
if (immutableFlags.has_value())
|
||||
mptIssuance.setFieldU32(xrpl::sfImmutableFlags, *immutableFlags);
|
||||
if (issuerEncryptionKey.has_value()) {
|
||||
xrpl::Slice const slice(issuerEncryptionKey->data(), issuerEncryptionKey->size());
|
||||
mptIssuance.setFieldVL(xrpl::sfIssuerEncryptionKey, slice);
|
||||
@@ -1893,6 +1895,30 @@ createVault(
|
||||
return vault;
|
||||
}
|
||||
|
||||
xrpl::Blob
|
||||
createDelegateBlob(std::string_view owner, std::string_view delegate)
|
||||
{
|
||||
xrpl::STObject obj(xrpl::sfTransaction);
|
||||
obj.setFieldU16(xrpl::sfTransactionType, xrpl::ttPAYMENT);
|
||||
|
||||
if (auto const acc = xrpl::parseBase58<xrpl::AccountID>(std::string(owner))) {
|
||||
obj.setAccountID(xrpl::sfAccount, *acc);
|
||||
obj.setAccountID(xrpl::sfDestination, *acc);
|
||||
}
|
||||
if (auto const acc = xrpl::parseBase58<xrpl::AccountID>(std::string(delegate)))
|
||||
obj.setAccountID(xrpl::sfDelegate, *acc);
|
||||
|
||||
obj.setFieldAmount(xrpl::sfAmount, xrpl::STAmount(100));
|
||||
obj.setFieldAmount(xrpl::sfFee, xrpl::STAmount(10));
|
||||
obj.setFieldU32(xrpl::sfSequence, 1);
|
||||
obj.setFieldVL(xrpl::sfSigningPubKey, xrpl::Slice(nullptr, 0));
|
||||
|
||||
xrpl::STTx const tx(std::move(obj));
|
||||
xrpl::Serializer s;
|
||||
tx.add(s);
|
||||
return s.getData();
|
||||
}
|
||||
|
||||
xrpl::STObject
|
||||
createLoanBroker(
|
||||
std::string_view owner,
|
||||
|
||||
@@ -466,7 +466,7 @@ createMptIssuanceObject(
|
||||
std::optional<std::uint64_t> maxAmount = std::nullopt,
|
||||
std::optional<std::uint64_t> lockedAmount = std::nullopt,
|
||||
std::optional<std::string_view> domainId = std::nullopt,
|
||||
std::optional<std::uint32_t> mutableFlags = std::nullopt,
|
||||
std::optional<std::uint32_t> immutableFlags = std::nullopt,
|
||||
std::optional<std::string_view> issuerEncryptionKey = std::nullopt,
|
||||
std::optional<std::string_view> auditorEncryptionKey = std::nullopt,
|
||||
std::optional<std::uint64_t> confidentialOutstandingAmount = std::nullopt
|
||||
@@ -598,6 +598,9 @@ createVault(
|
||||
uint32_t previousTxSeq
|
||||
);
|
||||
|
||||
[[nodiscard]] xrpl::Blob
|
||||
createDelegateBlob(std::string_view owner, std::string_view delegate);
|
||||
|
||||
[[nodiscard]] xrpl::STObject
|
||||
createLoanBroker(
|
||||
std::string_view owner,
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
@@ -52,7 +53,9 @@ struct MockHttpConnectionImpl : web::ng::impl::UpgradableConnection {
|
||||
MOCK_METHOD(
|
||||
UpgradeReturnType,
|
||||
upgrade,
|
||||
(util::TagDecoratorFactory const& tagDecoratorFactory, boost::asio::yield_context yield),
|
||||
(util::TagDecoratorFactory const& tagDecoratorFactory,
|
||||
size_t maxSendingQueueSize,
|
||||
boost::asio::yield_context yield),
|
||||
(override)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -52,7 +52,11 @@ protected:
|
||||
{"database.cassandra.write_batch_size", ConfigValue{ConfigType::Integer}.defaultValue(20)},
|
||||
{"database.cassandra.connect_timeout",
|
||||
ConfigValue{ConfigType::Integer}.defaultValue(1).optional()},
|
||||
{"database.cassandra.request_timeout", ConfigValue{ConfigType::Integer}.optional()},
|
||||
{"database.cassandra.initial_request_retry_delay",
|
||||
ConfigValue{ConfigType::Double}.defaultValue(0.5)},
|
||||
{"database.cassandra.max_request_retry_delay",
|
||||
ConfigValue{ConfigType::Double}.defaultValue(5.0)},
|
||||
{"database.cassandra.request_timeout", ConfigValue{ConfigType::Double}.optional()},
|
||||
{"database.cassandra.username", ConfigValue{ConfigType::String}.optional()},
|
||||
{"database.cassandra.password", ConfigValue{ConfigType::String}.optional()},
|
||||
{"database.cassandra.certfile", ConfigValue{ConfigType::String}.optional()},
|
||||
|
||||
@@ -96,8 +96,12 @@ protected:
|
||||
{"database.cassandra.write_batch_size", ConfigValue{ConfigType::Integer}.defaultValue(20)},
|
||||
{"database.cassandra.connect_timeout",
|
||||
ConfigValue{ConfigType::Integer}.defaultValue(10).optional()},
|
||||
{"database.cassandra.initial_request_retry_delay",
|
||||
ConfigValue{ConfigType::Double}.defaultValue(0.5)},
|
||||
{"database.cassandra.max_request_retry_delay",
|
||||
ConfigValue{ConfigType::Double}.defaultValue(5.0)},
|
||||
{"database.cassandra.request_timeout",
|
||||
ConfigValue{ConfigType::Integer}.defaultValue(10).optional()},
|
||||
ConfigValue{ConfigType::Double}.defaultValue(10.0).optional()},
|
||||
{"database.cassandra.username", ConfigValue{ConfigType::String}.optional()},
|
||||
{"database.cassandra.password", ConfigValue{ConfigType::String}.optional()},
|
||||
{"database.cassandra.certfile", ConfigValue{ConfigType::String}.optional()},
|
||||
|
||||
@@ -117,8 +117,12 @@ protected:
|
||||
ConfigValue{ConfigType::Integer}.defaultValue(20).withConstraint(gValidateUint16)},
|
||||
{"database.cassandra.connect_timeout",
|
||||
ConfigValue{ConfigType::Integer}.optional().withConstraint(gValidateUint32)},
|
||||
{"database.cassandra.initial_request_retry_delay",
|
||||
ConfigValue{ConfigType::Double}.defaultValue(0.5)},
|
||||
{"database.cassandra.max_request_retry_delay",
|
||||
ConfigValue{ConfigType::Double}.defaultValue(5.0)},
|
||||
{"database.cassandra.request_timeout",
|
||||
ConfigValue{ConfigType::Integer}.optional().withConstraint(gValidateUint32)},
|
||||
ConfigValue{ConfigType::Double}.optional().withConstraint(gValidatePositiveDouble)},
|
||||
{"database.cassandra.username", ConfigValue{ConfigType::String}.optional()},
|
||||
{"database.cassandra.password", ConfigValue{ConfigType::String}.optional()},
|
||||
{"database.cassandra.certfile", ConfigValue{ConfigType::String}.optional()},
|
||||
|
||||
@@ -15,6 +15,7 @@ target_sources(
|
||||
data/LedgerCacheLoadingStateTests.cpp
|
||||
data/LedgerCacheSaverTests.cpp
|
||||
data/cassandra/AsyncExecutorTests.cpp
|
||||
data/cassandra/ErrorTests.cpp
|
||||
data/cassandra/ExecutionStrategyTests.cpp
|
||||
data/cassandra/LedgerHeaderCacheTests.cpp
|
||||
data/cassandra/RetryPolicyTests.cpp
|
||||
@@ -101,6 +102,7 @@ target_sources(
|
||||
rpc/common/SpecsTests.cpp
|
||||
rpc/common/TypesTests.cpp
|
||||
rpc/common/impl/HandlerProviderTests.cpp
|
||||
rpc/filters/impl/DelegateTransactionsFilterTests.cpp
|
||||
rpc/handlers/AccountChannelsTests.cpp
|
||||
rpc/handlers/AccountCurrenciesTests.cpp
|
||||
rpc/handlers/AccountInfoTests.cpp
|
||||
@@ -127,6 +129,7 @@ target_sources(
|
||||
rpc/handlers/LedgerRangeTests.cpp
|
||||
rpc/handlers/LedgerTests.cpp
|
||||
rpc/handlers/MPTHoldersTests.cpp
|
||||
rpc/handlers/MPTokenIssuanceHistoryTests.cpp
|
||||
rpc/handlers/NFTBuyOffersTests.cpp
|
||||
rpc/handlers/NFTHistoryTests.cpp
|
||||
rpc/handlers/NFTInfoTests.cpp
|
||||
@@ -181,6 +184,7 @@ target_sources(
|
||||
util/ChannelTests.cpp
|
||||
util/CoroutineTest.cpp
|
||||
util/MoveTrackerTests.cpp
|
||||
util/MPTIssuanceUtilsTests.cpp
|
||||
util/ObservableValueTest.cpp
|
||||
util/ObservableValueAtomicTest.cpp
|
||||
util/RandomTests.cpp
|
||||
@@ -202,6 +206,7 @@ target_sources(
|
||||
web/dosguard/WeightsTests.cpp
|
||||
web/dosguard/WhitelistHandlerTests.cpp
|
||||
web/impl/ErrorHandlingTests.cpp
|
||||
web/LoadWarningTests.cpp
|
||||
web/ng/ResponseTests.cpp
|
||||
web/ng/RequestTests.cpp
|
||||
web/ng/RPCServerHandlerTests.cpp
|
||||
@@ -210,6 +215,7 @@ target_sources(
|
||||
web/ng/impl/ConnectionHandlerTests.cpp
|
||||
web/ng/impl/ErrorHandlingTests.cpp
|
||||
web/ng/impl/HttpConnectionTests.cpp
|
||||
web/ng/impl/SendingQueueTests.cpp
|
||||
web/ng/impl/ServerSslContextTests.cpp
|
||||
web/ng/impl/WsConnectionTests.cpp
|
||||
web/ProxyIpResolverTests.cpp
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#include "data/BackendInterface.hpp"
|
||||
#include "etl/CorruptionDetector.hpp"
|
||||
#include "etl/SystemState.hpp"
|
||||
#include "util/AsioContextTestFixture.hpp"
|
||||
#include "util/MockBackendTestFixture.hpp"
|
||||
#include "util/MockPrometheus.hpp"
|
||||
#include "util/Retry.hpp"
|
||||
#include "util/TestObject.hpp"
|
||||
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <xrpl/basics/Blob.h>
|
||||
@@ -12,7 +15,12 @@
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <exception>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
using namespace data;
|
||||
@@ -160,3 +168,156 @@ TEST_F(
|
||||
});
|
||||
EXPECT_FALSE(backend_->cache().isDisabled());
|
||||
}
|
||||
|
||||
// Loader and Extractor catch std::runtime_error to decide the server must amendment-block, so
|
||||
// DatabaseError has to stay outside that hierarchy.
|
||||
TEST(BackendInterfaceRetryTest, DatabaseErrorIsNotARuntimeError)
|
||||
{
|
||||
static_assert(std::is_base_of_v<std::exception, DatabaseError>);
|
||||
static_assert(not std::is_base_of_v<std::runtime_error, DatabaseError>);
|
||||
|
||||
try {
|
||||
throw DatabaseError{"transient"};
|
||||
} catch (std::runtime_error const&) {
|
||||
FAIL() << "DatabaseError must not be caught as std::runtime_error - doing so would "
|
||||
"amendment-block the server on a transient database error";
|
||||
} catch (std::exception const& e) {
|
||||
EXPECT_STREQ(e.what(), "transient");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(BackendInterfaceRetryTest, DatabaseErrorKeepsDefaultMessage)
|
||||
{
|
||||
EXPECT_STREQ(DatabaseError{}.what(), "Transient database error. Please retry the request");
|
||||
}
|
||||
|
||||
TEST(BackendInterfaceRetryTest, RetryOnTimeoutBlockingRetriesUntilSuccess)
|
||||
{
|
||||
std::size_t calls = 0;
|
||||
|
||||
auto const result = retryOnTimeout(
|
||||
[&calls]() -> int {
|
||||
if (++calls < 3)
|
||||
throw DatabaseError{};
|
||||
return 42;
|
||||
},
|
||||
util::Retry::Delays{
|
||||
.initial = std::chrono::milliseconds{1}, .max = std::chrono::milliseconds{2}
|
||||
}
|
||||
);
|
||||
|
||||
EXPECT_EQ(result, 42);
|
||||
EXPECT_EQ(calls, 3);
|
||||
}
|
||||
|
||||
TEST(BackendInterfaceRetryTest, RetryOnTimeoutBlockingDoesNotSwallowOtherExceptions)
|
||||
{
|
||||
EXPECT_THROW(
|
||||
retryOnTimeout(
|
||||
[]() -> int { throw std::runtime_error{"permanent"}; },
|
||||
util::Retry::Delays{
|
||||
.initial = std::chrono::milliseconds{1}, .max = std::chrono::milliseconds{1}
|
||||
}
|
||||
),
|
||||
std::runtime_error
|
||||
);
|
||||
}
|
||||
|
||||
struct BackendInterfaceRetryCoroTest : SyncAsioContextTest {};
|
||||
|
||||
TEST_F(BackendInterfaceRetryCoroTest, RetryOnTimeoutCoroRetriesUntilSuccess)
|
||||
{
|
||||
std::size_t calls = 0;
|
||||
|
||||
runSpawn([&calls](auto yield) {
|
||||
auto const result = retryOnTimeout(
|
||||
[&calls]() -> int {
|
||||
if (++calls < 3)
|
||||
throw DatabaseError{};
|
||||
return 42;
|
||||
},
|
||||
yield,
|
||||
util::Retry::Delays{
|
||||
.initial = std::chrono::milliseconds{1}, .max = std::chrono::milliseconds{2}
|
||||
}
|
||||
);
|
||||
|
||||
EXPECT_EQ(result, 42);
|
||||
});
|
||||
|
||||
EXPECT_EQ(calls, 3);
|
||||
}
|
||||
|
||||
TEST_F(BackendInterfaceRetryCoroTest, RetryOnTimeoutCoroDoesNotSwallowOtherExceptions)
|
||||
{
|
||||
runSpawn([](auto yield) {
|
||||
EXPECT_THROW(
|
||||
retryOnTimeout(
|
||||
[]() -> int { throw std::runtime_error{"permanent"}; },
|
||||
yield,
|
||||
util::Retry::Delays{
|
||||
.initial = std::chrono::milliseconds{1}, .max = std::chrono::milliseconds{1}
|
||||
}
|
||||
),
|
||||
std::runtime_error
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// An exception left in flight across a suspension point is visible to whatever else runs on that
|
||||
// thread meanwhile, and is corrupted when the handlers unwind out of order or on a different thread
|
||||
// of the pool. We prevent this.
|
||||
TEST_F(BackendInterfaceRetryCoroTest, RetryOnTimeoutCoroDoesNotWaitInsideCatchHandler)
|
||||
{
|
||||
std::optional<bool> exceptionInFlightDuringWait;
|
||||
|
||||
runSpawn([&exceptionInFlightDuringWait, this](auto yield) {
|
||||
// Runs on this thread while the retry below is suspended on its timer.
|
||||
boost::asio::post(ctx_, [&exceptionInFlightDuringWait]() {
|
||||
exceptionInFlightDuringWait = std::current_exception() != nullptr;
|
||||
});
|
||||
|
||||
std::size_t calls = 0;
|
||||
retryOnTimeout(
|
||||
[&calls]() -> int {
|
||||
if (++calls < 2)
|
||||
throw DatabaseError{};
|
||||
return 0;
|
||||
},
|
||||
yield,
|
||||
util::Retry::Delays{
|
||||
.initial = std::chrono::milliseconds{1}, .max = std::chrono::milliseconds{1}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
ASSERT_TRUE(exceptionInFlightDuringWait.has_value())
|
||||
<< "the posted handler was expected to run while the retry was waiting";
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
EXPECT_FALSE(*exceptionInFlightDuringWait)
|
||||
<< "retry.wait() must not be reached from inside a catch handler";
|
||||
}
|
||||
|
||||
TEST_F(BackendInterfaceRetryCoroTest, RetryOnTimeoutCoroDoesNotBlockItsThread)
|
||||
{
|
||||
bool ran = false;
|
||||
|
||||
runSpawn([&ran, this](auto yield) {
|
||||
boost::asio::post(ctx_, [&ran]() { ran = true; });
|
||||
|
||||
std::size_t calls = 0;
|
||||
retryOnTimeout(
|
||||
[&calls]() -> int {
|
||||
if (++calls < 2)
|
||||
throw DatabaseError{};
|
||||
return 0;
|
||||
},
|
||||
yield,
|
||||
util::Retry::Delays{
|
||||
.initial = std::chrono::milliseconds{1}, .max = std::chrono::milliseconds{1}
|
||||
}
|
||||
);
|
||||
|
||||
EXPECT_TRUE(ran);
|
||||
});
|
||||
}
|
||||
|
||||
43
tests/unit/data/cassandra/ErrorTests.cpp
Normal file
43
tests/unit/data/cassandra/ErrorTests.cpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#include "data/cassandra/Error.hpp"
|
||||
|
||||
#include <cassandra.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
using namespace data::cassandra;
|
||||
|
||||
namespace {
|
||||
|
||||
CassandraError
|
||||
makeError(uint32_t const code)
|
||||
{
|
||||
return CassandraError{"some error", code};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// isInvalidQuery is load bearing: DefaultExecutionStrategy::throwErrorIfNeeded treats it as the
|
||||
// only permanent failure and retries everything else, so anything wrongly reported here would be
|
||||
// retried forever (if false) or surfaced as a fatal std::runtime_error (if true).
|
||||
TEST(BackendCassandraErrorTest, IsInvalidQueryOnlyForInvalidQuery)
|
||||
{
|
||||
EXPECT_TRUE(makeError(CASS_ERROR_SERVER_INVALID_QUERY).isInvalidQuery());
|
||||
|
||||
EXPECT_FALSE(makeError(CASS_ERROR_SERVER_READ_FAILURE).isInvalidQuery());
|
||||
EXPECT_FALSE(makeError(CASS_ERROR_SERVER_WRITE_FAILURE).isInvalidQuery());
|
||||
EXPECT_FALSE(makeError(CASS_ERROR_SERVER_READ_TIMEOUT).isInvalidQuery());
|
||||
EXPECT_FALSE(makeError(CASS_ERROR_SERVER_UNAVAILABLE).isInvalidQuery());
|
||||
EXPECT_FALSE(makeError(CASS_ERROR_SERVER_SYNTAX_ERROR).isInvalidQuery());
|
||||
EXPECT_FALSE(makeError(CASS_OK).isInvalidQuery());
|
||||
}
|
||||
|
||||
TEST(BackendCassandraErrorTest, MessageAndCodeArePreserved)
|
||||
{
|
||||
// throwErrorIfNeeded puts message() into the DatabaseError it throws, so that treating an
|
||||
// unclassified error as a timeout still reports what actually failed
|
||||
auto const err = CassandraError{"received 1 responses and 1 failures", 0x1300};
|
||||
|
||||
EXPECT_EQ(err.message(), "received 1 responses and 1 failures");
|
||||
EXPECT_EQ(err.code(), 0x1300u);
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
@@ -154,7 +155,45 @@ TEST_F(BackendCassandraExecutionStrategyTest, ReadOneInCoroutineThrowsOnTimeoutF
|
||||
|
||||
runSpawn([&strat](boost::asio::yield_context yield) {
|
||||
auto statement = FakeStatement{};
|
||||
EXPECT_THROW(strat.read(yield, statement), data::DatabaseTimeout);
|
||||
EXPECT_THROW(strat.read(yield, statement), data::DatabaseError);
|
||||
});
|
||||
}
|
||||
|
||||
// A CL=QUORUM read failure is CASS_ERROR_SERVER_READ_FAILURE, which used to not throw at all,
|
||||
// leaving read() spinning; Times(1) pins that down.
|
||||
TEST_F(BackendCassandraExecutionStrategyTest, ReadOneInCoroutineThrowsOnQuorumReadFailure)
|
||||
{
|
||||
auto strat = makeStrategy();
|
||||
|
||||
ON_CALL(
|
||||
handle_,
|
||||
asyncExecute(A<FakeStatement const&>(), A<std::function<void(FakeResultOrError)>&&>())
|
||||
)
|
||||
.WillByDefault([](auto const&, auto&& cb) {
|
||||
auto res = FakeResultOrError{CassandraError{
|
||||
"received 1 responses and 1 failures", CASS_ERROR_SERVER_READ_FAILURE
|
||||
}};
|
||||
cb(res); // notify that item is ready
|
||||
return FakeFutureWithCallback{res};
|
||||
});
|
||||
EXPECT_CALL(
|
||||
handle_,
|
||||
asyncExecute(A<FakeStatement const&>(), A<std::function<void(FakeResultOrError)>&&>())
|
||||
)
|
||||
.Times(1);
|
||||
EXPECT_CALL(*counters_, registerReadStartedImpl(1));
|
||||
EXPECT_CALL(*counters_, registerReadErrorImpl(1));
|
||||
|
||||
runSpawn([&strat](boost::asio::yield_context yield) {
|
||||
auto statement = FakeStatement{};
|
||||
try {
|
||||
strat.read(yield, statement);
|
||||
FAIL() << "expected DatabaseError";
|
||||
} catch (data::DatabaseError const& e) {
|
||||
EXPECT_THAT(
|
||||
std::string{e.what()}, testing::HasSubstr("received 1 responses and 1 failures")
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -246,7 +285,7 @@ TEST_F(BackendCassandraExecutionStrategyTest, ReadBatchInCoroutineThrowsOnTimeou
|
||||
|
||||
runSpawn([&strat](boost::asio::yield_context yield) {
|
||||
auto statements = std::vector<FakeStatement>(kNumStatements);
|
||||
EXPECT_THROW(strat.read(yield, statements), data::DatabaseTimeout);
|
||||
EXPECT_THROW(strat.read(yield, statements), data::DatabaseError);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -384,7 +423,7 @@ TEST_F(BackendCassandraExecutionStrategyTest, ReadEachInCoroutineThrowsOnFailure
|
||||
|
||||
runSpawn([&strat](boost::asio::yield_context yield) {
|
||||
auto statements = std::vector<FakeStatement>(kNumStatements);
|
||||
EXPECT_THROW(strat.readEach(yield, statements), data::DatabaseTimeout);
|
||||
EXPECT_THROW(strat.readEach(yield, statements), data::DatabaseError);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,11 @@ getParseSettingsConfig(boost::json::value val)
|
||||
{"database.cassandra.write_batch_size", ConfigValue{ConfigType::Integer}.defaultValue(20)},
|
||||
{"database.cassandra.connect_timeout", ConfigValue{ConfigType::Integer}.optional()},
|
||||
{"database.cassandra.certfile", ConfigValue{ConfigType::String}.optional()},
|
||||
{"database.cassandra.request_timeout", ConfigValue{ConfigType::Integer}.defaultValue(0)},
|
||||
{"database.cassandra.initial_request_retry_delay",
|
||||
ConfigValue{ConfigType::Double}.defaultValue(0.5)},
|
||||
{"database.cassandra.max_request_retry_delay",
|
||||
ConfigValue{ConfigType::Double}.defaultValue(5.0)},
|
||||
{"database.cassandra.request_timeout", ConfigValue{ConfigType::Double}.optional()},
|
||||
{"database.cassandra.secure_connect_bundle", ConfigValue{ConfigType::String}.optional()},
|
||||
{"database.cassandra.username", ConfigValue{ConfigType::String}.optional()},
|
||||
{"database.cassandra.password", ConfigValue{ConfigType::String}.optional()},
|
||||
@@ -163,3 +167,72 @@ TEST_F(SettingsProviderTest, CertificateConfig)
|
||||
auto const settings = provider.getSettings();
|
||||
EXPECT_EQ(settings.certificate, "certificateData");
|
||||
}
|
||||
|
||||
TEST_F(SettingsProviderTest, RequestTimeoutAcceptsFractionalSeconds)
|
||||
{
|
||||
auto const cfg = getParseSettingsConfig(
|
||||
boost::json::parse(
|
||||
R"JSON({"database.cassandra.contact_points": "127.0.0.1",
|
||||
"database.cassandra.request_timeout": 2.5})JSON"
|
||||
)
|
||||
);
|
||||
SettingsProvider const provider{cfg.getObject("database.cassandra")};
|
||||
|
||||
EXPECT_EQ(provider.getSettings().requestTimeout, std::chrono::milliseconds{2500});
|
||||
}
|
||||
|
||||
TEST_F(SettingsProviderTest, RequestTimeoutStillAcceptsWholeSeconds)
|
||||
{
|
||||
// a JSON integer must still be accepted
|
||||
auto const cfg = getParseSettingsConfig(
|
||||
boost::json::parse(
|
||||
R"JSON({"database.cassandra.contact_points": "127.0.0.1",
|
||||
"database.cassandra.request_timeout": 3})JSON"
|
||||
)
|
||||
);
|
||||
SettingsProvider const provider{cfg.getObject("database.cassandra")};
|
||||
|
||||
EXPECT_EQ(provider.getSettings().requestTimeout, std::chrono::milliseconds{3000});
|
||||
}
|
||||
|
||||
TEST_F(SettingsProviderTest, RetryDelaysDefaults)
|
||||
{
|
||||
auto const cfg = getParseSettingsConfig(
|
||||
boost::json::parse(R"JSON({"database.cassandra.contact_points": "127.0.0.1"})JSON")
|
||||
);
|
||||
SettingsProvider const provider{cfg.getObject("database.cassandra")};
|
||||
|
||||
EXPECT_EQ(provider.getInitialRetryDelay(), std::chrono::milliseconds{500});
|
||||
EXPECT_EQ(provider.getMaxRetryDelay(), std::chrono::milliseconds{5000});
|
||||
}
|
||||
|
||||
TEST_F(SettingsProviderTest, RetryDelaysAreIndependentOfRequestTimeout)
|
||||
{
|
||||
auto const cfg = getParseSettingsConfig(
|
||||
boost::json::parse(
|
||||
R"JSON({"database.cassandra.contact_points": "127.0.0.1",
|
||||
"database.cassandra.request_timeout": 2.5,
|
||||
"database.cassandra.initial_request_retry_delay": 0.25,
|
||||
"database.cassandra.max_request_retry_delay": 1.5})JSON"
|
||||
)
|
||||
);
|
||||
SettingsProvider const provider{cfg.getObject("database.cassandra")};
|
||||
|
||||
EXPECT_EQ(provider.getSettings().requestTimeout, std::chrono::milliseconds{2500});
|
||||
EXPECT_EQ(provider.getInitialRetryDelay(), std::chrono::milliseconds{250});
|
||||
EXPECT_EQ(provider.getMaxRetryDelay(), std::chrono::milliseconds{1500});
|
||||
}
|
||||
|
||||
TEST_F(SettingsProviderTest, EqualRetryDelaysDisableBackoff)
|
||||
{
|
||||
auto const cfg = getParseSettingsConfig(
|
||||
boost::json::parse(
|
||||
R"JSON({"database.cassandra.contact_points": "127.0.0.1",
|
||||
"database.cassandra.initial_request_retry_delay": 0.5,
|
||||
"database.cassandra.max_request_retry_delay": 0.5})JSON"
|
||||
)
|
||||
);
|
||||
SettingsProvider const provider{cfg.getObject("database.cassandra")};
|
||||
|
||||
EXPECT_EQ(provider.getInitialRetryDelay(), provider.getMaxRetryDelay());
|
||||
}
|
||||
|
||||
@@ -14,6 +14,25 @@ struct FeedTrackableSignalTests : Test {
|
||||
web::SubscriptionContextPtr sessionPtr = std::make_shared<MockSession>();
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
struct DestructorDisconnectingSubscriber;
|
||||
using ReentrantSignalMap =
|
||||
feed::impl::TrackableSignalMap<std::string, DestructorDisconnectingSubscriber, int>;
|
||||
|
||||
struct DestructorDisconnectingSubscriber {
|
||||
ReentrantSignalMap* signalMap = nullptr;
|
||||
bool* disconnectFinished = nullptr;
|
||||
|
||||
~DestructorDisconnectingSubscriber()
|
||||
{
|
||||
signalMap->disconnect(this, "key");
|
||||
*disconnectFinished = true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(FeedTrackableSignalTests, Connect)
|
||||
{
|
||||
feed::impl::TrackableSignal<web::SubscriptionContextInterface, std::string> signal;
|
||||
@@ -107,3 +126,30 @@ TEST_F(FeedTrackableSignalTests, MapAutoDisconnect)
|
||||
signalMap.emit("test1", "test1");
|
||||
EXPECT_TRUE(testString.empty());
|
||||
}
|
||||
|
||||
TEST_F(FeedTrackableSignalTests, MapDisconnectFromTrackableDestructorWhileEmitting)
|
||||
{
|
||||
ReentrantSignalMap signalMap;
|
||||
bool disconnectFinished = false;
|
||||
|
||||
auto subscriber = std::make_shared<DestructorDisconnectingSubscriber>();
|
||||
subscriber->signalMap = &signalMap;
|
||||
subscriber->disconnectFinished = &disconnectFinished;
|
||||
|
||||
bool slotCalled = false;
|
||||
ASSERT_TRUE(signalMap.connectTrackableSlot(subscriber, "key", [&](int) {
|
||||
slotCalled = true;
|
||||
// The session died while the slot is running; the signal now holds the last reference.
|
||||
subscriber.reset();
|
||||
}));
|
||||
|
||||
signalMap.emit("key", 42);
|
||||
|
||||
EXPECT_TRUE(slotCalled);
|
||||
EXPECT_TRUE(disconnectFinished);
|
||||
|
||||
// The entry was removed by the destructor's disconnect, so nothing is left to notify.
|
||||
slotCalled = false;
|
||||
signalMap.emit("key", 42);
|
||||
EXPECT_FALSE(slotCalled);
|
||||
}
|
||||
|
||||
@@ -714,6 +714,92 @@ TEST_F(RPCBaseTest, IssuerValidator)
|
||||
ASSERT_FALSE(err);
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, BookTakerValidator)
|
||||
{
|
||||
auto const spec = RpcSpec{{"taker_gets", CustomValidators::bookTakerValidator}};
|
||||
|
||||
// An asset specified by currency, optionally with an issuer, is valid.
|
||||
auto passingInput = boost::json::parse(R"JSON({ "taker_gets": { "currency": "XRP" }})JSON");
|
||||
EXPECT_TRUE(spec.process(passingInput));
|
||||
|
||||
passingInput = boost::json::parse(
|
||||
R"JSON({ "taker_gets": { "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" }})JSON"
|
||||
);
|
||||
EXPECT_TRUE(spec.process(passingInput));
|
||||
|
||||
// An asset specified by mpt_issuance_id alone is valid.
|
||||
passingInput = boost::json::parse(
|
||||
R"JSON({ "taker_gets": { "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405DBADD" }})JSON"
|
||||
);
|
||||
EXPECT_TRUE(spec.process(passingInput));
|
||||
|
||||
// Non-object values are deferred to the type validator; the field being absent is deferred to
|
||||
// the required validator. Both pass here.
|
||||
passingInput = boost::json::parse(R"JSON({ "taker_gets": "not_an_object" })JSON");
|
||||
EXPECT_TRUE(spec.process(passingInput));
|
||||
|
||||
passingInput = boost::json::parse(R"JSON({ })JSON");
|
||||
EXPECT_TRUE(spec.process(passingInput));
|
||||
|
||||
// Neither currency nor mpt_issuance_id -> invalidParams, reporting the currency field.
|
||||
auto failingInput = boost::json::parse(R"JSON({ "taker_gets": { }})JSON");
|
||||
auto err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams});
|
||||
EXPECT_EQ(err.error().message, "Missing field 'taker_gets.currency'.");
|
||||
|
||||
// currency and mpt_issuance_id are mutually exclusive.
|
||||
failingInput = boost::json::parse(
|
||||
R"JSON({ "taker_gets": { "currency": "USD", "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405DBADD" }})JSON"
|
||||
);
|
||||
err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams});
|
||||
EXPECT_EQ(err.error().message, "Invalid field 'taker_gets'.");
|
||||
|
||||
// mpt_issuance_id must not be combined with an issuer.
|
||||
failingInput = boost::json::parse(
|
||||
R"JSON({ "taker_gets": { "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405DBADD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" }})JSON"
|
||||
);
|
||||
err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams});
|
||||
EXPECT_EQ(err.error().message, "Invalid field 'taker_gets'.");
|
||||
|
||||
// A present-but-non-string currency -> expectedFieldError against '<key>.currency'.
|
||||
failingInput = boost::json::parse(R"JSON({ "taker_gets": { "currency": 123 }})JSON");
|
||||
err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams});
|
||||
EXPECT_EQ(err.error().message, "Invalid field 'taker_gets.currency', not string.");
|
||||
|
||||
// A present-but-non-string mpt_issuance_id is reported the same way (against '.currency',
|
||||
// matching xrpld's validateTakerJSON).
|
||||
failingInput = boost::json::parse(R"JSON({ "taker_gets": { "mpt_issuance_id": 123 }})JSON");
|
||||
err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams});
|
||||
EXPECT_EQ(err.error().message, "Invalid field 'taker_gets.currency', not string.");
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, BookTakerValidatorUsesFieldKey)
|
||||
{
|
||||
// The error messages must reflect the validated field's key (taker_pays vs taker_gets).
|
||||
auto const spec = RpcSpec{{"taker_pays", CustomValidators::bookTakerValidator}};
|
||||
|
||||
auto failingInput = boost::json::parse(R"JSON({ "taker_pays": { }})JSON");
|
||||
auto err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
EXPECT_EQ(err.error().message, "Missing field 'taker_pays.currency'.");
|
||||
|
||||
failingInput = boost::json::parse(
|
||||
R"JSON({ "taker_pays": { "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405DBADD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" }})JSON"
|
||||
);
|
||||
err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
EXPECT_EQ(err.error().message, "Invalid field 'taker_pays'.");
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, SubscribeStreamValidator)
|
||||
{
|
||||
auto const spec = RpcSpec{{"streams", CustomValidators::subscribeStreamValidator}};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user