Files
xahaud/src/test/app/Offer_test.cpp

1930 lines
67 KiB
C++

//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012-2015 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/support/jtx.h>
#include <test/support/WSClient.h>
#include <test/support/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
{
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 ()
{
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 satisified. 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};
// ledger close times have a dynamic resolution depending on network
// conditions it appears the resolution in test is 10 seconds
env.close ();
auto const gw = Account {"gateway"};
auto const 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 ()
{
testcase ("Removing Canceled Offers");
using namespace jtx;
Env env {*this};
auto const gw = Account {"gateway"};
auto const alice = Account {"alice"};
auto const USD = gw["USD"];
env.fund (XRP (10000), alice, gw);
env.trust (USD (100), alice);
env (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 ()
{
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};
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 ()
{
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})
{
Env env {*this};
auto closeTime = [&]
{
auto const delta =
100 * env.closed ()->info ().closeTimeResolution;
if (withFix)
return STAmountSO::soTime2 + delta;
else
return STAmountSO::soTime2 - delta;
}();
auto offerCount = [&env](jtx::Account const& account)
{
auto count = 0;
forEachItem (*env.current (), account,
[&](std::shared_ptr<SLE const> const& sle)
{
if (sle->getType () == ltOFFER)
++count;
});
return count;
};
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 doen'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));
BEAST_EXPECT(offerCount (carol) == 0);
BEAST_EXPECT(offerCount (dan) == 1);
if (!withFix)
{
// funded offer was removed
BEAST_EXPECT(offerCount (erin) == 0);
env.require (balance ("erin", USD (1)));
}
else
{
// offer was correctly consumed. There is stil some
// liquidity left on that offer.
BEAST_EXPECT(offerCount (erin) == 1);
env.require (balance ("erin", USD (0.99999)));
}
}
}
}
void testEnforceNoRipple ()
{
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};
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), ter(tecPATH_DRY),
sendmax (XRP (50)), txflags (tfNoRippleDirect));
}
{
// Make sure payment works with default flags
Env env (*this);
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 ()
{
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};
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);
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};
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));
}
}
void
testFillModes ()
{
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};
env.fund (startBalance, gw);
auto const f = env.current ()->fees ().base;
env.fund (startBalance, alice, bob);
env (offer (bob, USD (500), XRP (500)), ter(tesSUCCESS));
env (trust (alice, USD (1000)), ter(tesSUCCESS));
env (pay (gw, alice, USD (1000)), ter(tesSUCCESS));
// 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};
env.fund (startBalance, gw);
auto const f = env.current ()->fees ().base;
env.fund (startBalance, 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));
}
}
void
testMalformed()
{
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};
env.fund (startBalance, gw);
env.fund (startBalance, 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()
{
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);
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()
{
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);
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)
{
testcase (std::string("Self-crossing") +
(use_partner ? ", with partner account" : ""));
using namespace jtx;
auto const gw = Account {"gateway"};
auto const partner = Account {"partner"};
auto const USD = gw["USD"];
auto const BTC = gw["BTC"];
Env env {*this};
env.fund (XRP (10000), gw);
if (use_partner)
{
env.fund (XRP (10000), partner);
env (trust (partner, USD (100)));
env (trust (partner, BTC (500)));
env (pay (gw, partner, USD(100)));
env (pay (gw, partner, BTC(500)));
}
auto const& account_to_test = use_partner ? partner : gw;
env.close();
env.require (offers (account_to_test, 0));
// PART 1:
// we will make two offers that can be used to bridge BTC to USD
// through XRP
env (offer (account_to_test, BTC (250), XRP (1000)),
offers (account_to_test, 1));
// validate that the book now shows a BTC for XRP offer
BEAST_EXPECT(isOffer(env, account_to_test, BTC(250), XRP(1000)));
auto const secondLegSeq = env.seq(account_to_test);
env (offer (account_to_test, XRP(1000), USD (50)),
offers (account_to_test, 2));
// validate that the book also shows a XRP for USD offer
BEAST_EXPECT(isOffer(env, account_to_test, XRP(1000), USD(50)));
// now make an offer that will cross and autobridge, meaning
// the outstanding offers will be taken leaving us with none
env (offer (account_to_test, USD (50), BTC (250)));
// NOTE :
// at this point, all offers are expected to be consumed.
// alas, they are not - because of bug in the current autobridging
// implementation (to be replaced in the not-so-distant future).
// The current implementation (incorrect) leaves an empty offer in the
// second leg of the bridge. validate the current behavior as-is and
// expect this test to be changed in the future.
env.require (offers (account_to_test, 1));
auto jrr = getBookOffers(env, USD, BTC);
BEAST_EXPECT(jrr[jss::offers].isArray());
BEAST_EXPECT(jrr[jss::offers].size() == 0);
jrr = getBookOffers(env, BTC, XRP);
BEAST_EXPECT(jrr[jss::offers].isArray());
BEAST_EXPECT(jrr[jss::offers].size() == 0);
BEAST_EXPECT(isOffer(env, account_to_test, XRP(0), USD(0)));
// cancel that lingering second offer so that it doesn't interfere with the
// next set of offers we test. this will not be needed once the bridging
// bug is fixed
Json::Value cancelOffer;
cancelOffer[jss::Account] = account_to_test.human();
cancelOffer[jss::OfferSequence] = secondLegSeq;
cancelOffer[jss::TransactionType] = "OfferCancel";
env (cancelOffer);
env.require (offers (account_to_test, 0));
// PART 2:
// simple direct crossing BTC to USD and then USD to BTC which causes
// the first offer to be replaced
env (offer (account_to_test, BTC (250), USD (50)),
offers (account_to_test, 1));
// validate that the book shows one BTC for USD offer and no USD for
// BTC offers
BEAST_EXPECT(isOffer(env, account_to_test, BTC(250), USD(50)));
jrr = getBookOffers(env, USD, BTC);
BEAST_EXPECT(jrr[jss::offers].isArray());
BEAST_EXPECT(jrr[jss::offers].size() == 0);
// this second offer would self-cross directly, so it causes the first
// offer by the same owner/taker to be removed
env (offer (account_to_test, USD (50), BTC (250)),
offers (account_to_test, 1));
// validate that we now have just the second offer...the first was removed
jrr = getBookOffers(env, BTC, USD);
BEAST_EXPECT(jrr[jss::offers].isArray());
BEAST_EXPECT(jrr[jss::offers].size() == 0);
BEAST_EXPECT(isOffer(env, account_to_test, USD(50), BTC(250)));
}
void
testNegativeBalance()
{
// This test creates an offer test for negative balance
// with transfer fees and miniscule funds.
testcase ("Negative Balance");
using namespace jtx;
Env env {*this};
auto const gw = Account {"gateway"};
auto const alice = Account {"alice"};
auto const bob = Account {"bob"};
auto const USD = gw["USD"];
auto const BTC = gw["BTC"];
// these *interesting* amounts were taken
// from the original JS test that was ported here
auto const gw_initial_balance = 1149999730;
auto const alice_initial_balance = 499946999680;
auto const bob_initial_balance = 10199999920;
auto const small_amount =
STAmount{ bob["USD"].issue(), UINT64_C(2710505431213761), -33};
env.fund (drops (gw_initial_balance), gw);
env.fund (drops (alice_initial_balance), alice);
env.fund (drops (bob_initial_balance), bob);
env (rate (gw, 1.005));
env (trust (alice, USD (500)));
env (trust (bob, USD (50)));
env (trust (gw, alice["USD"] (100)));
env (pay (gw, alice, alice["USD"] (50)));
env (pay (gw, bob, small_amount));
env (offer (alice, USD (50), XRP (150000)));
// unfund the offer
env (pay (alice, gw, USD (100)));
// drop the trust line (set to 0)
env (trust (gw, alice["USD"] (0)));
// verify balances
auto jrr = ledgerEntryState (env, alice, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "50");
jrr = ledgerEntryState (env, bob, gw, "USD");
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName][jss::value] ==
"-2710505431213761e-33");
// create crossing offer
env (offer (bob, XRP (2000), USD (1)));
// verify balances again
jrr = ledgerEntryState (env, alice, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "50");
jrr = ledgerEntryRoot (env, alice);
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName] ==
std::to_string(
alice_initial_balance - env.current ()->fees ().base * 3)
);
jrr = ledgerEntryState (env, bob, gw, "USD");
BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName][jss::value] == "0");
jrr = ledgerEntryRoot (env, bob);
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName] ==
std::to_string(
bob_initial_balance - env.current ()->fees ().base * 2)
);
}
void
testOfferCrossWithXRP(bool reverse_order)
{
testcase (std::string("Offer Crossing with XRP, ") +
(reverse_order ? "Reverse" : "Normal") +
" order");
using namespace jtx;
Env env {*this};
auto const gw = Account {"gateway"};
auto const alice = Account {"alice"};
auto const bob = Account {"bob"};
auto const USD = gw["USD"];
env.fund (XRP (10000), gw, alice, bob);
env (trust (alice, USD (1000)));
env (trust (bob, USD (1000)));
env (pay (gw, alice, alice["USD"] (500)));
if (reverse_order)
env (offer (bob, USD (1), XRP (4000)));
env (offer (alice, XRP (150000), USD (50)));
if (! reverse_order)
env (offer (bob, USD (1), XRP (4000)));
// Existing offer pays better than this wants.
// Fully consume existing offer.
// Pay 1 USD, get 4000 XRP.
auto jrr = ledgerEntryState (env, bob, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-1");
jrr = ledgerEntryRoot (env, bob);
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName] ==
std::to_string(
XRP (10000).value ().mantissa () -
XRP (reverse_order ? 4000 : 3000).value ().mantissa () -
env.current ()->fees ().base * 2)
);
jrr = ledgerEntryState (env, alice, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-499");
jrr = ledgerEntryRoot (env, alice);
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName] ==
std::to_string(
XRP (10000).value ().mantissa( ) +
XRP(reverse_order ? 4000 : 3000).value ().mantissa () -
env.current ()->fees ().base * 2)
);
}
void
testOfferCrossWithLimitOverride()
{
testcase ("Offer Crossing with Limit Override");
using namespace jtx;
Env env {*this};
auto const gw = Account {"gateway"};
auto const alice = Account {"alice"};
auto const bob = Account {"bob"};
auto const USD = gw["USD"];
env.fund (XRP (100000), gw, alice, bob);
env (trust (alice, USD (1000)));
env (pay (gw, alice, alice["USD"] (500)));
env (offer (alice, XRP (150000), USD (50)));
env (offer (bob, USD (1), XRP (3000)));
auto jrr = ledgerEntryState (env, bob, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-1");
jrr = ledgerEntryRoot(env, bob);
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName] ==
std::to_string(
XRP (100000).value ().mantissa () -
XRP (3000).value ().mantissa () -
env.current ()->fees ().base * 1)
);
jrr = ledgerEntryState (env, alice, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-499");
jrr = ledgerEntryRoot (env, alice);
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName] ==
std::to_string(
XRP (100000).value ().mantissa () +
XRP (3000).value ().mantissa () -
env.current ()->fees ().base * 2)
);
}
void
testOfferAcceptThenCancel()
{
testcase ("Offer Accept then Cancel.");
using namespace jtx;
Env env {*this};
auto const USD = env.master["USD"];
auto const nextOfferSeq = env.seq (env.master);
env (offer (env.master, XRP (500), USD (100)));
env.close ();
Json::Value cancelOffer;
cancelOffer[jss::Account] = env.master.human();
cancelOffer[jss::OfferSequence] = nextOfferSeq;
cancelOffer[jss::TransactionType] = "OfferCancel";
env (cancelOffer);
BEAST_EXPECT(env.seq (env.master) == nextOfferSeq + 2);
// ledger_accept, call twice and verify no odd behavior
env.close();
env.close();
BEAST_EXPECT(env.seq (env.master) == nextOfferSeq + 2);
}
void
testOfferCancelPastAndFuture()
{
testcase ("Offer Cancel Past and Future Sequence.");
using namespace jtx;
Env env {*this};
auto const alice = Account {"alice"};
auto const nextOfferSeq = env.seq (env.master);
env.fund (XRP (10000), alice);
Json::Value cancelOffer;
cancelOffer[jss::Account] = env.master.human();
cancelOffer[jss::OfferSequence] = nextOfferSeq;
cancelOffer[jss::TransactionType] = "OfferCancel";
env (cancelOffer);
cancelOffer[jss::OfferSequence] = env.seq (env.master);
env (cancelOffer, ter(temBAD_SEQUENCE));
cancelOffer[jss::OfferSequence] = env.seq (env.master) + 1;
env (cancelOffer, ter(temBAD_SEQUENCE));
env.close();
env.close();
}
void
testCurrencyConversionEntire()
{
testcase ("Currency Conversion: Entire Offer");
using namespace jtx;
Env env {*this};
auto const gw = Account {"gateway"};
auto const alice = Account {"alice"};
auto const bob = Account {"bob"};
auto const USD = gw["USD"];
env.fund (XRP (10000), gw, alice, bob);
env.require (owners (bob, 0));
env (trust (alice, USD (100)));
env (trust (bob, USD (1000)));
env.require (
owners (alice, 1),
owners (bob, 1));
env (pay (gw, alice, alice["USD"] (100)));
auto const bobOfferSeq = env.seq (bob);
env (offer (bob, USD (100), XRP (500)));
env.require (
owners (alice, 1),
owners (bob, 2));
auto jro = ledgerEntryOffer (env, bob, bobOfferSeq);
BEAST_EXPECT(
jro[jss::node][jss::TakerGets] == XRP (500).value ().getText ());
BEAST_EXPECT(
jro[jss::node][jss::TakerPays] == USD (100).value ().getJson (0));
env (pay (alice, alice, XRP (500)), sendmax (USD (100)));
auto jrr = ledgerEntryState (env, alice, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "0");
jrr = ledgerEntryRoot (env, alice);
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName] ==
std::to_string(
XRP (10000).value ().mantissa () +
XRP (500).value ().mantissa () -
env.current ()->fees ().base * 2)
);
jrr = ledgerEntryState (env, bob, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-100");
jro = ledgerEntryOffer(env, bob, bobOfferSeq);
BEAST_EXPECT(jro[jss::error] == "entryNotFound");
env.require (
owners (alice, 1),
owners (bob, 1));
}
void
testCurrencyConversionIntoDebt()
{
testcase ("Currency Conversion: Offerer Into Debt");
using namespace jtx;
Env env {*this};
auto const alice = Account {"alice"};
auto const bob = Account {"bob"};
auto const carol = Account {"carol"};
env.fund (XRP (10000), alice, bob, carol);
env (trust (alice, carol["EUR"] (2000)));
env (trust (bob, alice["USD"] (100)));
env (trust (carol, bob["EUR"] (1000)));
auto const bobOfferSeq = env.seq (bob);
env (offer (bob, alice["USD"] (50), carol["EUR"] (200)),
ter(tecUNFUNDED_OFFER));
env (offer (alice, carol["EUR"] (200), alice["USD"] (50)));
auto jro = ledgerEntryOffer(env, bob, bobOfferSeq);
BEAST_EXPECT(jro[jss::error] == "entryNotFound");
}
void
testCurrencyConversionInParts()
{
testcase ("Currency Conversion: In Parts");
using namespace jtx;
Env env {*this};
auto const gw = Account {"gateway"};
auto const alice = Account {"alice"};
auto const bob = Account {"bob"};
auto const USD = gw["USD"];
env.fund (XRP (10000), gw, alice, bob);
env (trust (alice, USD (200)));
env (trust (bob, USD (1000)));
env (pay (gw, alice, alice["USD"] (200)));
auto const bobOfferSeq = env.seq (bob);
env (offer (bob, USD (100), XRP (500)));
env (pay (alice, alice, XRP (200)), sendmax (USD (100)));
// The previous payment reduced the remaining offer amount by 200 XRP
auto jro = ledgerEntryOffer (env, bob, bobOfferSeq);
BEAST_EXPECT(
jro[jss::node][jss::TakerGets] == XRP (300).value ().getText ());
BEAST_EXPECT(
jro[jss::node][jss::TakerPays] == USD (60).value ().getJson (0));
// the balance between alice and gw is 160 USD..200 less the 40 taken
// by the offer
auto jrr = ledgerEntryState (env, alice, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-160");
// alice now has 200 more XRP from the payment
jrr = ledgerEntryRoot (env, alice);
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName] ==
std::to_string(
XRP (10000).value ().mantissa () +
XRP (200).value ().mantissa () -
env.current ()->fees ().base * 2)
);
// bob got 40 USD from partial consumption of the offer
jrr = ledgerEntryState (env, bob, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-40");
// Alice converts USD to XRP which should fail
// due to PartialPayment.
env (pay (alice, alice, XRP (600)), sendmax (USD (100)),
ter(tecPATH_PARTIAL));
// Alice converts USD to XRP, should succeed because
// we permit partial payment
env (pay (alice, alice, XRP (600)), sendmax (USD (100)),
txflags (tfPartialPayment));
// Verify the offer was consumed
jro = ledgerEntryOffer (env, bob, bobOfferSeq);
BEAST_EXPECT(jro[jss::error] == "entryNotFound");
// verify balances look right after the partial payment
// only 300 XRP should be have been payed since that's all
// that remained in the offer from bob. The alice balance is now
// 100 USD because another 60 USD were transferred to bob in the second
// payment
jrr = ledgerEntryState (env, alice, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-100");
jrr = ledgerEntryRoot (env, alice);
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName] ==
std::to_string(
XRP (10000).value ().mantissa () +
XRP (200).value ().mantissa () +
XRP (300).value ().mantissa () -
env.current ()->fees ().base * 4)
);
// bob now has 100 USD - 40 from the first payment and 60 from the
// second (partial) payment
jrr = ledgerEntryState (env, bob, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-100");
}
void
testCrossCurrencyStartXRP()
{
testcase ("Cross Currency Payment: Start with XRP");
using namespace jtx;
Env env {*this};
auto const gw = Account {"gateway"};
auto const alice = Account {"alice"};
auto const bob = Account {"bob"};
auto const carol = Account {"carol"};
auto const USD = gw["USD"];
env.fund (XRP (10000), gw, alice, bob, carol);
env (trust (carol, USD (1000)));
env (trust (bob, USD (2000)));
env (pay (gw, carol, carol["USD"] (500)));
auto const carolOfferSeq = env.seq (carol);
env (offer (carol, XRP (500), USD (50)));
env (pay (alice, bob, USD (25)), sendmax (XRP (333)));
auto jrr = ledgerEntryState (env, bob, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-25");
jrr = ledgerEntryState (env, carol, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-475");
auto jro = ledgerEntryOffer (env, carol, carolOfferSeq);
BEAST_EXPECT(
jro[jss::node][jss::TakerGets] == USD (25).value ().getJson (0));
BEAST_EXPECT(
jro[jss::node][jss::TakerPays] == XRP (250).value ().getText ());
}
void
testCrossCurrencyEndXRP()
{
testcase ("Cross Currency Payment: End with XRP");
using namespace jtx;
Env env {*this};
auto const gw = Account {"gateway"};
auto const alice = Account {"alice"};
auto const bob = Account {"bob"};
auto const carol = Account {"carol"};
auto const USD = gw["USD"];
env.fund (XRP (10000), gw, alice, bob, carol);
env (trust (alice, USD (1000)));
env (trust (carol, USD (2000)));
env (pay (gw, alice, alice["USD"] (500)));
auto const carolOfferSeq = env.seq (carol);
env (offer (carol, USD (50), XRP (500)));
env (pay (alice, bob, XRP (250)), sendmax (USD (333)));
auto jrr = ledgerEntryState (env, alice, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-475");
jrr = ledgerEntryState (env, carol, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-25");
jrr = ledgerEntryRoot (env, bob);
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName] ==
std::to_string(
XRP (10000).value ().mantissa () +
XRP (250).value ().mantissa ())
);
auto jro = ledgerEntryOffer (env, carol, carolOfferSeq);
BEAST_EXPECT(
jro[jss::node][jss::TakerGets] == XRP (250).value ().getText ());
BEAST_EXPECT(
jro[jss::node][jss::TakerPays] == USD (25).value ().getJson (0));
}
void
testCrossCurrencyBridged()
{
testcase ("Cross Currency Payment: Bridged");
using namespace jtx;
Env env {*this};
auto const gw1 = Account {"gateway_1"};
auto const gw2 = Account {"gateway_2"};
auto const alice = Account {"alice"};
auto const bob = Account {"bob"};
auto const carol = Account {"carol"};
auto const dan = Account {"dan"};
auto const USD = gw1["USD"];
auto const EUR = gw2["EUR"];
env.fund (XRP (10000), gw1, gw2, alice, bob, carol, dan);
env (trust (alice, USD (1000)));
env (trust (bob, EUR (1000)));
env (trust (carol, USD (1000)));
env (trust (dan, EUR (1000)));
env (pay (gw1, alice, alice["USD"] (500)));
env (pay (gw2, dan, dan["EUR"] (400)));
auto const carolOfferSeq = env.seq (carol);
env (offer (carol, USD (50), XRP (500)));
auto const danOfferSeq = env.seq (dan);
env (offer (dan, XRP (500), EUR (50)));
Json::Value jtp {Json::arrayValue};
jtp[0u][0u][jss::currency] = "XRP";
env (pay (alice, bob, EUR (30)),
json (jss::Paths, jtp),
sendmax (USD (333)));
auto jrr = ledgerEntryState (env, alice, gw1, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "470");
jrr = ledgerEntryState (env, bob, gw2, "EUR");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-30");
jrr = ledgerEntryState (env, carol, gw1, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-30");
jrr = ledgerEntryState (env, dan, gw2, "EUR");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-370");
auto jro = ledgerEntryOffer (env, carol, carolOfferSeq);
BEAST_EXPECT(
jro[jss::node][jss::TakerGets] == XRP (200).value ().getText ());
BEAST_EXPECT(
jro[jss::node][jss::TakerPays] == USD (20).value ().getJson (0));
jro = ledgerEntryOffer (env, dan, danOfferSeq);
BEAST_EXPECT(
jro[jss::node][jss::TakerGets] == gw2["EUR"] (20).value ().getJson (0));
BEAST_EXPECT(
jro[jss::node][jss::TakerPays] == XRP (200).value ().getText ());
}
void
testOfferFeesConsumeFunds()
{
testcase ("Offer Fees Consume Funds");
using namespace jtx;
Env env {*this};
auto const gw1 = Account {"gateway_1"};
auto const gw2 = Account {"gateway_2"};
auto const gw3 = Account {"gateway_3"};
auto const alice = Account {"alice"};
auto const bob = Account {"bob"};
auto const USD1 = gw1["USD"];
auto const USD2 = gw2["USD"];
auto const USD3 = gw3["USD"];
// Provide micro amounts to compensate for fees to make results round
// nice.
// reserve: Alice has 3 entries in the ledger, via trust lines
// fees:
// 1 for each trust limit == 3 (alice < mtgox/amazon/bitstamp) +
// 1 for payment == 4
auto const starting_xrp = XRP (100) +
env.current ()->fees ().accountReserve (3) +
env.current ()->fees ().base * 4;
env.fund (starting_xrp, gw1, gw2, gw3, alice, bob);
env (trust (alice, USD1 (1000)));
env (trust (alice, USD2 (1000)));
env (trust (alice, USD3 (1000)));
env (trust (bob, USD1 (1000)));
env (trust (bob, USD2 (1000)));
env (pay (gw1, bob, bob["USD"] (500)));
env (offer (bob, XRP (200), USD1 (200)));
// Alice has 350 fees - a reserve of 50 = 250 reserve = 100 available.
// Ask for more than available to prove reserve works.
env (offer (alice, USD1 (200), XRP (200)));
auto jrr = ledgerEntryState (env, alice, gw1, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "100");
jrr = ledgerEntryRoot (env, alice);
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName] == XRP (350).value ().getText ()
);
jrr = ledgerEntryState (env, bob, gw1, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-400");
}
void
testOfferCreateThenCross()
{
testcase ("Offer Create, then Cross");
using namespace jtx;
Env env {*this};
auto const gw = Account {"gateway"};
auto const alice = Account {"alice"};
auto const bob = Account {"bob"};
auto const USD = gw["USD"];
env.fund (XRP (10000), gw, alice, bob);
env (rate (gw, 1.005));
env (trust (alice, USD (1000)));
env (trust (bob, USD (1000)));
env (trust (gw, alice["USD"] (50)));
env (pay (gw, bob, bob["USD"] (1)));
env (pay (alice, gw, USD (50)));
env (trust (gw, alice["USD"] (0)));
env (offer (alice, USD (50), XRP (150000)));
env (offer (bob, XRP (100), USD (0.1)));
auto jrr = ledgerEntryState (env, alice, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "49.96666666666667");
jrr = ledgerEntryState (env, bob, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-0.966500000033334");
}
void
testSellFlagBasic()
{
testcase ("Offer tfSell: Basic Sell");
using namespace jtx;
Env env {*this};
auto const gw = Account {"gateway"};
auto const alice = Account {"alice"};
auto const bob = Account {"bob"};
auto const USD = gw["USD"];
auto const starting_xrp = XRP (100) +
env.current ()->fees ().accountReserve (1) +
env.current ()->fees ().base * 2;
env.fund (starting_xrp, gw, alice, bob);
env (trust (alice, USD (1000)));
env (trust (bob, USD (1000)));
env (pay (gw, bob, bob["USD"] (500)));
env (offer (bob, XRP (200), USD (200)), json(jss::Flags, tfSell));
// Alice has 350 + fees - a reserve of 50 = 250 reserve = 100 available.
// Alice has 350 + fees - a reserve of 50 = 250 reserve = 100 available.
// Ask for more than available to prove reserve works.
env (offer (alice, USD (200), XRP (200)), json(jss::Flags, tfSell));
auto jrr = ledgerEntryState (env, alice, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-100");
jrr = ledgerEntryRoot (env, alice);
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName] == XRP (250).value ().getText ()
);
jrr = ledgerEntryState (env, bob, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-400");
}
void
testSellFlagExceedLimit()
{
testcase ("Offer tfSell: 2x Sell Exceed Limit");
using namespace jtx;
Env env {*this};
auto const gw = Account {"gateway"};
auto const alice = Account {"alice"};
auto const bob = Account {"bob"};
auto const USD = gw["USD"];
auto const starting_xrp = XRP (100) +
env.current ()->fees ().accountReserve (1) +
env.current ()->fees ().base * 2;
env.fund (starting_xrp, gw, alice, bob);
env (trust (alice, USD (150)));
env (trust (bob, USD (1000)));
env (pay (gw, bob, bob["USD"] (500)));
env (offer (bob, XRP (100), USD (200)));
// Alice has 350 fees - a reserve of 50 = 250 reserve = 100 available.
// Ask for more than available to prove reserve works.
// Taker pays 100 USD for 100 XRP.
// Selling XRP.
// Will sell all 100 XRP and get more USD than asked for.
env (offer (alice, USD (100), XRP (100)), json(jss::Flags, tfSell));
auto jrr = ledgerEntryState (env, alice, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-200");
jrr = ledgerEntryRoot (env, alice);
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName] == XRP (250).value ().getText ()
);
jrr = ledgerEntryState (env, bob, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-300");
}
void
testGatewayCrossCurrency()
{
testcase ("Client Issue #535: Gateway Cross Currency");
using namespace jtx;
Env env {*this};
auto const gw = Account {"gateway"};
auto const alice = Account {"alice"};
auto const bob = Account {"bob"};
auto const XTS = gw["XTS"];
auto const XXX = gw["XXX"];
auto const starting_xrp = XRP (100.1) +
env.current ()->fees ().accountReserve (1) +
env.current ()->fees ().base * 2;
env.fund (starting_xrp, gw, alice, bob);
env (trust (alice, XTS (1000)));
env (trust (alice, XXX (1000)));
env (trust (bob, XTS (1000)));
env (trust (bob, XXX (1000)));
env (pay (gw, alice, alice["XTS"] (100)));
env (pay (gw, alice, alice["XXX"] (100)));
env (pay (gw, bob, bob["XTS"] (100)));
env (pay (gw, bob, bob["XXX"] (100)));
env (offer (alice, XTS (100), XXX (100)));
//WS client is used here because the RPC client could not
//be convinced to pass the build_path argument
auto wsc = makeWSClient(env.app().config());
Json::Value payment;
payment[jss::secret] = toBase58(generateSeed("bob"));
payment[jss::id] = env.seq (bob);
payment[jss::build_path] = true;
payment[jss::tx_json] = pay (bob, bob, bob["XXX"] (1));
payment[jss::tx_json][jss::Sequence] =
env.current ()->read (
keylet::account (bob.id ()))->getFieldU32 (sfSequence);
payment[jss::tx_json][jss::Fee] =
std::to_string( env.current ()->fees ().base);
payment[jss::tx_json][jss::SendMax] =
bob ["XTS"] (1.5).value ().getJson (0);
auto jrr = wsc->invoke("submit", payment);
BEAST_EXPECT(jrr[jss::status] == "success");
BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "tesSUCCESS");
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");
}
void testTickSize ()
{
testcase ("Tick Size");
using namespace jtx;
// Try to set tick size without enabling feature
{
Env env {*this};
auto const gw = Account {"gateway"};
env.fund (XRP(10000), gw);
auto txn = noop(gw);
txn[sfTickSize.fieldName] = 0;
env(txn, ter(temDISABLED));
}
// Try to set tick size out of range
{
Env env {*this, features (featureTickSize)};
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, features (featureTickSize)};
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 ()
{
testCanceledOffer ();
testRmFundedOffer ();
testTinyPayment ();
testXRPTinyPayment ();
testEnforceNoRipple ();
testInsufficientReserve ();
testFillModes ();
testMalformed ();
testExpiration ();
testUnfundedCross ();
testSelfCross (false);
testSelfCross (true);
testNegativeBalance ();
testOfferCrossWithXRP (true);
testOfferCrossWithXRP (false);
testOfferCrossWithLimitOverride ();
testOfferAcceptThenCancel ();
testOfferCancelPastAndFuture ();
testCurrencyConversionEntire ();
testCurrencyConversionIntoDebt ();
testCurrencyConversionInParts ();
testCrossCurrencyStartXRP ();
testCrossCurrencyEndXRP ();
testCrossCurrencyBridged ();
testOfferFeesConsumeFunds ();
testOfferCreateThenCross ();
testSellFlagBasic ();
testSellFlagExceedLimit ();
testGatewayCrossCurrency ();
testTickSize ();
}
};
BEAST_DEFINE_TESTSUITE (Offer, tx, ripple);
} // test
} // ripple