Files
rippled/src/test/ledger/PaymentSandbox_test.cpp
2026-03-31 17:29:45 +00:00

380 lines
13 KiB
C++

#include <test/jtx/PathSet.h>
#include <xrpl/ledger/ApplyViewImpl.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AmountConversions.h>
#include <xrpl/protocol/Feature.h>
namespace xrpl {
namespace test {
class PaymentSandbox_test : public beast::unit_test::suite
{
/*
Create paths so one path funds another path.
Two accounts: sender and receiver.
Two gateways: gw1 and gw2.
Sender and receiver both have trust lines to the gateways.
Sender has 2 gw1/USD and 4 gw2/USD.
Sender has offer to exchange 2 gw1 for gw2 and gw2 for gw1 1-for-1.
Paths are:
1) GW1 -> [OB GW1/USD->GW2/USD] -> GW2
2) GW2 -> [OB GW2/USD->GW1/USD] -> GW1
sender pays receiver 4 USD.
Path 1:
1) Sender exchanges 2 GW1/USD for 2 GW2/USD
2) Old code: the 2 GW1/USD is available to sender
New code: the 2 GW1/USD is not available until the
end of the transaction.
3) Receiver gets 2 GW2/USD
Path 2:
1) Old code: Sender exchanges 2 GW2/USD for 2 GW1/USD
2) Old code: Receiver get 2 GW1
2) New code: Path is dry because sender does not have any
GW1 to spend until the end of the transaction.
*/
void
testSelfFunding(FeatureBitset features)
{
testcase("selfFunding");
using namespace jtx;
Env env(*this, features);
Account const gw1("gw1");
Account const gw2("gw2");
Account const snd("snd");
Account const rcv("rcv");
env.fund(XRP(10000), snd, rcv, gw1, gw2);
auto const USD_gw1 = gw1["USD"];
auto const USD_gw2 = gw2["USD"];
env.trust(USD_gw1(10), snd);
env.trust(USD_gw2(10), snd);
env.trust(USD_gw1(100), rcv);
env.trust(USD_gw2(100), rcv);
env(pay(gw1, snd, USD_gw1(2)));
env(pay(gw2, snd, USD_gw2(4)));
env(offer(snd, USD_gw1(2), USD_gw2(2)), txflags(tfPassive));
env(offer(snd, USD_gw2(2), USD_gw1(2)), txflags(tfPassive));
PathSet const paths(Path(gw1, USD_gw2, gw2), Path(gw2, USD_gw1, gw1));
env(pay(snd, rcv, any(USD_gw1(4))),
json(paths.json()),
txflags(tfNoRippleDirect | tfPartialPayment));
env.require(balance("rcv", USD_gw1(0)));
env.require(balance("rcv", USD_gw2(2)));
}
void
testSubtractCredits(FeatureBitset features)
{
testcase("subtractCredits");
using namespace jtx;
Env env(*this, features);
Account const gw1("gw1");
Account const gw2("gw2");
Account const alice("alice");
env.fund(XRP(10000), alice, gw1, gw2);
auto j = env.app().getJournal("View");
auto const USD_gw1 = gw1["USD"];
auto const USD_gw2 = gw2["USD"];
env.trust(USD_gw1(100), alice);
env.trust(USD_gw2(100), alice);
env(pay(gw1, alice, USD_gw1(50)));
env(pay(gw2, alice, USD_gw2(50)));
STAmount const toCredit(USD_gw1(30));
STAmount const toDebit(USD_gw1(20));
{
// accountSend, no deferredCredits
ApplyViewImpl av(&*env.current(), tapNONE);
auto const iss = USD_gw1.issue();
auto const startingAmount =
accountHolds(av, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j);
{
auto r = accountSend(av, gw1, alice, toCredit, j);
BEAST_EXPECT(isTesSuccess(r));
}
BEAST_EXPECT(
accountHolds(av, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j) ==
startingAmount + toCredit);
{
auto r = accountSend(av, alice, gw1, toDebit, j);
BEAST_EXPECT(isTesSuccess(r));
}
BEAST_EXPECT(
accountHolds(av, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j) ==
startingAmount + toCredit - toDebit);
}
{
// rippleCredit, no deferredCredits
ApplyViewImpl av(&*env.current(), tapNONE);
auto const iss = USD_gw1.issue();
auto const startingAmount =
accountHolds(av, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j);
rippleCredit(av, gw1, alice, toCredit, true, j);
BEAST_EXPECT(
accountHolds(av, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j) ==
startingAmount + toCredit);
rippleCredit(av, alice, gw1, toDebit, true, j);
BEAST_EXPECT(
accountHolds(av, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j) ==
startingAmount + toCredit - toDebit);
}
{
// accountSend, w/ deferredCredits
ApplyViewImpl av(&*env.current(), tapNONE);
PaymentSandbox pv(&av);
auto const iss = USD_gw1.issue();
auto const startingAmount =
accountHolds(pv, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j);
{
auto r = accountSend(pv, gw1, alice, toCredit, j);
BEAST_EXPECT(isTesSuccess(r));
}
BEAST_EXPECT(
accountHolds(pv, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j) ==
startingAmount);
{
auto r = accountSend(pv, alice, gw1, toDebit, j);
BEAST_EXPECT(isTesSuccess(r));
}
BEAST_EXPECT(
accountHolds(pv, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j) ==
startingAmount - toDebit);
}
{
// rippleCredit, w/ deferredCredits
ApplyViewImpl av(&*env.current(), tapNONE);
PaymentSandbox pv(&av);
auto const iss = USD_gw1.issue();
auto const startingAmount =
accountHolds(pv, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j);
rippleCredit(pv, gw1, alice, toCredit, true, j);
BEAST_EXPECT(
accountHolds(pv, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j) ==
startingAmount);
}
{
// redeemIOU, w/ deferredCredits
ApplyViewImpl av(&*env.current(), tapNONE);
PaymentSandbox pv(&av);
auto const iss = USD_gw1.issue();
auto const startingAmount =
accountHolds(pv, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j);
BEAST_EXPECT(redeemIOU(pv, alice, toDebit, iss, j) == tesSUCCESS);
BEAST_EXPECT(
accountHolds(pv, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j) ==
startingAmount - toDebit);
}
{
// issueIOU, w/ deferredCredits
ApplyViewImpl av(&*env.current(), tapNONE);
PaymentSandbox pv(&av);
auto const iss = USD_gw1.issue();
auto const startingAmount =
accountHolds(pv, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j);
BEAST_EXPECT(issueIOU(pv, alice, toCredit, iss, j) == tesSUCCESS);
BEAST_EXPECT(
accountHolds(pv, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j) ==
startingAmount);
}
{
// accountSend, w/ deferredCredits and stacked views
ApplyViewImpl av(&*env.current(), tapNONE);
PaymentSandbox pv(&av);
auto const iss = USD_gw1.issue();
auto const startingAmount =
accountHolds(pv, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j);
{
auto r = accountSend(pv, gw1, alice, toCredit, j);
BEAST_EXPECT(isTesSuccess(r));
}
BEAST_EXPECT(
accountHolds(pv, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j) ==
startingAmount);
{
PaymentSandbox pv2(&pv);
BEAST_EXPECT(
accountHolds(pv2, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j) ==
startingAmount);
{
auto r = accountSend(pv2, gw1, alice, toCredit, j);
BEAST_EXPECT(isTesSuccess(r));
}
BEAST_EXPECT(
accountHolds(pv2, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j) ==
startingAmount);
}
{
auto r = accountSend(pv, alice, gw1, toDebit, j);
BEAST_EXPECT(isTesSuccess(r));
}
BEAST_EXPECT(
accountHolds(pv, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j) ==
startingAmount - toDebit);
}
}
void
testTinyBalance(FeatureBitset features)
{
testcase("Tiny balance");
// Add and subtract a huge credit from a tiny balance, expect the tiny
// balance back. Numerical stability problems could cause the balance to
// be zero.
using namespace jtx;
Env const env(*this, features);
Account const gw("gw");
Account const alice("alice");
auto const USD = gw["USD"];
auto const issue = USD.issue();
STAmount const tinyAmt(
issue, STAmount::cMinValue, STAmount::cMinOffset + 1, false, STAmount::unchecked{});
STAmount const hugeAmt(
issue, STAmount::cMaxValue, STAmount::cMaxOffset - 1, false, STAmount::unchecked{});
ApplyViewImpl av(&*env.current(), tapNONE);
PaymentSandbox pv(&av);
pv.creditHook(gw, alice, hugeAmt, -tinyAmt);
BEAST_EXPECT(pv.balanceHook(alice, gw, hugeAmt) == tinyAmt);
}
void
testReserve(FeatureBitset features)
{
testcase("Reserve");
using namespace jtx;
auto accountFundsXRP =
[](ReadView const& view, AccountID const& id, beast::Journal j) -> XRPAmount {
return toAmount<XRPAmount>(
accountHolds(view, id, xrpCurrency(), xrpAccount(), fhZERO_IF_FROZEN, j));
};
auto reserve = [](jtx::Env& env, std::uint32_t count) -> XRPAmount {
return env.current()->fees().accountReserve(count);
};
Env env(*this, features);
Account const alice("alice");
env.fund(reserve(env, 1), alice);
env.close();
ApplyViewImpl av(&*env.current(), tapNONE);
PaymentSandbox sb(&av);
{
// Send alice an amount and spend it. The deferredCredits will cause
// her balance to drop below the reserve. Make sure her funds are
// zero (there was a bug that caused her funds to become negative).
{
auto r = accountSend(sb, xrpAccount(), alice, XRP(100), env.journal);
BEAST_EXPECT(isTesSuccess(r));
}
{
auto r = accountSend(sb, alice, xrpAccount(), XRP(100), env.journal);
BEAST_EXPECT(isTesSuccess(r));
}
BEAST_EXPECT(accountFundsXRP(sb, alice, env.journal) == beast::zero);
}
}
void
testBalanceHook(FeatureBitset features)
{
// Make sure the Issue::Account returned by PAymentSandbox::balanceHook
// is correct.
testcase("balanceHook");
using namespace jtx;
Env const env(*this, features);
Account const gw("gw");
auto const USD = gw["USD"];
Account const alice("alice");
ApplyViewImpl av(&*env.current(), tapNONE);
PaymentSandbox sb(&av);
// The currency we pass for the last argument mimics the currency that
// is typically passed to creditHook, since it comes from a trust line.
Issue tlIssue = noIssue();
tlIssue.currency = USD.issue().currency;
sb.creditHook(gw.id(), alice.id(), {USD, 400}, {tlIssue, 600});
sb.creditHook(gw.id(), alice.id(), {USD, 100}, {tlIssue, 600});
// Expect that the STAmount issuer returned by balanceHook() is correct.
STAmount const balance = sb.balanceHook(gw.id(), alice.id(), {USD, 600});
BEAST_EXPECT(balance.getIssuer() == USD.issue().account);
}
public:
void
run() override
{
auto testAll = [this](FeatureBitset features) {
testSelfFunding(features);
testSubtractCredits(features);
testTinyBalance(features);
testReserve(features);
testBalanceHook(features);
};
using namespace jtx;
auto const sa = testable_amendments();
testAll(sa - featurePermissionedDEX);
testAll(sa);
}
};
BEAST_DEFINE_TESTSUITE(PaymentSandbox, ledger, xrpl);
} // namespace test
} // namespace xrpl