Files
rippled/src/test/app/Offer_test.cpp
2017-07-31 18:39:58 -04:00

4594 lines
172 KiB
C++

//------------------------------------------------------------------------------
/*
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 <BeastConfig.h>
#include <test/jtx.h>
#include <test/jtx/WSClient.h>
#include <test/jtx/PathSet.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/JsonFields.h>
#include <ripple/protocol/Quality.h>
namespace ripple {
namespace test {
class Offer_test : public beast::unit_test::suite
{
static bool hasFeature(uint256 const& feat, std::initializer_list<uint256> 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<std::int64_t>::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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<std::shared_ptr<SLE const>>
offersOnAccount (jtx::Env& env, jtx::Account account)
{
std::vector<std::shared_ptr<SLE const>> result;
forEachItem (*env.current (), account,
[&result](std::shared_ptr<SLE const> const& sle)
{
if (sle->getType() == ltOFFER)
result.push_back (sle);
});
return result;
}
void
testFillModes (std::initializer_list<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> fs)
{
testcase ("Self Cross Offer");
testSelfCrossOffer1 (fs);
testSelfCrossOffer2 (fs);
}
void
testSelfIssueOffer (std::initializer_list<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<Actor> 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<SLE const>& 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<uint256> 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<Actor> 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<SLE const>& 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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<uint256> 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 <std::uint32_t, std::pair<STAmount, STAmount>> offers;
forEachItem (*env.current(), alice,
[&](std::shared_ptr<SLE const> 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<uint256> 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