#include #include #include #include #include #include namespace xrpl { class TrustAndBalance_test : public beast::unit_test::suite { void testPayNonexistent(FeatureBitset features) { testcase("Payment to Nonexistent Account"); using namespace test::jtx; Env env{*this, features}; env(pay(env.master, "alice", XRP(1)), ter(tecNO_DST_INSUF_XRP)); env.close(); } void testTrustNonexistent() { testcase("Trust Nonexistent Account"); using namespace test::jtx; Env env{*this}; Account alice{"alice"}; env(trust(env.master, alice["USD"](100)), ter(tecNO_DST)); } void testCreditLimit() { testcase("Credit Limit"); using namespace test::jtx; Env env{*this}; Account gw{"gateway"}; Account alice{"alice"}; Account bob{"bob"}; env.fund(XRP(10000), gw, alice, bob); env.close(); // credit limit doesn't exist yet - verify ledger_entry // reflects this auto jrr = ledgerEntryState(env, gw, alice, "USD"); BEAST_EXPECT(jrr[jss::error] == "entryNotFound"); // now create a credit limit env(trust(alice, gw["USD"](800))); jrr = ledgerEntryState(env, gw, alice, "USD"); BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "0"); BEAST_EXPECT(jrr[jss::node][sfHighLimit.fieldName][jss::value] == "800"); BEAST_EXPECT(jrr[jss::node][sfHighLimit.fieldName][jss::issuer] == alice.human()); BEAST_EXPECT(jrr[jss::node][sfHighLimit.fieldName][jss::currency] == "USD"); BEAST_EXPECT(jrr[jss::node][sfLowLimit.fieldName][jss::value] == "0"); BEAST_EXPECT(jrr[jss::node][sfLowLimit.fieldName][jss::issuer] == gw.human()); BEAST_EXPECT(jrr[jss::node][sfLowLimit.fieldName][jss::currency] == "USD"); // modify the credit limit env(trust(alice, gw["USD"](700))); jrr = ledgerEntryState(env, gw, alice, "USD"); BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "0"); BEAST_EXPECT(jrr[jss::node][sfHighLimit.fieldName][jss::value] == "700"); BEAST_EXPECT(jrr[jss::node][sfHighLimit.fieldName][jss::issuer] == alice.human()); BEAST_EXPECT(jrr[jss::node][sfHighLimit.fieldName][jss::currency] == "USD"); BEAST_EXPECT(jrr[jss::node][sfLowLimit.fieldName][jss::value] == "0"); BEAST_EXPECT(jrr[jss::node][sfLowLimit.fieldName][jss::issuer] == gw.human()); BEAST_EXPECT(jrr[jss::node][sfLowLimit.fieldName][jss::currency] == "USD"); // set negative limit - expect failure env(trust(alice, gw["USD"](-1)), ter(temBAD_LIMIT)); // set zero limit env(trust(alice, gw["USD"](0))); // ensure line is deleted jrr = ledgerEntryState(env, gw, alice, "USD"); BEAST_EXPECT(jrr[jss::error] == "entryNotFound"); // TODO Check in both owner books. // set another credit limit env(trust(alice, bob["USD"](600))); // set limit on other side env(trust(bob, alice["USD"](500))); // check the ledger state for the trust line jrr = ledgerEntryState(env, alice, bob, "USD"); BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "0"); BEAST_EXPECT(jrr[jss::node][sfHighLimit.fieldName][jss::value] == "500"); BEAST_EXPECT(jrr[jss::node][sfHighLimit.fieldName][jss::issuer] == bob.human()); BEAST_EXPECT(jrr[jss::node][sfHighLimit.fieldName][jss::currency] == "USD"); BEAST_EXPECT(jrr[jss::node][sfLowLimit.fieldName][jss::value] == "600"); BEAST_EXPECT(jrr[jss::node][sfLowLimit.fieldName][jss::issuer] == alice.human()); BEAST_EXPECT(jrr[jss::node][sfLowLimit.fieldName][jss::currency] == "USD"); } void testDirectRipple(FeatureBitset features) { testcase("Direct Payment, Ripple"); using namespace test::jtx; Env env{*this, features}; Account alice{"alice"}; Account bob{"bob"}; env.fund(XRP(10000), alice, bob); env.close(); env(trust(alice, bob["USD"](600))); env(trust(bob, alice["USD"](700))); // alice sends bob partial with alice as issuer env(pay(alice, bob, alice["USD"](24))); env.require(balance(bob, alice["USD"](24))); // alice sends bob more with bob as issuer env(pay(alice, bob, bob["USD"](33))); env.require(balance(bob, alice["USD"](57))); // bob sends back more than sent env(pay(bob, alice, bob["USD"](90))); env.require(balance(bob, alice["USD"](-33))); // alice sends to her limit env(pay(alice, bob, bob["USD"](733))); env.require(balance(bob, alice["USD"](700))); // bob sends to his limit env(pay(bob, alice, bob["USD"](1300))); env.require(balance(bob, alice["USD"](-600))); // bob sends past limit env(pay(bob, alice, bob["USD"](1)), ter(tecPATH_DRY)); env.require(balance(bob, alice["USD"](-600))); } void testWithTransferFee(bool subscribe, bool with_rate, FeatureBitset features) { testcase( std::string("Direct Payment: ") + (with_rate ? "With " : "Without ") + " Xfer Fee, " + (subscribe ? "With " : "Without ") + " Subscribe"); using namespace test::jtx; Env env{*this, features}; auto wsc = test::makeWSClient(env.app().config()); Account gw{"gateway"}; Account alice{"alice"}; Account bob{"bob"}; env.fund(XRP(10000), gw, alice, bob); env.close(); env(trust(alice, gw["AUD"](100))); env(trust(bob, gw["AUD"](100))); env(pay(gw, alice, alice["AUD"](1))); env.close(); env.require(balance(alice, gw["AUD"](1))); // alice sends bob 1 AUD env(pay(alice, bob, gw["AUD"](1))); env.close(); env.require(balance(alice, gw["AUD"](0))); env.require(balance(bob, gw["AUD"](1))); env.require(balance(gw, bob["AUD"](-1))); if (with_rate) { // set a transfer rate env(rate(gw, 1.1)); env.close(); // bob sends alice 0.5 AUD with a max to spend env(pay(bob, alice, gw["AUD"](0.5)), sendmax(gw["AUD"](0.55))); } else { // bob sends alice 0.5 AUD env(pay(bob, alice, gw["AUD"](0.5))); } env.require(balance(alice, gw["AUD"](0.5))); env.require(balance(bob, gw["AUD"](with_rate ? 0.45 : 0.5))); env.require(balance(gw, bob["AUD"](with_rate ? -0.45 : -0.5))); if (subscribe) { Json::Value jvs; jvs[jss::accounts] = Json::arrayValue; jvs[jss::accounts].append(gw.human()); jvs[jss::streams] = Json::arrayValue; jvs[jss::streams].append("transactions"); jvs[jss::streams].append("ledger"); auto jv = wsc->invoke("subscribe", jvs); BEAST_EXPECT(jv[jss::status] == "success"); env.close(); using namespace std::chrono_literals; BEAST_EXPECT(wsc->findMsg(5s, [](auto const& jval) { auto const& t = jval[jss::transaction]; return t[jss::TransactionType] == jss::Payment; })); BEAST_EXPECT(wsc->findMsg(5s, [](auto const& jval) { return jval[jss::type] == "ledgerClosed"; })); BEAST_EXPECT(wsc->invoke("unsubscribe", jv)[jss::status] == "success"); } } void testWithPath(FeatureBitset features) { testcase("Payments With Paths and Fees"); using namespace test::jtx; Env env{*this, features}; Account gw{"gateway"}; Account alice{"alice"}; Account bob{"bob"}; env.fund(XRP(10000), gw, alice, bob); env.close(); // set a transfer rate env(rate(gw, 1.1)); env(trust(alice, gw["AUD"](100))); env(trust(bob, gw["AUD"](100))); env(pay(gw, alice, alice["AUD"](4.4))); env.require(balance(alice, gw["AUD"](4.4))); // alice sends gw issues to bob with a max spend that allows for the // xfer rate env(pay(alice, bob, gw["AUD"](1)), sendmax(gw["AUD"](1.1))); env.require(balance(alice, gw["AUD"](3.3))); env.require(balance(bob, gw["AUD"](1))); // alice sends bob issues to bob with a max spend env(pay(alice, bob, bob["AUD"](1)), sendmax(gw["AUD"](1.1))); env.require(balance(alice, gw["AUD"](2.2))); env.require(balance(bob, gw["AUD"](2))); // alice sends gw issues to bob with a max spend env(pay(alice, bob, gw["AUD"](1)), sendmax(alice["AUD"](1.1))); env.require(balance(alice, gw["AUD"](1.1))); env.require(balance(bob, gw["AUD"](3))); // alice sends bob issues to bob with a max spend in alice issues. // expect fail since gw is not involved env(pay(alice, bob, bob["AUD"](1)), sendmax(alice["AUD"](1.1)), ter(tecPATH_DRY)); env.require(balance(alice, gw["AUD"](1.1))); env.require(balance(bob, gw["AUD"](3))); } void testIndirect(FeatureBitset features) { testcase("Indirect Payment"); using namespace test::jtx; Env env{*this, features}; Account gw{"gateway"}; Account alice{"alice"}; Account bob{"bob"}; env.fund(XRP(10000), gw, alice, bob); env.close(); env(trust(alice, gw["USD"](600))); env(trust(bob, gw["USD"](700))); env(pay(gw, alice, alice["USD"](70))); env(pay(gw, bob, bob["USD"](50))); env.require(balance(alice, gw["USD"](70))); env.require(balance(bob, gw["USD"](50))); // alice sends more than has to issuer: 100 out of 70 env(pay(alice, gw, gw["USD"](100)), ter(tecPATH_PARTIAL)); // alice sends more than has to bob: 100 out of 70 env(pay(alice, bob, gw["USD"](100)), ter(tecPATH_PARTIAL)); env.close(); env.require(balance(alice, gw["USD"](70))); env.require(balance(bob, gw["USD"](50))); // send with an account path env(pay(alice, bob, gw["USD"](5)), test::jtx::path(gw)); env.require(balance(alice, gw["USD"](65))); env.require(balance(bob, gw["USD"](55))); } void testIndirectMultiPath(bool with_rate, FeatureBitset features) { testcase(std::string("Indirect Payment, Multi Path, ") + (with_rate ? "With " : "Without ") + " Xfer Fee, "); using namespace test::jtx; Env env{*this, features}; Account gw{"gateway"}; Account amazon{"amazon"}; Account alice{"alice"}; Account bob{"bob"}; Account carol{"carol"}; env.fund(XRP(10000), gw, amazon, alice, bob, carol); env.close(); env(trust(amazon, gw["USD"](2000))); env(trust(bob, alice["USD"](600))); env(trust(bob, gw["USD"](1000))); env(trust(carol, alice["USD"](700))); env(trust(carol, gw["USD"](1000))); if (with_rate) env(rate(gw, 1.1)); env(pay(gw, bob, bob["USD"](100))); env(pay(gw, carol, carol["USD"](100))); env.close(); // alice pays amazon via multiple paths if (with_rate) env(pay(alice, amazon, gw["USD"](150)), sendmax(alice["USD"](200)), test::jtx::path(bob), test::jtx::path(carol)); else env(pay(alice, amazon, gw["USD"](150)), test::jtx::path(bob), test::jtx::path(carol)); if (with_rate) { env.require( balance(alice, STAmount(carol["USD"].issue(), 6500000000000000ull, -14, true, STAmount::unchecked{}))); env.require(balance(carol, gw["USD"](35))); } else { env.require(balance(alice, carol["USD"](-50))); env.require(balance(carol, gw["USD"](50))); } env.require(balance(alice, bob["USD"](-100))); env.require(balance(amazon, gw["USD"](150))); env.require(balance(bob, gw["USD"](0))); } void testInvoiceID(FeatureBitset features) { testcase("Set Invoice ID on Payment"); using namespace test::jtx; Env env{*this, features}; Account alice{"alice"}; auto wsc = test::makeWSClient(env.app().config()); env.fund(XRP(10000), alice); env.close(); Json::Value jvs; jvs[jss::accounts] = Json::arrayValue; jvs[jss::accounts].append(env.master.human()); jvs[jss::streams] = Json::arrayValue; jvs[jss::streams].append("transactions"); BEAST_EXPECT(wsc->invoke("subscribe", jvs)[jss::status] == "success"); char const* invoiceId = "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89"; Json::Value jv; auto tx = env.jt(pay(env.master, alice, XRP(10000)), json(sfInvoiceID.fieldName, invoiceId)); jv[jss::tx_blob] = strHex(tx.stx->getSerializer().slice()); auto jrr = wsc->invoke("submit", jv)[jss::result]; BEAST_EXPECT(jrr[jss::status] == "success"); BEAST_EXPECT(jrr[jss::tx_json][sfInvoiceID.fieldName] == invoiceId); env.close(); using namespace std::chrono_literals; BEAST_EXPECT(wsc->findMsg(2s, [invoiceId](auto const& jval) { auto const& t = jval[jss::transaction]; return t[jss::TransactionType] == jss::Payment && t[sfInvoiceID.fieldName] == invoiceId; })); BEAST_EXPECT(wsc->invoke("unsubscribe", jv)[jss::status] == "success"); } void testOwnerCountOnBalanceChange(FeatureBitset features) { testcase("Reserve Edge Cases at Boundary"); using namespace test::jtx; Env env{*this, features}; Account gw{"gateway"}; Account market{"market"}; auto const USD = gw["USD"]; auto const fee = env.current()->fees().base; auto const acctReserve = env.current()->fees().accountReserve(0); auto const incReserve = env.current()->fees().increment; env.fund(XRP(10000), gw, market); env.close(); auto checkAccountUSD = [&](Account const& acc, bool const accHigh, std::uint32_t expectedOwnerCount, bool expectedReserveSet, STAmount expectedUsdBalance) { BEAST_EXPECT(env.le(acc)->getFieldU32(sfOwnerCount) == expectedOwnerCount); auto const line = env.le(keylet::line(acc, gw, to_currency("USD"))); BEAST_EXPECT(line && line->isFlag(accHigh ? lsfHighReserve : lsfLowReserve) == expectedReserveSet); BEAST_EXPECT(env.balance(acc, USD) == expectedUsdBalance); }; // Setup: fund account, create trust line with balance, clear default // ripple, set limit to 0, clear balance. Results in owner count 0 with // reserve flag cleared. auto setupAccount = [&](Account const& acc, bool const accHigh, STAmount const& fundAmount) { env.fund(fundAmount, acc); env.close(); env(fset(gw, asfDefaultRipple)); env.close(); env(trust(acc, USD(1000))); env.close(); env(pay(gw, acc, USD(1000))); env.close(); checkAccountUSD(acc, accHigh, 1, true, USD(1000)); env(fclear(gw, asfDefaultRipple)); env.close(); env(trust(acc, USD(0))); env.close(); env(pay(acc, gw, USD(1000))); env.close(); checkAccountUSD(acc, accHigh, 0, false, USD(0)); }; // Alice receives USD via offer crossing, there's enough balance to // cover everything (reserves, offer, fees etc.) { Account alice{"alice"}; bool const aliceHigh = alice.id() > gw.id(); setupAccount(alice, aliceHigh, acctReserve + incReserve + fee * 4 + XRP(25)); // Making a limit order to match against later env(offer(gw, XRP(100), USD(100))); env.close(); // Alice creates offer to buy USD - it should cross fully // immediately. Both cases should succeed because there is enough // balance to cover the reserve requirements and the offer, but // owner counter only increments in the fixed case. if (features[fixTrustLineOwnerCount]) { env(offer(alice, USD(25), XRP(25))); env.close(); // Offer crosses, alice gets USD, owner count becomes 1, reserve // flag set env.require(offers(alice, 0)); checkAccountUSD(alice, aliceHigh, 1, true, USD(25)); } else { env(offer(alice, USD(25), XRP(25))); env.close(); // Offer crosses, alice gets USD, owner count stays 0, reserve // flag not set env.require(offers(alice, 0)); checkAccountUSD(alice, aliceHigh, 0, false, USD(25)); } } // Bob receives USD via offer crossing, there's only 10 XRP available // for the offer to cross, so the unconsummed part of the offer would be // staying in the book and add another entry to the owner count. { Account bob{"bob"}; bool const bobHigh = bob.id() > gw.id(); setupAccount(bob, bobHigh, acctReserve + incReserve + fee * 4 + XRP(10)); // Making a limit order to match against later env(offer(gw, XRP(100), USD(100))); env.close(); // Bob creates offer to buy USD - it should cross immediately, but // Bob only has 10 USD available so the offer is not supposed to be // consumed completely. if (features[fixTrustLineOwnerCount]) { env(offer(bob, USD(25), XRP(25))); env.close(); // Offer crosses, bob gets 10 USD. It only consumes 10 USD, but // there's no more remaining unreserved XRPs available, so the // remaining quantity is unfunded. It does not land into the // book. The owner count is incremented and the reserve flag is // set. env.require(offers(bob, 0)); checkAccountUSD(bob, bobHigh, 1, true, USD(10)); } else { env(offer(bob, USD(25), XRP(25))); env.close(); // Offer crosses, bob gets 25 USD. The owner count is not // incremented and the reserve flag is not set. env.require(offers(bob, 0)); checkAccountUSD(bob, bobHigh, 0, false, USD(25)); } } // Carol creates offer to buy USD - but they only have enough XRPs for // reserves and transaction fees. { Account carol{"carol"}; bool const carolHigh = carol.id() > gw.id(); setupAccount(carol, carolHigh, acctReserve + incReserve + fee * 4); // Making a limit order to match against later env(offer(gw, XRP(100), USD(100))); env.close(); // Carol creates offer to buy USD - but there should be no // unreserved XRPs available to fund the offer. if (features[fixTrustLineOwnerCount]) { env(offer(carol, USD(25), XRP(25))); env.close(); // The offer is unfunded, but it still ends up in the book. // Carol does not get owner count due to the trustline state (it // does not change), but due to the offer. env.require(offers(carol, 1)); checkAccountUSD(carol, carolHigh, 1, false, USD(0)); } else { env(offer(carol, USD(25), XRP(25))); env.close(); // Offer crosses, bob gets 25 USD. The owner count is not // incremented and the reserve flag is not set. env.require(offers(carol, 0)); checkAccountUSD(carol, carolHigh, 0, false, USD(25)); } } // Receiving USD via CheckCash. rippleCreditIOU is reached // with the receiver's balance going from 0 to positive. { Account dave{"dave"}; bool const daveHigh = dave.id() > gw.id(); setupAccount(dave, daveHigh, acctReserve + fee * 4); uint256 const checkId{keylet::check(gw, env.seq(gw)).key}; env(check::create(gw, dave, USD(25))); env.close(); if (features[fixTrustLineOwnerCount]) { env(check::cash(dave, checkId, USD(25))); env.close(); // rippleCreditIOU set the reserve flag and incremented // owner count. No reserve requirements since it's the only // object owned by this account checkAccountUSD(dave, daveHigh, 1, true, USD(25)); } else { env(check::cash(dave, checkId, USD(25))); env.close(); // Without fix: owner count stays 0, reserve not set. checkAccountUSD(dave, daveHigh, 0, false, USD(25)); } } // Receiving USD via CheckCash. rippleCreditIOU is reached // with the receiver's balance going from 0 to positive. { Account ed{"ed"}; bool const edHigh = ed.id() > gw.id(); setupAccount(ed, edHigh, acctReserve + incReserve * 2 + fee * 4); env(trust(ed, gw["EUR"](1000))); env(trust(ed, gw["JPY"](1000))); uint256 const checkId{keylet::check(gw, env.seq(gw)).key}; env(check::create(gw, ed, USD(25))); env.close(); if (features[fixTrustLineOwnerCount]) { // rippleCreditIOU returns tecINSUFFICIENT_RESERVE here since // there is not enough balance for the reserve requirement on // 3rd trustline. Unfortunately payment engine somehow does not // propagate this error env(check::cash(ed, checkId, USD(25))); env.close(); // Check cash actually failed, no change to owner count or USD // balance checkAccountUSD(ed, edHigh, 2, false, USD(0)); } else { env(check::cash(ed, checkId, USD(25))); env.close(); // Without fix: owner count stays 2 (due to eur and jpy // trustlines), reserve not set. checkAccountUSD(ed, edHigh, 2, false, USD(25)); } } } public: void run() override { testTrustNonexistent(); testCreditLimit(); auto testWithFeatures = [this](FeatureBitset features) { testPayNonexistent(features); testDirectRipple(features); testWithTransferFee(false, false, features); testWithTransferFee(false, true, features); testWithTransferFee(true, false, features); testWithTransferFee(true, true, features); testWithPath(features); testIndirect(features); testIndirectMultiPath(true, features); testIndirectMultiPath(false, features); testInvoiceID(features); testOwnerCountOnBalanceChange(features); }; using namespace test::jtx; auto const sa = testable_amendments(); testWithFeatures(sa - featurePermissionedDEX); testWithFeatures(sa - fixTrustLineOwnerCount); testWithFeatures(sa); } }; BEAST_DEFINE_TESTSUITE(TrustAndBalance, app, xrpl); } // namespace xrpl