#include #include #include #include #include #include #include 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; 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); } { // directSendNoFee, no deferredCredits ApplyViewImpl av(&*env.current(), tapNONE); auto const iss = USD_gw1; auto const startingAmount = accountHolds(av, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j); directSendNoFee(av, gw1, alice, toCredit, true, j); BEAST_EXPECT( accountHolds(av, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j) == startingAmount + toCredit); directSendNoFee(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; 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); } { // directSendNoFee, w/ deferredCredits ApplyViewImpl av(&*env.current(), tapNONE); PaymentSandbox pv(&av); auto const iss = USD_gw1; auto const startingAmount = accountHolds(pv, alice, iss.currency, iss.account, fhIGNORE_FREEZE, j); directSendNoFee(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; 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; 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; 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; 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.creditHookIOU(gw, alice, hugeAmt, -tinyAmt); BEAST_EXPECT(pv.balanceHookIOU(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( 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::balanceHookIOU 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 creditHookIOU, since it comes from a trust // line. Issue tlIssue = noIssue(); tlIssue.currency = USD.currency; sb.creditHookIOU(gw.id(), alice.id(), {USD, 400}, {tlIssue, 600}); sb.creditHookIOU(gw.id(), alice.id(), {USD, 100}, {tlIssue, 600}); // Expect that the STAmount issuer returned by balanceHookIOU() is // correct. STAmount const balance = sb.balanceHookIOU(gw.id(), alice.id(), {USD, 600}); BEAST_EXPECT(balance.getIssuer() == USD.account.id()); } 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