diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index c464890ba..0a2ed9d41 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -18,13 +18,10 @@ //============================================================================== #include -#include -#include -#include #include -#include +#include #include -#include +#include namespace ripple { namespace test { @@ -46,10 +43,52 @@ class Offer_test : public beast::unit_test::suite -> jtx::PrettyAmount { using namespace jtx; - auto feeDrops = env.current ()->fees ().base; + auto feeDrops = env.current()->fees().base; return drops (dropsPerXRP::value * xrpAmount - feeDrops); }; + static auto + ledgerEntryState(jtx::Env & env, jtx::Account const& acct_a, 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]; + }; + + static auto + ledgerEntryRoot (jtx::Env & env, jtx::Account const& acct) + { + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::account_root] = acct.human(); + return env.rpc ("json", "ledger_entry", to_string(jvParams))[jss::result]; + }; + + static auto + ledgerEntryOffer (jtx::Env & env, jtx::Account const& acct, std::uint32_t offer_seq) + { + Json::Value jvParams; + jvParams[jss::offer][jss::account] = acct.human(); + jvParams[jss::offer][jss::seq] = offer_seq; + return env.rpc ("json", "ledger_entry", to_string(jvParams))[jss::result]; + }; + + static auto + getBookOffers(jtx::Env & env, Issue const& taker_pays, Issue const& taker_gets) + { + Json::Value jvbp; + jvbp[jss::ledger_index] = "current"; + jvbp[jss::taker_pays][jss::currency] = to_string(taker_pays.currency); + jvbp[jss::taker_pays][jss::issuer] = to_string(taker_pays.account); + jvbp[jss::taker_gets][jss::currency] = to_string(taker_gets.currency); + jvbp[jss::taker_gets][jss::issuer] = to_string(taker_gets.account); + return env.rpc("json", "book_offers", to_string(jvbp)) [jss::result]; + } + public: void testRmFundedOffer () { @@ -64,18 +103,18 @@ public: // still funded and not used for the payment. using namespace jtx; - Env env (*this); + Env env {*this}; // ledger close times have a dynamic resolution depending on network // conditions it appears the resolution in test is 10 seconds env.close (); - auto const gw = Account ("gateway"); + auto const gw = Account {"gateway"}; auto const USD = gw["USD"]; auto const BTC = gw["BTC"]; - Account const alice ("alice"); - Account const bob ("bob"); - Account const carol ("carol"); + Account const alice {"alice"}; + Account const bob {"bob"}; + Account const carol {"carol"}; env.fund (XRP (10000), alice, bob, carol, gw); env.trust (USD (1000), alice, bob, carol); @@ -102,12 +141,12 @@ public: PathSet paths (Path (XRP, USD), Path (USD)); - env (pay ("alice", "bob", USD (100)), json (paths.json ()), + env (pay (alice, bob, USD (100)), json (paths.json ()), sendmax (BTC (1000)), txflags (tfPartialPayment)); - env.require (balance ("bob", USD (100))); - BEAST_EXPECT(!isOffer (env, "carol", BTC (1), USD (100)) && - isOffer (env, "carol", BTC (49), XRP (49))); + env.require (balance (bob, USD (100))); + BEAST_EXPECT(!isOffer (env, carol, BTC (1), USD (100)) && + isOffer (env, carol, BTC (49), XRP (49))); } void testCanceledOffer () @@ -115,36 +154,54 @@ public: testcase ("Removing Canceled Offers"); using namespace jtx; - Env env (*this); - auto const gw = Account ("gateway"); + Env env {*this}; + auto const gw = Account {"gateway"}; + auto const alice = Account {"alice"}; auto const USD = gw["USD"]; - env.fund (XRP (10000), "alice", gw); - env.trust (USD (100), "alice"); + env.fund (XRP (10000), alice, gw); + env.trust (USD (100), alice); - env (pay (gw, "alice", USD (50))); + env (pay (gw, alice, USD (50))); - auto const firstOfferSeq = env.seq ("alice"); - Json::StaticString const osKey ("OfferSequence"); + auto const firstOfferSeq = env.seq (alice); - env (offer ("alice", XRP (500), USD (100)), - require (offers ("alice", 1))); + env (offer (alice, XRP (500), USD (100)), + require (offers (alice, 1))); - BEAST_EXPECT(isOffer (env, "alice", XRP (500), USD (100))); + BEAST_EXPECT(isOffer (env, alice, XRP (500), USD (100))); // cancel the offer above and replace it with a new offer - env (offer ("alice", XRP (300), USD (100)), json (osKey, firstOfferSeq), - require (offers ("alice", 1))); + env (offer (alice, XRP (300), USD (100)), + json (jss::OfferSequence, firstOfferSeq), + require (offers (alice, 1))); - BEAST_EXPECT(isOffer (env, "alice", XRP (300), USD (100)) && - !isOffer (env, "alice", XRP (500), USD (100))); + BEAST_EXPECT(isOffer (env, alice, XRP (300), USD (100)) && + !isOffer (env, alice, XRP (500), USD (100))); - // Test canceling non-existant offer. - env (offer ("alice", XRP (400), USD (200)), json (osKey, firstOfferSeq), - require (offers ("alice", 2))); + // Test canceling non-existent offer. + env (offer (alice, XRP (400), USD (200)), + json (jss::OfferSequence, firstOfferSeq), + require (offers (alice, 2))); - BEAST_EXPECT(isOffer (env, "alice", XRP (300), USD (100)) && - isOffer (env, "alice", XRP (400), USD (200))); + BEAST_EXPECT(isOffer (env, alice, XRP (300), USD (100)) && + isOffer (env, alice, XRP (400), USD (200))); + + // Test cancellation now with OfferCancel tx + auto const nextOfferSeq = env.seq (alice); + env (offer (alice, XRP (222), USD (111)), + require (offers (alice, 3))); + + BEAST_EXPECT(isOffer (env, alice, XRP (222), USD (111))); + + Json::Value cancelOffer; + cancelOffer[jss::Account] = alice.human(); + cancelOffer[jss::OfferSequence] = nextOfferSeq; + cancelOffer[jss::TransactionType] = "OfferCancel"; + env (cancelOffer); + BEAST_EXPECT(env.seq(alice) == nextOfferSeq + 2); + + BEAST_EXPECT(!isOffer (env, alice, XRP (222), USD (111))); } void testTinyPayment () @@ -154,16 +211,15 @@ public: // Regression test for tiny payments using namespace jtx; using namespace std::chrono_literals; - auto const alice = Account ("alice"); - auto const bob = Account ("bob"); - auto const carol = Account ("carol"); - auto const gw = Account ("gw"); + auto const alice = Account {"alice"}; + auto const bob = Account {"bob"}; + auto const carol = Account {"carol"}; + auto const gw = Account {"gw"}; auto const USD = gw["USD"]; auto const EUR = gw["EUR"]; - Env env (*this); - + Env env {*this}; env.fund (XRP (10000), alice, bob, carol, gw); env.trust (USD (1000), alice, bob, carol); @@ -185,7 +241,7 @@ public: // Will fail without the underflow fix auto expectedResult = *stAmountCalcSwitchover ? tesSUCCESS : tecPATH_PARTIAL; - env (pay ("alice", "bob", EUR (epsilon)), path (~EUR), + env (pay (alice, bob, EUR (epsilon)), path (~EUR), sendmax (USD (100)), ter (expectedResult)); } } @@ -210,18 +266,18 @@ public: using namespace jtx; using namespace std::chrono_literals; - auto const alice = Account ("alice"); - auto const bob = Account ("bob"); - auto const carol = Account ("carol"); - auto const dan = Account ("dan"); - auto const erin = Account ("erin"); - auto const gw = Account ("gw"); + auto const alice = Account {"alice"}; + auto const bob = Account {"bob"}; + auto const carol = Account {"carol"}; + auto const dan = Account {"dan"}; + auto const erin = Account {"erin"}; + auto const gw = Account {"gw"}; auto const USD = gw["USD"]; for (auto withFix : {false, true}) { - Env env (*this); + Env env {*this}; auto closeTime = [&] { @@ -300,21 +356,21 @@ public: using namespace jtx; - auto const gw = Account ("gateway"); + auto const gw = Account {"gateway"}; auto const USD = gw["USD"]; auto const BTC = gw["BTC"]; auto const EUR = gw["EUR"]; - Account const alice ("alice"); - Account const bob ("bob"); - Account const carol ("carol"); - Account const dan ("dan"); + Account const alice {"alice"}; + Account const bob {"bob"}; + Account const carol {"carol"}; + Account const dan {"dan"}; { // No ripple with an implied account step after an offer - Env env (*this); - auto const gw1 = Account ("gw1"); + Env env {*this}; + auto const gw1 = Account {"gw1"}; auto const USD1 = gw1["USD"]; - auto const gw2 = Account ("gw2"); + auto const gw2 = Account {"gw2"}; auto const USD2 = gw2["USD"]; env.fund (XRP (10000), alice, noripple (bob), carol, dan, gw1, gw2); @@ -373,90 +429,93 @@ public: using namespace jtx; - auto const gw = Account("gateway"); + auto const gw = Account {"gateway"}; + auto const alice = Account {"alice"}; + auto const bob = Account {"bob"}; + auto const carol = Account {"carol"}; auto const USD = gw["USD"]; - auto const usdOffer = USD(1000); - auto const xrpOffer = XRP(1000); + auto const usdOffer = USD (1000); + auto const xrpOffer = XRP (1000); // No crossing: { - Env env(*this); - env.fund (XRP(1000000), gw); + Env env {*this}; + env.fund (XRP (1000000), gw); auto const f = env.current ()->fees ().base; auto const r = reserve (env, 0); - env.fund (r + f, "alice"); + env.fund (r + f, alice); - env (trust ("alice", usdOffer), ter(tesSUCCESS)); - env (pay (gw, "alice", usdOffer), ter(tesSUCCESS)); - env (offer ("alice", xrpOffer, usdOffer), ter(tecINSUF_RESERVE_OFFER)); + env (trust (alice, usdOffer), ter(tesSUCCESS)); + env (pay (gw, alice, usdOffer), ter(tesSUCCESS)); + env (offer (alice, xrpOffer, usdOffer), ter(tecINSUF_RESERVE_OFFER)); env.require ( - balance ("alice", r - f), - owners ("alice", 1)); + balance (alice, r - f), + owners (alice, 1)); } // Partial cross: { Env env(*this); - env.fund (XRP(1000000), gw); + env.fund (XRP (1000000), gw); auto const f = env.current ()->fees ().base; auto const r = reserve (env, 0); - auto const usdOffer2 = USD(500); - auto const xrpOffer2 = XRP(500); + auto const usdOffer2 = USD (500); + auto const xrpOffer2 = XRP (500); - env.fund (r + f + xrpOffer, "bob"); - env (offer ("bob", usdOffer2, xrpOffer2), ter(tesSUCCESS)); - env.fund (r + f, "alice"); - env (trust ("alice", usdOffer), ter(tesSUCCESS)); - env (pay (gw, "alice", usdOffer), ter(tesSUCCESS)); - env (offer ("alice", xrpOffer, usdOffer), ter(tesSUCCESS)); + env.fund (r + f + xrpOffer, bob); + env (offer (bob, usdOffer2, xrpOffer2), ter(tesSUCCESS)); + env.fund (r + f, alice); + env (trust (alice, usdOffer), ter(tesSUCCESS)); + env (pay (gw, alice, usdOffer), ter(tesSUCCESS)); + env (offer (alice, xrpOffer, usdOffer), ter(tesSUCCESS)); env.require ( - balance ("alice", r - f + xrpOffer2), - balance ("alice", usdOffer2), - owners ("alice", 1), - balance ("bob", r + xrpOffer2), - balance ("bob", usdOffer2), - owners ("bob", 1)); + balance (alice, r - f + xrpOffer2), + balance (alice, usdOffer2), + owners (alice, 1), + balance (bob, r + xrpOffer2), + balance (bob, usdOffer2), + owners (bob, 1)); } // Account has enough reserve as is, but not enough // if an offer were added. Attempt to sell IOUs to // buy XRP. If it fully crosses, we succeed. { - Env env(*this); - env.fund (XRP(1000000), gw); + Env env {*this}; + env.fund (XRP (1000000), gw); auto const f = env.current ()->fees ().base; auto const r = reserve (env, 0); - auto const usdOffer2 = USD(500); - auto const xrpOffer2 = XRP(500); + auto const usdOffer2 = USD (500); + auto const xrpOffer2 = XRP (500); - env.fund (r + f + xrpOffer, "bob", "carol"); - env (offer ("bob", usdOffer2, xrpOffer2), ter(tesSUCCESS)); - env (offer ("carol", usdOffer, xrpOffer), ter(tesSUCCESS)); + env.fund (r + f + xrpOffer, bob, carol); + env (offer (bob, usdOffer2, xrpOffer2), ter(tesSUCCESS)); + env (offer (carol, usdOffer, xrpOffer), ter(tesSUCCESS)); - env.fund (r + f, "alice"); - env (trust ("alice", usdOffer), ter(tesSUCCESS)); - env (pay (gw, "alice", usdOffer), ter(tesSUCCESS)); - env (offer ("alice", xrpOffer, usdOffer), ter(tesSUCCESS)); + env.fund (r + f, alice); + env (trust (alice, usdOffer), ter(tesSUCCESS)); + env (pay (gw, alice, usdOffer), ter(tesSUCCESS)); + env (offer (alice, xrpOffer, usdOffer), ter(tesSUCCESS)); env.require ( - balance ("alice", r - f + xrpOffer), - balance ("alice", USD(0)), - owners ("alice", 1), - balance ("bob", r + xrpOffer2), - balance ("bob", usdOffer2), - owners ("bob", 1), - balance ("carol", r + xrpOffer2), - balance ("carol", usdOffer2), - owners ("carol", 2)); + balance (alice, r - f + xrpOffer), + balance (alice, USD(0)), + owners (alice, 1), + balance (bob, r + xrpOffer2), + balance (bob, usdOffer2), + owners (bob, 1), + balance (carol, r + xrpOffer2), + balance (carol, usdOffer2), + owners (carol, 2)); } } @@ -467,104 +526,106 @@ public: using namespace jtx; - auto const startBalance = XRP(1000000); - auto const gw = Account("gateway"); + auto const startBalance = XRP (1000000); + auto const gw = Account {"gateway"}; + auto const alice = Account {"alice"}; + auto const bob = Account {"bob"}; auto const USD = gw["USD"]; // Fill or Kill - unless we fully cross, just charge // a fee and not place the offer on the books: { - Env env(*this); + Env env {*this}; env.fund (startBalance, gw); auto const f = env.current ()->fees ().base; - env.fund (startBalance, "alice", "bob"); - env (offer ("bob", USD(500), XRP(500)), ter(tesSUCCESS)); - env (trust ("alice", USD(1000)), ter(tesSUCCESS)); - env (pay (gw, "alice", USD(1000)), ter(tesSUCCESS)); + env.fund (startBalance, alice, bob); + env (offer (bob, USD (500), XRP (500)), ter(tesSUCCESS)); + env (trust (alice, USD (1000)), ter(tesSUCCESS)); + env (pay (gw, alice, USD (1000)), ter(tesSUCCESS)); // Order that can't be filled: - env (offer ("alice", XRP(1000), USD(1000)), + env (offer (alice, XRP (1000), USD (1000)), txflags (tfFillOrKill), ter(tesSUCCESS)); env.require ( - balance ("alice", startBalance - f - f), - balance ("alice", USD(1000)), - owners ("alice", 1), - offers ("alice", 0), - balance ("bob", startBalance - f), - balance ("bob", USD(none)), - owners ("bob", 1), - offers ("bob", 1)); + balance (alice, startBalance - f - f), + balance (alice, USD (1000)), + owners (alice, 1), + offers (alice, 0), + balance (bob, startBalance - f), + balance (bob, USD (none)), + owners (bob, 1), + offers (bob, 1)); // Order that can be filled - env (offer ("alice", XRP(500), USD(500)), + env (offer (alice, XRP (500), USD (500)), txflags (tfFillOrKill), ter(tesSUCCESS)); env.require ( - balance ("alice", startBalance - f - f - f + XRP(500)), - balance ("alice", USD(500)), - owners ("alice", 1), - offers ("alice", 0), - balance ("bob", startBalance - f - XRP(500)), - balance ("bob", USD(500)), - owners ("bob", 1), - offers ("bob", 0)); + balance (alice, startBalance - f - f - f + XRP (500)), + balance (alice, USD (500)), + owners (alice, 1), + offers (alice, 0), + balance (bob, startBalance - f - XRP (500)), + balance (bob, USD (500)), + owners (bob, 1), + offers (bob, 0)); } // Immediate or Cancel - cross as much as possible // and add nothing on the books: { - Env env(*this); + Env env {*this}; env.fund (startBalance, gw); auto const f = env.current ()->fees ().base; - env.fund (startBalance, "alice", "bob"); + env.fund (startBalance, alice, bob); - env (trust ("alice", USD(1000)), ter(tesSUCCESS)); - env (pay (gw, "alice", USD(1000)), ter(tesSUCCESS)); + env (trust (alice, USD (1000)), ter(tesSUCCESS)); + env (pay (gw, alice, USD (1000)), ter(tesSUCCESS)); // No cross: - env (offer ("alice", XRP(1000), USD(1000)), + env (offer (alice, XRP (1000), USD (1000)), txflags (tfImmediateOrCancel), ter(tesSUCCESS)); env.require ( - balance ("alice", startBalance - f - f), - balance ("alice", USD(1000)), - owners ("alice", 1), - offers ("alice", 0)); + balance (alice, startBalance - f - f), + balance (alice, USD (1000)), + owners (alice, 1), + offers (alice, 0)); // Partially cross: - env (offer ("bob", USD(50), XRP(50)), ter(tesSUCCESS)); - env (offer ("alice", XRP(1000), USD(1000)), + env (offer (bob, USD (50), XRP (50)), ter(tesSUCCESS)); + env (offer (alice, XRP (1000), USD (1000)), txflags (tfImmediateOrCancel), ter(tesSUCCESS)); env.require ( - balance ("alice", startBalance - f - f - f + XRP(50)), - balance ("alice", USD(950)), - owners ("alice", 1), - offers ("alice", 0), - balance ("bob", startBalance - f - XRP(50)), - balance ("bob", USD(50)), - owners ("bob", 1), - offers ("bob", 0)); + balance (alice, startBalance - f - f - f + XRP (50)), + balance (alice, USD (950)), + owners (alice, 1), + offers (alice, 0), + balance (bob, startBalance - f - XRP (50)), + balance (bob, USD (50)), + owners (bob, 1), + offers (bob, 0)); // Fully cross: - env (offer ("bob", USD(50), XRP(50)), ter(tesSUCCESS)); - env (offer ("alice", XRP(50), USD(50)), + env (offer (bob, USD (50), XRP (50)), ter(tesSUCCESS)); + env (offer (alice, XRP (50), USD (50)), txflags (tfImmediateOrCancel), ter(tesSUCCESS)); env.require ( - balance ("alice", startBalance - f - f - f - f + XRP(100)), - balance ("alice", USD(900)), - owners ("alice", 1), - offers ("alice", 0), - balance ("bob", startBalance - f - f - XRP(100)), - balance ("bob", USD(100)), - owners ("bob", 1), - offers ("bob", 0)); + balance (alice, startBalance - f - f - f - f + XRP (100)), + balance (alice, USD (900)), + owners (alice, 1), + offers (alice, 0), + balance (bob, startBalance - f - f - XRP (100)), + balance (bob, USD (100)), + owners (bob, 1), + offers (bob, 0)); } } @@ -576,162 +637,163 @@ public: using namespace jtx; auto const startBalance = XRP(1000000); - auto const gw = Account("gateway"); + auto const gw = Account {"gateway"}; + auto const alice = Account {"alice"}; auto const USD = gw["USD"]; - Env env(*this); + Env env {*this}; env.fund (startBalance, gw); - env.fund (startBalance, "alice"); + env.fund (startBalance, alice); // Order that has invalid flags - env (offer ("alice", USD(1000), XRP(1000)), + env (offer (alice, USD (1000), XRP (1000)), txflags (tfImmediateOrCancel + 1), ter(temINVALID_FLAG)); env.require ( - balance ("alice", startBalance), - owners ("alice", 0), - offers ("alice", 0)); + balance (alice, startBalance), + owners (alice, 0), + offers (alice, 0)); // Order with incompatible flags - env (offer ("alice", USD(1000), XRP(1000)), + env (offer (alice, USD (1000), XRP (1000)), txflags (tfImmediateOrCancel | tfFillOrKill), ter(temINVALID_FLAG)); env.require ( - balance ("alice", startBalance), - owners ("alice", 0), - offers ("alice", 0)); + balance (alice, startBalance), + owners (alice, 0), + offers (alice, 0)); // Sell and buy the same asset { // Alice tries an XRP to XRP order: - env (offer ("alice", XRP(1000), XRP(1000)), ter(temBAD_OFFER)); + env (offer (alice, XRP (1000), XRP (1000)), ter(temBAD_OFFER)); env.require ( - owners ("alice", 0), - offers ("alice", 0)); + owners (alice, 0), + offers (alice, 0)); // Alice tries an IOU to IOU order: - env (trust ("alice", USD(1000)), ter(tesSUCCESS)); - env (pay (gw, "alice", USD(1000)), ter(tesSUCCESS)); - env (offer ("alice", USD(1000), USD(1000)), ter(temREDUNDANT)); + env (trust (alice, USD (1000)), ter(tesSUCCESS)); + env (pay (gw, alice, USD (1000)), ter(tesSUCCESS)); + env (offer (alice, USD (1000), USD (1000)), ter(temREDUNDANT)); env.require ( - owners ("alice", 1), - offers ("alice", 0)); + owners (alice, 1), + offers (alice, 0)); } // Offers with negative amounts { - env (offer ("alice", -USD(1000), XRP(1000)), ter(temBAD_OFFER)); + env (offer (alice, -USD (1000), XRP (1000)), ter(temBAD_OFFER)); env.require ( - owners ("alice", 1), - offers ("alice", 0)); + owners (alice, 1), + offers (alice, 0)); - env (offer ("alice", USD(1000), -XRP(1000)), ter(temBAD_OFFER)); + env (offer (alice, USD (1000), -XRP (1000)), ter(temBAD_OFFER)); env.require ( - owners ("alice", 1), - offers ("alice", 0)); + owners (alice, 1), + offers (alice, 0)); } // Offer with a bad expiration { - Json::StaticString const key ("Expiration"); + Json::StaticString const key {"Expiration"}; - env (offer ("alice", USD(1000), XRP(1000)), + env (offer (alice, USD (1000), XRP (1000)), json (key, std::uint32_t (0)), ter(temBAD_EXPIRATION)); env.require ( - owners ("alice", 1), - offers ("alice", 0)); + owners (alice, 1), + offers (alice, 0)); } // Offer with a bad offer sequence { - Json::StaticString const key ("OfferSequence"); - - env (offer ("alice", USD(1000), XRP(1000)), - json (key, std::uint32_t (0)), ter(temBAD_SEQUENCE)); + env (offer (alice, USD (1000), XRP (1000)), + json (jss::OfferSequence, std::uint32_t (0)), ter(temBAD_SEQUENCE)); env.require ( - owners ("alice", 1), - offers ("alice", 0)); + owners (alice, 1), + offers (alice, 0)); } // Use XRP as a currency code { auto const BAD = IOU(gw, badCurrency()); - env (offer ("alice", XRP(1000), BAD(1000)), ter(temBAD_CURRENCY)); + env (offer (alice, XRP (1000), BAD (1000)), ter(temBAD_CURRENCY)); env.require ( - owners ("alice", 1), - offers ("alice", 0)); + owners (alice, 1), + offers (alice, 0)); } } void - testExpiration () + testExpiration() { testcase ("Offer Expiration"); using namespace jtx; - auto const gw = Account("gateway"); + auto const gw = Account {"gateway"}; + auto const alice = Account {"alice"}; + auto const bob = Account {"bob"}; auto const USD = gw["USD"]; - auto const startBalance = XRP(1000000); - auto const usdOffer = USD(1000); - auto const xrpOffer = XRP(1000); + auto const startBalance = XRP (1000000); + auto const usdOffer = USD (1000); + auto const xrpOffer = XRP (1000); Json::StaticString const key ("Expiration"); Env env(*this); - env.fund (startBalance, gw, "alice", "bob"); + env.fund (startBalance, gw, alice, bob); env.close(); auto const f = env.current ()->fees ().base; // Place an offer that should have already expired - env (trust ("alice", usdOffer), ter(tesSUCCESS)); - env (pay (gw, "alice", usdOffer), ter(tesSUCCESS)); + env (trust (alice, usdOffer), ter(tesSUCCESS)); + env (pay (gw, alice, usdOffer), ter(tesSUCCESS)); env.close(); env.require ( - balance ("alice", startBalance - f), - balance ("alice", usdOffer), - offers ("alice", 0), - owners ("alice", 1)); + balance (alice, startBalance - f), + balance (alice, usdOffer), + offers (alice, 0), + owners (alice, 1)); - env (offer ("alice", xrpOffer, usdOffer), + env (offer (alice, xrpOffer, usdOffer), json (key, lastClose(env)), ter(tesSUCCESS)); env.require ( - balance ("alice", startBalance - f - f), - balance ("alice", usdOffer), - offers ("alice", 0), - owners ("alice", 1)); + balance (alice, startBalance - f - f), + balance (alice, usdOffer), + offers (alice, 0), + owners (alice, 1)); env.close(); // Add an offer that's expires before the next ledger close - env (offer ("alice", xrpOffer, usdOffer), + env (offer (alice, xrpOffer, usdOffer), json (key, lastClose(env) + 1), ter(tesSUCCESS)); env.require ( - balance ("alice", startBalance - f - f - f), - balance ("alice", usdOffer), - offers ("alice", 1), - owners ("alice", 2)); + balance (alice, startBalance - f - f - f), + balance (alice, usdOffer), + offers (alice, 1), + owners (alice, 2)); // The offer expires (it's not removed yet) env.close (); env.require ( - balance ("alice", startBalance - f - f - f), - balance ("alice", usdOffer), - offers ("alice", 1), - owners ("alice", 2)); + balance (alice, startBalance - f - f - f), + balance (alice, usdOffer), + offers (alice, 1), + owners (alice, 2)); // Add offer - the expired offer is removed - env (offer ("bob", usdOffer, xrpOffer), ter(tesSUCCESS)); + env (offer (bob, usdOffer, xrpOffer), ter(tesSUCCESS)); env.require ( - balance ("alice", startBalance - f - f - f), - balance ("alice", usdOffer), - offers ("alice", 0), - owners ("alice", 1), - balance ("bob", startBalance - f), - balance ("bob", USD(none)), - offers ("bob", 1), - owners ("bob", 1)); + balance (alice, startBalance - f - f - f), + balance (alice, usdOffer), + offers (alice, 0), + owners (alice, 1), + balance (bob, startBalance - f), + balance (bob, USD (none)), + offers (bob, 1), + owners (bob, 1)); } void @@ -741,11 +803,11 @@ public: using namespace jtx; - auto const gw = Account("gateway"); + auto const gw = Account {"gateway"}; auto const USD = gw["USD"]; - auto const usdOffer = USD(1000); - auto const xrpOffer = XRP(1000); + auto const usdOffer = USD (1000); + auto const xrpOffer = XRP (1000); Env env(*this); env.fund (XRP(1000000), gw); @@ -795,6 +857,905 @@ public: owners ("eve", 1)); } + void + testSelfCross(bool use_partner) + { + testcase (std::string("Self-crossing") + + (use_partner ? ", with partner account" : "")); + + using namespace jtx; + + auto const gw = Account {"gateway"}; + auto const partner = Account {"partner"}; + auto const USD = gw["USD"]; + auto const BTC = gw["BTC"]; + + Env env {*this}; + env.fund (XRP (10000), gw); + if (use_partner) + { + env.fund (XRP (10000), partner); + env (trust (partner, USD (100))); + env (trust (partner, BTC (500))); + env (pay (gw, partner, USD(100))); + env (pay (gw, partner, BTC(500))); + } + auto const& account_to_test = use_partner ? partner : gw; + + env.close(); + env.require (offers (account_to_test, 0)); + + // PART 1: + // we will make two offers that can be used to bridge BTC to USD + // through XRP + env (offer (account_to_test, BTC (250), XRP (1000)), + offers (account_to_test, 1)); + + // validate that the book now shows a BTC for XRP offer + BEAST_EXPECT(isOffer(env, account_to_test, BTC(250), XRP(1000))); + + auto const secondLegSeq = env.seq(account_to_test); + env (offer (account_to_test, XRP(1000), USD (50)), + offers (account_to_test, 2)); + + // validate that the book also shows a XRP for USD offer + BEAST_EXPECT(isOffer(env, account_to_test, XRP(1000), USD(50))); + + // now make an offer that will cross and autobridge, meaning + // the outstanding offers will be taken leaving us with none + env (offer (account_to_test, USD (50), BTC (250))); + + // NOTE : + // at this point, all offers are expected to be consumed. + // alas, they are not - because of bug in the current autobridging + // implementation (to be replaced in the not-so-distant future). + // The current implementation (incorrect) leaves an empty offer in the + // second leg of the bridge. validate the current behavior as-is and + // expect this test to be changed in the future. + env.require (offers (account_to_test, 1)); + + auto jrr = getBookOffers(env, USD, BTC); + BEAST_EXPECT(jrr[jss::offers].isArray()); + BEAST_EXPECT(jrr[jss::offers].size() == 0); + + jrr = getBookOffers(env, BTC, XRP); + BEAST_EXPECT(jrr[jss::offers].isArray()); + BEAST_EXPECT(jrr[jss::offers].size() == 0); + + BEAST_EXPECT(isOffer(env, account_to_test, XRP(0), USD(0))); + + // cancel that lingering second offer so that it doesn't interfere with the + // next set of offers we test. this will not be needed once the bridging + // bug is fixed + Json::Value cancelOffer; + cancelOffer[jss::Account] = account_to_test.human(); + cancelOffer[jss::OfferSequence] = secondLegSeq; + cancelOffer[jss::TransactionType] = "OfferCancel"; + env (cancelOffer); + env.require (offers (account_to_test, 0)); + + // PART 2: + // simple direct crossing BTC to USD and then USD to BTC which causes + // the first offer to be replaced + env (offer (account_to_test, BTC (250), USD (50)), + offers (account_to_test, 1)); + + // validate that the book shows one BTC for USD offer and no USD for + // BTC offers + BEAST_EXPECT(isOffer(env, account_to_test, BTC(250), USD(50))); + + jrr = getBookOffers(env, USD, BTC); + BEAST_EXPECT(jrr[jss::offers].isArray()); + BEAST_EXPECT(jrr[jss::offers].size() == 0); + + // this second offer would self-cross directly, so it causes the first + // offer by the same owner/taker to be removed + env (offer (account_to_test, USD (50), BTC (250)), + offers (account_to_test, 1)); + + // validate that we now have just the second offer...the first was removed + jrr = getBookOffers(env, BTC, USD); + BEAST_EXPECT(jrr[jss::offers].isArray()); + BEAST_EXPECT(jrr[jss::offers].size() == 0); + + BEAST_EXPECT(isOffer(env, account_to_test, USD(50), BTC(250))); + } + + void + testNegativeBalance() + { + // This test creates an offer test for negative balance + // with transfer fees and miniscule funds. + testcase ("Negative Balance"); + + using namespace jtx; + + Env env {*this}; + auto const gw = Account {"gateway"}; + auto const alice = Account {"alice"}; + auto const bob = Account {"bob"}; + auto const USD = gw["USD"]; + auto const BTC = gw["BTC"]; + + // these *interesting* amounts were taken + // from the original JS test that was ported here + auto const gw_initial_balance = 1149999730; + auto const alice_initial_balance = 499946999680; + auto const bob_initial_balance = 10199999920; + auto const small_amount = + STAmount{ bob["USD"].issue(), UINT64_C(2710505431213761), -33}; + + env.fund (drops (gw_initial_balance), gw); + env.fund (drops (alice_initial_balance), alice); + env.fund (drops (bob_initial_balance), bob); + + env (rate (gw, 1.005)); + + env (trust (alice, USD (500))); + env (trust (bob, USD (50))); + env (trust (gw, alice["USD"] (100))); + + env (pay (gw, alice, alice["USD"] (50))); + env (pay (gw, bob, small_amount)); + + env (offer (alice, USD (50), XRP (150000))); + + // unfund the offer + env (pay (alice, gw, USD (100))); + + // drop the trust line (set to 0) + env (trust (gw, alice["USD"] (0))); + + // verify balances + auto jrr = ledgerEntryState (env, alice, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "50"); + + jrr = ledgerEntryState (env, bob, gw, "USD"); + BEAST_EXPECT( + jrr[jss::node][sfBalance.fieldName][jss::value] == + "-2710505431213761e-33"); + + // create crossing offer + env (offer (bob, XRP (2000), USD (1))); + + // verify balances again + jrr = ledgerEntryState (env, alice, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "50"); + jrr = ledgerEntryRoot (env, alice); + BEAST_EXPECT( + jrr[jss::node][sfBalance.fieldName] == + std::to_string( + alice_initial_balance - env.current ()->fees ().base * 3) + ); + + jrr = ledgerEntryState (env, bob, gw, "USD"); + BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName][jss::value] == "0"); + jrr = ledgerEntryRoot (env, bob); + BEAST_EXPECT( + jrr[jss::node][sfBalance.fieldName] == + std::to_string( + bob_initial_balance - env.current ()->fees ().base * 2) + ); + } + + void + testOfferCrossWithXRP(bool reverse_order) + { + testcase (std::string("Offer Crossing with XRP, ") + + (reverse_order ? "Reverse" : "Normal") + + " order"); + + using namespace jtx; + + Env env {*this}; + auto const gw = Account {"gateway"}; + auto const alice = Account {"alice"}; + auto const bob = Account {"bob"}; + auto const USD = gw["USD"]; + + env.fund (XRP (10000), gw, alice, bob); + + env (trust (alice, USD (1000))); + env (trust (bob, USD (1000))); + + env (pay (gw, alice, alice["USD"] (500))); + + if (reverse_order) + env (offer (bob, USD (1), XRP (4000))); + + env (offer (alice, XRP (150000), USD (50))); + + if (! reverse_order) + env (offer (bob, USD (1), XRP (4000))); + + // Existing offer pays better than this wants. + // Fully consume existing offer. + // Pay 1 USD, get 4000 XRP. + + auto jrr = ledgerEntryState (env, bob, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-1"); + jrr = ledgerEntryRoot (env, bob); + BEAST_EXPECT( + jrr[jss::node][sfBalance.fieldName] == + std::to_string( + XRP (10000).value ().mantissa () - + XRP (reverse_order ? 4000 : 3000).value ().mantissa () - + env.current ()->fees ().base * 2) + ); + + jrr = ledgerEntryState (env, alice, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-499"); + jrr = ledgerEntryRoot (env, alice); + BEAST_EXPECT( + jrr[jss::node][sfBalance.fieldName] == + std::to_string( + XRP (10000).value ().mantissa( ) + + XRP(reverse_order ? 4000 : 3000).value ().mantissa () - + env.current ()->fees ().base * 2) + ); + } + + void + testOfferCrossWithLimitOverride() + { + testcase ("Offer Crossing with Limit Override"); + + using namespace jtx; + + Env env {*this}; + auto const gw = Account {"gateway"}; + auto const alice = Account {"alice"}; + auto const bob = Account {"bob"}; + auto const USD = gw["USD"]; + + env.fund (XRP (100000), gw, alice, bob); + + env (trust (alice, USD (1000))); + + env (pay (gw, alice, alice["USD"] (500))); + + env (offer (alice, XRP (150000), USD (50))); + env (offer (bob, USD (1), XRP (3000))); + + auto jrr = ledgerEntryState (env, bob, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-1"); + jrr = ledgerEntryRoot(env, bob); + BEAST_EXPECT( + jrr[jss::node][sfBalance.fieldName] == + std::to_string( + XRP (100000).value ().mantissa () - + XRP (3000).value ().mantissa () - + env.current ()->fees ().base * 1) + ); + + jrr = ledgerEntryState (env, alice, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-499"); + jrr = ledgerEntryRoot (env, alice); + BEAST_EXPECT( + jrr[jss::node][sfBalance.fieldName] == + std::to_string( + XRP (100000).value ().mantissa () + + XRP (3000).value ().mantissa () - + env.current ()->fees ().base * 2) + ); + } + + void + testOfferAcceptThenCancel() + { + testcase ("Offer Accept then Cancel."); + + using namespace jtx; + + Env env {*this}; + auto const USD = env.master["USD"]; + + auto const nextOfferSeq = env.seq (env.master); + env (offer (env.master, XRP (500), USD (100))); + env.close (); + + Json::Value cancelOffer; + cancelOffer[jss::Account] = env.master.human(); + cancelOffer[jss::OfferSequence] = nextOfferSeq; + cancelOffer[jss::TransactionType] = "OfferCancel"; + env (cancelOffer); + BEAST_EXPECT(env.seq (env.master) == nextOfferSeq + 2); + + // ledger_accept, call twice and verify no odd behavior + env.close(); + env.close(); + BEAST_EXPECT(env.seq (env.master) == nextOfferSeq + 2); + } + + void + testOfferCancelPastAndFuture() + { + + testcase ("Offer Cancel Past and Future Sequence."); + + using namespace jtx; + + Env env {*this}; + auto const alice = Account {"alice"}; + + auto const nextOfferSeq = env.seq (env.master); + env.fund (XRP (10000), alice); + + Json::Value cancelOffer; + cancelOffer[jss::Account] = env.master.human(); + cancelOffer[jss::OfferSequence] = nextOfferSeq; + cancelOffer[jss::TransactionType] = "OfferCancel"; + env (cancelOffer); + + cancelOffer[jss::OfferSequence] = env.seq (env.master); + env (cancelOffer, ter(temBAD_SEQUENCE)); + + cancelOffer[jss::OfferSequence] = env.seq (env.master) + 1; + env (cancelOffer, ter(temBAD_SEQUENCE)); + + env.close(); + env.close(); + } + + void + testCurrencyConversionEntire() + { + testcase ("Currency Conversion: Entire Offer"); + + using namespace jtx; + + Env env {*this}; + auto const gw = Account {"gateway"}; + auto const alice = Account {"alice"}; + auto const bob = Account {"bob"}; + auto const USD = gw["USD"]; + + env.fund (XRP (10000), gw, alice, bob); + env.require (owners (bob, 0)); + + env (trust (alice, USD (100))); + env (trust (bob, USD (1000))); + + env.require ( + owners (alice, 1), + owners (bob, 1)); + + env (pay (gw, alice, alice["USD"] (100))); + auto const bobOfferSeq = env.seq (bob); + env (offer (bob, USD (100), XRP (500))); + + env.require ( + owners (alice, 1), + owners (bob, 2)); + auto jro = ledgerEntryOffer (env, bob, bobOfferSeq); + BEAST_EXPECT( + jro[jss::node][jss::TakerGets] == XRP (500).value ().getText ()); + BEAST_EXPECT( + jro[jss::node][jss::TakerPays] == USD (100).value ().getJson (0)); + + env (pay (alice, alice, XRP (500)), sendmax (USD (100))); + + auto jrr = ledgerEntryState (env, alice, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "0"); + jrr = ledgerEntryRoot (env, alice); + BEAST_EXPECT( + jrr[jss::node][sfBalance.fieldName] == + std::to_string( + XRP (10000).value ().mantissa () + + XRP (500).value ().mantissa () - + env.current ()->fees ().base * 2) + ); + + jrr = ledgerEntryState (env, bob, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-100"); + + jro = ledgerEntryOffer(env, bob, bobOfferSeq); + BEAST_EXPECT(jro[jss::error] == "entryNotFound"); + + env.require ( + owners (alice, 1), + owners (bob, 1)); + } + + void + testCurrencyConversionIntoDebt() + { + testcase ("Currency Conversion: Offerer Into Debt"); + + using namespace jtx; + + Env env {*this}; + auto const alice = Account {"alice"}; + auto const bob = Account {"bob"}; + auto const carol = Account {"carol"}; + + env.fund (XRP (10000), alice, bob, carol); + + env (trust (alice, carol["EUR"] (2000))); + env (trust (bob, alice["USD"] (100))); + env (trust (carol, bob["EUR"] (1000))); + + auto const bobOfferSeq = env.seq (bob); + env (offer (bob, alice["USD"] (50), carol["EUR"] (200)), + ter(tecUNFUNDED_OFFER)); + + env (offer (alice, carol["EUR"] (200), alice["USD"] (50))); + + auto jro = ledgerEntryOffer(env, bob, bobOfferSeq); + BEAST_EXPECT(jro[jss::error] == "entryNotFound"); + } + + void + testCurrencyConversionInParts() + { + testcase ("Currency Conversion: In Parts"); + + using namespace jtx; + + Env env {*this}; + auto const gw = Account {"gateway"}; + auto const alice = Account {"alice"}; + auto const bob = Account {"bob"}; + auto const USD = gw["USD"]; + + env.fund (XRP (10000), gw, alice, bob); + + env (trust (alice, USD (200))); + env (trust (bob, USD (1000))); + + env (pay (gw, alice, alice["USD"] (200))); + + auto const bobOfferSeq = env.seq (bob); + env (offer (bob, USD (100), XRP (500))); + + env (pay (alice, alice, XRP (200)), sendmax (USD (100))); + + // The previous payment reduced the remaining offer amount by 200 XRP + auto jro = ledgerEntryOffer (env, bob, bobOfferSeq); + BEAST_EXPECT( + jro[jss::node][jss::TakerGets] == XRP (300).value ().getText ()); + BEAST_EXPECT( + jro[jss::node][jss::TakerPays] == USD (60).value ().getJson (0)); + + // the balance between alice and gw is 160 USD..200 less the 40 taken + // by the offer + auto jrr = ledgerEntryState (env, alice, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-160"); + // alice now has 200 more XRP from the payment + jrr = ledgerEntryRoot (env, alice); + BEAST_EXPECT( + jrr[jss::node][sfBalance.fieldName] == + std::to_string( + XRP (10000).value ().mantissa () + + XRP (200).value ().mantissa () - + env.current ()->fees ().base * 2) + ); + + // bob got 40 USD from partial consumption of the offer + jrr = ledgerEntryState (env, bob, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-40"); + + // Alice converts USD to XRP which should fail + // due to PartialPayment. + env (pay (alice, alice, XRP (600)), sendmax (USD (100)), + ter(tecPATH_PARTIAL)); + + // Alice converts USD to XRP, should succeed because + // we permit partial payment + env (pay (alice, alice, XRP (600)), sendmax (USD (100)), + txflags (tfPartialPayment)); + + // Verify the offer was consumed + jro = ledgerEntryOffer (env, bob, bobOfferSeq); + BEAST_EXPECT(jro[jss::error] == "entryNotFound"); + + // verify balances look right after the partial payment + // only 300 XRP should be have been payed since that's all + // that remained in the offer from bob. The alice balance is now + // 100 USD because another 60 USD were transferred to bob in the second + // payment + jrr = ledgerEntryState (env, alice, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-100"); + jrr = ledgerEntryRoot (env, alice); + BEAST_EXPECT( + jrr[jss::node][sfBalance.fieldName] == + std::to_string( + XRP (10000).value ().mantissa () + + XRP (200).value ().mantissa () + + XRP (300).value ().mantissa () - + env.current ()->fees ().base * 4) + ); + + // bob now has 100 USD - 40 from the first payment and 60 from the + // second (partial) payment + jrr = ledgerEntryState (env, bob, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-100"); + } + + void + testCrossCurrencyStartXRP() + { + testcase ("Cross Currency Payment: Start with XRP"); + + using namespace jtx; + + Env env {*this}; + auto const gw = Account {"gateway"}; + auto const alice = Account {"alice"}; + auto const bob = Account {"bob"}; + auto const carol = Account {"carol"}; + auto const USD = gw["USD"]; + + env.fund (XRP (10000), gw, alice, bob, carol); + + env (trust (carol, USD (1000))); + env (trust (bob, USD (2000))); + + env (pay (gw, carol, carol["USD"] (500))); + + auto const carolOfferSeq = env.seq (carol); + env (offer (carol, XRP (500), USD (50))); + + env (pay (alice, bob, USD (25)), sendmax (XRP (333))); + + auto jrr = ledgerEntryState (env, bob, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-25"); + + jrr = ledgerEntryState (env, carol, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-475"); + + auto jro = ledgerEntryOffer (env, carol, carolOfferSeq); + BEAST_EXPECT( + jro[jss::node][jss::TakerGets] == USD (25).value ().getJson (0)); + BEAST_EXPECT( + jro[jss::node][jss::TakerPays] == XRP (250).value ().getText ()); + } + + void + testCrossCurrencyEndXRP() + { + testcase ("Cross Currency Payment: End with XRP"); + + using namespace jtx; + + Env env {*this}; + auto const gw = Account {"gateway"}; + auto const alice = Account {"alice"}; + auto const bob = Account {"bob"}; + auto const carol = Account {"carol"}; + auto const USD = gw["USD"]; + + env.fund (XRP (10000), gw, alice, bob, carol); + + env (trust (alice, USD (1000))); + env (trust (carol, USD (2000))); + + env (pay (gw, alice, alice["USD"] (500))); + + auto const carolOfferSeq = env.seq (carol); + env (offer (carol, USD (50), XRP (500))); + + env (pay (alice, bob, XRP (250)), sendmax (USD (333))); + + auto jrr = ledgerEntryState (env, alice, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-475"); + + jrr = ledgerEntryState (env, carol, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-25"); + + jrr = ledgerEntryRoot (env, bob); + BEAST_EXPECT( + jrr[jss::node][sfBalance.fieldName] == + std::to_string( + XRP (10000).value ().mantissa () + + XRP (250).value ().mantissa ()) + ); + + auto jro = ledgerEntryOffer (env, carol, carolOfferSeq); + BEAST_EXPECT( + jro[jss::node][jss::TakerGets] == XRP (250).value ().getText ()); + BEAST_EXPECT( + jro[jss::node][jss::TakerPays] == USD (25).value ().getJson (0)); + } + + void + testCrossCurrencyBridged() + { + testcase ("Cross Currency Payment: Bridged"); + + using namespace jtx; + + Env env {*this}; + auto const gw1 = Account {"gateway_1"}; + auto const gw2 = Account {"gateway_2"}; + auto const alice = Account {"alice"}; + auto const bob = Account {"bob"}; + auto const carol = Account {"carol"}; + auto const dan = Account {"dan"}; + auto const USD = gw1["USD"]; + auto const EUR = gw2["EUR"]; + + env.fund (XRP (10000), gw1, gw2, alice, bob, carol, dan); + + env (trust (alice, USD (1000))); + env (trust (bob, EUR (1000))); + env (trust (carol, USD (1000))); + env (trust (dan, EUR (1000))); + + env (pay (gw1, alice, alice["USD"] (500))); + env (pay (gw2, dan, dan["EUR"] (400))); + + auto const carolOfferSeq = env.seq (carol); + env (offer (carol, USD (50), XRP (500))); + + auto const danOfferSeq = env.seq (dan); + env (offer (dan, XRP (500), EUR (50))); + + Json::Value jtp {Json::arrayValue}; + jtp[0u][0u][jss::currency] = "XRP"; + env (pay (alice, bob, EUR (30)), + json (jss::Paths, jtp), + sendmax (USD (333))); + + auto jrr = ledgerEntryState (env, alice, gw1, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "470"); + + jrr = ledgerEntryState (env, bob, gw2, "EUR"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-30"); + + jrr = ledgerEntryState (env, carol, gw1, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-30"); + + jrr = ledgerEntryState (env, dan, gw2, "EUR"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-370"); + + auto jro = ledgerEntryOffer (env, carol, carolOfferSeq); + BEAST_EXPECT( + jro[jss::node][jss::TakerGets] == XRP (200).value ().getText ()); + BEAST_EXPECT( + jro[jss::node][jss::TakerPays] == USD (20).value ().getJson (0)); + + jro = ledgerEntryOffer (env, dan, danOfferSeq); + BEAST_EXPECT( + jro[jss::node][jss::TakerGets] == gw2["EUR"] (20).value ().getJson (0)); + BEAST_EXPECT( + jro[jss::node][jss::TakerPays] == XRP (200).value ().getText ()); + } + + void + testOfferFeesConsumeFunds() + { + testcase ("Offer Fees Consume Funds"); + + using namespace jtx; + + Env env {*this}; + auto const gw1 = Account {"gateway_1"}; + auto const gw2 = Account {"gateway_2"}; + auto const gw3 = Account {"gateway_3"}; + auto const alice = Account {"alice"}; + auto const bob = Account {"bob"}; + auto const USD1 = gw1["USD"]; + auto const USD2 = gw2["USD"]; + auto const USD3 = gw3["USD"]; + + // Provide micro amounts to compensate for fees to make results round + // nice. + // reserve: Alice has 3 entries in the ledger, via trust lines + // fees: + // 1 for each trust limit == 3 (alice < mtgox/amazon/bitstamp) + + // 1 for payment == 4 + auto const starting_xrp = XRP (100) + + env.current ()->fees ().accountReserve (3) + + env.current ()->fees ().base * 4; + + env.fund (starting_xrp, gw1, gw2, gw3, alice, bob); + + env (trust (alice, USD1 (1000))); + env (trust (alice, USD2 (1000))); + env (trust (alice, USD3 (1000))); + env (trust (bob, USD1 (1000))); + env (trust (bob, USD2 (1000))); + + env (pay (gw1, bob, bob["USD"] (500))); + + env (offer (bob, XRP (200), USD1 (200))); + // Alice has 350 fees - a reserve of 50 = 250 reserve = 100 available. + // Ask for more than available to prove reserve works. + env (offer (alice, USD1 (200), XRP (200))); + + auto jrr = ledgerEntryState (env, alice, gw1, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "100"); + jrr = ledgerEntryRoot (env, alice); + BEAST_EXPECT( + jrr[jss::node][sfBalance.fieldName] == XRP (350).value ().getText () + ); + + jrr = ledgerEntryState (env, bob, gw1, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-400"); + } + + void + testOfferCreateThenCross() + { + testcase ("Offer Create, then Cross"); + + using namespace jtx; + + Env env {*this}; + auto const gw = Account {"gateway"}; + auto const alice = Account {"alice"}; + auto const bob = Account {"bob"}; + auto const USD = gw["USD"]; + + env.fund (XRP (10000), gw, alice, bob); + + env (rate (gw, 1.005)); + + env (trust (alice, USD (1000))); + env (trust (bob, USD (1000))); + env (trust (gw, alice["USD"] (50))); + + env (pay (gw, bob, bob["USD"] (1))); + env (pay (alice, gw, USD (50))); + + env (trust (gw, alice["USD"] (0))); + + env (offer (alice, USD (50), XRP (150000))); + env (offer (bob, XRP (100), USD (0.1))); + + auto jrr = ledgerEntryState (env, alice, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "49.96666666666667"); + jrr = ledgerEntryState (env, bob, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-0.966500000033334"); + } + + void + testSellFlagBasic() + { + testcase ("Offer tfSell: Basic Sell"); + + using namespace jtx; + + Env env {*this}; + auto const gw = Account {"gateway"}; + auto const alice = Account {"alice"}; + auto const bob = Account {"bob"}; + auto const USD = gw["USD"]; + + auto const starting_xrp = XRP (100) + + env.current ()->fees ().accountReserve (1) + + env.current ()->fees ().base * 2; + + env.fund (starting_xrp, gw, alice, bob); + + env (trust (alice, USD (1000))); + env (trust (bob, USD (1000))); + + env (pay (gw, bob, bob["USD"] (500))); + + env (offer (bob, XRP (200), USD (200)), json(jss::Flags, tfSell)); + // Alice has 350 + fees - a reserve of 50 = 250 reserve = 100 available. + // Alice has 350 + fees - a reserve of 50 = 250 reserve = 100 available. + // Ask for more than available to prove reserve works. + env (offer (alice, USD (200), XRP (200)), json(jss::Flags, tfSell)); + + auto jrr = ledgerEntryState (env, alice, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-100"); + jrr = ledgerEntryRoot (env, alice); + BEAST_EXPECT( + jrr[jss::node][sfBalance.fieldName] == XRP (250).value ().getText () + ); + + jrr = ledgerEntryState (env, bob, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-400"); + } + + void + testSellFlagExceedLimit() + { + testcase ("Offer tfSell: 2x Sell Exceed Limit"); + + using namespace jtx; + + Env env {*this}; + auto const gw = Account {"gateway"}; + auto const alice = Account {"alice"}; + auto const bob = Account {"bob"}; + auto const USD = gw["USD"]; + + auto const starting_xrp = XRP (100) + + env.current ()->fees ().accountReserve (1) + + env.current ()->fees ().base * 2; + + env.fund (starting_xrp, gw, alice, bob); + + env (trust (alice, USD (150))); + env (trust (bob, USD (1000))); + + env (pay (gw, bob, bob["USD"] (500))); + + env (offer (bob, XRP (100), USD (200))); + // Alice has 350 fees - a reserve of 50 = 250 reserve = 100 available. + // Ask for more than available to prove reserve works. + // Taker pays 100 USD for 100 XRP. + // Selling XRP. + // Will sell all 100 XRP and get more USD than asked for. + env (offer (alice, USD (100), XRP (100)), json(jss::Flags, tfSell)); + + auto jrr = ledgerEntryState (env, alice, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-200"); + jrr = ledgerEntryRoot (env, alice); + BEAST_EXPECT( + jrr[jss::node][sfBalance.fieldName] == XRP (250).value ().getText () + ); + + jrr = ledgerEntryState (env, bob, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-300"); + } + + void + testGatewayCrossCurrency() + { + testcase ("Client Issue #535: Gateway Cross Currency"); + + using namespace jtx; + + Env env {*this}; + auto const gw = Account {"gateway"}; + auto const alice = Account {"alice"}; + auto const bob = Account {"bob"}; + auto const XTS = gw["XTS"]; + auto const XXX = gw["XXX"]; + + auto const starting_xrp = XRP (100.1) + + env.current ()->fees ().accountReserve (1) + + env.current ()->fees ().base * 2; + + env.fund (starting_xrp, gw, alice, bob); + + env (trust (alice, XTS (1000))); + env (trust (alice, XXX (1000))); + env (trust (bob, XTS (1000))); + env (trust (bob, XXX (1000))); + + env (pay (gw, alice, alice["XTS"] (100))); + env (pay (gw, alice, alice["XXX"] (100))); + env (pay (gw, bob, bob["XTS"] (100))); + env (pay (gw, bob, bob["XXX"] (100))); + + env (offer (alice, XTS (100), XXX (100))); + + //WS client is used here because the RPC client could not + //be convinced to pass the build_path argument + auto wsc = makeWSClient(env.app().config()); + Json::Value payment; + payment[jss::secret] = toBase58(generateSeed("bob")); + payment[jss::id] = env.seq (bob); + payment[jss::build_path] = true; + payment[jss::tx_json] = pay (bob, bob, bob["XXX"] (1)); + payment[jss::tx_json][jss::Sequence] = + env.current ()->read ( + keylet::account (bob.id ()))->getFieldU32 (sfSequence); + payment[jss::tx_json][jss::Fee] = + std::to_string( env.current ()->fees ().base); + payment[jss::tx_json][jss::SendMax] = + bob ["XTS"] (1.5).value ().getJson (0); + auto jrr = wsc->invoke("submit", payment); + BEAST_EXPECT(jrr[jss::status] == "success"); + BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "tesSUCCESS"); + + jrr = ledgerEntryState (env, alice, gw, "XTS"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-101"); + jrr = ledgerEntryState (env, alice, gw, "XXX"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-99"); + + jrr = ledgerEntryState (env, bob, gw, "XTS"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-99"); + jrr = ledgerEntryState (env, bob, gw, "XXX"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-101"); + } + void run () { testCanceledOffer (); @@ -807,6 +1768,25 @@ public: testMalformed (); testExpiration (); testUnfundedCross (); + testSelfCross (false); + testSelfCross (true); + testNegativeBalance (); + testOfferCrossWithXRP (true); + testOfferCrossWithXRP (false); + testOfferCrossWithLimitOverride (); + testOfferAcceptThenCancel (); + testOfferCancelPastAndFuture (); + testCurrencyConversionEntire (); + testCurrencyConversionIntoDebt (); + testCurrencyConversionInParts (); + testCrossCurrencyStartXRP (); + testCrossCurrencyEndXRP (); + testCrossCurrencyBridged (); + testOfferFeesConsumeFunds (); + testOfferCreateThenCross (); + testSellFlagBasic (); + testSellFlagExceedLimit (); + testGatewayCrossCurrency (); } }; @@ -814,3 +1794,7 @@ BEAST_DEFINE_TESTSUITE (Offer, tx, ripple); } // test } // ripple + + + +