mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-14 02:30:20 +00:00
Compare commits
21 Commits
mvadari/le
...
ximinez/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
affea3d371 | ||
|
|
cd06ee221d | ||
|
|
752dab8b30 | ||
|
|
86583bc34e | ||
|
|
78f241d23c | ||
|
|
85c201a264 | ||
|
|
8938d26da5 | ||
|
|
098ae7e70c | ||
|
|
956ed0b1a8 | ||
|
|
5d2bc88a2e | ||
|
|
9c03931190 | ||
|
|
4f0738fff3 | ||
|
|
1a3d460046 | ||
|
|
c2e54d12e9 | ||
|
|
0ded97ba5b | ||
|
|
0e8714af73 | ||
|
|
d62ad9a8e7 | ||
|
|
d7e7baa675 | ||
|
|
054284701e | ||
|
|
eb4681da51 | ||
|
|
9b3dd7002d |
@@ -65,6 +65,7 @@ words:
|
||||
- Btrfs
|
||||
- Buildx
|
||||
- canonicality
|
||||
- canonicalised
|
||||
- changespq
|
||||
- checkme
|
||||
- choco
|
||||
|
||||
@@ -32,6 +32,11 @@ repos:
|
||||
# as standalone translation units, so they have no compile_commands.json
|
||||
# entry to lint (verify_headers checks them transitively).
|
||||
exclude: '^include/xrpl/protocol_autogen|\.ipp$'
|
||||
# run-clang-tidy --fix may edit headers included by files it is not run on,
|
||||
# so pre-commit must not split the files across parallel hook invocations.
|
||||
# The script determines the staged files itself and lets run-clang-tidy
|
||||
# handle parallelism internally.
|
||||
pass_filenames: false
|
||||
- id: fix-include-style
|
||||
name: fix include style
|
||||
entry: ./bin/pre-commit/fix_include_style.py
|
||||
|
||||
@@ -28,6 +28,9 @@ This section contains changes targeting a future version.
|
||||
|
||||
### Additions
|
||||
|
||||
- `account_tx`: Added an optional `delegate` request object to filter delegated transactions. The object requires `delegate_filter`, which must be either `actor` for transactions owned by the requested account but signed by another account, or `authorizer` for transactions signed by the requested account on behalf of another account. The optional `counter_party` account narrows the results to a specific signer/delegate for `actor` or a specific owner/delegator for `authorizer`. Malformed `delegate`, `delegate_filter`, and `counter_party` values return standard invalid field errors, and invalid account IDs return `actMalformed`.
|
||||
When paginating delegate-filtered queries, a marker from a delegate-filtered query includes a `delegate` flag and is only valid for follow-up requests that also supply `delegate` (mixing marker conventions returns `invalidParams`). Because filtering is applied after the ledger scan, a page may contain fewer results than `limit` (possibly zero) while still returning a marker, so callers must continue until no marker is present.
|
||||
|
||||
- `ledger_entry`, `account_objects`: The `Delegate` ledger entry now includes an optional `DestinationNode` field, which stores the index into the authorized account's owner directory. This field is present on entries created after bidirectional directory tracking was introduced and may appear in RPC responses for those entries. ([#6681](https://github.com/XRPLF/rippled/pull/6681))
|
||||
|
||||
- `server_definitions`: Added the following new sections to the response ([#6321](https://github.com/XRPLF/rippled/pull/6321)):
|
||||
|
||||
@@ -1,27 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Pre-commit hook that runs clang-tidy on changed files using run-clang-tidy.
|
||||
"""Pre-commit hook that runs clang-tidy on staged files using run-clang-tidy.
|
||||
|
||||
The set of files is chosen by pre-commit (see .pre-commit-config.yaml), which
|
||||
filters to C/C++ sources and excludes `.ipp` fragments. Headers are linted
|
||||
directly: the `verify_headers` build option (ON by default) compiles every
|
||||
`.h`/`.hpp` on its own, so each header is the main file of its own
|
||||
compile_commands.json entry and run-clang-tidy can analyse it just like a
|
||||
`.cpp`.
|
||||
The script determines the staged files itself (see `pass_filenames: false` in
|
||||
.pre-commit-config.yaml) so run-clang-tidy is run once and handles parallelism
|
||||
internally: pre-commit would otherwise split the files across parallel hook
|
||||
invocations that race when fixes edit a shared header.
|
||||
|
||||
Fixes are collected with `-export-fixes` and applied by clang-apply-replacements
|
||||
in a separate step rather than with run-clang-tidy's `-fix`. The `add_module`
|
||||
build isolates each module's headers behind a per-module symlink directory
|
||||
(build/modules/<module>/...), so a header reachable from several translation
|
||||
units is referenced through different paths that all resolve to the same source
|
||||
file. clang-apply-replacements deduplicates identical replacements by their
|
||||
literal path, so those paths must be canonicalised to the real source path
|
||||
first; otherwise the same fix is applied once per path and corrupts the header.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
CLANG_TIDY_VERSION = 22
|
||||
|
||||
# Extensions run-clang-tidy can analyse: `.cpp` translation units and, thanks to
|
||||
# the `verify_headers` build option, `.h`/`.hpp` headers (each has its own
|
||||
# compile_commands.json entry). `.ipp` fragments have no entry and are skipped.
|
||||
TIDY_EXTENSIONS = {".cpp", ".h", ".hpp"}
|
||||
|
||||
def find_run_clang_tidy() -> str | None:
|
||||
for candidate in (f"run-clang-tidy-{CLANG_TIDY_VERSION}", "run-clang-tidy"):
|
||||
# A single-quoted `FilePath:` entry in an -export-fixes YAML file, allowing the
|
||||
# `- ` marker that precedes it inside a `Replacements:` sequence. clang-tidy
|
||||
# emits paths single-quoted and doubles any embedded quote per YAML rules.
|
||||
FILEPATH_RE = re.compile(r"^(\s*(?:-\s+)?FilePath:\s*)'((?:[^']|'')*)'\s*$")
|
||||
|
||||
|
||||
def find_tool(name: str) -> str | None:
|
||||
for candidate in (f"{name}-{CLANG_TIDY_VERSION}", name):
|
||||
if path := shutil.which(candidate):
|
||||
return path
|
||||
return None
|
||||
@@ -35,23 +54,43 @@ def find_build_dir(repo_root: Path) -> Path | None:
|
||||
return None
|
||||
|
||||
|
||||
def staged_files(repo_root: Path) -> list[Path]:
|
||||
"""Return absolute paths of staged, lint-able C/C++ files.
|
||||
|
||||
`--diff-filter=d` excludes deletions so we never lint a removed file.
|
||||
"""
|
||||
output = subprocess.check_output(
|
||||
["git", "diff", "--staged", "--name-only", "--diff-filter=d", "--"]
|
||||
+ [f"*{ext}" for ext in TIDY_EXTENSIONS],
|
||||
text=True,
|
||||
cwd=repo_root,
|
||||
)
|
||||
return [repo_root / rel for rel in output.splitlines() if rel]
|
||||
|
||||
|
||||
def canonicalize_fix_paths(fixes_dir: Path) -> None:
|
||||
"""Rewrite every `FilePath` in the exported fixes to its real source path.
|
||||
|
||||
A header included through a module's isolation symlink is recorded under that
|
||||
symlink's path; collapsing all paths to the same real file lets
|
||||
clang-apply-replacements recognise the per-translation-unit duplicates and
|
||||
apply each fix once.
|
||||
"""
|
||||
for yaml in fixes_dir.glob("*.yaml"):
|
||||
lines = []
|
||||
for line in yaml.read_text().splitlines():
|
||||
if m := FILEPATH_RE.match(line):
|
||||
path = m.group(2).replace("''", "'")
|
||||
real = os.path.realpath(path).replace("'", "''")
|
||||
line = f"{m.group(1)}'{real}'"
|
||||
lines.append(line)
|
||||
yaml.write_text("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
if not os.environ.get("TIDY"):
|
||||
return 0
|
||||
|
||||
files = sys.argv[1:]
|
||||
if not files:
|
||||
return 0
|
||||
|
||||
run_clang_tidy = find_run_clang_tidy()
|
||||
if not run_clang_tidy:
|
||||
print(
|
||||
f"clang-tidy check failed: TIDY is enabled but neither "
|
||||
f"'run-clang-tidy-{CLANG_TIDY_VERSION}' nor 'run-clang-tidy' was found in PATH.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
repo_root = Path(
|
||||
subprocess.check_output(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
@@ -59,6 +98,29 @@ def main():
|
||||
text=True,
|
||||
).strip()
|
||||
)
|
||||
|
||||
files = staged_files(repo_root)
|
||||
if not files:
|
||||
return 0
|
||||
|
||||
run_clang_tidy = find_tool("run-clang-tidy")
|
||||
clang_apply_replacements = find_tool("clang-apply-replacements")
|
||||
missing = [
|
||||
name
|
||||
for name, path in (
|
||||
("run-clang-tidy", run_clang_tidy),
|
||||
("clang-apply-replacements", clang_apply_replacements),
|
||||
)
|
||||
if not path
|
||||
]
|
||||
if missing:
|
||||
print(
|
||||
f"clang-tidy check failed: TIDY is enabled but {' and '.join(missing)} "
|
||||
f"was not found in PATH (tried the '-{CLANG_TIDY_VERSION}' suffix too).",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
build_dir = find_build_dir(repo_root)
|
||||
if not build_dir:
|
||||
print(
|
||||
@@ -68,11 +130,23 @@ def main():
|
||||
)
|
||||
return 1
|
||||
|
||||
result = subprocess.run(
|
||||
[run_clang_tidy, "-quiet", "-p", str(build_dir), "-fix", "-allow-no-checks"]
|
||||
+ files
|
||||
)
|
||||
return result.returncode
|
||||
with tempfile.TemporaryDirectory() as fixes_dir:
|
||||
result = subprocess.run(
|
||||
[
|
||||
run_clang_tidy,
|
||||
"-quiet",
|
||||
"-p",
|
||||
build_dir,
|
||||
"-export-fixes",
|
||||
fixes_dir,
|
||||
"-allow-no-checks",
|
||||
]
|
||||
+ files
|
||||
)
|
||||
canonicalize_fix_paths(Path(fixes_dir))
|
||||
applied = subprocess.run([clang_apply_replacements, fixes_dir])
|
||||
|
||||
return result.returncode or applied.returncode
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#include <xrpl/basics/IntrusivePointer.ipp>
|
||||
#include <xrpl/basics/Log.h> // IWYU pragma: keep
|
||||
#include <xrpl/basics/TaggedCache.h>
|
||||
#include <xrpl/basics/scope.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -601,8 +604,39 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
|
||||
std::vector<key_type> v;
|
||||
|
||||
{
|
||||
std::scoped_lock const lock(mutex_);
|
||||
v.reserve(cache_.size());
|
||||
// Keep track of how many iterations are needed. Exit the loop if the number of retries gets
|
||||
// absurd. (Note that if this somehow ever happens, one more allocation will be done under
|
||||
// lock, which is undesirable, but really should be almost impossible.)
|
||||
std::size_t allocationIterations = 0;
|
||||
std::unique_lock lock(mutex_);
|
||||
for (auto size = cache_.size(); v.capacity() < size && allocationIterations < 20;
|
||||
size = cache_.size())
|
||||
{
|
||||
ScopeUnlock const unlock(lock);
|
||||
// Allocate the current size plus a little extra, in case the cache grows while
|
||||
// allocating. Each time another allocation is needed, the extra also gets bigger until
|
||||
// it ultimately doubles the size + 1.
|
||||
size += (size >> (4 - std::min(allocationIterations, std::size_t{4}))) + 1;
|
||||
v.reserve(size);
|
||||
++allocationIterations;
|
||||
}
|
||||
// In a normal operating environment, because of the padding added to size before
|
||||
// allocating, even 2 iterations is going to be very rare. If 3 or more are ever needed,
|
||||
// that's unusual enough that I want to know about it. Don't ask me to change it without
|
||||
// empirical data. - Ed H.
|
||||
XRPL_ASSERT(
|
||||
allocationIterations < 3,
|
||||
"xrpl::TaggedCache::getKeys(): limited allocation iterations");
|
||||
if (v.capacity() < cache_.size())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::TaggedCache::getKeys(): failed to allocate sufficient capacity");
|
||||
v.reserve(cache_.size());
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
XRPL_ASSERT(lock.owns_lock(), "xrpl::TaggedCache::getKeys(): owns lock");
|
||||
XRPL_ASSERT(
|
||||
v.capacity() >= cache_.size(), "xrpl::TaggedCache::getKeys(): sufficient capacity");
|
||||
for (auto const& _ : cache_)
|
||||
v.push_back(_.first);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/STXChainBridge.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
@@ -423,21 +422,8 @@ struct KeyletDesc
|
||||
bool includeInTests{};
|
||||
};
|
||||
|
||||
// This list should include all of the keylet functions that take a single
|
||||
// AccountID parameter.
|
||||
std::array<KeyletDesc<AccountID const&>, 6> const kDirectAccountKeylets{
|
||||
{{.function = &keylet::account, .expectedLEName = jss::AccountRoot, .includeInTests = false},
|
||||
{.function = &keylet::ownerDir, .expectedLEName = jss::DirectoryNode, .includeInTests = true},
|
||||
{.function = &keylet::signerList, .expectedLEName = jss::SignerList, .includeInTests = true},
|
||||
// It's normally impossible to create an item at nftpage_min, but
|
||||
// test it anyway, since the invariant checks for it.
|
||||
{.function = &keylet::nftokenPageMin,
|
||||
.expectedLEName = jss::NFTokenPage,
|
||||
.includeInTests = true},
|
||||
{.function = &keylet::nftokenPageMax,
|
||||
.expectedLEName = jss::NFTokenPage,
|
||||
.includeInTests = true},
|
||||
{.function = &keylet::did, .expectedLEName = jss::DID, .includeInTests = true}}};
|
||||
// This list should include all of the keylet functions that take a single AccountID parameter.
|
||||
extern std::array<KeyletDesc<AccountID const&>, 6> const kDirectAccountKeylets;
|
||||
|
||||
MPTID
|
||||
makeMptID(std::uint32_t sequence, AccountID const& account);
|
||||
|
||||
@@ -110,6 +110,7 @@ JSS(accounts); // in: LedgerEntry, Subscribe, handlers/Ledger
|
||||
JSS(accounts_proposed); // in: Subscribe, Unsubscribe
|
||||
JSS(action); //
|
||||
JSS(active); // out: OverlayImpl
|
||||
JSS(actor); // in/out: AccountTx
|
||||
JSS(acquiring); // out: LedgerRequest
|
||||
JSS(address); // out: PeerImp
|
||||
JSS(affected); // out: AcceptedLedgerTx
|
||||
@@ -133,6 +134,7 @@ JSS(attestation_reward_account); //
|
||||
JSS(auction_slot); // out: amm_info
|
||||
JSS(authorized); // out: AccountLines
|
||||
JSS(authorize); // out: delegate
|
||||
JSS(authorizer); // in/out: AccountTx
|
||||
JSS(authorized_credentials); // in: ledger_entry DepositPreauth
|
||||
JSS(auth_accounts); // out: amm_info
|
||||
JSS(auth_change); // out: AccountInfo
|
||||
@@ -191,6 +193,7 @@ JSS(converge_time); // out: NetworkOPs
|
||||
JSS(converge_time_s); // out: NetworkOPs
|
||||
JSS(cookie); // out: NetworkOPs
|
||||
JSS(count); // in: AccountTx*, ValidatorList
|
||||
JSS(counter_party); // in/out: AccountTx
|
||||
JSS(counters); // in/out: retrieve counters
|
||||
JSS(credentials); // in: deposit_authorized
|
||||
JSS(credential_type); // in: LedgerEntry DepositPreauth
|
||||
@@ -270,6 +273,7 @@ JSS(freeze); // out: AccountLines
|
||||
JSS(freeze_peer); // out: AccountLines
|
||||
JSS(deep_freeze); // out: AccountLines
|
||||
JSS(deep_freeze_peer); // out: AccountLines
|
||||
JSS(delegate_filter); // in/out: AccountTx
|
||||
JSS(frozen_balances); // out: GatewayBalances
|
||||
JSS(full); // in: LedgerClearer, handlers/Ledger
|
||||
JSS(full_reply); // out: PathFind
|
||||
|
||||
@@ -46,6 +46,22 @@ struct LedgerRange
|
||||
uint32_t max;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Enumeration of possible delegate types that can occur during filtering in account_tx
|
||||
*/
|
||||
enum class DelegateType {
|
||||
Actor, ///< Another account signed and submitted transactions on behalf of this account (this
|
||||
///< account is the owner/delegator).
|
||||
Authorizer ///< This account signed and submitted transactions on behalf of another account
|
||||
///< (this account is the signer/delegatee).
|
||||
};
|
||||
|
||||
struct DelegateFilter
|
||||
{
|
||||
DelegateType type = DelegateType::Actor;
|
||||
std::optional<AccountID> counterparty;
|
||||
};
|
||||
|
||||
class RelationalDatabase
|
||||
{
|
||||
public:
|
||||
@@ -82,6 +98,7 @@ public:
|
||||
std::optional<AccountTxMarker> marker;
|
||||
std::uint32_t limit = 0;
|
||||
bool bAdmin = false;
|
||||
std::optional<DelegateFilter> delegate;
|
||||
};
|
||||
|
||||
using AccountTx = std::pair<std::shared_ptr<Transaction>, std::shared_ptr<TxMeta>>;
|
||||
@@ -101,6 +118,7 @@ public:
|
||||
bool forward = false;
|
||||
uint32_t limit = 0;
|
||||
std::optional<AccountTxMarker> marker;
|
||||
std::optional<DelegateFilter> delegate;
|
||||
};
|
||||
|
||||
struct AccountTxResult
|
||||
@@ -109,6 +127,7 @@ public:
|
||||
LedgerRange ledgerRange{};
|
||||
uint32_t limit = 0;
|
||||
std::optional<AccountTxMarker> marker;
|
||||
std::optional<DelegateFilter> delegate;
|
||||
};
|
||||
|
||||
virtual ~RelationalDatabase() = default;
|
||||
|
||||
@@ -29,7 +29,7 @@ src:test/beast/beast_PropertyStream_test.cpp
|
||||
src:src/test/app/Invariants_test.cpp
|
||||
|
||||
# ASan false positive: stack-use-after-scope in ErrorCodes.h inline functions.
|
||||
# When Clang inlines the StaticString overloads (e.g. invalid_field_error(StaticString)),
|
||||
# When Clang inlines the StaticString overloads (e.g. invalidFieldError(StaticString)),
|
||||
# ASan scope-poisons the temporary std::string before the inlined callee finishes reading
|
||||
# through the const ref. This corrupts the coroutine stack and crashes the Simulate test.
|
||||
# See asan.supp comments for full explanation and planned fix.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol/nftPageMask.h>
|
||||
|
||||
#include <boost/endian/conversion.hpp>
|
||||
@@ -32,6 +33,23 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// This list should include all of the keylet functions that take a single
|
||||
// AccountID parameter. Declared in Indexes.h; defined here so the header need
|
||||
// not include jss.h.
|
||||
std::array<KeyletDesc<AccountID const&>, 6> const kDirectAccountKeylets{
|
||||
{{.function = &keylet::account, .expectedLEName = jss::AccountRoot, .includeInTests = false},
|
||||
{.function = &keylet::ownerDir, .expectedLEName = jss::DirectoryNode, .includeInTests = true},
|
||||
{.function = &keylet::signerList, .expectedLEName = jss::SignerList, .includeInTests = true},
|
||||
// It's normally impossible to create an item at nftpage_min, but
|
||||
// test it anyway, since the invariant checks for it.
|
||||
{.function = &keylet::nftokenPageMin,
|
||||
.expectedLEName = jss::NFTokenPage,
|
||||
.includeInTests = true},
|
||||
{.function = &keylet::nftokenPageMax,
|
||||
.expectedLEName = jss::NFTokenPage,
|
||||
.includeInTests = true},
|
||||
{.function = &keylet::did, .expectedLEName = jss::DID, .includeInTests = true}}};
|
||||
|
||||
/**
|
||||
* Type-specific prefix for calculating ledger indices.
|
||||
*
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/balance.h> // IWYU pragma: keep
|
||||
#include <test/jtx/check.h>
|
||||
#include <test/jtx/delegate.h>
|
||||
#include <test/jtx/deposit.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <test/jtx/fee.h>
|
||||
@@ -28,6 +29,7 @@
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -45,6 +47,7 @@
|
||||
#include <initializer_list>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
@@ -889,6 +892,426 @@ class AccountTx_test : public beast::unit_test::Suite
|
||||
checkAliceAcctTx(9, jss::Payment);
|
||||
}
|
||||
|
||||
void
|
||||
testDelegation()
|
||||
{
|
||||
testcase("Delegation Filtering");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this);
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
env.fund(XRP(10000), alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
// Normal TX: Alice pays Carol (Signed by Alice's Master Key)
|
||||
env(pay(alice, carol, XRP(10)));
|
||||
env.close();
|
||||
|
||||
// Setup Delegation: Alice allows Bob to sign Payments for her
|
||||
env(delegate::set(alice, bob, {"Payment"}));
|
||||
env.close();
|
||||
|
||||
// Delegated TX: Alice pays Bob (Signed by Bob using Delegation)
|
||||
env(pay(alice, bob, XRP(20)), delegate::As(bob));
|
||||
env.close();
|
||||
|
||||
// Normal TX: Bob pays Carol (Signed by Bob for himself)
|
||||
env(pay(bob, carol, XRP(30)));
|
||||
env.close();
|
||||
|
||||
auto const countTxs = [&](AccountID const& account,
|
||||
json::Value const& delegateParams,
|
||||
std::optional<std::uint32_t> const limit = std::nullopt) -> int {
|
||||
int count = 0;
|
||||
json::Value marker;
|
||||
bool haveMarker = false;
|
||||
int pages = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
json::Value params;
|
||||
params[jss::account] = toBase58(account);
|
||||
params[jss::ledger_index_min] = -1;
|
||||
params[jss::ledger_index_max] = -1;
|
||||
|
||||
if (!delegateParams.isNull())
|
||||
params[jss::delegate] = delegateParams;
|
||||
if (limit)
|
||||
params[jss::limit] = *limit;
|
||||
if (haveMarker)
|
||||
params[jss::marker] = marker;
|
||||
|
||||
auto const res = env.rpc("json", "account_tx", to_string(params));
|
||||
auto const& result = res[jss::result];
|
||||
|
||||
if (result.isMember(jss::transactions))
|
||||
count += result[jss::transactions].size();
|
||||
|
||||
if (!limit || !result.isMember(jss::marker))
|
||||
break;
|
||||
|
||||
marker = result[jss::marker];
|
||||
haveMarker = true;
|
||||
++pages;
|
||||
if (!BEAST_EXPECT(pages < 20))
|
||||
break;
|
||||
}
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
auto const checkError = [&](json::Value const& delegateParams,
|
||||
std::string const& errToken) {
|
||||
json::Value params;
|
||||
params[jss::account] = alice.human();
|
||||
params[jss::delegate] = delegateParams;
|
||||
auto res = env.rpc("json", "account_tx", to_string(params));
|
||||
BEAST_EXPECT(res[jss::result][jss::error] == errToken);
|
||||
};
|
||||
|
||||
// Filter: Delegatee. Expects TX #2 (Signed by Bob)
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "actor";
|
||||
BEAST_EXPECT(countTxs(alice.id(), p) == 1);
|
||||
}
|
||||
|
||||
// Filter: Delegatee + Counterparty Bob. Expects TX #2.
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "actor";
|
||||
p[jss::counter_party] = bob.human();
|
||||
BEAST_EXPECT(countTxs(alice.id(), p) == 1);
|
||||
}
|
||||
|
||||
// Filter: Delegatee + Counterparty Carol. Expects 0.
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "actor";
|
||||
p[jss::counter_party] = carol.human();
|
||||
BEAST_EXPECT(countTxs(alice.id(), p) == 0);
|
||||
}
|
||||
|
||||
// Filter: Delegator. Expects TX #2.
|
||||
// (Bob signed it, but Alice is the owner).
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "authorizer";
|
||||
BEAST_EXPECT(countTxs(bob.id(), p) == 1);
|
||||
}
|
||||
|
||||
// Filter: Delegator + Counterparty Alice. Expects TX #2.
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "authorizer";
|
||||
p[jss::counter_party] = alice.human();
|
||||
BEAST_EXPECT(countTxs(bob.id(), p) == 1);
|
||||
}
|
||||
|
||||
// Filter: Authorizer. Expect: None.
|
||||
// TX #2 has sfDelegate present, but Alice is the delegator/owner, not
|
||||
// the delegate signer
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "authorizer";
|
||||
BEAST_EXPECT(countTxs(alice.id(), p) == 0);
|
||||
}
|
||||
|
||||
// Query Bob (Signer), Filter: Delegator, Counterparty: Carol
|
||||
// Expect: None (Alice is Owner, not Carol)
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "authorizer";
|
||||
p[jss::counter_party] = carol.human();
|
||||
BEAST_EXPECT(countTxs(bob.id(), p) == 0);
|
||||
}
|
||||
|
||||
// Query Bob (Signer), Filter: Delegatee
|
||||
// Expect: None. Bob did not employ a delegatee for his own TXs (TX C).
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "actor";
|
||||
BEAST_EXPECT(countTxs(bob.id(), p) == 0);
|
||||
}
|
||||
|
||||
// "delegate" is not an object (e.g., string)
|
||||
{
|
||||
json::Value const p = "not_an_object";
|
||||
checkError(p, "invalidParams");
|
||||
}
|
||||
|
||||
// Missing "delegate_filter" inside object
|
||||
{
|
||||
json::Value const p(json::ValueType::Object);
|
||||
checkError(p, "invalidParams");
|
||||
}
|
||||
|
||||
// "delegate_filter" is not a string (e.g., int)
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = 123;
|
||||
checkError(p, "invalidParams");
|
||||
}
|
||||
|
||||
// "delegate_filter" has invalid value
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "random_string";
|
||||
checkError(p, "invalidParams");
|
||||
}
|
||||
|
||||
// "counterparty" is not a string
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "actor";
|
||||
p[jss::counter_party] = 123;
|
||||
checkError(p, "invalidParams");
|
||||
}
|
||||
|
||||
// "counterparty" is malformed base58
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "actor";
|
||||
p[jss::counter_party] = "not_an_account";
|
||||
checkError(p, "actMalformed");
|
||||
}
|
||||
|
||||
// Multi-signed non-delegated TX: Alice pays Carol via multi-sig.
|
||||
// sfDelegate is absent and sfSigningPubKey is empty — the filter
|
||||
// must skip it without crashing.
|
||||
{
|
||||
Account const daria{"daria"};
|
||||
Account const edward{"edward"};
|
||||
env.fund(XRP(1000), daria, edward);
|
||||
env.close();
|
||||
env(signers(alice, 2, {{daria, 1}, {edward, 1}}));
|
||||
env.close();
|
||||
env(pay(alice, carol, XRP(1)),
|
||||
Fee(drops(env.current()->fees().increment * 2)),
|
||||
Msig(daria, edward));
|
||||
env.close();
|
||||
|
||||
// Alice's actor filter should still see only the 1 delegated tx,
|
||||
// not the multi-signed one.
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "actor";
|
||||
BEAST_EXPECT(countTxs(alice.id(), p) == 1);
|
||||
}
|
||||
|
||||
// Regular-key-signed non-delegated TX: Alice pays Bob, signed by Bob
|
||||
// as Alice's regular key. This must not be treated as delegation.
|
||||
{
|
||||
env(regkey(alice, bob));
|
||||
env.close();
|
||||
env(pay(alice, bob, XRP(1)));
|
||||
env.close();
|
||||
|
||||
json::Value actorFilter;
|
||||
actorFilter[jss::delegate_filter] = "actor";
|
||||
BEAST_EXPECT(countTxs(alice.id(), actorFilter) == 1);
|
||||
// limit: 1 forces pagination past newer non-delegated rows.
|
||||
BEAST_EXPECT(countTxs(alice.id(), actorFilter, 1) == 1);
|
||||
|
||||
actorFilter[jss::counter_party] = bob.human();
|
||||
BEAST_EXPECT(countTxs(alice.id(), actorFilter) == 1);
|
||||
BEAST_EXPECT(countTxs(alice.id(), actorFilter, 1) == 1);
|
||||
|
||||
json::Value authorizerFilter;
|
||||
authorizerFilter[jss::delegate_filter] = "authorizer";
|
||||
BEAST_EXPECT(countTxs(bob.id(), authorizerFilter) == 1);
|
||||
BEAST_EXPECT(countTxs(bob.id(), authorizerFilter, 1) == 1);
|
||||
|
||||
authorizerFilter[jss::counter_party] = alice.human();
|
||||
BEAST_EXPECT(countTxs(bob.id(), authorizerFilter) == 1);
|
||||
BEAST_EXPECT(countTxs(bob.id(), authorizerFilter, 1) == 1);
|
||||
}
|
||||
|
||||
// Pagination marker/delegate-filter consistency. A marker returned by a
|
||||
// delegate-filtered query carries a `delegate` flag and is only valid
|
||||
// for a follow-up request that repeats the filter; mixing the two
|
||||
// marker conventions must be rejected with invalidParams.
|
||||
{
|
||||
json::Value actorFilter;
|
||||
actorFilter[jss::delegate_filter] = "actor";
|
||||
|
||||
// Obtain a delegate-filtered marker (limit 1 forces pagination).
|
||||
json::Value dp;
|
||||
dp[jss::account] = alice.human();
|
||||
dp[jss::ledger_index_min] = -1;
|
||||
dp[jss::ledger_index_max] = -1;
|
||||
dp[jss::delegate] = actorFilter;
|
||||
dp[jss::limit] = 1;
|
||||
auto const dRes = env.rpc("json", "account_tx", to_string(dp));
|
||||
BEAST_EXPECT(dRes[jss::result].isMember(jss::marker));
|
||||
json::Value const delegateMarker = dRes[jss::result][jss::marker];
|
||||
BEAST_EXPECT(
|
||||
delegateMarker.isMember(jss::delegate) && delegateMarker[jss::delegate].asBool());
|
||||
|
||||
// Reusing a delegate marker without the delegate filter is rejected.
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::account] = alice.human();
|
||||
p[jss::ledger_index_min] = -1;
|
||||
p[jss::ledger_index_max] = -1;
|
||||
p[jss::limit] = 1;
|
||||
p[jss::marker] = delegateMarker;
|
||||
auto const r = env.rpc("json", "account_tx", to_string(p));
|
||||
BEAST_EXPECT(r[jss::result][jss::error] == "invalidParams");
|
||||
}
|
||||
|
||||
// Obtain a non-delegate marker; it must not carry the flag.
|
||||
json::Value np;
|
||||
np[jss::account] = alice.human();
|
||||
np[jss::ledger_index_min] = -1;
|
||||
np[jss::ledger_index_max] = -1;
|
||||
np[jss::limit] = 1;
|
||||
auto const nRes = env.rpc("json", "account_tx", to_string(np));
|
||||
BEAST_EXPECT(nRes[jss::result].isMember(jss::marker));
|
||||
json::Value const normalMarker = nRes[jss::result][jss::marker];
|
||||
BEAST_EXPECT(!normalMarker.isMember(jss::delegate));
|
||||
|
||||
// Reusing a non-delegate marker with a delegate filter is rejected.
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::account] = alice.human();
|
||||
p[jss::ledger_index_min] = -1;
|
||||
p[jss::ledger_index_max] = -1;
|
||||
p[jss::limit] = 1;
|
||||
p[jss::delegate] = actorFilter;
|
||||
p[jss::marker] = normalMarker;
|
||||
auto const r = env.rpc("json", "account_tx", to_string(p));
|
||||
BEAST_EXPECT(r[jss::result][jss::error] == "invalidParams");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDelegationMultiSign()
|
||||
{
|
||||
testcase("Delegation filter with multi-signed delegatee");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this);
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const carol{"carol"};
|
||||
Account const daria{"daria"};
|
||||
Account const edward{"edward"};
|
||||
|
||||
env.fund(XRP(10000), alice, bob, carol, daria, edward);
|
||||
env.close();
|
||||
|
||||
// Bob's identity is established via multi-sig (daria + edward)
|
||||
env(signers(bob, 2, {{daria, 1}, {edward, 1}}));
|
||||
env.close();
|
||||
|
||||
env(delegate::set(alice, bob, {"Payment"}));
|
||||
env.close();
|
||||
|
||||
// Delegated tx: Alice pays Carol, Bob signs via multi-sig
|
||||
env(pay(alice, carol, XRP(10)), Fee(XRP(1)), delegate::As(bob), Msig(daria, edward));
|
||||
env.close();
|
||||
|
||||
auto const countTxs = [&](AccountID const& account,
|
||||
json::Value const& delegateParams) -> int {
|
||||
json::Value params;
|
||||
params[jss::account] = toBase58(account);
|
||||
params[jss::ledger_index_min] = -1;
|
||||
params[jss::ledger_index_max] = -1;
|
||||
params[jss::delegate] = delegateParams;
|
||||
|
||||
auto const res = env.rpc("json", "account_tx", to_string(params));
|
||||
|
||||
if (res[jss::result].isMember(jss::transactions))
|
||||
return res[jss::result][jss::transactions].size();
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Alice (owner) finds the tx with actor filter
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "actor";
|
||||
BEAST_EXPECT(countTxs(alice.id(), p) == 1);
|
||||
}
|
||||
|
||||
// Alice (owner) + counterparty Bob finds the tx
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "actor";
|
||||
p[jss::counter_party] = bob.human();
|
||||
BEAST_EXPECT(countTxs(alice.id(), p) == 1);
|
||||
}
|
||||
|
||||
// Bob (delegatee) finds the tx with authorizer filter
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "authorizer";
|
||||
BEAST_EXPECT(countTxs(bob.id(), p) == 1);
|
||||
}
|
||||
|
||||
// Bob (delegatee) + counterparty Alice finds the tx
|
||||
{
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "authorizer";
|
||||
p[jss::counter_party] = alice.human();
|
||||
BEAST_EXPECT(countTxs(bob.id(), p) == 1);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDelegationMarkerWithinPage()
|
||||
{
|
||||
testcase("Delegation filter marker within a single query page");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this);
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
env.fund(XRP(10000), alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
env(delegate::set(alice, bob, {"Payment"}));
|
||||
env.close();
|
||||
|
||||
auto const startLedger = env.closed()->header().seq + 1;
|
||||
|
||||
env(pay(alice, carol, XRP(1)), delegate::As(bob));
|
||||
env.close();
|
||||
env(pay(alice, carol, XRP(1)), delegate::As(bob));
|
||||
env.close();
|
||||
|
||||
json::Value p;
|
||||
p[jss::delegate_filter] = "actor";
|
||||
|
||||
json::Value params;
|
||||
params[jss::account] = alice.human();
|
||||
params[jss::ledger_index_min] = startLedger;
|
||||
params[jss::ledger_index_max] = -1;
|
||||
params[jss::delegate] = p;
|
||||
params[jss::limit] = 1;
|
||||
|
||||
auto const res = env.rpc("json", "account_tx", to_string(params));
|
||||
auto const& result = res[jss::result];
|
||||
|
||||
// The first page emits only the first delegated payment. (as page limit is set to 1)
|
||||
BEAST_EXPECT(result[jss::transactions].size() == 1);
|
||||
BEAST_EXPECT(result.isMember(jss::marker));
|
||||
|
||||
// Following the marker resumes right after the first payment and
|
||||
// returns the second one.
|
||||
json::Value page2 = params;
|
||||
page2[jss::marker] = result[jss::marker];
|
||||
auto const res2 = env.rpc("json", "account_tx", to_string(page2));
|
||||
BEAST_EXPECT(res2[jss::result][jss::transactions].size() == 1);
|
||||
}
|
||||
|
||||
void
|
||||
testSponsorship()
|
||||
{
|
||||
@@ -973,6 +1396,9 @@ public:
|
||||
testContents();
|
||||
testAccountDelete();
|
||||
testMPT();
|
||||
testDelegation();
|
||||
testDelegationMultiSign();
|
||||
testDelegationMarkerWithinPage();
|
||||
testSponsorship();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3880,7 +3880,8 @@ NetworkOPsImp::addAccountHistoryJob(SubAccountHistoryInfoWeak subInfo)
|
||||
.ledgerRange = {.min = minLedger, .max = maxLedger},
|
||||
.marker = marker,
|
||||
.limit = 0,
|
||||
.bAdmin = true};
|
||||
.bAdmin = true,
|
||||
.delegate = std::nullopt};
|
||||
return db.newestAccountTxPage(options);
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <xrpl/basics/ByteUtilities.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/RangeSet.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
@@ -28,6 +29,7 @@
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/LedgerHeader.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/TxMeta.h>
|
||||
@@ -990,6 +992,57 @@ getNewestAccountTxsB(
|
||||
return getAccountTxsB(session, app, options, true, j);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Determines whether a transaction should be included in account_tx
|
||||
* results based on a delegation filter.
|
||||
* @param rawData Serialized transaction blob.
|
||||
* @param filter The delegate filter specifying the role of the queried account
|
||||
* (Actor or Authorizer) and an optional counterparty to match against.
|
||||
* @param contextAccount The account passed to account_tx (the queried account).
|
||||
* @return True if the transaction passes the filter and should be included,
|
||||
* false if it should be skipped.
|
||||
*/
|
||||
static bool
|
||||
passesDelegateFilter(
|
||||
Blob const& rawData,
|
||||
DelegateFilter const& filter,
|
||||
AccountID const& contextAccount)
|
||||
{
|
||||
SerialIter sit{makeSlice(rawData)};
|
||||
STTx const tx{sit};
|
||||
|
||||
AccountID const txOwner = tx.getAccountID(sfAccount);
|
||||
|
||||
if (!tx.isFieldPresent(sfDelegate))
|
||||
return false;
|
||||
|
||||
AccountID const txSigner = tx.getAccountID(sfDelegate);
|
||||
|
||||
switch (filter.type)
|
||||
{
|
||||
case DelegateType::Actor: {
|
||||
// Keep txns where the queried account (A) is the owner but
|
||||
// another account (C) was the delegatee that signed.
|
||||
bool const isDelegated = (txOwner == contextAccount) && (txSigner != contextAccount);
|
||||
if (!isDelegated)
|
||||
return false;
|
||||
return !filter.counterparty || (txSigner == *filter.counterparty);
|
||||
}
|
||||
|
||||
case DelegateType::Authorizer: {
|
||||
// Keep txns where the queried account (C) is the signer acting
|
||||
// on behalf of another account (A, the delegator/owner).
|
||||
bool const isActingAsDelegate =
|
||||
(txSigner == contextAccount) && (txOwner != contextAccount);
|
||||
if (!isActingAsDelegate)
|
||||
return false;
|
||||
return !filter.counterparty || (txOwner == *filter.counterparty);
|
||||
}
|
||||
}
|
||||
|
||||
return false; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief accountTxPage Searches for the oldest or newest transactions for the
|
||||
* account that matches the given criteria starting from the provided
|
||||
@@ -1020,6 +1073,7 @@ accountTxPage(
|
||||
{
|
||||
int total = 0;
|
||||
|
||||
bool const hasDelegateFilter = options.delegate.has_value();
|
||||
bool lookingForMarker = options.marker.has_value();
|
||||
|
||||
std::uint32_t numberOfResults = 0;
|
||||
@@ -1107,6 +1161,11 @@ accountTxPage(
|
||||
{
|
||||
Blob rawData;
|
||||
Blob rawMeta;
|
||||
// Delegate filtering happens after SQL, so skipped rows need their own
|
||||
// continuation marker accounting.
|
||||
std::uint32_t fetchedRows = 0;
|
||||
std::optional<RelationalDatabase::AccountTxMarker> lastEmitted;
|
||||
std::optional<RelationalDatabase::AccountTxMarker> lastScanned;
|
||||
|
||||
// SOCI requires boost::optional (not std::optional) as parameters.
|
||||
boost::optional<std::uint64_t> ledgerSeq;
|
||||
@@ -1128,18 +1187,31 @@ accountTxPage(
|
||||
|
||||
while (st.fetch())
|
||||
{
|
||||
if (hasDelegateFilter)
|
||||
{
|
||||
++fetchedRows;
|
||||
lastScanned = {
|
||||
.ledgerSeq = rangeCheckedCast<std::uint32_t>(ledgerSeq.value_or(0)),
|
||||
.txnSeq = txnSeq.value_or(0)};
|
||||
}
|
||||
|
||||
if (lookingForMarker)
|
||||
{
|
||||
if (findLedger == ledgerSeq.value_or(0) && findSeq == txnSeq.value_or(0))
|
||||
{
|
||||
lookingForMarker = false;
|
||||
// Delegate markers are continuation cursors for the last
|
||||
// scanned row, so resume after the marker row.
|
||||
if (hasDelegateFilter)
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (numberOfResults == 0)
|
||||
|
||||
if (!hasDelegateFilter && numberOfResults == 0)
|
||||
{
|
||||
newmarker = {
|
||||
.ledgerSeq = rangeCheckedCast<std::uint32_t>(ledgerSeq.value_or(0)),
|
||||
@@ -1165,6 +1237,23 @@ accountTxPage(
|
||||
rawMeta.clear();
|
||||
}
|
||||
|
||||
if (hasDelegateFilter)
|
||||
{
|
||||
if (rawData.empty() ||
|
||||
!passesDelegateFilter(rawData, options.delegate.value(), options.account))
|
||||
{
|
||||
rawData.clear();
|
||||
rawMeta.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (numberOfResults == 0)
|
||||
{
|
||||
newmarker = lastEmitted;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Work around a bug that could leave the metadata missing
|
||||
if (rawMeta.empty())
|
||||
onUnsavedLedger(ledgerSeq.value_or(0));
|
||||
@@ -1186,7 +1275,18 @@ accountTxPage(
|
||||
|
||||
--numberOfResults;
|
||||
total++;
|
||||
if (hasDelegateFilter)
|
||||
{
|
||||
lastEmitted = {
|
||||
.ledgerSeq = rangeCheckedCast<std::uint32_t>(ledgerSeq.value_or(0)),
|
||||
.txnSeq = txnSeq.value_or(0)};
|
||||
}
|
||||
}
|
||||
|
||||
// If this filtered page did not fill the requested number of results,
|
||||
// still return a marker so the caller can continue scanning later rows.
|
||||
if (hasDelegateFilter && !newmarker && !lookingForMarker && fetchedRows == queryLimit)
|
||||
newmarker = lastScanned;
|
||||
}
|
||||
|
||||
return {newmarker, total};
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <xrpl/resource/Fees.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
@@ -38,6 +39,48 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
static std::expected<DelegateFilter, json::Value>
|
||||
parseDelegateFilter(json::Value const& delegateNode)
|
||||
{
|
||||
if (!delegateNode.isObject())
|
||||
return std::unexpected(RPC::invalidFieldError(jss::delegate));
|
||||
|
||||
if (!delegateNode.isMember(jss::delegate_filter) ||
|
||||
!delegateNode[jss::delegate_filter].isString())
|
||||
return std::unexpected(RPC::invalidFieldError(jss::delegate_filter));
|
||||
|
||||
auto const& delegateFilterStr = delegateNode[jss::delegate_filter].asString();
|
||||
|
||||
auto typeResult = [&] -> std::expected<DelegateType, json::Value> {
|
||||
if (delegateFilterStr == "actor")
|
||||
return DelegateType::Actor;
|
||||
|
||||
if (delegateFilterStr == "authorizer")
|
||||
return DelegateType::Authorizer;
|
||||
|
||||
return std::unexpected(RPC::invalidFieldError(jss::delegate_filter));
|
||||
}();
|
||||
|
||||
if (!typeResult)
|
||||
return std::unexpected(typeResult.error());
|
||||
|
||||
DelegateType const type = *typeResult;
|
||||
|
||||
std::optional<AccountID> counterparty;
|
||||
if (delegateNode.isMember(jss::counter_party))
|
||||
{
|
||||
if (!delegateNode[jss::counter_party].isString())
|
||||
return std::unexpected(RPC::invalidFieldError(jss::counter_party));
|
||||
|
||||
counterparty = parseBase58<AccountID>(delegateNode[jss::counter_party].asString());
|
||||
|
||||
if (!counterparty)
|
||||
return std::unexpected(rpcError(RpcActMalformed));
|
||||
}
|
||||
|
||||
return DelegateFilter{.type = type, .counterparty = counterparty};
|
||||
}
|
||||
|
||||
using TxnsData = RelationalDatabase::AccountTxs;
|
||||
using TxnsDataBinary = RelationalDatabase::MetaTxsList;
|
||||
using TxnDataBinary = RelationalDatabase::txnMetaLedgerType;
|
||||
@@ -231,7 +274,8 @@ doAccountTxHelp(RPC::Context& context, AccountTxArgs const& args)
|
||||
.ledgerRange = result.ledgerRange,
|
||||
.marker = result.marker,
|
||||
.limit = args.limit,
|
||||
.bAdmin = isUnlimited(context.role)};
|
||||
.bAdmin = isUnlimited(context.role),
|
||||
.delegate = args.delegate};
|
||||
|
||||
auto& db = context.app.getRelationalDatabase();
|
||||
|
||||
@@ -370,6 +414,9 @@ populateJsonResponse(
|
||||
response[jss::marker] = json::ValueType::Object;
|
||||
response[jss::marker][jss::ledger] = result.marker->ledgerSeq;
|
||||
response[jss::marker][jss::seq] = result.marker->txnSeq;
|
||||
|
||||
if (args.delegate)
|
||||
response[jss::marker][jss::delegate] = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,7 +433,17 @@ populateJsonResponse(
|
||||
// limit: integer, // optional
|
||||
// marker: object {ledger: ledger_index, seq: txn_sequence} // optional,
|
||||
// resume previous query
|
||||
// delegate: object { // optional
|
||||
// delegate_filter: string, // required; "actor" or "authorizer"
|
||||
// counter_party: account // optional
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Pagination note for delegate-filtered queries: the `delegate` object (both
|
||||
// `delegate_filter` and `counter_party`) must be supplied unchanged on every
|
||||
// paginated request until the query completes. A marker returned by a
|
||||
// delegate-filtered query is only valid for a follow-up request that repeats
|
||||
// the same `delegate` object
|
||||
json::Value
|
||||
doAccountTx(RPC::JsonContext& context)
|
||||
{
|
||||
@@ -454,6 +511,38 @@ doAccountTx(RPC::JsonContext& context)
|
||||
.ledgerSeq = token[jss::ledger].asUInt(), .txnSeq = token[jss::seq].asUInt()};
|
||||
}
|
||||
|
||||
if (params.isMember(jss::delegate))
|
||||
{
|
||||
if (auto const filter = parseDelegateFilter(params[jss::delegate]); filter.has_value())
|
||||
{
|
||||
args.delegate = *filter;
|
||||
}
|
||||
else
|
||||
{
|
||||
return filter.error();
|
||||
}
|
||||
}
|
||||
|
||||
// A marker produced by a delegate-filtered query uses a different
|
||||
// pagination cursor than a normal query, so it is only valid when the same
|
||||
// `delegate` object is supplied again. Reject any mismatch so pagination
|
||||
// cannot silently skip or duplicate results.
|
||||
if (args.marker)
|
||||
{
|
||||
bool const markerFromDelegate = params[jss::marker].isMember(jss::delegate) &&
|
||||
params[jss::marker][jss::delegate].isBool() &&
|
||||
params[jss::marker][jss::delegate].asBool();
|
||||
if (markerFromDelegate != args.delegate.has_value())
|
||||
{
|
||||
RPC::Status const status{
|
||||
RpcInvalidParams,
|
||||
"Do not mix delegate and non-delegate pagination markers in account_tx; "
|
||||
"repeat the same `delegate` object when using a delegate marker."};
|
||||
status.inject(response);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
auto res = doAccountTxHelp(context, args);
|
||||
JLOG(context.j.debug()) << __func__ << " populating response";
|
||||
return populateJsonResponse(res, args, context);
|
||||
|
||||
Reference in New Issue
Block a user