mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-23 15:20:54 +00:00
feat: Add delegate filter param for account_tx RPC (#6126)
This commit is contained in:
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user