mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-24 04:55:52 +00:00
A recent version of clang notes a number of places in range for loops where the code base was making unnecessary copies or using const lvalue references to extend lifetimes. This fixes the places that clang identified.
5222 lines
189 KiB
C++
5222 lines
189 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 <ripple/protocol/Feature.h>
|
|
#include <ripple/protocol/Quality.h>
|
|
#include <ripple/protocol/jss.h>
|
|
#include <test/jtx.h>
|
|
#include <test/jtx/PathSet.h>
|
|
#include <test/jtx/WSClient.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 * 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(FeatureBitset features)
|
|
{
|
|
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, features};
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
testcase("Removing Canceled Offers");
|
|
|
|
using namespace jtx;
|
|
Env env{*this, features};
|
|
|
|
auto const gw = Account{"gateway"};
|
|
auto const alice = Account{"alice"};
|
|
auto const USD = gw["USD"];
|
|
|
|
env.fund(XRP(10000), alice, gw);
|
|
env.close();
|
|
env.trust(USD(100), alice);
|
|
env.close();
|
|
|
|
env(pay(gw, alice, USD(50)));
|
|
env.close();
|
|
|
|
auto const offer1Seq = env.seq(alice);
|
|
|
|
env(offer(alice, XRP(500), USD(100)), require(offers(alice, 1)));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(isOffer(env, alice, XRP(500), USD(100)));
|
|
|
|
// cancel the offer above and replace it with a new offer
|
|
auto const offer2Seq = env.seq(alice);
|
|
|
|
env(offer(alice, XRP(300), USD(100)),
|
|
json(jss::OfferSequence, offer1Seq),
|
|
require(offers(alice, 1)));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(
|
|
isOffer(env, alice, XRP(300), USD(100)) &&
|
|
!isOffer(env, alice, XRP(500), USD(100)));
|
|
|
|
// Test canceling non-existent offer.
|
|
// auto const offer3Seq = env.seq (alice);
|
|
|
|
env(offer(alice, XRP(400), USD(200)),
|
|
json(jss::OfferSequence, offer1Seq),
|
|
require(offers(alice, 2)));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(
|
|
isOffer(env, alice, XRP(300), USD(100)) &&
|
|
isOffer(env, alice, XRP(400), USD(200)));
|
|
|
|
// Test cancellation now with OfferCancel tx
|
|
auto const offer4Seq = env.seq(alice);
|
|
env(offer(alice, XRP(222), USD(111)), require(offers(alice, 3)));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(isOffer(env, alice, XRP(222), USD(111)));
|
|
{
|
|
Json::Value cancelOffer;
|
|
cancelOffer[jss::Account] = alice.human();
|
|
cancelOffer[jss::OfferSequence] = offer4Seq;
|
|
cancelOffer[jss::TransactionType] = jss::OfferCancel;
|
|
env(cancelOffer);
|
|
}
|
|
env.close();
|
|
BEAST_EXPECT(env.seq(alice) == offer4Seq + 2);
|
|
|
|
BEAST_EXPECT(!isOffer(env, alice, XRP(222), USD(111)));
|
|
|
|
// Create an offer that both fails with a tecEXPIRED code and removes
|
|
// an offer. Show that the attempt to remove the offer fails.
|
|
env.require(offers(alice, 2));
|
|
|
|
// featureDepositPreauths changes the return code on an expired Offer.
|
|
// Adapt to that.
|
|
bool const featPreauth{features[featureDepositPreauth]};
|
|
env(offer(alice, XRP(5), USD(2)),
|
|
json(sfExpiration.fieldName, lastClose(env)),
|
|
json(jss::OfferSequence, offer2Seq),
|
|
ter(featPreauth ? TER{tecEXPIRED} : TER{tesSUCCESS}));
|
|
env.close();
|
|
|
|
env.require(offers(alice, 2));
|
|
BEAST_EXPECT(isOffer(env, alice, XRP(300), USD(100))); // offer2
|
|
BEAST_EXPECT(!isOffer(env, alice, XRP(5), USD(2))); // expired
|
|
}
|
|
|
|
void
|
|
testTinyPayment(FeatureBitset features)
|
|
{
|
|
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, features};
|
|
|
|
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
|
|
// Note: the DeliverNodeReverse code has been removed; however since
|
|
// this is a regression test the original test is being left as-is for
|
|
// now.
|
|
for (int i = 0; i < 101; ++i)
|
|
env(offer(carol, USD(1), EUR(2)));
|
|
|
|
env(pay(alice, bob, EUR(epsilon)), path(~EUR), sendmax(USD(100)));
|
|
}
|
|
|
|
void
|
|
testXRPTinyPayment(FeatureBitset features)
|
|
{
|
|
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"];
|
|
Env env{*this, features};
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, dan, erin, gw);
|
|
env.close();
|
|
env.trust(USD(1000), alice, bob, carol, dan, erin);
|
|
env.close();
|
|
env(pay(gw, carol, USD(0.99999)));
|
|
env(pay(gw, dan, USD(1)));
|
|
env(pay(gw, erin, USD(1)));
|
|
env.close();
|
|
|
|
// 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 now removed
|
|
// stAmountCalcSwitchover2 patch was inactive)
|
|
env(offer(carol, drops(1), USD(0.99999)));
|
|
// 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();
|
|
// 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 now removed
|
|
// stAmountCalcSwitchover2 was inactive.)
|
|
env(offer(erin, drops(2), USD(1)));
|
|
|
|
env(pay(alice, bob, USD(1)),
|
|
path(~USD),
|
|
sendmax(XRP(102)),
|
|
txflags(tfNoRippleDirect | tfPartialPayment));
|
|
|
|
env.require(offers(carol, 0), offers(dan, 1));
|
|
|
|
// offer was correctly consumed. There is still some
|
|
// liquidity left on that offer.
|
|
env.require(balance(erin, USD(0.99999)), offers(erin, 1));
|
|
}
|
|
|
|
void
|
|
testRmSmallIncreasedQOffersXRP(FeatureBitset features)
|
|
{
|
|
testcase("Rm small increased q offers XRP");
|
|
|
|
// Carol places an offer, but cannot fully fund the offer. When her
|
|
// funding is taken into account, the offer's quality drops below its
|
|
// initial quality and has an input amount of 1 drop. This is removed as
|
|
// an offer that may block offer books.
|
|
|
|
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"];
|
|
|
|
// Test offer crossing
|
|
for (auto crossBothOffers : {false, true})
|
|
{
|
|
Env env{*this, features};
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, gw);
|
|
env.close();
|
|
env.trust(USD(1000), alice, bob, carol);
|
|
// underfund carol's offer
|
|
auto initialCarolUSD = USD(0.499);
|
|
env(pay(gw, carol, initialCarolUSD));
|
|
env(pay(gw, bob, USD(100)));
|
|
env.close();
|
|
// This offer is underfunded
|
|
env(offer(carol, drops(1), USD(1)));
|
|
env.close();
|
|
// offer at a lower quality
|
|
env(offer(bob, drops(2), USD(1), tfPassive));
|
|
env.close();
|
|
env.require(offers(bob, 1), offers(carol, 1));
|
|
|
|
// alice places an offer that crosses carol's; depending on
|
|
// "crossBothOffers" it may cross bob's as well
|
|
auto aliceTakerGets = crossBothOffers ? drops(2) : drops(1);
|
|
env(offer(alice, USD(1), aliceTakerGets));
|
|
env.close();
|
|
|
|
if (features[fixRmSmallIncreasedQOffers])
|
|
{
|
|
env.require(
|
|
offers(carol, 0),
|
|
balance(
|
|
carol,
|
|
initialCarolUSD)); // offer is removed but not taken
|
|
if (crossBothOffers)
|
|
{
|
|
env.require(
|
|
offers(alice, 0),
|
|
balance(alice, USD(1))); // alice's offer is crossed
|
|
}
|
|
else
|
|
{
|
|
env.require(
|
|
offers(alice, 1),
|
|
balance(
|
|
alice, USD(0))); // alice's offer is not crossed
|
|
}
|
|
}
|
|
else
|
|
{
|
|
env.require(
|
|
offers(alice, 1),
|
|
offers(bob, 1),
|
|
offers(carol, 1),
|
|
balance(alice, USD(0)),
|
|
balance(
|
|
carol,
|
|
initialCarolUSD)); // offer is not crossed at all
|
|
}
|
|
}
|
|
|
|
// Test payments
|
|
for (auto partialPayment : {false, true})
|
|
{
|
|
Env env{*this, features};
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, gw);
|
|
env.close();
|
|
env.trust(USD(1000), alice, bob, carol);
|
|
env.close();
|
|
auto const initialCarolUSD = USD(0.999);
|
|
env(pay(gw, carol, initialCarolUSD));
|
|
env.close();
|
|
env(pay(gw, bob, USD(100)));
|
|
env.close();
|
|
env(offer(carol, drops(1), USD(1)));
|
|
env.close();
|
|
env(offer(bob, drops(2), USD(2), tfPassive));
|
|
env.close();
|
|
env.require(offers(bob, 1), offers(carol, 1));
|
|
|
|
std::uint32_t const flags = partialPayment
|
|
? (tfNoRippleDirect | tfPartialPayment)
|
|
: tfNoRippleDirect;
|
|
|
|
TER const expectedTer =
|
|
partialPayment ? TER{tesSUCCESS} : TER{tecPATH_PARTIAL};
|
|
|
|
env(pay(alice, bob, USD(5)),
|
|
path(~USD),
|
|
sendmax(XRP(1)),
|
|
txflags(flags),
|
|
ter(expectedTer));
|
|
env.close();
|
|
|
|
if (features[fixRmSmallIncreasedQOffers])
|
|
{
|
|
if (expectedTer == tesSUCCESS)
|
|
{
|
|
env.require(offers(carol, 0));
|
|
env.require(balance(
|
|
carol,
|
|
initialCarolUSD)); // offer is removed but not taken
|
|
}
|
|
else
|
|
{
|
|
// TODO: Offers are not removed when payments fail
|
|
// If that is addressed, the test should show that carol's
|
|
// offer is removed but not taken, as in the other branch of
|
|
// this if statement
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (partialPayment)
|
|
{
|
|
env.require(offers(carol, 0));
|
|
env.require(
|
|
balance(carol, USD(0))); // offer is removed and taken
|
|
}
|
|
else
|
|
{
|
|
// offer is not removed or taken
|
|
BEAST_EXPECT(isOffer(env, carol, drops(1), USD(1)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
testRmSmallIncreasedQOffersIOU(FeatureBitset features)
|
|
{
|
|
testcase("Rm small increased q offers IOU");
|
|
|
|
// Carol places an offer, but cannot fully fund the offer. When her
|
|
// funding is taken into account, the offer's quality drops below its
|
|
// initial quality and has an input amount of 1 drop. This is removed as
|
|
// an offer that may block offer books.
|
|
|
|
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"];
|
|
|
|
auto tinyAmount = [&](IOU const& iou) -> PrettyAmount {
|
|
STAmount amt(
|
|
iou.issue(),
|
|
/*mantissa*/ 1,
|
|
/*exponent*/ -81);
|
|
return PrettyAmount(amt, iou.account.name());
|
|
};
|
|
|
|
// Test offer crossing
|
|
for (auto crossBothOffers : {false, true})
|
|
{
|
|
Env env{*this, features};
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, gw);
|
|
env.close();
|
|
env.trust(USD(1000), alice, bob, carol);
|
|
env.trust(EUR(1000), alice, bob, carol);
|
|
// underfund carol's offer
|
|
auto initialCarolUSD = tinyAmount(USD);
|
|
env(pay(gw, carol, initialCarolUSD));
|
|
env(pay(gw, bob, USD(100)));
|
|
env(pay(gw, alice, EUR(100)));
|
|
env.close();
|
|
// This offer is underfunded
|
|
env(offer(carol, EUR(1), USD(10)));
|
|
env.close();
|
|
// offer at a lower quality
|
|
env(offer(bob, EUR(1), USD(5), tfPassive));
|
|
env.close();
|
|
env.require(offers(bob, 1), offers(carol, 1));
|
|
|
|
// alice places an offer that crosses carol's; depending on
|
|
// "crossBothOffers" it may cross bob's as well
|
|
// Whatever
|
|
auto aliceTakerGets = crossBothOffers ? EUR(0.2) : EUR(0.1);
|
|
env(offer(alice, USD(1), aliceTakerGets));
|
|
env.close();
|
|
|
|
if (features[fixRmSmallIncreasedQOffers])
|
|
{
|
|
env.require(
|
|
offers(carol, 0),
|
|
balance(
|
|
carol,
|
|
initialCarolUSD)); // offer is removed but not taken
|
|
if (crossBothOffers)
|
|
{
|
|
env.require(
|
|
offers(alice, 0),
|
|
balance(alice, USD(1))); // alice's offer is crossed
|
|
}
|
|
else
|
|
{
|
|
env.require(
|
|
offers(alice, 1),
|
|
balance(
|
|
alice, USD(0))); // alice's offer is not crossed
|
|
}
|
|
}
|
|
else
|
|
{
|
|
env.require(
|
|
offers(alice, 1),
|
|
offers(bob, 1),
|
|
offers(carol, 1),
|
|
balance(alice, USD(0)),
|
|
balance(
|
|
carol,
|
|
initialCarolUSD)); // offer is not crossed at all
|
|
}
|
|
}
|
|
|
|
// Test payments
|
|
for (auto partialPayment : {false, true})
|
|
{
|
|
Env env{*this, features};
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, gw);
|
|
env.close();
|
|
env.trust(USD(1000), alice, bob, carol);
|
|
env.trust(EUR(1000), alice, bob, carol);
|
|
env.close();
|
|
// underfund carol's offer
|
|
auto const initialCarolUSD = tinyAmount(USD);
|
|
env(pay(gw, carol, initialCarolUSD));
|
|
env(pay(gw, bob, USD(100)));
|
|
env(pay(gw, alice, EUR(100)));
|
|
env.close();
|
|
// This offer is underfunded
|
|
env(offer(carol, EUR(1), USD(2)));
|
|
env.close();
|
|
env(offer(bob, EUR(2), USD(4), tfPassive));
|
|
env.close();
|
|
env.require(offers(bob, 1), offers(carol, 1));
|
|
|
|
std::uint32_t const flags = partialPayment
|
|
? (tfNoRippleDirect | tfPartialPayment)
|
|
: tfNoRippleDirect;
|
|
|
|
TER const expectedTer =
|
|
partialPayment ? TER{tesSUCCESS} : TER{tecPATH_PARTIAL};
|
|
|
|
env(pay(alice, bob, USD(5)),
|
|
path(~USD),
|
|
sendmax(EUR(10)),
|
|
txflags(flags),
|
|
ter(expectedTer));
|
|
env.close();
|
|
|
|
if (features[fixRmSmallIncreasedQOffers])
|
|
{
|
|
if (expectedTer == tesSUCCESS)
|
|
{
|
|
env.require(offers(carol, 0));
|
|
env.require(balance(
|
|
carol,
|
|
initialCarolUSD)); // offer is removed but not taken
|
|
}
|
|
else
|
|
{
|
|
// TODO: Offers are not removed when payments fail
|
|
// If that is addressed, the test should show that carol's
|
|
// offer is removed but not taken, as in the other branch of
|
|
// this if statement
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (partialPayment)
|
|
{
|
|
env.require(offers(carol, 0));
|
|
env.require(
|
|
balance(carol, USD(0))); // offer is removed and taken
|
|
}
|
|
else
|
|
{
|
|
// offer is not removed or taken
|
|
BEAST_EXPECT(isOffer(env, carol, EUR(1), USD(2)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
testEnforceNoRipple(FeatureBitset features)
|
|
{
|
|
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, features};
|
|
|
|
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, features};
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
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, features};
|
|
|
|
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, features};
|
|
|
|
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, features};
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
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 don't
|
|
// place the offer on the books. But also clean up expired offers
|
|
// that are discovered along the way.
|
|
//
|
|
// fix1578 changes the return code. Verify expected behavior
|
|
// without and with fix1578.
|
|
for (auto const& tweakedFeatures :
|
|
{features - fix1578, features | fix1578})
|
|
{
|
|
Env env{*this, tweakedFeatures};
|
|
|
|
auto const f = env.current()->fees().base;
|
|
|
|
env.fund(startBalance, gw, alice, bob);
|
|
|
|
// bob creates an offer that expires before the next ledger close.
|
|
env(offer(bob, USD(500), XRP(500)),
|
|
json(sfExpiration.fieldName, lastClose(env) + 1),
|
|
ter(tesSUCCESS));
|
|
|
|
// The offer expires (it's not removed yet).
|
|
env.close();
|
|
env.require(owners(bob, 1), offers(bob, 1));
|
|
|
|
// bob creates the offer that will be crossed.
|
|
env(offer(bob, USD(500), XRP(500)), ter(tesSUCCESS));
|
|
env.close();
|
|
env.require(owners(bob, 2), offers(bob, 2));
|
|
|
|
env(trust(alice, USD(1000)), ter(tesSUCCESS));
|
|
env(pay(gw, alice, USD(1000)), ter(tesSUCCESS));
|
|
|
|
// Order that can't be filled but will remove bob's expired offer:
|
|
{
|
|
TER const killedCode{
|
|
tweakedFeatures[fix1578] ? TER{tecKILLED}
|
|
: TER{tesSUCCESS}};
|
|
env(offer(alice, XRP(1000), USD(1000)),
|
|
txflags(tfFillOrKill),
|
|
ter(killedCode));
|
|
}
|
|
env.require(
|
|
balance(alice, startBalance - (f * 2)),
|
|
balance(alice, USD(1000)),
|
|
owners(alice, 1),
|
|
offers(alice, 0),
|
|
balance(bob, startBalance - (f * 2)),
|
|
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 * 3) + XRP(500)),
|
|
balance(alice, USD(500)),
|
|
owners(alice, 1),
|
|
offers(alice, 0),
|
|
balance(bob, startBalance - (f * 2) - 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, features};
|
|
|
|
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, features);
|
|
|
|
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, features);
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
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, features};
|
|
|
|
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
|
|
{
|
|
env(offer(alice, USD(1000), XRP(1000)),
|
|
json(sfExpiration.fieldName, 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(FeatureBitset features)
|
|
{
|
|
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);
|
|
|
|
Env env{*this, features};
|
|
|
|
env.fund(startBalance, gw, alice, bob);
|
|
env.close();
|
|
|
|
auto const f = env.current()->fees().base;
|
|
|
|
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));
|
|
|
|
// Place an offer that should have already expired.
|
|
// The DepositPreauth amendment changes the return code; adapt to that.
|
|
bool const featPreauth{features[featureDepositPreauth]};
|
|
|
|
env(offer(alice, xrpOffer, usdOffer),
|
|
json(sfExpiration.fieldName, lastClose(env)),
|
|
ter(featPreauth ? TER{tecEXPIRED} : TER{tesSUCCESS}));
|
|
|
|
env.require(
|
|
balance(alice, startBalance - f - f),
|
|
balance(alice, usdOffer),
|
|
offers(alice, 0),
|
|
owners(alice, 1));
|
|
env.close();
|
|
|
|
// Add an offer that expires before the next ledger close
|
|
env(offer(alice, xrpOffer, usdOffer),
|
|
json(sfExpiration.fieldName, 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(FeatureBitset features)
|
|
{
|
|
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, features};
|
|
|
|
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, FeatureBitset features)
|
|
{
|
|
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, features};
|
|
env.close();
|
|
|
|
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)));
|
|
env.require(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)));
|
|
env.require(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 which is addressed by fixTakerDryOfferRemoval.
|
|
// The pre-fixTakerDryOfferRemoval 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);
|
|
bool const noStaleOffers{
|
|
features[featureFlowCross] ||
|
|
features[fixTakerDryOfferRemoval]};
|
|
|
|
BEAST_EXPECT(acctOffers.size() == (noStaleOffers ? 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] = jss::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)));
|
|
env.require(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)));
|
|
env.require(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(FeatureBitset features)
|
|
{
|
|
// This test creates an offer test for negative balance
|
|
// with transfer fees and miniscule funds.
|
|
testcase("Negative Balance");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
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 =
|
|
features[featureFlowCross] ? 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, FeatureBitset features)
|
|
{
|
|
testcase(
|
|
std::string("Offer Crossing with XRP, ") +
|
|
(reverse_order ? "Reverse" : "Normal") + " order");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
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] ==
|
|
to_string((XRP(10000) - XRP(reverse_order ? 4000 : 3000) -
|
|
env.current()->fees().base * 2)
|
|
.xrp()));
|
|
|
|
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] ==
|
|
to_string((XRP(10000) + XRP(reverse_order ? 4000 : 3000) -
|
|
env.current()->fees().base * 2)
|
|
.xrp()));
|
|
}
|
|
|
|
void
|
|
testOfferCrossWithLimitOverride(FeatureBitset features)
|
|
{
|
|
testcase("Offer Crossing with Limit Override");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
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] ==
|
|
to_string((XRP(100000) - XRP(3000) - env.current()->fees().base * 1)
|
|
.xrp()));
|
|
|
|
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] ==
|
|
to_string((XRP(100000) + XRP(3000) - env.current()->fees().base * 2)
|
|
.xrp()));
|
|
}
|
|
|
|
void
|
|
testOfferAcceptThenCancel(FeatureBitset features)
|
|
{
|
|
testcase("Offer Accept then Cancel.");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
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] = jss::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(FeatureBitset features)
|
|
{
|
|
testcase("Offer Cancel Past and Future Sequence.");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
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] = jss::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(FeatureBitset features)
|
|
{
|
|
testcase("Currency Conversion: Entire Offer");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
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(JsonOptions::none));
|
|
|
|
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] ==
|
|
to_string((XRP(10000) + XRP(500) - env.current()->fees().base * 2)
|
|
.xrp()));
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
testcase("Currency Conversion: Offerer Into Debt");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
testcase("Currency Conversion: In Parts");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
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(JsonOptions::none));
|
|
|
|
// 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] ==
|
|
to_string((XRP(10000) + XRP(200) - env.current()->fees().base * 2)
|
|
.xrp()));
|
|
|
|
// 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] ==
|
|
to_string((XRP(10000) + XRP(200) + XRP(300) -
|
|
env.current()->fees().base * 4)
|
|
.xrp()));
|
|
|
|
// 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(FeatureBitset features)
|
|
{
|
|
testcase("Cross Currency Payment: Start with XRP");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
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(JsonOptions::none));
|
|
BEAST_EXPECT(
|
|
jro[jss::node][jss::TakerPays] == XRP(250).value().getText());
|
|
}
|
|
|
|
void
|
|
testCrossCurrencyEndXRP(FeatureBitset features)
|
|
{
|
|
testcase("Cross Currency Payment: End with XRP");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
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(JsonOptions::none));
|
|
}
|
|
|
|
void
|
|
testCrossCurrencyBridged(FeatureBitset features)
|
|
{
|
|
testcase("Cross Currency Payment: Bridged");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
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(JsonOptions::none));
|
|
|
|
jro = ledgerEntryOffer(env, dan, danOfferSeq);
|
|
BEAST_EXPECT(
|
|
jro[jss::node][jss::TakerGets] ==
|
|
gw2["EUR"](20).value().getJson(JsonOptions::none));
|
|
BEAST_EXPECT(
|
|
jro[jss::node][jss::TakerPays] == XRP(200).value().getText());
|
|
}
|
|
|
|
void
|
|
testBridgedSecondLegDry(FeatureBitset features)
|
|
{
|
|
// At least with Taker bridging, a sensitivity was identified if the
|
|
// second leg goes dry before the first one. This test exercises that
|
|
// case.
|
|
testcase("Auto Bridged Second Leg Dry");
|
|
|
|
using namespace jtx;
|
|
Env env(*this, features);
|
|
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const carol{"carol"};
|
|
Account const gw{"gateway"};
|
|
auto const USD = gw["USD"];
|
|
auto const EUR = gw["EUR"];
|
|
|
|
env.fund(XRP(100000000), alice, bob, carol, gw);
|
|
|
|
env.trust(USD(10), alice);
|
|
env.close();
|
|
env(pay(gw, alice, USD(10)));
|
|
env.trust(USD(10), carol);
|
|
env.close();
|
|
env(pay(gw, carol, USD(3)));
|
|
|
|
env(offer(alice, EUR(2), XRP(1)));
|
|
env(offer(alice, EUR(2), XRP(1)));
|
|
|
|
env(offer(alice, XRP(1), USD(4)));
|
|
env(offer(carol, XRP(1), USD(3)));
|
|
env.close();
|
|
|
|
// Bob offers to buy 10 USD for 10 EUR.
|
|
// 1. He spends 2 EUR taking Alice's auto-bridged offers and
|
|
// gets 4 USD for that.
|
|
// 2. He spends another 2 EUR taking Alice's last EUR->XRP offer and
|
|
// Carol's XRP-USD offer. He gets 3 USD for that.
|
|
// The key for this test is that Alice's XRP->USD leg goes dry before
|
|
// Alice's EUR->XRP. The XRP->USD leg is the second leg which showed
|
|
// some sensitivity.
|
|
env.trust(EUR(10), bob);
|
|
env.close();
|
|
env(pay(gw, bob, EUR(10)));
|
|
env.close();
|
|
env(offer(bob, USD(10), EUR(10)));
|
|
env.close();
|
|
|
|
env.require(balance(bob, USD(7)));
|
|
env.require(balance(bob, EUR(6)));
|
|
env.require(offers(bob, 1));
|
|
env.require(owners(bob, 3));
|
|
|
|
env.require(balance(alice, USD(6)));
|
|
env.require(balance(alice, EUR(4)));
|
|
env.require(offers(alice, 0));
|
|
env.require(owners(alice, 2));
|
|
|
|
env.require(balance(carol, USD(0)));
|
|
env.require(balance(carol, EUR(none)));
|
|
// If neither featureFlowCross nor fixTakerDryOfferRemoval are defined
|
|
// then carol's offer will be left on the books, but with zero value.
|
|
int const emptyOfferCount{
|
|
features[featureFlowCross] || features[fixTakerDryOfferRemoval]
|
|
? 0
|
|
: 1};
|
|
|
|
env.require(offers(carol, 0 + emptyOfferCount));
|
|
env.require(owners(carol, 1 + emptyOfferCount));
|
|
}
|
|
|
|
void
|
|
testOfferFeesConsumeFunds(FeatureBitset features)
|
|
{
|
|
testcase("Offer Fees Consume Funds");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
testcase("Offer Create, then Cross");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
testcase("Offer tfSell: Basic Sell");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
testcase("Offer tfSell: 2x Sell Exceed Limit");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
testcase("Client Issue #535: Gateway Cross Currency");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
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] = to_string(env.current()->fees().base);
|
|
payment[jss::tx_json][jss::SendMax] =
|
|
bob["XTS"](1.5).value().getJson(JsonOptions::none);
|
|
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: one that has
|
|
// no unusual flags set and doesn't have high or low limits set. Such a
|
|
// trustline may have an actual balance (it can be created automatically
|
|
// if a user places an offer to acquire an IOU for which they don't have
|
|
// a trust line defined). 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(FeatureBitset features)
|
|
{
|
|
// 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, features};
|
|
|
|
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
|
|
};
|
|
|
|
// clang-format off
|
|
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},
|
|
};
|
|
// clang-format on
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
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, features};
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
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, features};
|
|
|
|
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));
|
|
|
|
// Alice's offer crossing created a default EUR trustline and
|
|
// Bob's offer crossing created a default USD trustline:
|
|
verifyDefaultTrustline(env, alice, eurOffer);
|
|
verifyDefaultTrustline(env, bob, usdOffer);
|
|
|
|
// Make two more offers that leave one of the offers non-dry.
|
|
// Guarantee the order of application by putting a close()
|
|
// between them.
|
|
env(offer(bob, eurOffer, usdOffer));
|
|
env.close();
|
|
|
|
env(offer(alice, USD(999), eurOffer));
|
|
env.close();
|
|
|
|
env.require(offers(alice, 0));
|
|
env.require(offers(bob, 1));
|
|
|
|
env.require(balance(alice, USD(999)));
|
|
env.require(balance(alice, EUR(1)));
|
|
env.require(balance(bob, USD(1)));
|
|
env.require(balance(bob, EUR(999)));
|
|
|
|
{
|
|
auto bobsOffers = offersOnAccount(env, bob);
|
|
if (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())));
|
|
|
|
// Make two more offers that leave one of the offers non-dry. We
|
|
// need to properly sequence the transactions:
|
|
env(offer(alice, EUR(999), usdOffer));
|
|
env.close();
|
|
|
|
env(offer(bob, usdOffer, eurOffer));
|
|
env.close();
|
|
|
|
env.require(offers(alice, 0));
|
|
env.require(offers(bob, 0));
|
|
|
|
env.require(balance(alice, USD(0)));
|
|
env.require(balance(alice, EUR(999)));
|
|
env.require(balance(bob, USD(1000)));
|
|
env.require(balance(bob, EUR(1)));
|
|
}
|
|
|
|
void
|
|
testBridgedCross(FeatureBitset features)
|
|
{
|
|
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, features};
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
// 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, features};
|
|
|
|
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})
|
|
{
|
|
}
|
|
};
|
|
|
|
// clang-format off
|
|
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)},
|
|
};
|
|
// clang-format on
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
// 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, features};
|
|
|
|
env.fund(XRP(10000000), gw, alice, bob);
|
|
|
|
// Code returned if an offer is killed.
|
|
TER const killedCode{
|
|
features[fix1578] ? TER{tecKILLED} : TER{tesSUCCESS}};
|
|
|
|
// 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.
|
|
env(offer(alice, USD(21), XRP(2100), tfSell | tfFillOrKill),
|
|
ter(killedCode));
|
|
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),
|
|
ter(killedCode));
|
|
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(FeatureBitset features)
|
|
{
|
|
testcase("Transfer Rate Offer");
|
|
|
|
using namespace jtx;
|
|
|
|
auto const gw1 = Account("gateway1");
|
|
auto const USD = gw1["USD"];
|
|
|
|
Env env{*this, features};
|
|
|
|
// 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(FeatureBitset features)
|
|
{
|
|
// 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, features};
|
|
|
|
// 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(FeatureBitset features)
|
|
{
|
|
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, features};
|
|
|
|
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
|
|
};
|
|
|
|
// clang-format off
|
|
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},
|
|
};
|
|
//clang-format on
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
testcase("Self Cross Offer");
|
|
testSelfCrossOffer1(features);
|
|
testSelfCrossOffer2(features);
|
|
}
|
|
|
|
void
|
|
testSelfIssueOffer(FeatureBitset features)
|
|
{
|
|
// 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, features};
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
// 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, features | featureOwnerPaysFee};
|
|
|
|
// 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();
|
|
|
|
// This payment caused the assert.
|
|
env(pay(ann, ann, D_BUX(30)),
|
|
path(A_BUX, D_BUX),
|
|
sendmax(A_BUX(30)),
|
|
ter(temBAD_PATH));
|
|
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(FeatureBitset features)
|
|
{
|
|
// 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, features};
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
// 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, features};
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
// 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, features};
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
// 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, features};
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
// 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, features};
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
testcase("Tiny Offer");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
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, features};
|
|
|
|
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;
|
|
};
|
|
|
|
// clang-format off
|
|
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, 1, 0, BTC( 5), {{"deb", 1, drops(4039999999960), BTC( 0.0), USD(2000)}, {"dan", 1, drops(3959999999970), BTC( 4), USD( 0)}}}, // no USD xfer fee
|
|
};
|
|
// clang-format on
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
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, features};
|
|
|
|
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;
|
|
};
|
|
|
|
// clang-format off
|
|
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
|
|
};
|
|
// clang-format on
|
|
|
|
// Pick the right tests.
|
|
auto const& tests = features[featureFlowCross] ? 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(FeatureBitset features)
|
|
{
|
|
testcase("lsfRequireAuth");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
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, features};
|
|
|
|
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 = features[featureFlowCross];
|
|
|
|
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(FeatureBitset features)
|
|
{
|
|
testcase("RippleConnect Smoketest payment flow");
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
// 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(FeatureBitset features)
|
|
{
|
|
testcase("Self Auth");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
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 DepositPreauth.
|
|
bool const preauth = features[featureDepositPreauth];
|
|
|
|
// Before DepositPreauth an account with lsfRequireAuth set could not
|
|
// create an offer to buy their own currency. After DepositPreauth
|
|
// they can.
|
|
env(offer(gw, gwUSD(40), XRP(4000)),
|
|
ter(preauth ? TER{tesSUCCESS} : TER{tecNO_LINE}));
|
|
env.close();
|
|
|
|
env.require(offers(gw, preauth ? 1 : 0));
|
|
|
|
if (!preauth)
|
|
// The rest of the test verifies DepositPreauth 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
|
|
testDeletedOfferIssuer(FeatureBitset features)
|
|
{
|
|
// Show that an offer who's issuer has been deleted cannot be crossed.
|
|
using namespace jtx;
|
|
|
|
testcase("Deleted offer issuer");
|
|
|
|
auto trustLineExists = [](jtx::Env const& env,
|
|
jtx::Account const& src,
|
|
jtx::Account const& dst,
|
|
Currency const& cur) -> bool {
|
|
return bool(env.le(keylet::line(src, dst, cur)));
|
|
};
|
|
|
|
Account const alice("alice");
|
|
Account const becky("becky");
|
|
Account const carol("carol");
|
|
Account const gw("gateway");
|
|
auto const USD = gw["USD"];
|
|
auto const BUX = alice["BUX"];
|
|
|
|
Env env{*this, features};
|
|
|
|
env.fund(XRP(10000), alice, becky, carol, noripple(gw));
|
|
env.trust(USD(1000), becky);
|
|
env(pay(gw, becky, USD(5)));
|
|
env.close();
|
|
BEAST_EXPECT(trustLineExists(env, gw, becky, USD.currency));
|
|
|
|
// Make offers that produce USD and can be crossed two ways:
|
|
// direct XRP -> USD
|
|
// direct BUX -> USD
|
|
env(offer(becky, XRP(2), USD(2)), txflags(tfPassive));
|
|
std::uint32_t const beckyBuxUsdSeq{env.seq(becky)};
|
|
env(offer(becky, BUX(3), USD(3)), txflags(tfPassive));
|
|
env.close();
|
|
|
|
// becky keeps the offers, but removes the trustline.
|
|
env(pay(becky, gw, USD(5)));
|
|
env.trust(USD(0), becky);
|
|
env.close();
|
|
BEAST_EXPECT(!trustLineExists(env, gw, becky, USD.currency));
|
|
BEAST_EXPECT(isOffer(env, becky, XRP(2), USD(2)));
|
|
BEAST_EXPECT(isOffer(env, becky, BUX(3), USD(3)));
|
|
|
|
// Delete gw's account.
|
|
{
|
|
// The ledger sequence needs to far enough ahead of the account
|
|
// sequence before the account can be deleted.
|
|
int const delta =
|
|
[&env, &gw, openLedgerSeq = env.current()->seq()]() -> int {
|
|
std::uint32_t const gwSeq{env.seq(gw)};
|
|
if (gwSeq + 255 > openLedgerSeq)
|
|
return gwSeq - openLedgerSeq + 255;
|
|
return 0;
|
|
}();
|
|
|
|
for (int i = 0; i < delta; ++i)
|
|
env.close();
|
|
|
|
// Account deletion has a high fee. Account for that.
|
|
env(acctdelete(gw, alice),
|
|
fee(drops(env.current()->fees().increment)));
|
|
env.close();
|
|
|
|
// Verify that gw's account root is gone from the ledger.
|
|
BEAST_EXPECT(!env.closed()->exists(keylet::account(gw.id())));
|
|
}
|
|
|
|
// alice crosses becky's first offer. The offer create fails because
|
|
// the USD issuer is not in the ledger.
|
|
env(offer(alice, USD(2), XRP(2)), ter(tecNO_ISSUER));
|
|
env.close();
|
|
env.require(offers(alice, 0));
|
|
BEAST_EXPECT(isOffer(env, becky, XRP(2), USD(2)));
|
|
BEAST_EXPECT(isOffer(env, becky, BUX(3), USD(3)));
|
|
|
|
// alice crosses becky's second offer. Again, the offer create fails
|
|
// because the USD issuer is not in the ledger.
|
|
env(offer(alice, USD(3), BUX(3)), ter(tecNO_ISSUER));
|
|
env.require(offers(alice, 0));
|
|
BEAST_EXPECT(isOffer(env, becky, XRP(2), USD(2)));
|
|
BEAST_EXPECT(isOffer(env, becky, BUX(3), USD(3)));
|
|
|
|
// Cancel becky's BUX -> USD offer so we can try auto-bridging.
|
|
env(offer_cancel(becky, beckyBuxUsdSeq));
|
|
env.close();
|
|
BEAST_EXPECT(!isOffer(env, becky, BUX(3), USD(3)));
|
|
|
|
// alice creates an offer that can be auto-bridged with becky's
|
|
// remaining offer.
|
|
env.trust(BUX(1000), carol);
|
|
env(pay(alice, carol, BUX(2)));
|
|
|
|
env(offer(alice, BUX(2), XRP(2)));
|
|
env.close();
|
|
|
|
// carol attempts the auto-bridge. Again, the offer create fails
|
|
// because the USD issuer is not in the ledger.
|
|
env(offer(carol, USD(2), BUX(2)), ter(tecNO_ISSUER));
|
|
env.close();
|
|
BEAST_EXPECT(isOffer(env, alice, BUX(2), XRP(2)));
|
|
BEAST_EXPECT(isOffer(env, becky, XRP(2), USD(2)));
|
|
}
|
|
|
|
void
|
|
testTickSize(FeatureBitset features)
|
|
{
|
|
testcase("Tick Size");
|
|
|
|
using namespace jtx;
|
|
|
|
// Try to set tick size out of range
|
|
{
|
|
Env env{*this, features};
|
|
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);
|
|
BEAST_EXPECT(!env.le(gw)->isFieldPresent(sfTickSize));
|
|
}
|
|
|
|
Env env{*this, features};
|
|
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());
|
|
}
|
|
|
|
// Helper function that returns offers on an account sorted by sequence.
|
|
static std::vector<std::shared_ptr<SLE const>>
|
|
sortedOffersOnAccount(jtx::Env& env, jtx::Account const& acct)
|
|
{
|
|
std::vector<std::shared_ptr<SLE const>> offers{
|
|
offersOnAccount(env, acct)};
|
|
std::sort(
|
|
offers.begin(),
|
|
offers.end(),
|
|
[](std::shared_ptr<SLE const> const& rhs,
|
|
std::shared_ptr<SLE const> const& lhs) {
|
|
return (*rhs)[sfSequence] < (*lhs)[sfSequence];
|
|
});
|
|
return offers;
|
|
}
|
|
|
|
void
|
|
testTicketOffer(FeatureBitset features)
|
|
{
|
|
testcase("Ticket Offers");
|
|
|
|
using namespace jtx;
|
|
|
|
// Two goals for this test.
|
|
//
|
|
// o Verify that offers can be created using tickets.
|
|
//
|
|
// o Show that offers in the _same_ order book remain in
|
|
// chronological order regardless of sequence/ticket numbers.
|
|
Env env{*this, features};
|
|
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.close();
|
|
|
|
env(trust(alice, USD(1000)));
|
|
env(trust(bob, USD(1000)));
|
|
env.close();
|
|
|
|
env(pay(gw, alice, USD(200)));
|
|
env.close();
|
|
|
|
// Create four offers from the same account with identical quality
|
|
// so they go in the same order book. Each offer goes in a different
|
|
// ledger so the chronology is clear.
|
|
std::uint32_t const offerId_0{env.seq(alice)};
|
|
env(offer(alice, XRP(50), USD(50)));
|
|
env.close();
|
|
|
|
// Create two tickets.
|
|
std::uint32_t const ticketSeq{env.seq(alice) + 1};
|
|
env(ticket::create(alice, 2));
|
|
env.close();
|
|
|
|
// Create another sequence-based offer.
|
|
std::uint32_t const offerId_1{env.seq(alice)};
|
|
BEAST_EXPECT(offerId_1 == offerId_0 + 4);
|
|
env(offer(alice, XRP(50), USD(50)));
|
|
env.close();
|
|
|
|
// Create two ticket based offers in reverse order.
|
|
std::uint32_t const offerId_2{ticketSeq + 1};
|
|
env(offer(alice, XRP(50), USD(50)), ticket::use(offerId_2));
|
|
env.close();
|
|
|
|
// Create the last offer.
|
|
std::uint32_t const offerId_3{ticketSeq};
|
|
env(offer(alice, XRP(50), USD(50)), ticket::use(offerId_3));
|
|
env.close();
|
|
|
|
// Verify that all of alice's offers are present.
|
|
{
|
|
auto offers = sortedOffersOnAccount(env, alice);
|
|
BEAST_EXPECT(offers.size() == 4);
|
|
BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerId_0);
|
|
BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerId_3);
|
|
BEAST_EXPECT(offers[2]->getFieldU32(sfSequence) == offerId_2);
|
|
BEAST_EXPECT(offers[3]->getFieldU32(sfSequence) == offerId_1);
|
|
env.require(balance(alice, USD(200)));
|
|
env.require(owners(alice, 5));
|
|
}
|
|
|
|
// Cross alice's first offer.
|
|
env(offer(bob, USD(50), XRP(50)));
|
|
env.close();
|
|
|
|
// Verify that the first offer alice created was consumed.
|
|
{
|
|
auto offers = sortedOffersOnAccount(env, alice);
|
|
BEAST_EXPECT(offers.size() == 3);
|
|
BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerId_3);
|
|
BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerId_2);
|
|
BEAST_EXPECT(offers[2]->getFieldU32(sfSequence) == offerId_1);
|
|
}
|
|
|
|
// Cross alice's second offer.
|
|
env(offer(bob, USD(50), XRP(50)));
|
|
env.close();
|
|
|
|
// Verify that the second offer alice created was consumed.
|
|
{
|
|
auto offers = sortedOffersOnAccount(env, alice);
|
|
BEAST_EXPECT(offers.size() == 2);
|
|
BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerId_3);
|
|
BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerId_2);
|
|
}
|
|
|
|
// Cross alice's third offer.
|
|
env(offer(bob, USD(50), XRP(50)));
|
|
env.close();
|
|
|
|
// Verify that the third offer alice created was consumed.
|
|
{
|
|
auto offers = sortedOffersOnAccount(env, alice);
|
|
BEAST_EXPECT(offers.size() == 1);
|
|
BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerId_3);
|
|
}
|
|
|
|
// Cross alice's last offer.
|
|
env(offer(bob, USD(50), XRP(50)));
|
|
env.close();
|
|
|
|
// Verify that the third offer alice created was consumed.
|
|
{
|
|
auto offers = sortedOffersOnAccount(env, alice);
|
|
BEAST_EXPECT(offers.size() == 0);
|
|
}
|
|
env.require(balance(alice, USD(0)));
|
|
env.require(owners(alice, 1));
|
|
env.require(balance(bob, USD(200)));
|
|
env.require(owners(bob, 1));
|
|
}
|
|
|
|
void
|
|
testTicketCancelOffer(FeatureBitset features)
|
|
{
|
|
testcase("Ticket Cancel Offers");
|
|
|
|
using namespace jtx;
|
|
|
|
// Verify that offers created with or without tickets can be canceled
|
|
// by transactions with or without tickets.
|
|
Env env{*this, features};
|
|
auto const gw = Account{"gateway"};
|
|
auto const alice = Account{"alice"};
|
|
auto const USD = gw["USD"];
|
|
|
|
env.fund(XRP(10000), gw, alice);
|
|
env.close();
|
|
|
|
env(trust(alice, USD(1000)));
|
|
env.close();
|
|
env.require(owners(alice, 1), tickets(alice, 0));
|
|
|
|
env(pay(gw, alice, USD(200)));
|
|
env.close();
|
|
|
|
// Create the first of four offers using a sequence.
|
|
std::uint32_t const offerSeqId_0{env.seq(alice)};
|
|
env(offer(alice, XRP(50), USD(50)));
|
|
env.close();
|
|
env.require(owners(alice, 2), tickets(alice, 0));
|
|
|
|
// Create four tickets.
|
|
std::uint32_t const ticketSeq{env.seq(alice) + 1};
|
|
env(ticket::create(alice, 4));
|
|
env.close();
|
|
env.require(owners(alice, 6), tickets(alice, 4));
|
|
|
|
// Create the second (also sequence-based) offer.
|
|
std::uint32_t const offerSeqId_1{env.seq(alice)};
|
|
BEAST_EXPECT(offerSeqId_1 == offerSeqId_0 + 6);
|
|
env(offer(alice, XRP(50), USD(50)));
|
|
env.close();
|
|
|
|
// Create the third (ticket-based) offer.
|
|
std::uint32_t const offerTixId_0{ticketSeq + 1};
|
|
env(offer(alice, XRP(50), USD(50)), ticket::use(offerTixId_0));
|
|
env.close();
|
|
|
|
// Create the last offer.
|
|
std::uint32_t const offerTixId_1{ticketSeq};
|
|
env(offer(alice, XRP(50), USD(50)), ticket::use(offerTixId_1));
|
|
env.close();
|
|
|
|
// Verify that all of alice's offers are present.
|
|
{
|
|
auto offers = sortedOffersOnAccount(env, alice);
|
|
BEAST_EXPECT(offers.size() == 4);
|
|
BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerSeqId_0);
|
|
BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerTixId_1);
|
|
BEAST_EXPECT(offers[2]->getFieldU32(sfSequence) == offerTixId_0);
|
|
BEAST_EXPECT(offers[3]->getFieldU32(sfSequence) == offerSeqId_1);
|
|
env.require(balance(alice, USD(200)));
|
|
env.require(owners(alice, 7));
|
|
}
|
|
|
|
// Use a ticket to cancel an offer created with a sequence.
|
|
env(offer_cancel(alice, offerSeqId_0), ticket::use(ticketSeq + 2));
|
|
env.close();
|
|
|
|
// Verify that offerSeqId_0 was canceled.
|
|
{
|
|
auto offers = sortedOffersOnAccount(env, alice);
|
|
BEAST_EXPECT(offers.size() == 3);
|
|
BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerTixId_1);
|
|
BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerTixId_0);
|
|
BEAST_EXPECT(offers[2]->getFieldU32(sfSequence) == offerSeqId_1);
|
|
}
|
|
|
|
// Use a ticket to cancel an offer created with a ticket.
|
|
env(offer_cancel(alice, offerTixId_0), ticket::use(ticketSeq + 3));
|
|
env.close();
|
|
|
|
// Verify that offerTixId_0 was canceled.
|
|
{
|
|
auto offers = sortedOffersOnAccount(env, alice);
|
|
BEAST_EXPECT(offers.size() == 2);
|
|
BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerTixId_1);
|
|
BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerSeqId_1);
|
|
}
|
|
|
|
// All of alice's tickets should now be used up.
|
|
env.require(owners(alice, 3), tickets(alice, 0));
|
|
|
|
// Use a sequence to cancel an offer created with a ticket.
|
|
env(offer_cancel(alice, offerTixId_1));
|
|
env.close();
|
|
|
|
// Verify that offerTixId_1 was canceled.
|
|
{
|
|
auto offers = sortedOffersOnAccount(env, alice);
|
|
BEAST_EXPECT(offers.size() == 1);
|
|
BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerSeqId_1);
|
|
}
|
|
|
|
// Use a sequence to cancel an offer created with a sequence.
|
|
env(offer_cancel(alice, offerSeqId_1));
|
|
env.close();
|
|
|
|
// Verify that offerSeqId_1 was canceled.
|
|
// All of alice's tickets should now be used up.
|
|
env.require(owners(alice, 1), tickets(alice, 0), offers(alice, 0));
|
|
}
|
|
|
|
void
|
|
testFalseAssert()
|
|
{
|
|
// An assert was falsely triggering when computing rates for offers.
|
|
// This unit test would trigger that assert (which has been removed).
|
|
testcase("incorrect assert fixed");
|
|
using namespace jtx;
|
|
|
|
Env env{*this};
|
|
auto const alice = Account("alice");
|
|
auto const USD = alice["USD"];
|
|
|
|
env.fund(XRP(10000), alice);
|
|
env.close();
|
|
env(offer(alice, XRP(100000000000), USD(100000000)));
|
|
pass();
|
|
}
|
|
|
|
void
|
|
testAll(FeatureBitset features)
|
|
{
|
|
testCanceledOffer(features);
|
|
testRmFundedOffer(features);
|
|
testTinyPayment(features);
|
|
testXRPTinyPayment(features);
|
|
testEnforceNoRipple(features);
|
|
testInsufficientReserve(features);
|
|
testFillModes(features);
|
|
testMalformed(features);
|
|
testExpiration(features);
|
|
testUnfundedCross(features);
|
|
testSelfCross(false, features);
|
|
testSelfCross(true, features);
|
|
testNegativeBalance(features);
|
|
testOfferCrossWithXRP(true, features);
|
|
testOfferCrossWithXRP(false, features);
|
|
testOfferCrossWithLimitOverride(features);
|
|
testOfferAcceptThenCancel(features);
|
|
testOfferCancelPastAndFuture(features);
|
|
testCurrencyConversionEntire(features);
|
|
testCurrencyConversionIntoDebt(features);
|
|
testCurrencyConversionInParts(features);
|
|
testCrossCurrencyStartXRP(features);
|
|
testCrossCurrencyEndXRP(features);
|
|
testCrossCurrencyBridged(features);
|
|
testBridgedSecondLegDry(features);
|
|
testOfferFeesConsumeFunds(features);
|
|
testOfferCreateThenCross(features);
|
|
testSellFlagBasic(features);
|
|
testSellFlagExceedLimit(features);
|
|
testGatewayCrossCurrency(features);
|
|
testPartialCross(features);
|
|
testXRPDirectCross(features);
|
|
testDirectCross(features);
|
|
testBridgedCross(features);
|
|
testSellOffer(features);
|
|
testSellWithFillOrKill(features);
|
|
testTransferRateOffer(features);
|
|
testSelfCrossOffer(features);
|
|
testSelfIssueOffer(features);
|
|
testBadPathAssert(features);
|
|
testDirectToDirectPath(features);
|
|
testSelfCrossLowQualityOffer(features);
|
|
testOfferInScaling(features);
|
|
testOfferInScalingWithXferRate(features);
|
|
testOfferThresholdWithReducedFunds(features);
|
|
testTinyOffer(features);
|
|
testSelfPayXferFeeOffer(features);
|
|
testSelfPayUnlimitedFunds(features);
|
|
testRequireAuth(features);
|
|
testMissingAuth(features);
|
|
testRCSmoketest(features);
|
|
testSelfAuth(features);
|
|
testDeletedOfferIssuer(features);
|
|
testTickSize(features);
|
|
testTicketOffer(features);
|
|
testTicketCancelOffer(features);
|
|
testRmSmallIncreasedQOffersXRP(features);
|
|
testRmSmallIncreasedQOffersIOU(features);
|
|
}
|
|
|
|
void
|
|
run() override
|
|
{
|
|
using namespace jtx;
|
|
FeatureBitset const all{supported_amendments()};
|
|
FeatureBitset const flowCross{featureFlowCross};
|
|
FeatureBitset const takerDryOffer{fixTakerDryOfferRemoval};
|
|
FeatureBitset const rmSmallIncreasedQOffers{fixRmSmallIncreasedQOffers};
|
|
|
|
testAll(all - takerDryOffer);
|
|
testAll(all - flowCross - takerDryOffer);
|
|
testAll(all - flowCross);
|
|
testAll(all - rmSmallIncreasedQOffers);
|
|
testAll(all);
|
|
testFalseAssert();
|
|
}
|
|
};
|
|
|
|
class Offer_manual_test : public Offer_test
|
|
{
|
|
void
|
|
run() override
|
|
{
|
|
using namespace jtx;
|
|
FeatureBitset const all{supported_amendments()};
|
|
FeatureBitset const flowCross{featureFlowCross};
|
|
FeatureBitset const f1513{fix1513};
|
|
FeatureBitset const takerDryOffer{fixTakerDryOfferRemoval};
|
|
|
|
testAll(all - flowCross - f1513);
|
|
testAll(all - flowCross);
|
|
testAll(all - f1513);
|
|
testAll(all);
|
|
|
|
testAll(all - flowCross - takerDryOffer);
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE_PRIO(Offer, tx, ripple, 4);
|
|
BEAST_DEFINE_TESTSUITE_MANUAL_PRIO(Offer_manual, tx, ripple, 20);
|
|
|
|
} // namespace test
|
|
} // namespace ripple
|