Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7a58d95
feat: add delegate filter param's to account_tx RPC
PeterChen13579 Dec 1, 2025
141aa14
fix doxygen
PeterChen13579 Dec 1, 2025
18bcdf3
remove try catch
PeterChen13579 Dec 2, 2025
55f5961
decouple filtering
PeterChen13579 Dec 5, 2025
300219a
fix doxygen
PeterChen13579 Dec 5, 2025
eb83ca3
Merge branch 'develop' into fix-delegate-filter
PeterChen13579 Dec 19, 2025
7ee435e
Merge remote-tracking branch 'upstream/develop' into fix-delegate-filter
PeterChen13579 Apr 8, 2026
d8ffa5d
Merge branch 'fix-delegate-filter' of https://github.com/PeterChen135…
PeterChen13579 Apr 8, 2026
efe8a49
merge with latest develop
PeterChen13579 Apr 8, 2026
fbdef94
Merge branch 'develop' into fix-delegate-filter
PeterChen13579 May 12, 2026
902668e
Merge remote-tracking branch 'upstream' into fix-delegate-filter
PeterChen13579 Jul 23, 2026
595e1b9
fix the rest
PeterChen13579 Jul 23, 2026
5553eee
fix magic strings
PeterChen13579 Jul 23, 2026
2f4987e
fix comments to match new JS field and add test
PeterChen13579 Jul 23, 2026
909573d
fix clang tidy
PeterChen13579 Jul 23, 2026
541c0a0
fix clang tidy now..
PeterChen13579 Jul 23, 2026
bfc6675
f clang tidy
PeterChen13579 Jul 23, 2026
d5846f6
fix comment
PeterChen13579 Jul 29, 2026
42c4721
Merge branch 'develop' into fix-delegate-filter
PeterChen13579 Jul 29, 2026
5d9a924
fix comments
PeterChen13579 Jul 30, 2026
9e8da15
Merge remote-tracking branch 'upstream/develop' into fix-delegate-filter
PeterChen13579 Aug 5, 2026
533c924
test
PeterChen13579 Aug 6, 2026
983768c
fix comments
PeterChen13579 Aug 6, 2026
223c8ed
fix clang tidy
PeterChen13579 Aug 7, 2026
6c09975
fix clang tidy
PeterChen13579 Aug 8, 2026
73a70c3
Merge branch 'develop' into fix-delegate-filter
PeterChen13579 Aug 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/rpc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ target_sources(
common/MetaProcessors.cpp
common/impl/APIVersionParser.cpp
common/impl/HandlerProvider.cpp
filters/impl/DelegateTransactionsFilter.cpp
handlers/AccountChannels.cpp
handlers/AccountCurrencies.cpp
handlers/AccountInfo.cpp
Expand Down
44 changes: 44 additions & 0 deletions src/rpc/RPCHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1653,4 +1653,48 @@ toJsonWithBinaryTx(data::TransactionAndMetadata const& txnPlusMeta, std::uint32_
return obj;
}

std::optional<DelegateFilter::Role>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::expected<DelegateFilter::Role, void> better shows what you're trying to achieve.
Below as well

@PeterChen13579 PeterChen13579 Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized the void in std::expected<DelegateFilter::Role, void> is not valid code. I'd probably leave it as is as customValidator checks for if parsing failed or not ( has_value())

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)
{
DelegateFilter delegate{};
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 {};

delegate.delegateType = *delegateTypeOpt;
if (delegateObject.contains(JS(counter_party))) {
auto const& counterpartyVal = delegateObject.at(JS(counter_party));

if (!counterpartyVal.is_string())
return {};

delegate.counterParty = counterpartyVal.as_string();
}

return delegate;
}

} // namespace rpc
18 changes: 18 additions & 0 deletions src/rpc/RPCHelpers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -853,4 +853,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
21 changes: 20 additions & 1 deletion src/rpc/common/Types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

#include <cstdint>
#include <expected>
#include <optional>
#include <string>
#include <utility>
#include <variant>

