diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj index 178183238..187968521 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj +++ b/Builds/VisualStudio2015/RippleD.vcxproj @@ -4444,6 +4444,10 @@ True True + + True + True + True True diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters index f5ad53e34..c5e936c3b 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters @@ -5256,6 +5256,9 @@ test\app + + test\app + test\app diff --git a/src/test/app/TrustAndBalance_test.cpp b/src/test/app/TrustAndBalance_test.cpp new file mode 100644 index 000000000..7d0071d85 --- /dev/null +++ b/src/test/app/TrustAndBalance_test.cpp @@ -0,0 +1,512 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2016 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include + +namespace ripple { + +class TrustAndBalance_test : public beast::unit_test::suite +{ + static auto + ledgerEntryState( + test::jtx::Env & env, + test::jtx::Account const& acct_a, + test::jtx::Account const & acct_b, + std::string const& currency) + { + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::ripple_state][jss::currency] = currency; + jvParams[jss::ripple_state][jss::accounts] = Json::arrayValue; + jvParams[jss::ripple_state][jss::accounts].append(acct_a.human()); + jvParams[jss::ripple_state][jss::accounts].append(acct_b.human()); + return env.rpc ("json", "ledger_entry", to_string(jvParams))[jss::result]; + }; + + void + testPayNonexistent () + { + testcase ("Payment to Nonexistent Account"); + using namespace test::jtx; + + Env env {*this}; + 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 () + { + testcase ("Direct Payment, Ripple"); + using namespace test::jtx; + + Env env {*this}; + 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) + { + testcase(std::string("Direct Payment: ") + + (with_rate ? "With " : "Without ") + " Xfer Fee, " + + (subscribe ? "With " : "Without ") + " Subscribe"); + using namespace test::jtx; + + Env env {*this}; + 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(); + + BEAST_EXPECT(wsc->findMsg(5s, + [](auto const& jv) + { + auto const& t = jv[jss::transaction]; + return t[jss::TransactionType] == "Payment"; + })); + BEAST_EXPECT(wsc->findMsg(5s, + [](auto const& jv) + { + return jv[jss::type] == "ledgerClosed"; + })); + + BEAST_EXPECT(wsc->invoke("unsubscribe",jv)[jss::status] == "success"); + } + } + + void + testWithPath () + { + testcase ("Payments With Paths and Fees"); + 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(); + + // 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 () + { + testcase ("Indirect Payment"); + 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(); + + 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) + { + testcase (std::string("Indirect Payment, Multi Path, ") + + (with_rate ? "With " : "Without ") + " Xfer Fee, "); + using namespace test::jtx; + + Env env {*this}; + 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) + { + // 65.00000000000001 is correct. + // This is result of limited precision. + env.require (balance ( + alice, + STAmount( + carol["USD"].issue(), + 6500000000000001ull, + -14, + false, + 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 () + { + testcase ("Set Invoice ID on Payment"); + using namespace test::jtx; + + Env env {*this}; + 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"); + + Json::Value jv; + auto tx = env.jt ( + pay (env.master, alice, XRP(10000)), + json(sfInvoiceID.fieldName, "DEADBEEF")); + 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] == + "0000000000000000" + "0000000000000000" + "0000000000000000" + "00000000DEADBEEF"); + env.close(); + + BEAST_EXPECT(wsc->findMsg(2s, + [](auto const& jv) + { + auto const& t = jv[jss::transaction]; + return + t[jss::TransactionType] == "Payment" && + t[sfInvoiceID.fieldName] == + "0000000000000000" + "0000000000000000" + "0000000000000000" + "00000000DEADBEEF"; + })); + + BEAST_EXPECT(wsc->invoke("unsubscribe",jv)[jss::status] == "success"); + } + +public: + void run () + { + testPayNonexistent (); + testTrustNonexistent (); + testCreditLimit (); + testDirectRipple (); + testWithTransferFee (false, false); + testWithTransferFee (false, true); + testWithTransferFee (true, false); + testWithTransferFee (true, true); + testWithPath (); + testIndirect (); + testIndirectMultiPath (true); + testIndirectMultiPath (false); + testInvoiceID (); + } +}; + +BEAST_DEFINE_TESTSUITE (TrustAndBalance, app, ripple); + +} // ripple + + diff --git a/src/unity/app_test_unity.cpp b/src/unity/app_test_unity.cpp index e736107a0..913d514db 100644 --- a/src/unity/app_test_unity.cpp +++ b/src/unity/app_test_unity.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include