#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"); } 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); }; using namespace test::jtx; auto const sa = testable_amendments(); testWithFeatures(sa - featurePermissionedDEX); testWithFeatures(sa); } }; BEAST_DEFINE_TESTSUITE(TrustAndBalance, app, xrpl); } // namespace xrpl