//------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled Copyright (c) 2012-2017 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 #include namespace ripple { namespace test { class Offer_test : public beast::unit_test::suite { static bool hasFeature(uint256 const& feat, std::initializer_list args) { for(auto const& f : args) if (f == feat) return true; return false; } XRPAmount reserve(jtx::Env& env, std::uint32_t count) { return env.current()->fees().accountReserve (count); } std::uint32_t lastClose (jtx::Env& env) { return env.current()->info().parentCloseTime.time_since_epoch().count(); } static auto xrpMinusFee (jtx::Env const& env, std::int64_t xrpAmount) -> jtx::PrettyAmount { using namespace jtx; 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 (std::initializer_list fs) { testcase ("Incorrect Removal of Funded Offers"); // We need at least two paths. One at good quality and one at bad // quality. The bad quality path needs two offer books in a row. // Each offer book should have two offers at the same quality, the // offers should be completely consumed, and the payment should // should require both offers to be satisfied. The first offer must // be "taker gets" XRP. Old, broken would remove the first // "taker gets" xrp offer, even though the offer is still funded and // not used for the payment. using namespace jtx; Env env {*this, with_features(fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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"}; env.fund (XRP (10000), alice, bob, carol, gw); env.trust (USD (1000), alice, bob, carol); env.trust (BTC (1000), alice, bob, carol); env (pay (gw, alice, BTC (1000))); env (pay (gw, carol, USD (1000))); env (pay (gw, carol, BTC (1000))); // Must be two offers at the same quality // "taker gets" must be XRP // (Different amounts so I can distinguish the offers) env (offer (carol, BTC (49), XRP (49))); env (offer (carol, BTC (51), XRP (51))); // Offers for the poor quality path // Must be two offers at the same quality env (offer (carol, XRP (50), USD (50))); env (offer (carol, XRP (50), USD (50))); // Offers for the good quality path env (offer (carol, BTC (1), USD (100))); PathSet paths (Path (XRP, USD), Path (USD)); 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))); } void testCanceledOffer (std::initializer_list fs) { testcase ("Removing Canceled Offers"); using namespace jtx; Env env {*this, with_features(fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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 (pay (gw, alice, USD (50))); auto const firstOfferSeq = env.seq (alice); env (offer (alice, XRP (500), USD (100)), require (offers (alice, 1))); 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 (jss::OfferSequence, firstOfferSeq), require (offers (alice, 1))); BEAST_EXPECT(isOffer (env, alice, XRP (300), USD (100)) && !isOffer (env, alice, XRP (500), USD (100))); // 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))); // 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 (std::initializer_list fs) { testcase ("Tiny payments"); // 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 USD = gw["USD"]; auto const EUR = gw["EUR"]; Env env {*this, with_features(fs)}; env.fund (XRP (10000), alice, bob, carol, gw); env.trust (USD (1000), alice, bob, carol); env.trust (EUR (1000), alice, bob, carol); env (pay (gw, alice, USD (100))); env (pay (gw, carol, EUR (100))); // Create more offers than the loop max count in DeliverNodeReverse for (int i=0;i<101;++i) env (offer (carol, USD (1), EUR (2))); for (auto timeDelta : { - env.closed()->info().closeTimeResolution, env.closed()->info().closeTimeResolution} ) { auto const closeTime = STAmountSO::soTime + timeDelta; env.close (closeTime); *stAmountCalcSwitchover = closeTime > STAmountSO::soTime; // Will fail without the underflow fix auto expectedResult = *stAmountCalcSwitchover ? tesSUCCESS : tecPATH_PARTIAL; env (pay (alice, bob, EUR (epsilon)), path (~EUR), sendmax (USD (100)), ter (expectedResult)); } } void testXRPTinyPayment (std::initializer_list fs) { testcase ("XRP Tiny payments"); // Regression test for tiny xrp payments // In some cases, when the payment code calculates // the amount of xrp needed as input to an xrp->iou offer // it would incorrectly round the amount to zero (even when // round-up was set to true). // The bug would cause funded offers to be incorrectly removed // because the code thought they were unfunded. // The conditions to trigger the bug are: // 1) When we calculate the amount of input xrp needed for an offer // from xrp->iou, the amount is less than 1 drop (after rounding // up the float representation). // 2) There is another offer in the same book with a quality // sufficiently bad that when calculating the input amount // needed the amount is not set to zero. 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 USD = gw["USD"]; for (auto withFix : {false, true}) { if (!withFix && fs.size()) continue; Env env {*this, with_features(fs)}; auto closeTime = [&] { auto const delta = 100 * env.closed ()->info ().closeTimeResolution; if (withFix) return STAmountSO::soTime2 + delta; else return STAmountSO::soTime2 - delta; }(); env.fund (XRP (10000), alice, bob, carol, dan, erin, gw); env.trust (USD (1000), alice, bob, carol, dan, erin); env (pay (gw, carol, USD (0.99999))); env (pay (gw, dan, USD (1))); env (pay (gw, erin, USD (1))); // Carol doesn't quite have enough funds for this offer // The amount left after this offer is taken will cause // STAmount to incorrectly round to zero when the next offer // (at a good quality) is considered. (when the // stAmountCalcSwitchover2 patch is inactive) env (offer (carol, drops (1), USD (1))); // Offer at a quality poor enough so when the input xrp is // calculated in the reverse pass, the amount is not zero. env (offer (dan, XRP (100), USD (1))); env.close (closeTime); // This is the funded offer that will be incorrectly removed. // It is considered after the offer from carol, which leaves a // tiny amount left to pay. When calculating the amount of xrp // needed for this offer, it will incorrectly compute zero in both // the forward and reverse passes (when the stAmountCalcSwitchover2 // is inactive.) env (offer (erin, drops (1), USD (1))); { env (pay (alice, bob, USD (1)), path (~USD), sendmax (XRP (102)), txflags (tfNoRippleDirect | tfPartialPayment)); env.require ( offers (carol, 0), offers (dan, 1)); if (!withFix) { // funded offer was removed env.require ( balance (erin, USD (1)), offers (erin, 0)); } else { // offer was correctly consumed. There is still some // liquidity left on that offer. env.require ( balance (erin, USD (0.99999)), offers (erin, 1)); } } } } void testEnforceNoRipple (std::initializer_list fs) { testcase ("Enforce No Ripple"); using namespace jtx; 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"}; { // No ripple with an implied account step after an offer Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); auto const gw1 = Account {"gw1"}; auto const USD1 = gw1["USD"]; auto const gw2 = Account {"gw2"}; auto const USD2 = gw2["USD"]; env.fund (XRP (10000), alice, noripple (bob), carol, dan, gw1, gw2); env.trust (USD1 (1000), alice, carol, dan); env(trust (bob, USD1 (1000), tfSetNoRipple)); env.trust (USD2 (1000), alice, carol, dan); env(trust (bob, USD2 (1000), tfSetNoRipple)); env (pay (gw1, dan, USD1 (50))); env (pay (gw1, bob, USD1 (50))); env (pay (gw2, bob, USD2 (50))); env (offer (dan, XRP (50), USD1 (50))); env (pay (alice, carol, USD2 (50)), path (~USD1, bob), sendmax (XRP (50)), txflags (tfNoRippleDirect), ter(tecPATH_DRY)); } { // Make sure payment works with default flags Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); auto const gw1 = Account {"gw1"}; auto const USD1 = gw1["USD"]; auto const gw2 = Account {"gw2"}; auto const USD2 = gw2["USD"]; env.fund (XRP (10000), alice, bob, carol, dan, gw1, gw2); env.trust (USD1 (1000), alice, bob, carol, dan); env.trust (USD2 (1000), alice, bob, carol, dan); env (pay (gw1, dan, USD1 (50))); env (pay (gw1, bob, USD1 (50))); env (pay (gw2, bob, USD2 (50))); env (offer (dan, XRP (50), USD1 (50))); env (pay (alice, carol, USD2 (50)), path (~USD1, bob), sendmax (XRP (50)), txflags (tfNoRippleDirect)); env.require (balance (alice, xrpMinusFee (env, 10000 - 50))); env.require (balance (bob, USD1 (100))); env.require (balance (bob, USD2 (0))); env.require (balance (carol, USD2 (50))); } } void testInsufficientReserve (std::initializer_list fs) { testcase ("Insufficient Reserve"); // If an account places an offer and its balance // *before* the transaction began isn't high enough // to meet the reserve *after* the transaction runs, // then no offer should go on the books but if the // offer partially or fully crossed the tx succeeds. using namespace jtx; 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); // No crossing: { Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); env.fund (XRP (1000000), gw); auto const f = env.current ()->fees ().base; auto const r = reserve (env, 0); 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.require ( balance (alice, r - f), owners (alice, 1)); } // Partial cross: { Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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); 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)); } // 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, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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); 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.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)); } } // Helper function that returns the Offers on an account. static std::vector> offersOnAccount (jtx::Env& env, jtx::Account account) { std::vector> result; forEachItem (*env.current (), account, [&result](std::shared_ptr const& sle) { if (sle->getType() == ltOFFER) result.push_back (sle); }); return result; } void testFillModes (std::initializer_list fs) { testcase ("Fill Modes"); using namespace jtx; 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, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); auto const f = env.current ()->fees ().base; env.fund (startBalance, gw, 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)), 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)); // Order that can be filled 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)); } // Immediate or Cancel - cross as much as possible // and add nothing on the books: { Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); auto const f = env.current ()->fees ().base; env.fund (startBalance, gw, alice, bob); env (trust (alice, USD (1000)), ter(tesSUCCESS)); env (pay (gw, alice, USD (1000)), ter(tesSUCCESS)); // No cross: 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)); // Partially cross: 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)); // Fully cross: 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)); } // tfPassive -- place the offer without crossing it. { Env env (*this, with_features (fs)); auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); env.fund (startBalance, gw, alice, bob); env.close(); env (trust (bob, USD(1000))); env.close(); env (pay (gw, bob, USD(1000))); env.close(); env (offer (alice, USD(1000), XRP(2000))); env.close(); auto const aliceOffers = offersOnAccount (env, alice); BEAST_EXPECT (aliceOffers.size() == 1); for (auto offerPtr : aliceOffers) { auto const& offer = *offerPtr; BEAST_EXPECT (offer[sfTakerGets] == XRP (2000)); BEAST_EXPECT (offer[sfTakerPays] == USD (1000)); } // bob creates a passive offer that could cross alice's. // bob's offer should stay in the ledger. env (offer (bob, XRP(2000), USD(1000), tfPassive)); env.close(); env.require (offers (alice, 1)); auto const bobOffers = offersOnAccount (env, bob); BEAST_EXPECT (bobOffers.size() == 1); for (auto offerPtr : bobOffers) { auto const& offer = *offerPtr; BEAST_EXPECT (offer[sfTakerGets] == USD (1000)); BEAST_EXPECT (offer[sfTakerPays] == XRP (2000)); } // It should be possible for gw to cross both of those offers. env (offer (gw, XRP(2000), USD(1000))); env.close(); env.require (offers (alice, 0)); env.require (offers (gw, 0)); env.require (offers (bob, 1)); env (offer (gw, USD(1000), XRP(2000))); env.close(); env.require (offers (bob, 0)); env.require (offers (gw, 0)); } // tfPassive -- cross only offers of better quality. { Env env (*this, with_features (fs)); auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); env.fund (startBalance, gw, "alice", "bob"); env.close(); env (trust ("bob", USD(1000))); env.close(); env (pay (gw, "bob", USD(1000))); env (offer ("alice", USD(500), XRP(1001))); env.close(); env (offer ("alice", USD(500), XRP(1000))); env.close(); auto const aliceOffers = offersOnAccount (env, "alice"); BEAST_EXPECT (aliceOffers.size() == 2); // bob creates a passive offer. That offer should cross one // of alice's (the one with better quality) and leave alice's // other offer untouched. env (offer ("bob", XRP(2000), USD(1000), tfPassive)); env.close(); env.require (offers ("alice", 1)); auto const bobOffers = offersOnAccount (env, "bob"); BEAST_EXPECT (bobOffers.size() == 1); for (auto offerPtr : bobOffers) { auto const& offer = *offerPtr; BEAST_EXPECT (offer[sfTakerGets] == USD (499.5)); BEAST_EXPECT (offer[sfTakerPays] == XRP (999)); } } } void testMalformed(std::initializer_list fs) { testcase ("Malformed Detection"); using namespace jtx; auto const startBalance = XRP(1000000); auto const gw = Account {"gateway"}; auto const alice = Account {"alice"}; auto const USD = gw["USD"]; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); env.fund (startBalance, gw, alice); // Order that has invalid flags env (offer (alice, USD (1000), XRP (1000)), txflags (tfImmediateOrCancel + 1), ter(temINVALID_FLAG)); env.require ( balance (alice, startBalance), owners (alice, 0), offers (alice, 0)); // Order with incompatible flags env (offer (alice, USD (1000), XRP (1000)), txflags (tfImmediateOrCancel | tfFillOrKill), ter(temINVALID_FLAG)); env.require ( 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.require ( 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.require ( owners (alice, 1), offers (alice, 0)); } // Offers with negative amounts { env (offer (alice, -USD (1000), XRP (1000)), ter(temBAD_OFFER)); env.require ( owners (alice, 1), offers (alice, 0)); env (offer (alice, USD (1000), -XRP (1000)), ter(temBAD_OFFER)); env.require ( owners (alice, 1), offers (alice, 0)); } // Offer with a bad expiration { Json::StaticString const key {"Expiration"}; env (offer (alice, USD (1000), XRP (1000)), json (key, std::uint32_t (0)), ter(temBAD_EXPIRATION)); env.require ( owners (alice, 1), offers (alice, 0)); } // Offer with a bad offer 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)); } // Use XRP as a currency code { auto const BAD = IOU(gw, badCurrency()); env (offer (alice, XRP (1000), BAD (1000)), ter(temBAD_CURRENCY)); env.require ( owners (alice, 1), offers (alice, 0)); } } void testExpiration(std::initializer_list fs) { testcase ("Offer Expiration"); using namespace jtx; 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); Json::StaticString const key ("Expiration"); Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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.close(); env.require ( balance (alice, startBalance - f), balance (alice, usdOffer), offers (alice, 0), owners (alice, 1)); 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)); env.close(); // Add an offer that's expires before the next ledger close 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)); // 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)); // Add offer - the expired offer is removed 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)); } void testUnfundedCross(std::initializer_list fs) { testcase ("Unfunded Crossing"); using namespace jtx; auto const gw = Account {"gateway"}; auto const USD = gw["USD"]; auto const usdOffer = USD (1000); auto const xrpOffer = XRP (1000); Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); env.fund (XRP(1000000), gw); // The fee that's charged for transactions auto const f = env.current ()->fees ().base; // Account is at the reserve, and will dip below once // fees are subtracted. env.fund (reserve (env, 0), "alice"); env (offer ("alice", usdOffer, xrpOffer), ter(tecUNFUNDED_OFFER)); env.require ( balance ("alice", reserve (env, 0) - f), owners ("alice", 0)); // Account has just enough for the reserve and the // fee. env.fund (reserve (env, 0) + f, "bob"); env (offer ("bob", usdOffer, xrpOffer), ter(tecUNFUNDED_OFFER)); env.require ( balance ("bob", reserve (env, 0)), owners ("bob", 0)); // Account has enough for the reserve, the fee and // the offer, and a bit more, but not enough for the // reserve after the offer is placed. env.fund (reserve (env, 0) + f + XRP(1), "carol"); env (offer ("carol", usdOffer, xrpOffer), ter(tecINSUF_RESERVE_OFFER)); env.require ( balance ("carol", reserve (env, 0) + XRP(1)), owners ("carol", 0)); // Account has enough for the reserve plus one // offer, and the fee. env.fund (reserve (env, 1) + f, "dan"); env (offer ("dan", usdOffer, xrpOffer), ter(tesSUCCESS)); env.require ( balance ("dan", reserve (env, 1)), owners ("dan", 1)); // Account has enough for the reserve plus one // offer, the fee and the entire offer amount. env.fund (reserve (env, 1) + f + xrpOffer, "eve"); env (offer ("eve", usdOffer, xrpOffer), ter(tesSUCCESS)); env.require ( balance ("eve", reserve (env, 1) + xrpOffer), owners ("eve", 1)); } void testSelfCross(bool use_partner, std::initializer_list fs) { 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, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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 auto-bridge, meaning // the outstanding offers will be taken leaving us with none env (offer (account_to_test, USD (50), BTC (250))); 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); // NOTE : // at this point, all offers are expected to be consumed. // alas, they are not - because of a bug in the Taker auto-bridging // 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 both the old and the new // behavior. { auto acctOffers = offersOnAccount (env, account_to_test); BEAST_EXPECT(acctOffers.size() == (hasFeature (featureFlowCross, fs) ? 0 : 1)); for (auto const& offerPtr : acctOffers) { auto const& offer = *offerPtr; BEAST_EXPECT (offer[sfLedgerEntryType] == ltOFFER); BEAST_EXPECT (offer[sfTakerGets] == USD (0)); BEAST_EXPECT (offer[sfTakerPays] == XRP (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(std::initializer_list fs) { // This test creates an offer test for negative balance // with transfer fees and miniscule funds. testcase ("Negative Balance"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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 = drops (1149999730); auto const alice_initial_balance = drops (499946999680); auto const bob_initial_balance = drops (10199999920); auto const small_amount = STAmount { bob["USD"].issue(), UINT64_C(2710505431213761), -33}; env.fund (gw_initial_balance, gw); env.fund (alice_initial_balance, alice); env.fund (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. // // NOTE : // Here a difference in the rounding modes of our two offer crossing // algorithms becomes apparent. The old offer crossing would consume // small_amount and transfer no XRP. The new offer crossing transfers // a single drop, rather than no drops. auto const crossingDelta = (hasFeature (featureFlowCross, fs) ? drops (1) : drops (0)); jrr = ledgerEntryState (env, alice, gw, "USD"); BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "50"); BEAST_EXPECT(env.balance (alice, xrpIssue()) == alice_initial_balance - env.current ()->fees ().base * 3 - crossingDelta ); jrr = ledgerEntryState (env, bob, gw, "USD"); BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName][jss::value] == "0"); BEAST_EXPECT(env.balance (bob, xrpIssue()) == bob_initial_balance - env.current ()->fees ().base * 2 + crossingDelta ); } void testOfferCrossWithXRP(bool reverse_order, std::initializer_list fs) { testcase (std::string("Offer Crossing with XRP, ") + (reverse_order ? "Reverse" : "Normal") + " order"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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(std::initializer_list fs) { testcase ("Offer Crossing with Limit Override"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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(std::initializer_list fs) { testcase ("Offer Accept then Cancel."); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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(std::initializer_list fs) { testcase ("Offer Cancel Past and Future Sequence."); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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(std::initializer_list fs) { testcase ("Currency Conversion: Entire Offer"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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(std::initializer_list fs) { testcase ("Currency Conversion: Offerer Into Debt"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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(std::initializer_list fs) { testcase ("Currency Conversion: In Parts"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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(std::initializer_list fs) { testcase ("Cross Currency Payment: Start with XRP"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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(std::initializer_list fs) { testcase ("Cross Currency Payment: End with XRP"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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(std::initializer_list fs) { testcase ("Cross Currency Payment: Bridged"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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(std::initializer_list fs) { testcase ("Offer Fees Consume Funds"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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(std::initializer_list fs) { testcase ("Offer Create, then Cross"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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(std::initializer_list fs) { testcase ("Offer tfSell: Basic Sell"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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(std::initializer_list fs) { testcase ("Offer tfSell: 2x Sell Exceed Limit"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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(std::initializer_list fs) { testcase ("Client Issue #535: Gateway Cross Currency"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); 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"); if (wsc->version() == 2) { BEAST_EXPECT(jrr.isMember(jss::jsonrpc) && jrr[jss::jsonrpc] == "2.0"); BEAST_EXPECT(jrr.isMember(jss::ripplerpc) && jrr[jss::ripplerpc] == "2.0"); BEAST_EXPECT(jrr.isMember(jss::id) && jrr[jss::id] == 5); } 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"); } // Helper function that validates a *defaulted* trustline. If the // trustline is not defaulted then the tests will not pass. void verifyDefaultTrustline (jtx::Env& env, jtx::Account const& account, jtx::PrettyAmount const& expectBalance) { auto const sleTrust = env.le (keylet::line(account.id(), expectBalance.value().issue())); BEAST_EXPECT (sleTrust); if (sleTrust) { Issue const issue = expectBalance.value().issue(); bool const accountLow = account.id() < issue.account; STAmount low {issue}; STAmount high {issue}; low.setIssuer (accountLow ? account.id() : issue.account); high.setIssuer (accountLow ? issue.account : account.id()); BEAST_EXPECT (sleTrust->getFieldAmount (sfLowLimit) == low); BEAST_EXPECT (sleTrust->getFieldAmount (sfHighLimit) == high); STAmount actualBalance {sleTrust->getFieldAmount (sfBalance)}; if (! accountLow) actualBalance.negate(); BEAST_EXPECT (actualBalance == expectBalance); } } void testPartialCross (std::initializer_list fs) { // Test a number of different corner cases regarding adding a // possibly crossable offer to an account. The test is table // driven so it should be easy to add or remove tests. testcase ("Partial Crossing"); using namespace jtx; auto const gw = Account("gateway"); auto const USD = gw["USD"]; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); env.fund (XRP(10000000), gw); // The fee that's charged for transactions auto const f = env.current ()->fees ().base; // To keep things simple all offers are 1 : 1 for XRP : USD. enum preTrustType {noPreTrust, gwPreTrust, acctPreTrust}; struct TestData { std::string account; // Account operated on STAmount fundXrp; // Account funded with int bookAmount; // USD -> XRP offer on the books preTrustType preTrust; // If true, pre-establish trust line int offerAmount; // Account offers this much XRP -> USD TER tec; // Returned tec code STAmount spentXrp; // Amount removed from fundXrp PrettyAmount balanceUsd; // Balance on account end int offers; // Offers on account int owners; // Owners on account }; TestData const tests[] { //acct fundXrp bookAmt preTrust offerAmt tec spentXrp balanceUSD offers owners {"ann", reserve (env, 0) + 0*f, 1, noPreTrust, 1000, tecUNFUNDED_OFFER, f, USD( 0), 0, 0}, // Account is at the reserve, and will dip below once fees are subtracted. {"bev", reserve (env, 0) + 1*f, 1, noPreTrust, 1000, tecUNFUNDED_OFFER, f, USD( 0), 0, 0}, // Account has just enough for the reserve and the fee. {"cam", reserve (env, 0) + 2*f, 0, noPreTrust, 1000, tecINSUF_RESERVE_OFFER, f, USD( 0), 0, 0}, // Account has enough for the reserve, the fee and the offer, and a bit more, but not enough for the reserve after the offer is placed. {"deb", reserve (env, 0) + 2*f, 1, noPreTrust, 1000, tesSUCCESS, 2*f, USD(0.00001), 0, 1}, // Account has enough to buy a little USD then the offer runs dry. {"eve", reserve (env, 1) + 0*f, 0, noPreTrust, 1000, tesSUCCESS, f, USD( 0), 1, 1}, // No offer to cross {"flo", reserve (env, 1) + 0*f, 1, noPreTrust, 1000, tesSUCCESS, XRP( 1) + f, USD( 1), 0, 1}, {"gay", reserve (env, 1) + 1*f, 1000, noPreTrust, 1000, tesSUCCESS, XRP( 50) + f, USD( 50), 0, 1}, {"hye", XRP(1000) + 1*f, 1000, noPreTrust, 1000, tesSUCCESS, XRP( 800) + f, USD( 800), 0, 1}, {"ivy", XRP( 1) + reserve (env, 1) + 1*f, 1, noPreTrust, 1000, tesSUCCESS, XRP( 1) + f, USD( 1), 0, 1}, {"joy", XRP( 1) + reserve (env, 2) + 1*f, 1, noPreTrust, 1000, tesSUCCESS, XRP( 1) + f, USD( 1), 1, 2}, {"kim", XRP( 900) + reserve (env, 2) + 1*f, 999, noPreTrust, 1000, tesSUCCESS, XRP( 999) + f, USD( 999), 0, 1}, {"liz", XRP( 998) + reserve (env, 0) + 1*f, 999, noPreTrust, 1000, tesSUCCESS, XRP( 998) + f, USD( 998), 0, 1}, {"meg", XRP( 998) + reserve (env, 1) + 1*f, 999, noPreTrust, 1000, tesSUCCESS, XRP( 999) + f, USD( 999), 0, 1}, {"nia", XRP( 998) + reserve (env, 2) + 1*f, 999, noPreTrust, 1000, tesSUCCESS, XRP( 999) + f, USD( 999), 1, 2}, {"ova", XRP( 999) + reserve (env, 0) + 1*f, 1000, noPreTrust, 1000, tesSUCCESS, XRP( 999) + f, USD( 999), 0, 1}, {"pam", XRP( 999) + reserve (env, 1) + 1*f, 1000, noPreTrust, 1000, tesSUCCESS, XRP(1000) + f, USD( 1000), 0, 1}, {"rae", XRP( 999) + reserve (env, 2) + 1*f, 1000, noPreTrust, 1000, tesSUCCESS, XRP(1000) + f, USD( 1000), 0, 1}, {"sue", XRP(1000) + reserve (env, 2) + 1*f, 0, noPreTrust, 1000, tesSUCCESS, f, USD( 0), 1, 1}, //---------------------Pre-established trust lines ----------------------------- {"abe", reserve (env, 0) + 0*f, 1, gwPreTrust, 1000, tecUNFUNDED_OFFER, f, USD( 0), 0, 0}, {"bud", reserve (env, 0) + 1*f, 1, gwPreTrust, 1000, tecUNFUNDED_OFFER, f, USD( 0), 0, 0}, {"che", reserve (env, 0) + 2*f, 0, gwPreTrust, 1000, tecINSUF_RESERVE_OFFER, f, USD( 0), 0, 0}, {"dan", reserve (env, 0) + 2*f, 1, gwPreTrust, 1000, tesSUCCESS, 2*f, USD(0.00001), 0, 0}, {"eli", XRP( 20) + reserve (env, 0) + 1*f, 1000, gwPreTrust, 1000, tesSUCCESS, XRP(20) + 1*f, USD( 20), 0, 0}, {"fyn", reserve (env, 1) + 0*f, 0, gwPreTrust, 1000, tesSUCCESS, f, USD( 0), 1, 1}, {"gar", reserve (env, 1) + 0*f, 1, gwPreTrust, 1000, tesSUCCESS, XRP( 1) + f, USD( 1), 1, 1}, {"hal", reserve (env, 1) + 1*f, 1, gwPreTrust, 1000, tesSUCCESS, XRP( 1) + f, USD( 1), 1, 1}, {"ned", reserve (env, 1) + 0*f, 1, acctPreTrust, 1000, tecUNFUNDED_OFFER, 2*f, USD( 0), 0, 1}, {"ole", reserve (env, 1) + 1*f, 1, acctPreTrust, 1000, tecUNFUNDED_OFFER, 2*f, USD( 0), 0, 1}, {"pat", reserve (env, 1) + 2*f, 0, acctPreTrust, 1000, tecUNFUNDED_OFFER, 2*f, USD( 0), 0, 1}, {"quy", reserve (env, 1) + 2*f, 1, acctPreTrust, 1000, tecUNFUNDED_OFFER, 2*f, USD( 0), 0, 1}, {"ron", reserve (env, 1) + 3*f, 0, acctPreTrust, 1000, tecINSUF_RESERVE_OFFER, 2*f, USD( 0), 0, 1}, {"syd", reserve (env, 1) + 3*f, 1, acctPreTrust, 1000, tesSUCCESS, 3*f, USD(0.00001), 0, 1}, {"ted", XRP( 20) + reserve (env, 1) + 2*f, 1000, acctPreTrust, 1000, tesSUCCESS, XRP(20) + 2*f, USD( 20), 0, 1}, {"uli", reserve (env, 2) + 0*f, 0, acctPreTrust, 1000, tecINSUF_RESERVE_OFFER, 2*f, USD( 0), 0, 1}, {"vic", reserve (env, 2) + 0*f, 1, acctPreTrust, 1000, tesSUCCESS, XRP( 1) + 2*f, USD( 1), 0, 1}, {"wes", reserve (env, 2) + 1*f, 0, acctPreTrust, 1000, tesSUCCESS, 2*f, USD( 0), 1, 2}, {"xan", reserve (env, 2) + 1*f, 1, acctPreTrust, 1000, tesSUCCESS, XRP( 1) + 2*f, USD( 1), 1, 2}, }; for (auto const& t : tests) { auto const acct = Account(t.account); env.fund (t.fundXrp, acct); env.close(); // Make sure gateway has no current offers. env.require (offers (gw, 0)); // The gateway optionally creates an offer that would be crossed. auto const book = t.bookAmount; if (book) env (offer (gw, XRP (book), USD (book))); env.close(); std::uint32_t const gwOfferSeq = env.seq (gw) - 1; // Optionally pre-establish a trustline between gw and acct. if (t.preTrust == gwPreTrust) env (trust (gw, acct["USD"] (1))); // Optionally pre-establish a trustline between acct and gw. // Note this is not really part of the test, so we expect there // to be enough XRP reserve for acct to create the trust line. if (t.preTrust == acctPreTrust) env (trust (acct, USD (1))); env.close(); // Acct creates an offer. This is the heart of the test. auto const acctOffer = t.offerAmount; env (offer (acct, USD (acctOffer), XRP (acctOffer)), ter (t.tec)); env.close(); std::uint32_t const acctOfferSeq = env.seq (acct) - 1; BEAST_EXPECT (env.balance (acct, USD.issue()) == t.balanceUsd); BEAST_EXPECT ( env.balance (acct, xrpIssue()) == t.fundXrp - t.spentXrp); env.require (offers (acct, t.offers)); env.require (owners (acct, t.owners)); auto acctOffers = offersOnAccount (env, acct); BEAST_EXPECT (acctOffers.size() == t.offers); if (acctOffers.size() && t.offers) { auto const& acctOffer = *(acctOffers.front()); auto const leftover = t.offerAmount - t.bookAmount; BEAST_EXPECT (acctOffer[sfTakerGets] == XRP (leftover)); BEAST_EXPECT (acctOffer[sfTakerPays] == USD (leftover)); } if (t.preTrust == noPreTrust) { if (t.balanceUsd.value().signum()) { // Verify the correct contents of the trustline verifyDefaultTrustline (env, acct, t.balanceUsd); } else { // Verify that no trustline was created. auto const sleTrust = env.le (keylet::line(acct, USD.issue())); BEAST_EXPECT (! sleTrust); } } // Give the next loop a clean slate by canceling any left-overs // in the offers. env (offer_cancel (acct, acctOfferSeq)); env (offer_cancel (gw, gwOfferSeq)); env.close(); } } void testXRPDirectCross (std::initializer_list fs) { testcase ("XRP Direct Crossing"); using namespace jtx; auto const gw = Account("gateway"); auto const alice = Account("alice"); auto const bob = Account("bob"); auto const USD = gw["USD"]; auto const usdOffer = USD(1000); auto const xrpOffer = XRP(1000); Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); env.fund (XRP(1000000), gw, bob); env.close(); // The fee that's charged for transactions. auto const fee = env.current ()->fees ().base; // alice's account has enough for the reserve, one trust line plus two // offers, and two fees. env.fund (reserve (env, 2) + (fee * 2), alice); env.close(); env (trust(alice, usdOffer)); env.close(); env (pay(gw, alice, usdOffer)); env.close(); env.require ( balance (alice, usdOffer), offers (alice, 0), offers (bob, 0)); // The scenario: // o alice has USD but wants XRP. // o bob has XRP but wants USD. auto const alicesXRP = env.balance (alice); auto const bobsXRP = env.balance (bob); env (offer (alice, xrpOffer, usdOffer)); env.close(); env (offer (bob, usdOffer, xrpOffer)); env.close(); env.require ( balance (alice, USD(0)), balance (bob, usdOffer), balance (alice, alicesXRP + xrpOffer - fee), balance (bob, bobsXRP - xrpOffer - fee), offers (alice, 0), offers (bob, 0)); verifyDefaultTrustline (env, bob, usdOffer); // Make two more offers that leave one of the offers non-dry. env (offer (alice, USD(999), XRP(999))); env (offer (bob, xrpOffer, usdOffer)); env.close(); env.require (balance (alice, USD(999))); env.require (balance (bob, USD(1))); env.require (offers (alice, 0)); verifyDefaultTrustline (env, bob, USD(1)); { auto const bobsOffers = offersOnAccount (env, bob); BEAST_EXPECT (bobsOffers.size() == 1); auto const& bobsOffer = *(bobsOffers.front()); BEAST_EXPECT (bobsOffer[sfLedgerEntryType] == ltOFFER); BEAST_EXPECT (bobsOffer[sfTakerGets] == USD (1)); BEAST_EXPECT (bobsOffer[sfTakerPays] == XRP (1)); } } void testDirectCross (std::initializer_list fs) { testcase ("Direct Crossing"); using namespace jtx; auto const gw = Account("gateway"); auto const alice = Account("alice"); auto const bob = Account("bob"); auto const USD = gw["USD"]; auto const EUR = gw["EUR"]; auto const usdOffer = USD(1000); auto const eurOffer = EUR(1000); Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); env.fund (XRP(1000000), gw); env.close(); // The fee that's charged for transactions. auto const fee = env.current ()->fees ().base; // Each account has enough for the reserve, two trust lines, one // offer, and two fees. env.fund (reserve (env, 3) + (fee * 3), alice); env.fund (reserve (env, 3) + (fee * 2), bob); env.close(); env (trust(alice, usdOffer)); env (trust(bob, eurOffer)); env.close(); env (pay(gw, alice, usdOffer)); env (pay(gw, bob, eurOffer)); env.close(); env.require ( balance (alice, usdOffer), balance (bob, eurOffer)); // The scenario: // o alice has USD but wants EUR. // o bob has EUR but wants USD. env (offer (alice, eurOffer, usdOffer)); env (offer (bob, usdOffer, eurOffer)); env.close(); env.require ( balance (alice, eurOffer), balance (bob, usdOffer), offers (alice, 0), offers (bob, 0)); verifyDefaultTrustline (env, alice, eurOffer); verifyDefaultTrustline (env, bob, usdOffer); // Make two more offers that leave one of the offers non-dry. env (offer (alice, USD(999), eurOffer)); env (offer (bob, eurOffer, usdOffer)); env.close(); env.require (balance (alice, USD(999))); env.require (balance (alice, EUR(1))); env.require (balance (bob, USD(1))); env.require (balance (bob, EUR(999))); env.require (offers (alice, 0)); verifyDefaultTrustline (env, alice, EUR(1)); verifyDefaultTrustline (env, bob, USD(1)); { auto bobsOffers = offersOnAccount (env, bob); BEAST_EXPECT (bobsOffers.size() == 1); auto const& bobsOffer = *(bobsOffers.front()); BEAST_EXPECT (bobsOffer[sfTakerGets] == USD (1)); BEAST_EXPECT (bobsOffer[sfTakerPays] == EUR (1)); } // alice makes one more offer that cleans out bob's offer. env (offer (alice, USD(1), EUR(1))); env.close(); env.require (balance (alice, USD(1000))); env.require (balance (alice, EUR(none))); env.require (balance (bob, USD(none))); env.require (balance (bob, EUR(1000))); env.require (offers (alice, 0)); env.require (offers (bob, 0)); // The two trustlines that were generated by offers should be gone. BEAST_EXPECT (! env.le (keylet::line (alice.id(), EUR.issue()))); BEAST_EXPECT (! env.le (keylet::line (bob.id(), USD.issue()))); } void testBridgedCross (std::initializer_list fs) { testcase ("Bridged Crossing"); using namespace jtx; 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 EUR = gw["EUR"]; auto const usdOffer = USD(1000); auto const eurOffer = EUR(1000); Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); env.fund (XRP(1000000), gw, alice, bob, carol); env.close(); env (trust(alice, usdOffer)); env (trust(carol, eurOffer)); env.close(); env (pay(gw, alice, usdOffer)); env (pay(gw, carol, eurOffer)); env.close(); // The scenario: // o alice has USD but wants XPR. // o bob has XRP but wants EUR. // o carol has EUR but wants USD. // Note that carol's offer must come last. If carol's offer is placed // before bob's or alice's, then autobridging will not occur. env (offer (alice, XRP(1000), usdOffer)); env (offer (bob, eurOffer, XRP(1000))); auto const bobXrpBalance = env.balance (bob); env.close(); // carol makes an offer that partially consumes alice and bob's offers. env (offer (carol, USD(400), EUR(400))); env.close(); env.require ( balance (alice, USD(600)), balance (bob, EUR(400)), balance (carol, USD(400)), balance (bob, bobXrpBalance - XRP(400)), offers (carol, 0)); verifyDefaultTrustline (env, bob, EUR(400)); verifyDefaultTrustline (env, carol, USD(400)); { auto const alicesOffers = offersOnAccount (env, alice); BEAST_EXPECT (alicesOffers.size() == 1); auto const& alicesOffer = *(alicesOffers.front()); BEAST_EXPECT (alicesOffer[sfLedgerEntryType] == ltOFFER); BEAST_EXPECT (alicesOffer[sfTakerGets] == USD (600)); BEAST_EXPECT (alicesOffer[sfTakerPays] == XRP (600)); } { auto const bobsOffers = offersOnAccount (env, bob); BEAST_EXPECT (bobsOffers.size() == 1); auto const& bobsOffer = *(bobsOffers.front()); BEAST_EXPECT (bobsOffer[sfLedgerEntryType] == ltOFFER); BEAST_EXPECT (bobsOffer[sfTakerGets] == XRP (600)); BEAST_EXPECT (bobsOffer[sfTakerPays] == EUR (600)); } // carol makes an offer that exactly consumes alice and bob's offers. env (offer (carol, USD(600), EUR(600))); env.close(); env.require ( balance (alice, USD(0)), balance (bob, eurOffer), balance (carol, usdOffer), balance (bob, bobXrpBalance - XRP(1000)), offers (bob, 0), offers (carol, 0)); verifyDefaultTrustline (env, bob, EUR(1000)); verifyDefaultTrustline (env, carol, USD(1000)); // In pre-flow code alice's offer is left empty in the ledger. auto const alicesOffers = offersOnAccount (env, alice); if (alicesOffers.size() != 0) { BEAST_EXPECT (alicesOffers.size() == 1); auto const& alicesOffer = *(alicesOffers.front()); BEAST_EXPECT (alicesOffer[sfLedgerEntryType] == ltOFFER); BEAST_EXPECT (alicesOffer[sfTakerGets] == USD (0)); BEAST_EXPECT (alicesOffer[sfTakerPays] == XRP (0)); } } void testSellOffer (std::initializer_list fs) { // Test a number of different corner cases regarding offer crossing // when the tfSell flag is set. The test is table driven so it // should be easy to add or remove tests. testcase ("Sell Offer"); using namespace jtx; auto const gw = Account("gateway"); auto const USD = gw["USD"]; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); env.fund (XRP(10000000), gw); // The fee that's charged for transactions auto const f = env.current ()->fees ().base; // To keep things simple all offers are 1 : 1 for XRP : USD. enum preTrustType {noPreTrust, gwPreTrust, acctPreTrust}; struct TestData { std::string account; // Account operated on STAmount fundXrp; // XRP acct funded with STAmount fundUSD; // USD acct funded with STAmount gwGets; // gw's offer STAmount gwPays; // STAmount acctGets; // acct's offer STAmount acctPays; // TER tec; // Returned tec code STAmount spentXrp; // Amount removed from fundXrp STAmount finalUsd; // Final USD balance on acct int offers; // Offers on acct int owners; // Owners on acct STAmount takerGets; // Remainder of acct's offer STAmount takerPays; // // Constructor with takerGets/takerPays TestData ( std::string&& account_, // Account operated on STAmount const& fundXrp_, // XRP acct funded with STAmount const& fundUSD_, // USD acct funded with STAmount const& gwGets_, // gw's offer STAmount const& gwPays_, // STAmount const& acctGets_, // acct's offer STAmount const& acctPays_, // TER tec_, // Returned tec code STAmount const& spentXrp_, // Amount removed from fundXrp STAmount const& finalUsd_, // Final USD balance on acct int offers_, // Offers on acct int owners_, // Owners on acct STAmount const& takerGets_, // Remainder of acct's offer STAmount const& takerPays_) // : account (std::move(account_)) , fundXrp (fundXrp_) , fundUSD (fundUSD_) , gwGets (gwGets_) , gwPays (gwPays_) , acctGets (acctGets_) , acctPays (acctPays_) , tec (tec_) , spentXrp (spentXrp_) , finalUsd (finalUsd_) , offers (offers_) , owners (owners_) , takerGets (takerGets_) , takerPays (takerPays_) { } // Constructor without takerGets/takerPays TestData ( std::string&& account_, // Account operated on STAmount const& fundXrp_, // XRP acct funded with STAmount const& fundUSD_, // USD acct funded with STAmount const& gwGets_, // gw's offer STAmount const& gwPays_, // STAmount const& acctGets_, // acct's offer STAmount const& acctPays_, // TER tec_, // Returned tec code STAmount const& spentXrp_, // Amount removed from fundXrp STAmount const& finalUsd_, // Final USD balance on acct int offers_, // Offers on acct int owners_) // Owners on acct : TestData (std::move(account_), fundXrp_, fundUSD_, gwGets_, gwPays_, acctGets_, acctPays_, tec_, spentXrp_, finalUsd_, offers_, owners_, STAmount {0}, STAmount {0}) { } }; TestData const tests[] { // acct pays XRP //acct fundXrp fundUSD gwGets gwPays acctGets acctPays tec spentXrp finalUSD offers owners takerGets takerPays {"ann", XRP(10) + reserve (env, 0) + 1*f, USD( 0), XRP(10), USD( 5), USD(10), XRP(10), tecINSUF_RESERVE_OFFER, XRP( 0) + (1*f), USD( 0), 0, 0}, {"bev", XRP(10) + reserve (env, 1) + 1*f, USD( 0), XRP(10), USD( 5), USD(10), XRP(10), tesSUCCESS, XRP( 0) + (1*f), USD( 0), 1, 1, XRP(10), USD(10)}, {"cam", XRP(10) + reserve (env, 0) + 1*f, USD( 0), XRP(10), USD(10), USD(10), XRP(10), tesSUCCESS, XRP(10) + (1*f), USD(10), 0, 1}, {"deb", XRP(10) + reserve (env, 0) + 1*f, USD( 0), XRP(10), USD(20), USD(10), XRP(10), tesSUCCESS, XRP(10) + (1*f), USD(20), 0, 1}, {"eve", XRP(10) + reserve (env, 0) + 1*f, USD( 0), XRP(10), USD(20), USD( 5), XRP( 5), tesSUCCESS, XRP( 5) + (1*f), USD(10), 0, 1}, {"flo", XRP(10) + reserve (env, 0) + 1*f, USD( 0), XRP(10), USD(20), USD(20), XRP(20), tesSUCCESS, XRP(10) + (1*f), USD(20), 0, 1}, {"gay", XRP(20) + reserve (env, 1) + 1*f, USD( 0), XRP(10), USD(20), USD(20), XRP(20), tesSUCCESS, XRP(10) + (1*f), USD(20), 0, 1}, {"hye", XRP(20) + reserve (env, 2) + 1*f, USD( 0), XRP(10), USD(20), USD(20), XRP(20), tesSUCCESS, XRP(10) + (1*f), USD(20), 1, 2, XRP(10), USD(10)}, // acct pays USD {"meg", reserve (env, 1) + 2*f, USD(10), USD(10), XRP( 5), XRP(10), USD(10), tecINSUF_RESERVE_OFFER, XRP( 0) + (2*f), USD(10), 0, 1}, {"nia", reserve (env, 2) + 2*f, USD(10), USD(10), XRP( 5), XRP(10), USD(10), tesSUCCESS, XRP( 0) + (2*f), USD(10), 1, 2, USD(10), XRP(10)}, {"ova", reserve (env, 1) + 2*f, USD(10), USD(10), XRP(10), XRP(10), USD(10), tesSUCCESS, XRP(-10) + (2*f), USD( 0), 0, 1}, {"pam", reserve (env, 1) + 2*f, USD(10), USD(10), XRP(20), XRP(10), USD(10), tesSUCCESS, XRP(-20) + (2*f), USD( 0), 0, 1}, {"qui", reserve (env, 1) + 2*f, USD(10), USD(20), XRP(40), XRP(10), USD(10), tesSUCCESS, XRP(-20) + (2*f), USD( 0), 0, 1}, {"rae", reserve (env, 2) + 2*f, USD(10), USD( 5), XRP( 5), XRP(10), USD(10), tesSUCCESS, XRP( -5) + (2*f), USD( 5), 1, 2, USD( 5), XRP( 5)}, {"sue", reserve (env, 2) + 2*f, USD(10), USD( 5), XRP(10), XRP(10), USD(10), tesSUCCESS, XRP(-10) + (2*f), USD( 5), 1, 2, USD( 5), XRP( 5)}, }; auto const zeroUsd = USD(0); for (auto const& t : tests) { // Make sure gateway has no current offers. env.require (offers (gw, 0)); auto const acct = Account(t.account); env.fund (t.fundXrp, acct); env.close(); // Optionally give acct some USD. This is not part of the test, // so we assume that acct has sufficient USD to cover the reserve // on the trust line. if (t.fundUSD != zeroUsd) { env (trust (acct, t.fundUSD)); env.close(); env (pay (gw, acct, t.fundUSD)); env.close(); } env (offer (gw, t.gwGets, t.gwPays)); env.close(); std::uint32_t const gwOfferSeq = env.seq (gw) - 1; // Acct creates a tfSell offer. This is the heart of the test. env (offer (acct, t.acctGets, t.acctPays, tfSell), ter (t.tec)); env.close(); std::uint32_t const acctOfferSeq = env.seq (acct) - 1; // Check results BEAST_EXPECT (env.balance (acct, USD.issue()) == t.finalUsd); BEAST_EXPECT ( env.balance (acct, xrpIssue()) == t.fundXrp - t.spentXrp); env.require (offers (acct, t.offers)); env.require (owners (acct, t.owners)); if (t.offers) { auto const acctOffers = offersOnAccount (env, acct); if (acctOffers.size() > 0) { BEAST_EXPECT (acctOffers.size() == 1); auto const& acctOffer = *(acctOffers.front()); BEAST_EXPECT (acctOffer[sfLedgerEntryType] == ltOFFER); BEAST_EXPECT (acctOffer[sfTakerGets] == t.takerGets); BEAST_EXPECT (acctOffer[sfTakerPays] == t.takerPays); } } // Give the next loop a clean slate by canceling any left-overs // in the offers. env (offer_cancel (acct, acctOfferSeq)); env (offer_cancel (gw, gwOfferSeq)); env.close(); } } void testSellWithFillOrKill (std::initializer_list fs) { // Test a number of different corner cases regarding offer crossing // when both the tfSell flag and tfFillOrKill flags are set. testcase ("Combine tfSell with tfFillOrKill"); using namespace jtx; auto const gw = Account("gateway"); auto const alice = Account("alice"); auto const bob = Account("bob"); auto const USD = gw["USD"]; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); env.fund (XRP(10000000), gw, alice, bob); // bob offers XRP for USD. env (trust(bob, USD(200))); env.close(); env (pay(gw, bob, USD(100))); env.close(); env (offer(bob, XRP(2000), USD(20))); env.close(); { // alice submits a tfSell | tfFillOrKill offer that does not cross. // It's still a tesSUCCESS, since the offer was successfully killed. env (offer(alice, USD(21), XRP(2100), tfSell | tfFillOrKill)); env.close(); env.require (balance (alice, USD(none))); env.require (offers (alice, 0)); env.require (balance (bob, USD(100))); } { // alice submits a tfSell | tfFillOrKill offer that crosses. // Even though tfSell is present it doesn't matter this time. env (offer(alice, USD(20), XRP(2000), tfSell | tfFillOrKill)); env.close(); env.require (balance (alice, USD(20))); env.require (offers (alice, 0)); env.require (balance (bob, USD(80))); } { // alice submits a tfSell | tfFillOrKill offer that crosses and // returns more than was asked for (because of the tfSell flag). env (offer(bob, XRP(2000), USD(20))); env.close(); env (offer(alice, USD(10), XRP(1500), tfSell | tfFillOrKill)); env.close(); env.require (balance (alice, USD(35))); env.require (offers (alice, 0)); env.require (balance (bob, USD(65))); } { // alice submits a tfSell | tfFillOrKill offer that doesn't cross. // This would have succeeded with a regular tfSell, but the // fillOrKill prevents the transaction from crossing since not // all of the offer is consumed. // We're using bob's left-over offer for XRP(500), USD(5) env (offer(alice, USD(1), XRP(501), tfSell | tfFillOrKill)); env.close(); env.require (balance (alice, USD(35))); env.require (offers (alice, 0)); env.require (balance (bob, USD(65))); } { // Alice submits a tfSell | tfFillOrKill offer that finishes // off the remainder of bob's offer. // We're using bob's left-over offer for XRP(500), USD(5) env (offer(alice, USD(1), XRP(500), tfSell | tfFillOrKill)); env.close(); env.require (balance (alice, USD(40))); env.require (offers (alice, 0)); env.require (balance (bob, USD(60))); } } void testTransferRateOffer (std::initializer_list fs) { testcase ("Transfer Rate Offer"); using namespace jtx; auto const gw1 = Account("gateway1"); auto const USD = gw1["USD"]; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); // The fee that's charged for transactions. auto const fee = env.current ()->fees ().base; env.fund (XRP(100000), gw1); env.close(); env(rate(gw1, 1.25)); { auto const ann = Account("ann"); auto const bob = Account("bob"); env.fund (XRP(100) + reserve(env, 2) + (fee*2), ann, bob); env.close(); env (trust(ann, USD(200))); env (trust(bob, USD(200))); env.close(); env (pay (gw1, bob, USD(125))); env.close(); // bob offers to sell USD(100) for XRP. alice takes bob's offer. // Notice that although bob only offered USD(100), USD(125) was // removed from his account due to the gateway fee. // // A comparable payment would look like this: // env (pay (bob, alice, USD(100)), sendmax(USD(125))) env (offer (bob, XRP(1), USD(100))); env.close(); env (offer (ann, USD(100), XRP(1))); env.close(); env.require (balance (ann, USD(100))); env.require (balance (ann, XRP( 99) + reserve(env, 2))); env.require (offers (ann, 0)); env.require (balance (bob, USD( 0))); env.require (balance (bob, XRP(101) + reserve(env, 2))); env.require (offers (bob, 0)); } { // Reverse the order, so the offer in the books is to sell XRP // in return for USD. Gateway rate should still apply identically. auto const che = Account("che"); auto const deb = Account("deb"); env.fund (XRP(100) + reserve(env, 2) + (fee*2), che, deb); env.close(); env (trust(che, USD(200))); env (trust(deb, USD(200))); env.close(); env (pay (gw1, deb, USD(125))); env.close(); env (offer (che, USD(100), XRP(1))); env.close(); env (offer (deb, XRP(1), USD(100))); env.close(); env.require (balance (che, USD(100))); env.require (balance (che, XRP( 99) + reserve(env, 2))); env.require (offers (che, 0)); env.require (balance (deb, USD( 0))); env.require (balance (deb, XRP(101) + reserve(env, 2))); env.require (offers (deb, 0)); } { auto const eve = Account("eve"); auto const fyn = Account("fyn"); env.fund (XRP(20000) + fee*2, eve, fyn); env.close(); env (trust (eve, USD(1000))); env (trust (fyn, USD(1000))); env.close(); env (pay (gw1, eve, USD(100))); env (pay (gw1, fyn, USD(100))); env.close(); // This test verifies that the amount removed from an offer // accounts for the transfer fee that is removed from the // account but not from the remaining offer. env (offer (eve, USD(10), XRP(4000))); env.close(); std::uint32_t const eveOfferSeq = env.seq (eve) - 1; env (offer (fyn, XRP(2000), USD(5))); env.close(); env.require (balance (eve, USD(105))); env.require (balance (eve, XRP(18000))); auto const evesOffers = offersOnAccount (env, eve); BEAST_EXPECT (evesOffers.size() == 1); if (evesOffers.size() != 0) { auto const& evesOffer = *(evesOffers.front()); BEAST_EXPECT (evesOffer[sfLedgerEntryType] == ltOFFER); BEAST_EXPECT (evesOffer[sfTakerGets] == XRP (2000)); BEAST_EXPECT (evesOffer[sfTakerPays] == USD (5)); } env (offer_cancel (eve, eveOfferSeq)); // For later tests env.require (balance (fyn, USD(93.75))); env.require (balance (fyn, XRP(22000))); env.require (offers (fyn, 0)); } // Start messing with two non-native currencies. auto const gw2 = Account("gateway2"); auto const EUR = gw2["EUR"]; env.fund (XRP(100000), gw2); env.close(); env(rate(gw2, 1.5)); { // Remove XRP from the equation. Give the two currencies two // different transfer rates so we can see both transfer rates // apply in the same transaction. auto const gay = Account("gay"); auto const hal = Account("hal"); env.fund (reserve(env, 3) + (fee*3), gay, hal); env.close(); env (trust(gay, USD(200))); env (trust(gay, EUR(200))); env (trust(hal, USD(200))); env (trust(hal, EUR(200))); env.close(); env (pay (gw1, gay, USD(125))); env (pay (gw2, hal, EUR(150))); env.close(); env (offer (gay, EUR(100), USD(100))); env.close(); env (offer (hal, USD(100), EUR(100))); env.close(); env.require (balance (gay, USD( 0))); env.require (balance (gay, EUR(100))); env.require (balance (gay, reserve(env, 3))); env.require (offers (gay, 0)); env.require (balance (hal, USD(100))); env.require (balance (hal, EUR( 0))); env.require (balance (hal, reserve(env, 3))); env.require (offers (hal, 0)); } { // A trust line's QualityIn should not affect offer crossing. auto const ivy = Account("ivy"); auto const joe = Account("joe"); env.fund (reserve(env, 3) + (fee*3), ivy, joe); env.close(); env (trust(ivy, USD(400)), qualityInPercent (90)); env (trust(ivy, EUR(400)), qualityInPercent (80)); env (trust(joe, USD(400)), qualityInPercent (70)); env (trust(joe, EUR(400)), qualityInPercent (60)); env.close(); env (pay (gw1, ivy, USD(270)), sendmax (USD(500))); env (pay (gw2, joe, EUR(150)), sendmax (EUR(300))); env.close(); env.require (balance (ivy, USD(300))); env.require (balance (joe, EUR(250))); env (offer (ivy, EUR(100), USD(200))); env.close(); env (offer (joe, USD(200), EUR(100))); env.close(); env.require (balance (ivy, USD( 50))); env.require (balance (ivy, EUR(100))); env.require (balance (ivy, reserve(env, 3))); env.require (offers (ivy, 0)); env.require (balance (joe, USD(200))); env.require (balance (joe, EUR(100))); env.require (balance (joe, reserve(env, 3))); env.require (offers (joe, 0)); } { // A trust line's QualityOut should not affect offer crossing. auto const kim = Account("kim"); auto const K_BUX = kim["BUX"]; auto const lex = Account("lex"); auto const meg = Account("meg"); auto const ned = Account("ned"); auto const N_BUX = ned["BUX"]; // Verify trust line QualityOut affects payments. env.fund (reserve(env, 4) + (fee*4), kim, lex, meg, ned); env.close(); env (trust (lex, K_BUX(400))); env (trust (lex, N_BUX(200)), qualityOutPercent (120)); env (trust (meg, N_BUX(100))); env.close(); env (pay (ned, lex, N_BUX(100))); env.close(); env.require (balance (lex, N_BUX(100))); env (pay (kim, meg, N_BUX(60)), path (lex, ned), sendmax (K_BUX(200))); env.close(); env.require (balance (kim, K_BUX(none))); env.require (balance (kim, N_BUX(none))); env.require (balance (lex, K_BUX( 72))); env.require (balance (lex, N_BUX( 40))); env.require (balance (meg, K_BUX(none))); env.require (balance (meg, N_BUX( 60))); env.require (balance (ned, K_BUX(none))); env.require (balance (ned, N_BUX(none))); // Now verify that offer crossing is unaffected by QualityOut. env (offer (lex, K_BUX(30), N_BUX(30))); env.close(); env (offer (kim, N_BUX(30), K_BUX(30))); env.close(); env.require (balance (kim, K_BUX(none))); env.require (balance (kim, N_BUX( 30))); env.require (balance (lex, K_BUX( 102))); env.require (balance (lex, N_BUX( 10))); env.require (balance (meg, K_BUX(none))); env.require (balance (meg, N_BUX( 60))); env.require (balance (ned, K_BUX( -30))); env.require (balance (ned, N_BUX(none))); } { // Make sure things work right when we're auto-bridging as well. auto const ova = Account("ova"); auto const pat = Account("pat"); auto const qae = Account("qae"); env.fund (XRP(2) + reserve(env, 3) + (fee*3), ova, pat, qae); env.close(); // o ova has USD but wants XPR. // o pat has XRP but wants EUR. // o qae has EUR but wants USD. env (trust(ova, USD(200))); env (trust(ova, EUR(200))); env (trust(pat, USD(200))); env (trust(pat, EUR(200))); env (trust(qae, USD(200))); env (trust(qae, EUR(200))); env.close(); env (pay (gw1, ova, USD(125))); env (pay (gw2, qae, EUR(150))); env.close(); env (offer (ova, XRP(2), USD(100))); env (offer (pat, EUR(100), XRP(2))); env.close(); env (offer (qae, USD(100), EUR(100))); env.close(); env.require (balance (ova, USD( 0))); env.require (balance (ova, EUR( 0))); env.require (balance (ova, XRP(4) + reserve(env, 3))); // In pre-flow code ova's offer is left empty in the ledger. auto const ovasOffers = offersOnAccount (env, ova); if (ovasOffers.size() != 0) { BEAST_EXPECT (ovasOffers.size() == 1); auto const& ovasOffer = *(ovasOffers.front()); BEAST_EXPECT (ovasOffer[sfLedgerEntryType] == ltOFFER); BEAST_EXPECT (ovasOffer[sfTakerGets] == USD (0)); BEAST_EXPECT (ovasOffer[sfTakerPays] == XRP (0)); } env.require (balance (pat, USD( 0))); env.require (balance (pat, EUR(100))); env.require (balance (pat, XRP(0) + reserve(env, 3))); env.require (offers (pat, 0)); env.require (balance (qae, USD(100))); env.require (balance (qae, EUR( 0))); env.require (balance (qae, XRP(2) + reserve(env, 3))); env.require (offers (qae, 0)); } } void testSelfCrossOffer1 (std::initializer_list fs) { // The following test verifies some correct but slightly surprising // behavior in offer crossing. The scenario: // // o An entity has created one or more offers. // o The entity creates another offer that can be directly crossed // (not autobridged) by the previously created offer(s). // o Rather than self crossing the offers, delete the old offer(s). // // See a more complete explanation in the comments for // BookOfferCrossingStep::limitSelfCrossQuality(). // // Note that, in this particular example, one offer causes several // crossable offers (worth considerably more than the new offer) // to be removed from the book. using namespace jtx; auto const gw = Account("gateway"); auto const USD = gw["USD"]; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); // The fee that's charged for transactions. auto const fee = env.current ()->fees ().base; auto const startBalance = XRP(1000000); env.fund (startBalance + (fee*4), gw); env.close(); env (offer (gw, USD(60), XRP(600))); env.close(); env (offer (gw, USD(60), XRP(600))); env.close(); env (offer (gw, USD(60), XRP(600))); env.close(); env.require (owners (gw, 3)); env.require (balance (gw, startBalance + fee)); auto gwOffers = offersOnAccount (env, gw); BEAST_EXPECT (gwOffers.size() == 3); for (auto const& offerPtr : gwOffers) { auto const& offer = *offerPtr; BEAST_EXPECT (offer[sfLedgerEntryType] == ltOFFER); BEAST_EXPECT (offer[sfTakerGets] == XRP (600)); BEAST_EXPECT (offer[sfTakerPays] == USD ( 60)); } // Since this offer crosses the first offers, the previous offers // will be deleted and this offer will be put on the order book. env (offer (gw, XRP(1000), USD(100))); env.close(); env.require (owners (gw, 1)); env.require (offers (gw, 1)); env.require (balance (gw, startBalance)); gwOffers = offersOnAccount (env, gw); BEAST_EXPECT (gwOffers.size() == 1); for (auto const& offerPtr : gwOffers) { auto const& offer = *offerPtr; BEAST_EXPECT (offer[sfLedgerEntryType] == ltOFFER); BEAST_EXPECT (offer[sfTakerGets] == USD (100)); BEAST_EXPECT (offer[sfTakerPays] == XRP (1000)); } } void testSelfCrossOffer2 (std::initializer_list fs) { using namespace jtx; auto const gw1 = Account("gateway1"); auto const gw2 = Account("gateway2"); auto const alice = Account("alice"); auto const USD = gw1["USD"]; auto const EUR = gw2["EUR"]; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); env.fund (XRP(1000000), gw1, gw2); env.close(); // The fee that's charged for transactions. auto const f = env.current ()->fees ().base; // Test cases struct TestData { std::string acct; // Account operated on STAmount fundXRP; // XRP acct funded with STAmount fundUSD; // USD acct funded with STAmount fundEUR; // EUR acct funded with TER firstOfferTec; // tec code on first offer TER secondOfferTec; // tec code on second offer }; TestData const tests[] { // acct fundXRP fundUSD fundEUR firstOfferTec secondOfferTec {"ann", reserve(env, 3) + f*4, USD(1000), EUR(1000), tesSUCCESS, tesSUCCESS}, {"bev", reserve(env, 3) + f*4, USD( 1), EUR(1000), tesSUCCESS, tesSUCCESS}, {"cam", reserve(env, 3) + f*4, USD(1000), EUR( 1), tesSUCCESS, tesSUCCESS}, {"deb", reserve(env, 3) + f*4, USD( 0), EUR( 1), tesSUCCESS, tecUNFUNDED_OFFER}, {"eve", reserve(env, 3) + f*4, USD( 1), EUR( 0), tecUNFUNDED_OFFER, tesSUCCESS}, {"flo", reserve(env, 3) + 0, USD(1000), EUR(1000), tecINSUF_RESERVE_OFFER, tecINSUF_RESERVE_OFFER}, }; for (auto const& t : tests) { auto const acct = Account {t.acct}; env.fund (t.fundXRP, acct); env.close(); env (trust (acct, USD(1000))); env (trust (acct, EUR(1000))); env.close(); if (t.fundUSD > USD(0)) env (pay (gw1, acct, t.fundUSD)); if (t.fundEUR > EUR(0)) env (pay (gw2, acct, t.fundEUR)); env.close(); env (offer (acct, USD(500), EUR(600)), ter (t.firstOfferTec)); env.close(); std::uint32_t const firstOfferSeq = env.seq (acct) - 1; int offerCount = t.firstOfferTec == tesSUCCESS ? 1 : 0; env.require (owners (acct, 2 + offerCount)); env.require (balance (acct, t.fundUSD)); env.require (balance (acct, t.fundEUR)); auto acctOffers = offersOnAccount (env, acct); BEAST_EXPECT (acctOffers.size() == offerCount); for (auto const& offerPtr : acctOffers) { auto const& offer = *offerPtr; BEAST_EXPECT (offer[sfLedgerEntryType] == ltOFFER); BEAST_EXPECT (offer[sfTakerGets] == EUR (600)); BEAST_EXPECT (offer[sfTakerPays] == USD (500)); } env (offer (acct, EUR(600), USD(500)), ter (t.secondOfferTec)); env.close(); std::uint32_t const secondOfferSeq = env.seq (acct) - 1; offerCount = t.secondOfferTec == tesSUCCESS ? 1 : offerCount; env.require (owners (acct, 2 + offerCount)); env.require (balance (acct, t.fundUSD)); env.require (balance (acct, t.fundEUR)); acctOffers = offersOnAccount (env, acct); BEAST_EXPECT (acctOffers.size() == offerCount); for (auto const& offerPtr : acctOffers) { auto const& offer = *offerPtr; BEAST_EXPECT (offer[sfLedgerEntryType] == ltOFFER); if (offer[sfSequence] == firstOfferSeq) { BEAST_EXPECT (offer[sfTakerGets] == EUR (600)); BEAST_EXPECT (offer[sfTakerPays] == USD (500)); } else { BEAST_EXPECT (offer[sfTakerGets] == USD (500)); BEAST_EXPECT (offer[sfTakerPays] == EUR (600)); } } // Remove any offers from acct for the next pass. env (offer_cancel (acct, firstOfferSeq)); env.close(); env (offer_cancel (acct, secondOfferSeq)); env.close(); } } void testSelfCrossOffer (std::initializer_list fs) { testcase ("Self Cross Offer"); testSelfCrossOffer1 (fs); testSelfCrossOffer2 (fs); } void testSelfIssueOffer (std::initializer_list fs) { // Folks who issue their own currency have, in effect, as many // funds as they are trusted for. This test used to fail because // self-issuing was not properly checked. Verify that it works // correctly now. using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); auto const alice = Account("alice"); auto const bob = Account("bob"); auto const USD = bob["USD"]; auto const f = env.current ()->fees ().base; env.fund(XRP(50000) + f, alice, bob); env.close(); env(offer(alice, USD(5000), XRP(50000))); env.close(); // This offer should take alice's offer up to Alice's reserve. env(offer(bob, XRP(50000), USD(5000))); env.close(); // alice's offer should have been removed, since she's down to her // XRP reserve. env.require (balance (alice, XRP(250))); env.require (owners (alice, 1)); env.require (lines (alice, 1)); // However bob's offer should be in the ledger, since it was not // fully crossed. auto const bobOffers = offersOnAccount (env, bob); BEAST_EXPECT(bobOffers.size() == 1); for (auto const& offerPtr : bobOffers) { auto const& offer = *offerPtr; BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER); BEAST_EXPECT(offer[sfTakerGets] == USD ( 25)); BEAST_EXPECT(offer[sfTakerPays] == XRP (250)); } } void testBadPathAssert (std::initializer_list fs) { // At one point in the past this invalid path caused an assert. It // should not be possible for user-supplied data to cause an assert. // Make sure the assert is gone. testcase ("Bad path assert"); using namespace jtx; // The problem was identified when featureOwnerPaysFee was enabled, // so make sure that gets included. Env env {*this, with_features(fs) | with_features(featureOwnerPaysFee)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); // The fee that's charged for transactions. auto const fee = env.current ()->fees ().base; { // A trust line's QualityOut should not affect offer crossing. auto const ann = Account("ann"); auto const A_BUX = ann["BUX"]; auto const bob = Account("bob"); auto const cam = Account("cam"); auto const dan = Account("dan"); auto const D_BUX = dan["BUX"]; // Verify trust line QualityOut affects payments. env.fund (reserve(env, 4) + (fee*4), ann, bob, cam, dan); env.close(); env (trust (bob, A_BUX(400))); env (trust (bob, D_BUX(200)), qualityOutPercent (120)); env (trust (cam, D_BUX(100))); env.close(); env (pay (dan, bob, D_BUX(100))); env.close(); env.require (balance (bob, D_BUX(100))); env (pay (ann, cam, D_BUX(60)), path (bob, dan), sendmax (A_BUX(200))); env.close(); env.require (balance (ann, A_BUX(none))); env.require (balance (ann, D_BUX(none))); env.require (balance (bob, A_BUX( 72))); env.require (balance (bob, D_BUX( 40))); env.require (balance (cam, A_BUX(none))); env.require (balance (cam, D_BUX( 60))); env.require (balance (dan, A_BUX(none))); env.require (balance (dan, D_BUX(none))); env (offer (bob, A_BUX(30), D_BUX(30))); env.close(); env (trust (ann, D_BUX(100))); env.close(); // Determine which TEC code we expect. TER const tecExpect = hasFeature(featureFlow, fs) ? temBAD_PATH : tecPATH_DRY; // This payment caused the assert. env (pay (ann, ann, D_BUX(30)), path (A_BUX, D_BUX), sendmax (A_BUX(30)), ter (tecExpect)); env.close(); env.require (balance (ann, A_BUX(none))); env.require (balance (ann, D_BUX( 0))); env.require (balance (bob, A_BUX( 72))); env.require (balance (bob, D_BUX( 40))); env.require (balance (cam, A_BUX(none))); env.require (balance (cam, D_BUX( 60))); env.require (balance (dan, A_BUX( 0))); env.require (balance (dan, D_BUX(none))); } } void testDirectToDirectPath (std::initializer_list fs) { // The offer crossing code expects that a DirectStep is always // preceded by a BookStep. In one instance the default path // was not matching that assumption. Here we recreate that case // so we can prove the bug stays fixed. testcase ("Direct to Direct path"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); auto const ann = Account("ann"); auto const bob = Account("bob"); auto const cam = Account("cam"); auto const A_BUX = ann["BUX"]; auto const B_BUX = bob["BUX"]; auto const fee = env.current ()->fees ().base; env.fund (reserve(env, 4) + (fee*5), ann, bob, cam); env.close(); env (trust (ann, B_BUX(40))); env (trust (cam, A_BUX(40))); env (trust (cam, B_BUX(40))); env.close(); env (pay (ann, cam, A_BUX(35))); env (pay (bob, cam, B_BUX(35))); env (offer (bob, A_BUX(30), B_BUX(30))); env.close(); // cam puts an offer on the books that her upcoming offer could cross. // But this offer should be deleted, not crossed, by her upcoming // offer. env (offer (cam, A_BUX(29), B_BUX(30), tfPassive)); env.close(); env.require (balance (cam, A_BUX(35))); env.require (balance (cam, B_BUX(35))); env.require (offers (cam, 1)); // This offer caused the assert. env (offer (cam, B_BUX(30), A_BUX(30))); env.close(); env.require (balance (bob, A_BUX(30))); env.require (balance (cam, A_BUX( 5))); env.require (balance (cam, B_BUX(65))); env.require (offers (cam, 0)); } void testSelfCrossLowQualityOffer (std::initializer_list fs) { // The Flow offer crossing code used to assert if an offer was made // for more XRP than the offering account held. This unit test // reproduces that failing case. testcase ("Self crossing low quality offer"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); auto const ann = Account("ann"); auto const gw = Account("gateway"); auto const BTC = gw["BTC"]; auto const fee = env.current ()->fees ().base; env.fund (reserve(env, 2) + drops (9999640) + (fee), ann); env.fund (reserve(env, 2) + (fee*4), gw); env.close(); env (rate(gw, 1.002)); env (trust(ann, BTC(10))); env.close(); env (pay(gw, ann, BTC(2.856))); env.close(); env (offer(ann, drops(365611702030), BTC(5.713))); env.close(); // This offer caused the assert. env (offer(ann, BTC(0.687), drops(20000000000)), ter (tecINSUF_RESERVE_OFFER)); } void testOfferInScaling (std::initializer_list fs) { // The Flow offer crossing code had a case where it was not rounding // the offer crossing correctly after a partial crossing. The // failing case was found on the network. Here we add the case to // the unit tests. testcase ("Offer In Scaling"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); auto const gw = Account("gateway"); auto const alice = Account("alice"); auto const bob = Account("bob"); auto const CNY = gw["CNY"]; auto const fee = env.current ()->fees ().base; env.fund (reserve(env, 2) + drops (400000000000) + (fee), alice, bob); env.fund (reserve(env, 2) + (fee*4), gw); env.close(); env (trust(bob, CNY(500))); env.close(); env (pay(gw, bob, CNY(300))); env.close(); env (offer(bob, drops(5400000000), CNY(216.054))); env.close(); // This offer did not round result of partial crossing correctly. env (offer(alice, CNY(13562.0001), drops(339000000000))); env.close(); auto const aliceOffers = offersOnAccount (env, alice); BEAST_EXPECT(aliceOffers.size() == 1); for (auto const& offerPtr : aliceOffers) { auto const& offer = *offerPtr; BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER); BEAST_EXPECT(offer[sfTakerGets] == drops (333599446582)); BEAST_EXPECT(offer[sfTakerPays] == CNY (13345.9461)); } } void testOfferInScalingWithXferRate (std::initializer_list fs) { // After adding the previous case, there were still failing rounding // cases in Flow offer crossing. This one was because the gateway // transfer rate was not being correctly handled. testcase ("Offer In Scaling With Xfer Rate"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); auto const gw = Account("gateway"); auto const alice = Account("alice"); auto const bob = Account("bob"); auto const BTC = gw["BTC"]; auto const JPY = gw["JPY"]; auto const fee = env.current ()->fees ().base; env.fund (reserve(env, 2) + drops (400000000000) + (fee), alice, bob); env.fund (reserve(env, 2) + (fee*4), gw); env.close(); env (rate(gw, 1.002)); env (trust(alice, JPY(4000))); env (trust(bob, BTC(2))); env.close(); env (pay(gw, alice, JPY(3699.034802280317))); env (pay(gw, bob, BTC(1.156722559140311))); env.close(); env (offer(bob, JPY(1241.913390770747), BTC(0.01969825690469254))); env.close(); // This offer did not round result of partial crossing correctly. env (offer(alice, BTC(0.05507568706427876), JPY(3472.696773391072))); env.close(); auto const aliceOffers = offersOnAccount (env, alice); BEAST_EXPECT(aliceOffers.size() == 1); for (auto const& offerPtr : aliceOffers) { auto const& offer = *offerPtr; BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER); BEAST_EXPECT(offer[sfTakerGets] == STAmount (JPY.issue(), std::uint64_t(2230682446713524ul), -12)); BEAST_EXPECT(offer[sfTakerPays] == BTC (0.035378)); } } void testOfferThresholdWithReducedFunds (std::initializer_list fs) { // Another instance where Flow offer crossing was not always // working right was if the Taker had fewer funds than the Offer // was offering. The basis for this test came off the network. testcase ("Offer Threshold With Reduced Funds"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time () + 100 * env.closed ()->info ().closeTimeResolution; env.close (closeTime); auto const gw1 = Account("gw1"); auto const gw2 = Account("gw2"); auto const alice = Account("alice"); auto const bob = Account("bob"); auto const USD = gw1["USD"]; auto const JPY = gw2["JPY"]; auto const fee = env.current ()->fees ().base; env.fund (reserve(env, 2) + drops (400000000000) + (fee), alice, bob); env.fund (reserve(env, 2) + (fee*4), gw1, gw2); env.close(); env (rate(gw1, 1.002)); env (trust(alice, USD(1000))); env (trust(bob, JPY(100000))); env.close(); env (pay(gw1, alice, STAmount {USD.issue(), std::uint64_t(2185410179555600), -14})); env (pay(gw2, bob, STAmount {JPY.issue(), std::uint64_t(6351823459548956), -12})); env.close(); env (offer(bob, STAmount {USD.issue(), std::uint64_t(4371257532306000), -17}, STAmount {JPY.issue(), std::uint64_t(4573216636606000), -15})); env.close(); // This offer did not partially cross correctly. env (offer(alice, STAmount {JPY.issue(), std::uint64_t(2291181510070762), -12}, STAmount {USD.issue(), std::uint64_t(2190218999914694), -14})); env.close(); auto const aliceOffers = offersOnAccount (env, alice); BEAST_EXPECT(aliceOffers.size() == 1); for (auto const& offerPtr : aliceOffers) { auto const& offer = *offerPtr; BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER); BEAST_EXPECT(offer[sfTakerGets] == STAmount (USD.issue(), std::uint64_t(2185847305256635), -14)); BEAST_EXPECT(offer[sfTakerPays] == STAmount (JPY.issue(), std::uint64_t(2286608293434156), -12)); } } void testTinyOffer (std::initializer_list fs) { testcase ("Tiny Offer"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time() + 100 * env.closed()->info().closeTimeResolution; env.close (closeTime); auto const gw = Account("gw"); auto const alice = Account("alice"); auto const bob = Account("bob"); auto const CNY = gw["CNY"]; auto const fee = env.current()->fees().base; auto const startXrpBalance = drops (400000000000) + (fee * 2); env.fund (startXrpBalance, gw, alice, bob); env.close(); env (trust (bob, CNY(100000))); env.close(); // Place alice's tiny offer in the book first. Let's see what happens // when a reasonable offer crosses it. STAmount const alicesCnyOffer { CNY.issue(), std::uint64_t(4926000000000000), -23 }; env (offer (alice, alicesCnyOffer, drops (1), tfPassive)); env.close(); // bob places an ordinary offer STAmount const bobsCnyStartBalance { CNY.issue(), std::uint64_t(3767479960090235), -15}; env (pay(gw, bob, bobsCnyStartBalance)); env.close(); env (offer (bob, drops (203), STAmount {CNY.issue(), std::uint64_t(1000000000000000), -20})); env.close(); env.require (balance (alice, alicesCnyOffer)); env.require (balance (alice, startXrpBalance - fee - drops(1))); env.require (balance (bob, bobsCnyStartBalance - alicesCnyOffer)); env.require (balance (bob, startXrpBalance - (fee * 2) + drops(1))); } void testSelfPayXferFeeOffer (std::initializer_list fs) { testcase ("Self Pay Xfer Fee"); // The old offer crossing code does not charge a transfer fee // if alice pays alice. That's different from how payments work. // Payments always charge a transfer fee even if the money is staying // in the same hands. // // What's an example where alice pays alice? There are three actors: // gw, alice, and bob. // // 1. gw issues BTC and USD. qw charges a 0.2% transfer fee. // // 2. alice makes an offer to buy XRP and sell USD. // 3. bob makes an offer to buy BTC and sell XRP. // // 4. alice now makes an offer to sell BTC and buy USD. // // This last offer crosses using auto-bridging. // o alice's last offer sells BTC to... // o bob' offer which takes alice's BTC and sells XRP to... // o alice's first offer which takes bob's XRP and sells USD to... // o alice's last offer. // // So alice sells USD to herself. // // There are six cases that we need to test: // o alice crosses her own offer on the first leg (BTC). // o alice crosses her own offer on the second leg (USD). // o alice crosses her own offers on both legs. // All three cases need to be tested: // o In reverse (alice has enough BTC to cover her offer) and // o Forward (alice owns less BTC than is in her final offer. // // It turns out that two of the forward cases fail for a different // reason. They are therefore commented out here, But they are // revisited in the testSelfPayUnlimitedFunds() unit test. using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time() + 100 * env.closed()->info().closeTimeResolution; env.close (closeTime); auto const gw = Account("gw"); auto const BTC = gw["BTC"]; auto const USD = gw["USD"]; auto const startXrpBalance = XRP (4000000); env.fund (startXrpBalance, gw); env.close(); env (rate (gw, 1.25)); env.close(); // Test cases struct Actor { Account acct; int offers; // offers on account after crossing PrettyAmount xrp; // final expected after crossing PrettyAmount btc; // final expected after crossing PrettyAmount usd; // final expected after crossing }; struct TestData { // The first three three integers give the *index* in actors // to assign each of the three roles. By using indices it is // easy for alice to own the offer in the first leg, the second // leg, or both. std::size_t self; std::size_t leg0; std::size_t leg1; PrettyAmount btcStart; std::vector actors; }; TestData const tests[] { // btcStart --------------------- actor[0] --------------------- -------------------- actor[1] ------------------- { 0, 0, 1, BTC(20), { {"ann", 0, drops(3899999999960), BTC(20.0), USD(3000)}, {"abe", 0, drops(4099999999970), BTC( 0), USD(750)} } }, // no BTC xfer fee { 0, 1, 0, BTC(20), { {"bev", 0, drops(4099999999960), BTC( 7.5), USD(2000)}, {"bob", 0, drops(3899999999970), BTC(10), USD( 0)} } }, // no USD xfer fee { 0, 0, 0, BTC(20), { {"cam", 0, drops(3999999999950), BTC(20.0), USD(2000)} } }, // no xfer fee // { 0, 0, 1, BTC( 5), { {"deb", 0, drops(3899999999960), BTC( 5.0), USD(3000)}, {"dan", 0, drops(4099999999970), BTC( 0), USD(750)} } }, // no BTC xfer fee { 0, 1, 0, BTC( 5), { {"eve", 1, drops(4039999999960), BTC( 0.0), USD(2000)}, {"eli", 1, drops(3959999999970), BTC( 4), USD( 0)} } }, // no USD xfer fee // { 0, 0, 0, BTC( 5), { {"flo", 0, drops(3999999999950), BTC( 5.0), USD(2000)} } } // no xfer fee }; for (auto const& t : tests) { Account const& self = t.actors[t.self].acct; Account const& leg0 = t.actors[t.leg0].acct; Account const& leg1 = t.actors[t.leg1].acct; for (auto const& actor : t.actors) { env.fund (XRP (4000000), actor.acct); env.close(); env (trust (actor.acct, BTC(40))); env (trust (actor.acct, USD(8000))); env.close(); } env (pay (gw, self, t.btcStart)); env (pay (gw, self, USD(2000))); if (self.id() != leg1.id()) env (pay (gw, leg1, USD(2000))); env.close(); // Get the initial offers in place. Remember their sequences // so we can delete them later. env (offer (leg0, BTC(10), XRP(100000), tfPassive)); env.close(); std::uint32_t const leg0OfferSeq = env.seq (leg0) - 1; env (offer (leg1, XRP(100000), USD(1000), tfPassive)); env.close(); std::uint32_t const leg1OfferSeq = env.seq (leg1) - 1; // This is the offer that matters. env (offer (self, USD(1000), BTC(10))); env.close(); std::uint32_t const selfOfferSeq = env.seq (self) - 1; // Verify results. for (auto const& actor : t.actors) { // Sometimes Taker crossing gets lazy about deleting offers. // Treat an empty offer as though it is deleted. auto actorOffers = offersOnAccount (env, actor.acct); auto const offerCount = std::distance (actorOffers.begin(), std::remove_if (actorOffers.begin(), actorOffers.end(), [] (std::shared_ptr& offer) { return (*offer)[sfTakerGets].signum() == 0; })); BEAST_EXPECT (offerCount == actor.offers); env.require (balance (actor.acct, actor.xrp)); env.require (balance (actor.acct, actor.btc)); env.require (balance (actor.acct, actor.usd)); } // Remove any offers that might be left hanging around. They // could bollix up later loops. env (offer_cancel (leg0, leg0OfferSeq)); env.close(); env (offer_cancel (leg1, leg1OfferSeq)); env.close(); env (offer_cancel (self, selfOfferSeq)); env.close(); } } void testSelfPayUnlimitedFunds (std::initializer_list fs) { testcase ("Self Pay Unlimited Funds"); // The Taker offer crossing code recognized when Alice was paying // Alice the same denomination. In this case, as long as Alice // has a little bit of that denomination, it treats Alice as though // she has unlimited funds in that denomination. // // Huh? What kind of sense does that make? // // One way to think about it is to break a single payment into a // series of very small payments executed sequentially but very // quickly. Alice needs to pay herself 1 USD, but she only has // 0.01 USD. Alice says, "Hey Alice, let me pay you a penny." // Alice does this, taking the penny out of her pocket and then // putting it back in her pocket. Then she says, "Hey Alice, // I found another penny. I can pay you another penny." Repeat // these steps 100 times and Alice has paid herself 1 USD even though // she only owns 0.01 USD. // // That's all very nice, but the payment code does not support this // optimization. In part that's because the payment code can // operate on a whole batch of offers. As a matter of fact, it can // deal in two consecutive batches of offers. It would take a great // deal of sorting out to figure out which offers in the two batches // had the same owner and give them special processing. And, // honestly, it's a weird little corner case. // // So, since Flow offer crossing uses the payments engine, Flow // offer crossing no longer supports this optimization. // // The following test shows the difference in the behaviors between // Taker offer crossing and Flow offer crossing. using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time() + 100 * env.closed()->info().closeTimeResolution; env.close (closeTime); auto const gw = Account("gw"); auto const BTC = gw["BTC"]; auto const USD = gw["USD"]; auto const startXrpBalance = XRP (4000000); env.fund (startXrpBalance, gw); env.close(); env (rate (gw, 1.25)); env.close(); // Test cases struct Actor { Account acct; int offers; // offers on account after crossing PrettyAmount xrp; // final expected after crossing PrettyAmount btc; // final expected after crossing PrettyAmount usd; // final expected after crossing }; struct TestData { // The first three three integers give the *index* in actors // to assign each of the three roles. By using indices it is // easy for alice to own the offer in the first leg, the second // leg, or both. std::size_t self; std::size_t leg0; std::size_t leg1; PrettyAmount btcStart; std::vector actors; }; TestData const takerTests[] { // btcStart ------------------- actor[0] -------------------- ------------------- actor[1] -------------------- { 0, 0, 1, BTC( 5), { {"deb", 0, drops(3899999999960), BTC(5), USD(3000)}, {"dan", 0, drops(4099999999970), BTC(0), USD( 750)} } }, // no BTC xfer fee { 0, 0, 0, BTC( 5), { {"flo", 0, drops(3999999999950), BTC(5), USD(2000)} } } // no xfer fee }; TestData const flowTests[] { // btcStart ------------------- actor[0] -------------------- ------------------- actor[1] -------------------- { 0, 0, 1, BTC( 5), { {"gay", 1, drops(3949999999960), BTC(5), USD(2500)}, {"gar", 1, drops(4049999999970), BTC(0), USD(1375)} } }, // no BTC xfer fee { 0, 0, 0, BTC( 5), { {"hye", 2, drops(3999999999950), BTC(5), USD(2000)} } } // no xfer fee }; // Pick the right tests. auto const& tests = hasFeature(featureFlowCross, fs) ? flowTests : takerTests; for (auto const& t : tests) { Account const& self = t.actors[t.self].acct; Account const& leg0 = t.actors[t.leg0].acct; Account const& leg1 = t.actors[t.leg1].acct; for (auto const& actor : t.actors) { env.fund (XRP (4000000), actor.acct); env.close(); env (trust (actor.acct, BTC(40))); env (trust (actor.acct, USD(8000))); env.close(); } env (pay (gw, self, t.btcStart)); env (pay (gw, self, USD(2000))); if (self.id() != leg1.id()) env (pay (gw, leg1, USD(2000))); env.close(); // Get the initial offers in place. Remember their sequences // so we can delete them later. env (offer (leg0, BTC(10), XRP(100000), tfPassive)); env.close(); std::uint32_t const leg0OfferSeq = env.seq (leg0) - 1; env (offer (leg1, XRP(100000), USD(1000), tfPassive)); env.close(); std::uint32_t const leg1OfferSeq = env.seq (leg1) - 1; // This is the offer that matters. env (offer (self, USD(1000), BTC(10))); env.close(); std::uint32_t const selfOfferSeq = env.seq (self) - 1; // Verify results. for (auto const& actor : t.actors) { // Sometimes Taker offer crossing gets lazy about deleting // offers. Treat an empty offer as though it is deleted. auto actorOffers = offersOnAccount (env, actor.acct); auto const offerCount = std::distance (actorOffers.begin(), std::remove_if (actorOffers.begin(), actorOffers.end(), [] (std::shared_ptr& offer) { return (*offer)[sfTakerGets].signum() == 0; })); BEAST_EXPECT (offerCount == actor.offers); env.require (balance (actor.acct, actor.xrp)); env.require (balance (actor.acct, actor.btc)); env.require (balance (actor.acct, actor.usd)); } // Remove any offers that might be left hanging around. They // could bollix up later loops. env (offer_cancel (leg0, leg0OfferSeq)); env.close(); env (offer_cancel (leg1, leg1OfferSeq)); env.close(); env (offer_cancel (self, selfOfferSeq)); env.close(); } } void testRequireAuth (std::initializer_list fs) { testcase ("lsfRequireAuth"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time() + 100 * env.closed()->info().closeTimeResolution; env.close (closeTime); auto const gw = Account("gw"); auto const alice = Account("alice"); auto const bob = Account("bob"); auto const gwUSD = gw["USD"]; auto const aliceUSD = alice["USD"]; auto const bobUSD = bob["USD"]; env.fund (XRP(400000), gw, alice, bob); env.close(); // GW requires authorization for holders of its IOUs env(fset (gw, asfRequireAuth)); env.close(); // Properly set trust and have gw authorize bob and alice env (trust (gw, bobUSD(100)), txflags (tfSetfAuth)); env (trust (bob, gwUSD(100))); env (trust (gw, aliceUSD(100)), txflags (tfSetfAuth)); env (trust (alice, gwUSD(100))); // Alice is able to place the offer since the GW has authorized her env (offer (alice, gwUSD(40), XRP(4000))); env.close(); env.require (offers (alice, 1)); env.require (balance (alice, gwUSD(0))); env (pay(gw, bob, gwUSD(50))); env.close(); env.require (balance (bob, gwUSD(50))); // Bob's offer should cross Alice's env (offer (bob, XRP(4000), gwUSD(40))); env.close(); env.require (offers (alice, 0)); env.require (balance (alice, gwUSD(40))); env.require (offers (bob, 0)); env.require (balance (bob, gwUSD(10))); } void testMissingAuth (std::initializer_list fs) { testcase ("Missing Auth"); // 1. alice creates an offer to acquire USD/gw, an asset for which // she does not have a trust line. At some point in the future, // gw adds lsfRequireAuth. Then, later, alice's offer is crossed. // a. With Taker alice's unauthorized offer is consumed. // b. With FlowCross alice's offer is deleted, not consumed, // since alice is not authorized to hold USD/gw. // // 2. alice tries to create an offer for USD/gw, now that gw has // lsfRequireAuth set. This time the offer create fails because // alice is not authorized to hold USD/gw. // // 3. Next, gw creates a trust line to alice, but does not set // tfSetfAuth on that trust line. alice attempts to create an // offer and again fails. // // 4. Finally, gw sets tsfSetAuth on the trust line authorizing // alice to own USD/gw. At this point alice successfully // creates and crosses an offer for USD/gw. using namespace jtx; Env env {*this, with_features(fs)}; auto const closeTime = fix1449Time() + 100 * env.closed()->info().closeTimeResolution; env.close (closeTime); auto const gw = Account("gw"); auto const alice = Account("alice"); auto const bob = Account("bob"); auto const gwUSD = gw["USD"]; auto const aliceUSD = alice["USD"]; auto const bobUSD = bob["USD"]; env.fund (XRP(400000), gw, alice, bob); env.close(); env (offer (alice, gwUSD(40), XRP(4000))); env.close(); env.require (offers (alice, 1)); env.require (balance (alice, gwUSD(none))); env(fset (gw, asfRequireAuth)); env.close(); env (trust (gw, bobUSD(100)), txflags (tfSetfAuth)); env.close(); env (trust (bob, gwUSD(100))); env.close(); env (pay(gw, bob, gwUSD(50))); env.close(); env.require (balance (bob, gwUSD(50))); // gw now requires authorization and bob has gwUSD(50). Let's see if // bob can cross alice's offer. // // o With Taker bob's offer should cross alice's. // o With FlowCross bob's offer shouldn't cross and alice's // unauthorized offer should be deleted. env (offer (bob, XRP(4000), gwUSD(40))); env.close(); std::uint32_t const bobOfferSeq = env.seq (bob) - 1; bool const flowCross = hasFeature (featureFlowCross, fs); env.require (offers (alice, 0)); if (flowCross) { // alice's unauthorized offer is deleted & bob's offer not crossed. env.require (balance (alice, gwUSD(none))); env.require (offers (bob, 1)); env.require (balance (bob, gwUSD(50))); } else { // alice's offer crosses bob's env.require (balance (alice, gwUSD(40))); env.require (offers (bob, 0)); env.require (balance (bob, gwUSD(10))); // The rest of the test verifies FlowCross behavior. return; } // See if alice can create an offer without authorization. alice // should not be able to create the offer and bob's offer should be // untouched. env (offer (alice, gwUSD(40), XRP(4000)), ter(tecNO_LINE)); env.close(); env.require (offers (alice, 0)); env.require (balance (alice, gwUSD(none))); env.require (offers (bob, 1)); env.require (balance (bob, gwUSD(50))); // Set up a trust line for alice, but don't authorize it. alice // should still not be able to create an offer for USD/gw. env (trust (gw, aliceUSD(100))); env.close(); env (offer (alice, gwUSD(40), XRP(4000)), ter(tecNO_AUTH)); env.close(); env.require (offers (alice, 0)); env.require (balance (alice, gwUSD(0))); env.require (offers (bob, 1)); env.require (balance (bob, gwUSD(50))); // Delete bob's offer so alice can create an offer without crossing. env (offer_cancel (bob, bobOfferSeq)); env.close(); env.require (offers (bob, 0)); // Finally, set up an authorized trust line for alice. Now alice's // offer should succeed. Note that, since this is an offer rather // than a payment, alice does not need to set a trust line limit. env (trust (gw, aliceUSD(100)), txflags (tfSetfAuth)); env.close(); env (offer (alice, gwUSD(40), XRP(4000))); env.close(); env.require (offers (alice, 1)); // Now bob creates his offer again. alice's offer should cross. env (offer (bob, XRP(4000), gwUSD(40))); env.close(); env.require (offers (alice, 0)); env.require (balance (alice, gwUSD(40))); env.require (offers (bob, 0)); env.require (balance (bob, gwUSD(10))); } void testRCSmoketest(std::initializer_list fs) { testcase("RippleConnect Smoketest payment flow"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time() + 100 * env.closed()->info().closeTimeResolution; env.close (closeTime); // This test mimics the payment flow used in the Ripple Connect // smoke test. The players: // A USD gateway with hot and cold wallets // A EUR gateway with hot and cold walllets // A MM gateway that will provide offers from USD->EUR and EUR->USD // A path from hot US to cold EUR is found and then used to send // USD for EUR that goes through the market maker auto const hotUS = Account("hotUS"); auto const coldUS = Account("coldUS"); auto const hotEU = Account("hotEU"); auto const coldEU = Account("coldEU"); auto const mm = Account("mm"); auto const USD = coldUS["USD"]; auto const EUR = coldEU["EUR"]; env.fund (XRP(100000), hotUS, coldUS, hotEU, coldEU, mm); env.close(); // Cold wallets require trust but will ripple by default for (auto const& cold : {coldUS, coldEU}) { env(fset (cold, asfRequireAuth)); env(fset (cold, asfDefaultRipple)); } env.close(); // Each hot wallet trusts the related cold wallet for a large amount env (trust(hotUS, USD(10000000)), txflags (tfSetNoRipple)); env (trust(hotEU, EUR(10000000)), txflags (tfSetNoRipple)); // Market maker trusts both cold wallets for a large amount env (trust(mm, USD(10000000)), txflags (tfSetNoRipple)); env (trust(mm, EUR(10000000)), txflags (tfSetNoRipple)); env.close(); // Gateways authorize the trustlines of hot and market maker env (trust (coldUS, USD(0), hotUS, tfSetfAuth)); env (trust (coldEU, EUR(0), hotEU, tfSetfAuth)); env (trust (coldUS, USD(0), mm, tfSetfAuth)); env (trust (coldEU, EUR(0), mm, tfSetfAuth)); env.close(); // Issue currency from cold wallets to hot and market maker env (pay(coldUS, hotUS, USD(5000000))); env (pay(coldEU, hotEU, EUR(5000000))); env (pay(coldUS, mm, USD(5000000))); env (pay(coldEU, mm, EUR(5000000))); env.close(); // MM places offers float const rate = 0.9f; // 0.9 USD = 1 EUR env (offer(mm, EUR(4000000 * rate), USD(4000000)), json(jss::Flags, tfSell)); float const reverseRate = 1.0f/rate * 1.00101f; env (offer(mm, USD(4000000 * reverseRate), EUR(4000000)), json(jss::Flags, tfSell)); env.close(); // There should be a path available from hot US to cold EUR { Json::Value jvParams; jvParams[jss::destination_account] = coldEU.human(); jvParams[jss::destination_amount][jss::issuer] = coldEU.human(); jvParams[jss::destination_amount][jss::currency] = "EUR"; jvParams[jss::destination_amount][jss::value] = 10; jvParams[jss::source_account] = hotUS.human(); Json::Value const jrr {env.rpc( "json", "ripple_path_find", to_string(jvParams))[jss::result]}; BEAST_EXPECT(jrr[jss::status] == "success"); BEAST_EXPECT( jrr[jss::alternatives].isArray() && jrr[jss::alternatives].size() > 0); } // Send the payment using the found path. env (pay (hotUS, coldEU, EUR(10)), sendmax (USD(11.1223326))); } void testSelfAuth (std::initializer_list fs) { testcase ("Self Auth"); using namespace jtx; Env env {*this, with_features (fs)}; auto const closeTime = fix1449Time() + 100 * env.closed()->info().closeTimeResolution; env.close (closeTime); auto const gw = Account("gw"); auto const alice = Account("alice"); auto const gwUSD = gw["USD"]; auto const aliceUSD = alice["USD"]; env.fund (XRP(400000), gw, alice); env.close(); // Test that gw can create an offer to buy gw's currency. env (offer (gw, gwUSD(40), XRP(4000))); env.close(); std::uint32_t const gwOfferSeq = env.seq (gw) - 1; env.require (offers (gw, 1)); // Since gw has an offer out, gw should not be able to set RequireAuth. env(fset (gw, asfRequireAuth), ter (tecOWNERS)); env.close(); // Cancel gw's offer so we can set RequireAuth. env (offer_cancel (gw, gwOfferSeq)); env.close(); env.require (offers (gw, 0)); // gw now requires authorization for holders of its IOUs env(fset (gw, asfRequireAuth)); env.close(); // The test behaves differently with or without FlowCross. bool const flowCross = std::find (fs.begin(), fs.end(), featureFlowCross) != fs.end(); // Before FlowCross an account with lsfRequireAuth set could not // create an offer to buy their own currency. After FlowCross // they can. env (offer (gw, gwUSD(40), XRP(4000)), ter (flowCross ? tesSUCCESS : tecNO_LINE)); env.close(); env.require (offers (gw, flowCross ? 1 : 0)); if (!flowCross) // The rest of the test verifies FlowCross behavior. return; // Set up an authorized trust line and pay alice gwUSD 50. env (trust (gw, aliceUSD(100)), txflags (tfSetfAuth)); env (trust (alice, gwUSD(100))); env.close(); env (pay(gw, alice, gwUSD(50))); env.close(); env.require (balance (alice, gwUSD(50))); // alice's offer should cross gw's env (offer (alice, XRP(4000), gwUSD(40))); env.close(); env.require (offers (alice, 0)); env.require (balance (alice, gwUSD(10))); env.require (offers (gw, 0)); } void testTickSize (std::initializer_list fs) { testcase ("Tick Size"); using namespace jtx; // Try to set tick size without enabling feature { Env env {*this, with_features(fs)}; auto const gw = Account {"gateway"}; env.fund (XRP(10000), gw); auto txn = noop(gw); txn[sfTickSize.fieldName] = 0; env(txn, ter(temDISABLED)); } auto const fsPlus = with_features(fs) | with_features(featureTickSize); // Try to set tick size out of range { Env env {*this, fsPlus}; auto const gw = Account {"gateway"}; env.fund (XRP(10000), gw); auto txn = noop(gw); txn[sfTickSize.fieldName] = Quality::minTickSize - 1; env(txn, ter (temBAD_TICK_SIZE)); txn[sfTickSize.fieldName] = Quality::minTickSize; env(txn); BEAST_EXPECT ((*env.le(gw))[sfTickSize] == Quality::minTickSize); txn = noop (gw); txn[sfTickSize.fieldName] = Quality::maxTickSize; env(txn); BEAST_EXPECT (! env.le(gw)->isFieldPresent (sfTickSize)); txn = noop (gw); txn[sfTickSize.fieldName] = Quality::maxTickSize - 1; env(txn); BEAST_EXPECT ((*env.le(gw))[sfTickSize] == Quality::maxTickSize - 1); txn = noop (gw); txn[sfTickSize.fieldName] = Quality::maxTickSize + 1; env(txn, ter (temBAD_TICK_SIZE)); txn[sfTickSize.fieldName] = 0; env(txn, tesSUCCESS); BEAST_EXPECT (! env.le(gw)->isFieldPresent (sfTickSize)); } Env env {*this, fsPlus}; auto const gw = Account {"gateway"}; auto const alice = Account {"alice"}; auto const XTS = gw["XTS"]; auto const XXX = gw["XXX"]; env.fund (XRP (10000), gw, alice); { // Gateway sets its tick size to 5 auto txn = noop(gw); txn[sfTickSize.fieldName] = 5; env(txn); BEAST_EXPECT ((*env.le(gw))[sfTickSize] == 5); } env (trust (alice, XTS (1000))); env (trust (alice, XXX (1000))); env (pay (gw, alice, alice["XTS"] (100))); env (pay (gw, alice, alice["XXX"] (100))); env (offer (alice, XTS (10), XXX (30))); env (offer (alice, XTS (30), XXX (10))); env (offer (alice, XTS (10), XXX (30)), json(jss::Flags, tfSell)); env (offer (alice, XTS (30), XXX (10)), json(jss::Flags, tfSell)); std::map > offers; forEachItem (*env.current(), alice, [&](std::shared_ptr const& sle) { if (sle->getType() == ltOFFER) offers.emplace((*sle)[sfSequence], std::make_pair((*sle)[sfTakerPays], (*sle)[sfTakerGets])); }); // first offer auto it = offers.begin(); BEAST_EXPECT (it != offers.end()); BEAST_EXPECT (it->second.first == XTS(10) && it->second.second < XXX(30) && it->second.second > XXX(29.9994)); // second offer ++it; BEAST_EXPECT (it != offers.end()); BEAST_EXPECT (it->second.first == XTS(30) && it->second.second == XXX(10)); // third offer ++it; BEAST_EXPECT (it != offers.end()); BEAST_EXPECT (it->second.first == XTS(10.0002) && it->second.second == XXX(30)); // fourth offer // exact TakerPays is XTS(1/.033333) ++it; BEAST_EXPECT (it != offers.end()); BEAST_EXPECT (it->second.first == XTS(30) && it->second.second == XXX(10)); BEAST_EXPECT (++it == offers.end()); } void run () { auto testAll = [this](std::initializer_list fs) { testCanceledOffer(fs); testRmFundedOffer(fs); testTinyPayment(fs); testXRPTinyPayment(fs); testEnforceNoRipple(fs); testInsufficientReserve(fs); testFillModes(fs); testMalformed(fs); testExpiration(fs); testUnfundedCross(fs); testSelfCross(false, fs); testSelfCross(true, fs); testNegativeBalance(fs); testOfferCrossWithXRP(true, fs); testOfferCrossWithXRP(false, fs); testOfferCrossWithLimitOverride(fs); testOfferAcceptThenCancel(fs); testOfferCancelPastAndFuture(fs); testCurrencyConversionEntire(fs); testCurrencyConversionIntoDebt(fs); testCurrencyConversionInParts(fs); testCrossCurrencyStartXRP(fs); testCrossCurrencyEndXRP(fs); testCrossCurrencyBridged(fs); testOfferFeesConsumeFunds(fs); testOfferCreateThenCross(fs); testSellFlagBasic(fs); testSellFlagExceedLimit(fs); testGatewayCrossCurrency(fs); testPartialCross (fs); testXRPDirectCross (fs); testDirectCross (fs); testBridgedCross (fs); testSellOffer (fs); testSellWithFillOrKill (fs); testTransferRateOffer(fs); testSelfCrossOffer (fs); testSelfIssueOffer (fs); testBadPathAssert (fs); testDirectToDirectPath (fs); testSelfCrossLowQualityOffer (fs); testOfferInScaling (fs); testOfferInScalingWithXferRate (fs); testOfferThresholdWithReducedFunds (fs); testTinyOffer (fs); testSelfPayXferFeeOffer (fs); testSelfPayUnlimitedFunds (fs); testRequireAuth (fs); testMissingAuth (fs); testRCSmoketest (fs); testSelfAuth (fs); testTickSize (fs); }; // The first three test variants below passed at one time in the past (and // should still pass) but are commented out to conserve test time. // testAll(jtx::no_features ); // testAll({ featureFlowCross }); // testAll({featureFlow }); testAll({featureFlow, featureFlowCross }); testAll({featureFlow, fix1373 }); testAll({featureFlow, fix1373, featureFlowCross }); } }; BEAST_DEFINE_TESTSUITE (Offer, tx, ripple); } // test } // ripple