feat: XLS-68: Sponsor, #5887 continuation (#7350)

Co-authored-by: tequ <git@tequ.dev>
Co-authored-by: yinyiqian1 <yqian@ripple.com>
Co-authored-by: Mayukha Vadari <mvadari@ripple.com>
Co-authored-by: Mayukha Vadari <mvadari@gmail.com>
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
Co-authored-by: Peter Chen <34582813+PeterChen13579@users.noreply.github.com>
Co-authored-by: Zhiyuan Wang <96991820+Kassaking7@users.noreply.github.com>
Co-authored-by: Ayaz Salikhov <asalikhov@ripple.com>
Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
Co-authored-by: Zhiyuan Wang <1830604455@qq.com>
This commit is contained in:
Olek
2026-07-10 17:58:19 -04:00
committed by GitHub
parent c7adb215ed
commit fd2cc6dcb3
170 changed files with 13151 additions and 660 deletions

View File

@@ -1,17 +1,23 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/PathSet.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/balance.h>
#include <test/jtx/jtx_json.h>
#include <test/jtx/offer.h>
#include <test/jtx/pay.h>
#include <test/jtx/sig.h>
#include <test/jtx/sponsor.h>
#include <test/jtx/ter.h>
#include <test/jtx/trust.h>
#include <test/jtx/txflags.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ApplyViewImpl.h>
#include <xrpl/ledger/OwnerCounts.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
@@ -19,7 +25,9 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/AmountConversions.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
@@ -236,7 +244,7 @@ class PaymentSandbox_test : public beast::unit_test::Suite
auto const startingAmount =
accountHolds(pv, alice, iss.currency, iss.account, FreezeHandling::IgnoreFreeze, j);
BEAST_EXPECT(issueIOU(pv, alice, toCredit, iss, j) == tesSUCCESS);
BEAST_EXPECT(issueIOU(pv, alice, toCredit, iss, {}, j) == tesSUCCESS);
BEAST_EXPECT(
accountHolds(
pv, alice, iss.currency, iss.account, FreezeHandling::IgnoreFreeze, j) ==
@@ -330,7 +338,7 @@ class PaymentSandbox_test : public beast::unit_test::Suite
};
auto reserve = [](jtx::Env& env, std::uint32_t count) -> XRPAmount {
return env.current()->fees().accountReserve(count);
return env.current()->fees().accountReserve(count, 1);
};
Env env(*this, features);
@@ -389,6 +397,217 @@ class PaymentSandbox_test : public beast::unit_test::Suite
BEAST_EXPECT(balance.getIssuer() == usd.account.id());
}
void
testOwnerCountHook(FeatureBitset features)
{
// Test that PaymentSandbox::adjustOwnerCountHook and ownerCountHook
// correctly track and return the maximum owner counts during a payment.
testcase("ownerCountHook");
using namespace jtx;
Env env(*this, features);
Account const alice("alice");
Account const sponsor("sponsor");
env.fund(XRP(10000), alice, sponsor);
env.close();
ApplyViewImpl av(&*env.current(), TapNone);
PaymentSandbox sb(&av);
// Test basic owner count hook without sponsor
{
auto const aliceSle = sb.peek(keylet::account(alice));
BEAST_EXPECT(aliceSle);
OwnerCounts const initial(aliceSle);
OwnerCounts updated = initial;
updated.owner = initial.owner + 2;
// Simulate adjusting owner count
sb.adjustOwnerCountHook(alice, initial, updated);
// ownerCountHook should return the max value
OwnerCounts const retrieved = sb.ownerCountHook(alice, initial);
BEAST_EXPECT(retrieved.owner == updated.owner);
BEAST_EXPECT(retrieved.sponsored == updated.sponsored);
BEAST_EXPECT(retrieved.sponsoring == updated.sponsoring);
}
// Test owner count hook with sponsor-related counts
{
auto const sponsorSle = sb.peek(keylet::account(sponsor));
BEAST_EXPECT(sponsorSle);
OwnerCounts const sponsorInitial(sponsorSle);
OwnerCounts sponsorUpdated = sponsorInitial;
sponsorUpdated.owner = sponsorInitial.owner + 1;
sponsorUpdated.sponsoring = sponsorInitial.sponsoring + 1;
sb.adjustOwnerCountHook(sponsor, sponsorInitial, sponsorUpdated);
OwnerCounts const sponsorRetrieved = sb.ownerCountHook(sponsor, sponsorInitial);
BEAST_EXPECT(sponsorRetrieved.owner == sponsorUpdated.owner);
BEAST_EXPECT(sponsorRetrieved.sponsoring == sponsorUpdated.sponsoring);
}
// Test with stacked PaymentSandboxes
{
PaymentSandbox sb2(&sb);
auto const aliceSle = sb2.peek(keylet::account(alice));
OwnerCounts const current(aliceSle);
OwnerCounts further = current;
further.owner = current.owner + 3;
sb2.adjustOwnerCountHook(alice, current, further);
// The nested sandbox should see the max from both levels
OwnerCounts const retrieved = sb2.ownerCountHook(alice, OwnerCounts());
BEAST_EXPECT(retrieved.owner >= further.owner);
}
// Test that max logic works correctly
{
auto const aliceSle = sb.peek(keylet::account(alice));
OwnerCounts const current(aliceSle);
OwnerCounts lower = current;
lower.owner = (current.owner > 0) ? current.owner - 1 : 0;
// Adjusting to a lower value
sb.adjustOwnerCountHook(alice, current, lower);
// Should still return the higher value seen previously
OwnerCounts const retrieved = sb.ownerCountHook(alice, OwnerCounts());
BEAST_EXPECT(retrieved.owner >= lower.owner);
}
}
void
testOwnerCountWithTransaction(FeatureBitset features)
{
// Test that owner count hooks work correctly during actual transactions.
// This verifies that when transactions modify owner counts (by creating
// or deleting ledger objects), the hooks properly track these changes.
testcase(
std::string("ownerCountWithTransaction") +
(features[featureSponsor] ? " with sponsor" : " without sponsor"));
using namespace jtx;
auto reserve = [](jtx::Env& env, std::uint32_t count) -> XRPAmount {
return env.current()->fees().accountReserve(count, 1);
};
Env env(*this, features);
Account const gw("gw");
Account const alice("alice");
Account const bob("bob");
Account const sponsor("sponsor");
auto const usd = gw["USD"];
// Fund accounts. Alice starts with exactly enough for base reserve + 2 objects
env.fund(XRP(10000), gw, bob, sponsor);
env.fund(reserve(env, 3) + XRP(100), alice); // Base + 2 objects + extra for fees
env.close();
// Verify initial state - no owner count
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(ownerCount(env, bob) == 0);
// Create a trust line - this increases owner count
env(trust(alice, usd(1000)));
env.close();
// alice now has 1 object (owner count = 1)
BEAST_EXPECT(ownerCount(env, alice) == 1);
// Create an offer - this further increases owner count
env(trust(bob, usd(1000)));
env(pay(gw, alice, usd(100)));
env.close();
auto const aliceOfferSeq = env.seq(alice); // Capture the sequence before creating offer
env(offer(alice, usd(50), XRP(50)));
env.close();
// alice now has 2 objects (trust line + offer)
BEAST_EXPECT(ownerCount(env, alice) == 2);
// If sponsor feature is enabled, test sponsorship transfer
if (features[featureSponsor])
{
auto const trustId = keylet::trustLine(alice, gw, usd.currency);
BEAST_EXPECT(env.le(trustId));
// Transfer sponsorship - sponsor now sponsors alice's trust line
env(sponsor::transfer(alice, tfSponsorshipCreate, trustId.key),
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor));
env.close();
// alice still has 2 objects but 1 is sponsored
BEAST_EXPECT(ownerCount(env, alice) == 2);
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
// sponsor's sponsoring count should increase
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
}
// Verify alice's available balance respects the reserve
auto const aliceSle = env.le(keylet::account(alice));
BEAST_EXPECT(aliceSle);
auto const aliceBalance = aliceSle->getFieldAmount(sfBalance);
// With sponsor, 1 object is sponsored so only 1 counts for reserve
auto const aliceReserve = reserve(env, features[featureSponsor] ? 1 : 2);
// alice should have limited available balance after accounting for reserve
auto const available = aliceBalance.xrp() - aliceReserve;
if (features[featureSponsor])
{
// With sponsor, alice has more available (1 sponsored object = less reserve)
BEAST_EXPECT(available > XRP(150));
}
else
{
BEAST_EXPECT(available < XRP(150)); // Most of the balance is in reserve
}
// Try to send nearly all balance - should fail due to reserve in both cases
auto const tooMuch = aliceBalance.xrp() - XRP(1);
env(pay(alice, bob, tooMuch), Ter(tecUNFUNDED_PAYMENT));
env.close();
// Verify owner count hasn't changed
BEAST_EXPECT(ownerCount(env, alice) == 2);
// Cancel the offer - this decreases owner count
env(offerCancel(alice, aliceOfferSeq));
env.close();
// alice now has 1 object (just the trust line)
BEAST_EXPECT(ownerCount(env, alice) == 1);
if (features[featureSponsor])
{
// Verify sponsored count stayed the same (trust line is still sponsored)
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
}
// Now alice should have more available balance (less reserve needed)
auto const aliceSle2 = env.le(keylet::account(alice));
auto const aliceBalance2 = aliceSle2->getFieldAmount(sfBalance);
// With sponsor, trust line is still sponsored so 0 objects for reserve
// Without sponsor, 1 object for reserve
auto const aliceReserve2 = reserve(env, features[featureSponsor] ? 0 : 1);
auto const available2 = aliceBalance2.xrp() - aliceReserve2;
// available2 should be greater than available (less reserve needed)
BEAST_EXPECT(available2 > available);
}
public:
void
run() override
@@ -399,11 +618,16 @@ public:
testTinyBalance(features);
testReserve(features);
testBalanceHook(features);
testOwnerCountHook(features);
};
using namespace jtx;
auto const sa = testableAmendments();
testAll(sa - featurePermissionedDEX);
testAll(sa);
// Test owner count with transactions
testOwnerCountWithTransaction(sa - featureSponsor);
testOwnerCountWithTransaction(sa);
}
};