namespace etl {
class LoadBalancer;
Expand Down Expand Up @@ -175,6 +175,25 @@ 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 {
Comment thread
PeterChen13579 marked this conversation as resolved.
Outdated
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." */
};

Role delegateType = Role::Actor;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need do add a default value here?
I would probably avoid it if possible, to not to forget setting the right one when needed

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed as otherwise clang-tidy complains; Role is an enum class with no default initialization

std::optional<std::string> counterParty;
};

/**
* @brief Convert an empty output to a JSON object
*
Expand Down
32 changes: 32 additions & 0 deletions src/rpc/common/Validators.cpp
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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>
Expand Down Expand Up @@ -383,4 +385,34 @@ CustomValidator CustomValidators::authorizeCredentialValidator =
return MaybeError{};
}};

CustomValidator CustomValidators::delegateValidator =
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
if (!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
7 changes: 7 additions & 0 deletions src/rpc/common/Validators.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,13 @@ struct CustomValidators final {
* Used by AuthorizeCredentialValidator in deposit_preauth.
*/
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;
};

/**
Expand Down
54 changes: 54 additions & 0 deletions src/rpc/filters/TransactionFilter.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//------------------------------------------------------------------------------
Comment thread
PeterChen13579 marked this conversation as resolved.
Outdated
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.

Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================

#pragma once

#include "data/Types.hpp"

#include <xrpl/protocol/AccountID.h>

#include <optional>

namespace rpc {

/**
* @brief Result of a filter check.
*/
struct FilterResult {
bool shouldInclude = false;
std::optional<xrpl::AccountID> relevantAccount;
Comment thread
PeterChen13579 marked this conversation as resolved.
Outdated
};

/**
* @brief Interface for filtering transactions.
*/
class TransactionFilter {
public:
virtual ~TransactionFilter() = default;

/**
* @brief Check if a transaction blob matches the filter criteria.
* @param txnPlusMeta The transaction and metadata blob from the backend.
* @return FilterResult indicating if the txn should be included in the output Json or not
*/
[[nodiscard]] virtual FilterResult
check(data::TransactionAndMetadata const& txnPlusMeta) const = 0;
};

} // namespace rpc
85 changes: 85 additions & 0 deletions src/rpc/filters/impl/DelegateTransactionsFilter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//------------------------------------------------------------------------------
Comment thread
PeterChen13579 marked this conversation as resolved.
Outdated
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.

Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================

#include "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);
}

FilterResult
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 {.shouldInclude = false, .relevantAccount = std::nullopt};

// Filter by "authorizer" ie. the queried account is the actor (signer) and the user wants to
// find the authorizer (owner) it acted for.
if (delegateFilter_.delegateType == rpc::DelegateFilter::Role::Authorizer) {
if (*txDelegate == queriedAccount_) {
if (!counterparty_ || *counterparty_ == txAccount)
return {.shouldInclude = true, .relevantAccount = txAccount};
}
}

Comment thread
PeterChen13579 marked this conversation as resolved.
// Filter by "actor" ie. the queried account is the authorizer (owner) and the user wants to
// find the actor (signer) that acted on its behalf.
else if (delegateFilter_.delegateType == rpc::DelegateFilter::Role::Actor) {
if (txAccount == queriedAccount_) {
if (!counterparty_ || *counterparty_ == *txDelegate)
return {.shouldInclude = true, .relevantAccount = txDelegate};
}
}

return {.shouldInclude = false, .relevantAccount = std::nullopt};
}

} // namespace rpc
54 changes: 54 additions & 0 deletions src/rpc/filters/impl/DelegateTransactionsFilter.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//------------------------------------------------------------------------------
Comment thread
PeterChen13579 marked this conversation as resolved.
Outdated
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.

Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================

#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 {
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]] FilterResult
check(data::TransactionAndMetadata const& txnPlusMeta) const override;

private:
rpc::DelegateFilter delegateFilter_;
xrpl::AccountID queriedAccount_;
std::optional<xrpl::AccountID> counterparty_;
};

} // namespace rpc
Loading
Loading