mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
1404 lines
52 KiB
C++
1404 lines
52 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
This file is part of rippled: https://github.com/ripple/rippled
|
|
Copyright (c) 2012, 2013 Ripple Labs Inc.
|
|
Permission to use, copy, modify, and/or distribute this software for any
|
|
purpose with or without fee is hereby granted, provided that the above
|
|
copyright notice and this permission notice appear in all copies.
|
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
//==============================================================================
|
|
|
|
#include <BeastConfig.h>
|
|
#include <test/support/jtx.h>
|
|
#include <ripple/app/paths/Flow.h>
|
|
#include <ripple/app/paths/impl/Steps.h>
|
|
#include <ripple/basics/contract.h>
|
|
#include <ripple/core/Config.h>
|
|
#include <ripple/ledger/PaymentSandbox.h>
|
|
#include <ripple/ledger/Sandbox.h>
|
|
#include <test/support/PathSet.h>
|
|
#include <ripple/protocol/Feature.h>
|
|
#include <ripple/protocol/JsonFields.h>
|
|
namespace ripple {
|
|
namespace test {
|
|
|
|
struct Flow_test;
|
|
|
|
struct DirectStepInfo
|
|
{
|
|
AccountID src;
|
|
AccountID dst;
|
|
Currency currency;
|
|
};
|
|
|
|
struct XRPEndpointStepInfo
|
|
{
|
|
AccountID acc;
|
|
};
|
|
|
|
enum class TrustFlag {freeze, auth};
|
|
|
|
/*constexpr*/ std::uint32_t trustFlag (TrustFlag f, bool useHigh)
|
|
{
|
|
switch(f)
|
|
{
|
|
case TrustFlag::freeze:
|
|
if (useHigh)
|
|
return lsfHighFreeze;
|
|
return lsfLowFreeze;
|
|
case TrustFlag::auth:
|
|
if (useHigh)
|
|
return lsfHighAuth;
|
|
return lsfLowAuth;
|
|
}
|
|
return 0; // Silence warning about end of non-void function
|
|
}
|
|
|
|
bool getTrustFlag (jtx::Env const& env,
|
|
jtx::Account const& src,
|
|
jtx::Account const& dst,
|
|
Currency const& cur,
|
|
TrustFlag flag)
|
|
{
|
|
if (auto sle = env.le (keylet::line (src, dst, cur)))
|
|
{
|
|
auto const useHigh = src.id() > dst.id();
|
|
return sle->isFlag (trustFlag (flag, useHigh));
|
|
}
|
|
Throw<std::runtime_error> ("No line in getTrustFlag");
|
|
return false; // silence warning
|
|
}
|
|
|
|
jtx::PrettyAmount
|
|
xrpMinusFee (jtx::Env const& env, std::int64_t xrpAmount)
|
|
{
|
|
using namespace jtx;
|
|
auto feeDrops = env.current ()->fees ().base;
|
|
return drops (
|
|
dropsPerXRP<std::int64_t>::value * xrpAmount - feeDrops);
|
|
};
|
|
|
|
bool equal (std::unique_ptr<Step> const& s1,
|
|
DirectStepInfo const& dsi)
|
|
{
|
|
if (!s1)
|
|
return false;
|
|
return test::directStepEqual (*s1, dsi.src, dsi.dst, dsi.currency);
|
|
}
|
|
|
|
bool equal (std::unique_ptr<Step> const& s1,
|
|
XRPEndpointStepInfo const& xrpsi)
|
|
{
|
|
if (!s1)
|
|
return false;
|
|
return test::xrpEndpointStepEqual (*s1, xrpsi.acc);
|
|
}
|
|
|
|
bool equal (std::unique_ptr<Step> const& s1, ripple::Book const& bsi)
|
|
{
|
|
if (!s1)
|
|
return false;
|
|
return bookStepEqual (*s1, bsi);
|
|
}
|
|
|
|
template <class Iter>
|
|
bool strandEqualHelper (Iter i)
|
|
{
|
|
// base case. all args processed and found equal.
|
|
return true;
|
|
}
|
|
|
|
template <class Iter, class StepInfo, class... Args>
|
|
bool strandEqualHelper (Iter i, StepInfo&& si, Args&&... args)
|
|
{
|
|
if (!equal (*i, std::forward<StepInfo> (si)))
|
|
return false;
|
|
return strandEqualHelper (++i, std::forward<Args> (args)...);
|
|
}
|
|
|
|
template <class... Args>
|
|
bool equal (Strand const& strand, Args&&... args)
|
|
{
|
|
if (strand.size () != sizeof...(Args))
|
|
return false;
|
|
if (strand.empty ())
|
|
return true;
|
|
return strandEqualHelper (strand.begin (), std::forward<Args> (args)...);
|
|
}
|
|
|
|
struct Flow_test : public beast::unit_test::suite
|
|
{
|
|
// Account path element
|
|
static auto APE(AccountID const& a)
|
|
{
|
|
return STPathElement (
|
|
STPathElement::typeAccount, a, xrpCurrency (), xrpAccount ());
|
|
};
|
|
|
|
// Issue path element
|
|
static auto IPE(Issue const& iss)
|
|
{
|
|
return STPathElement (
|
|
STPathElement::typeCurrency | STPathElement::typeIssuer,
|
|
xrpAccount (), iss.currency, iss.account);
|
|
};
|
|
|
|
// Issuer path element
|
|
static auto IAPE(AccountID const& account)
|
|
{
|
|
return STPathElement (
|
|
STPathElement::typeIssuer,
|
|
xrpAccount (), xrpCurrency (), account);
|
|
};
|
|
|
|
// Currency path element
|
|
static auto CPE(Currency const& c)
|
|
{
|
|
return STPathElement (
|
|
STPathElement::typeCurrency, xrpAccount (), c, xrpAccount ());
|
|
};
|
|
|
|
void testToStrand ()
|
|
{
|
|
testcase ("To Strand");
|
|
|
|
using namespace jtx;
|
|
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 const eurC = EUR.currency;
|
|
auto const usdC = USD.currency;
|
|
|
|
using D = DirectStepInfo;
|
|
using B = ripple::Book;
|
|
using XRPS = XRPEndpointStepInfo;
|
|
|
|
auto test = [&, this](jtx::Env& env, Issue const& deliver,
|
|
boost::optional<Issue> const& sendMaxIssue, STPath const& path,
|
|
TER expTer, auto&&... expSteps)
|
|
{
|
|
auto r = toStrand (*env.current (), alice, bob,
|
|
deliver, sendMaxIssue, path, true, env.app ().logs ().journal ("Flow"));
|
|
BEAST_EXPECT(r.first == expTer);
|
|
if (sizeof...(expSteps))
|
|
BEAST_EXPECT(equal (
|
|
r.second, std::forward<decltype (expSteps)> (expSteps)...));
|
|
};
|
|
|
|
{
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
env.fund (XRP (10000), alice, bob, carol, gw);
|
|
|
|
test (env, USD, boost::none, STPath(), terNO_LINE);
|
|
|
|
env.trust (USD (1000), alice, bob, carol);
|
|
test (env, USD, boost::none, STPath(), tecPATH_DRY);
|
|
|
|
env (pay (gw, alice, USD (100)));
|
|
env (pay (gw, carol, USD (100)));
|
|
|
|
// Insert implied account
|
|
test (env, USD, boost::none, STPath(), tesSUCCESS,
|
|
D{alice, gw, usdC}, D{gw, bob, usdC});
|
|
env.trust (EUR (1000), alice, bob);
|
|
|
|
// Insert implied offer
|
|
test (env, EUR, USD.issue (), STPath(), tesSUCCESS,
|
|
D{alice, gw, usdC}, B{USD, EUR}, D{gw, bob, eurC});
|
|
|
|
// Path with explicit offer
|
|
test (env, EUR, USD.issue (), STPath ({IPE (EUR)}),
|
|
tesSUCCESS, D{alice, gw, usdC}, B{USD, EUR}, D{gw, bob, eurC});
|
|
|
|
// Path with offer that changes issuer only
|
|
env.trust (carol["USD"] (1000), bob);
|
|
test (env, carol["USD"], USD.issue (), STPath ({IAPE (carol)}),
|
|
tesSUCCESS,
|
|
D{alice, gw, usdC}, B{USD, carol["USD"]}, D{carol, bob, usdC});
|
|
|
|
// Path with XRP src currency
|
|
test (env, USD, xrpIssue (), STPath ({IPE (USD)}), tesSUCCESS,
|
|
XRPS{alice}, B{XRP, USD}, D{gw, bob, usdC});
|
|
|
|
// Path with XRP dst currency
|
|
test (env, xrpIssue(), USD.issue (), STPath ({IPE (XRP)}),
|
|
tesSUCCESS, D{alice, gw, usdC}, B{USD, XRP}, XRPS{bob});
|
|
|
|
// Path with XRP cross currency bridged payment
|
|
test (env, EUR, USD.issue (), STPath ({CPE (xrpCurrency ())}),
|
|
tesSUCCESS,
|
|
D{alice, gw, usdC}, B{USD, XRP}, B{XRP, EUR}, D{gw, bob, eurC});
|
|
|
|
// XRP -> XRP transaction can't include a path
|
|
test (env, XRP, boost::none, STPath ({APE (carol)}), temBAD_PATH);
|
|
|
|
{
|
|
// The root account can't be the src or dst
|
|
auto flowJournal = env.app ().logs ().journal ("Flow");
|
|
{
|
|
// The root account can't be the dst
|
|
auto r = toStrand (*env.current (), alice,
|
|
xrpAccount (), XRP, USD.issue (), STPath (), true, flowJournal);
|
|
BEAST_EXPECT(r.first == temBAD_PATH);
|
|
}
|
|
{
|
|
// The root account can't be the src
|
|
auto r =
|
|
toStrand (*env.current (), xrpAccount (),
|
|
alice, XRP, boost::none, STPath (), true, flowJournal);
|
|
BEAST_EXPECT(r.first == temBAD_PATH);
|
|
}
|
|
{
|
|
// The root account can't be the src
|
|
auto r = toStrand (*env.current (),
|
|
noAccount (), bob, USD, boost::none, STPath (), true, flowJournal);
|
|
BEAST_EXPECT(r.first == terNO_ACCOUNT);
|
|
}
|
|
}
|
|
|
|
// Create an offer with the same in/out issue
|
|
test (env, EUR, USD.issue (), STPath ({IPE (USD), IPE (EUR)}),
|
|
temBAD_PATH);
|
|
|
|
// Path element with type zero
|
|
test (env, USD, boost::none,
|
|
STPath ({STPathElement (
|
|
0, xrpAccount (), xrpCurrency (), xrpAccount ())}),
|
|
temBAD_PATH);
|
|
|
|
// The same account can't appear more than once on a path
|
|
// `gw` will be used from alice->carol and implied between carol
|
|
// and bob
|
|
test (env, USD, boost::none, STPath ({APE (gw), APE (carol)}),
|
|
temBAD_PATH_LOOP);
|
|
|
|
// The same offer can't appear more than once on a path
|
|
test (env, EUR, USD.issue (), STPath ({IPE (EUR), IPE (USD), IPE (EUR)}),
|
|
temBAD_PATH_LOOP);
|
|
}
|
|
|
|
{
|
|
// cannot have more than one offer with the same output issue
|
|
|
|
using namespace jtx;
|
|
Env env (*this, features (featureFlow), features(featureOwnerPaysFee));
|
|
|
|
env.fund (XRP (10000), alice, bob, carol, gw);
|
|
env.trust (USD (10000), alice, bob, carol);
|
|
env.trust (EUR (10000), alice, bob, carol);
|
|
|
|
env (pay (gw, bob, USD (100)));
|
|
env (pay (gw, bob, EUR (100)));
|
|
|
|
env (offer (bob, XRP (100), USD (100)));
|
|
env (offer (bob, USD (100), EUR (100)), txflags (tfPassive));
|
|
env (offer (bob, EUR (100), USD (100)), txflags (tfPassive));
|
|
|
|
// payment path: XRP -> XRP/USD -> USD/EUR -> EUR/USD
|
|
env (pay (alice, carol, USD (100)), path (~USD, ~EUR, ~USD),
|
|
sendmax (XRP (200)), txflags (tfNoRippleDirect),
|
|
ter (temBAD_PATH_LOOP));
|
|
}
|
|
|
|
{
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
env.fund (XRP (10000), alice, bob, noripple (gw));
|
|
env.trust (USD (1000), alice, bob);
|
|
env (pay (gw, alice, USD (100)));
|
|
test (env, USD, boost::none, STPath (), terNO_RIPPLE);
|
|
}
|
|
|
|
{
|
|
// check global freeze
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
env.fund (XRP (10000), alice, bob, gw);
|
|
env.trust (USD (1000), alice, bob);
|
|
env (pay (gw, alice, USD (100)));
|
|
|
|
// Account can still issue payments
|
|
env(fset(alice, asfGlobalFreeze));
|
|
test (env, USD, boost::none, STPath (), tesSUCCESS);
|
|
env(fclear(alice, asfGlobalFreeze));
|
|
test (env, USD, boost::none, STPath (), tesSUCCESS);
|
|
|
|
// Account can not issue funds
|
|
env(fset(gw, asfGlobalFreeze));
|
|
test (env, USD, boost::none, STPath (), terNO_LINE);
|
|
env(fclear(gw, asfGlobalFreeze));
|
|
test (env, USD, boost::none, STPath (), tesSUCCESS);
|
|
|
|
// Account can not receive funds
|
|
env(fset(bob, asfGlobalFreeze));
|
|
test (env, USD, boost::none, STPath (), terNO_LINE);
|
|
env(fclear(bob, asfGlobalFreeze));
|
|
test (env, USD, boost::none, STPath (), tesSUCCESS);
|
|
}
|
|
{
|
|
// Freeze between gw and alice
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
env.fund (XRP (10000), alice, bob, gw);
|
|
env.trust (USD (1000), alice, bob);
|
|
env (pay (gw, alice, USD (100)));
|
|
test (env, USD, boost::none, STPath (), tesSUCCESS);
|
|
env (trust (gw, alice["USD"] (0), tfSetFreeze));
|
|
BEAST_EXPECT(getTrustFlag (env, gw, alice, usdC, TrustFlag::freeze));
|
|
test (env, USD, boost::none, STPath (), terNO_LINE);
|
|
}
|
|
{
|
|
// check no auth
|
|
// An account may require authorization to receive IOUs from an
|
|
// issuer
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
env.fund (XRP (10000), alice, bob, gw);
|
|
env (fset (gw, asfRequireAuth));
|
|
env.trust (USD (1000), alice, bob);
|
|
// Authorize alice but not bob
|
|
env (trust (gw, alice ["USD"] (1000), tfSetfAuth));
|
|
BEAST_EXPECT(getTrustFlag (env, gw, alice, usdC, TrustFlag::auth));
|
|
env (pay (gw, alice, USD (100)));
|
|
env.require (balance (alice, USD (100)));
|
|
test (env, USD, boost::none, STPath (), terNO_AUTH);
|
|
|
|
// Check pure issue redeem still works
|
|
auto r = toStrand (*env.current (), alice, gw, USD,
|
|
boost::none, STPath (), true, env.app ().logs ().journal ("Flow"));
|
|
BEAST_EXPECT(r.first == tesSUCCESS);
|
|
BEAST_EXPECT(equal (r.second, D{alice, gw, usdC}));
|
|
}
|
|
{
|
|
// Check path with sendMax and node with correct sendMax already set
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
env.fund (XRP (10000), alice, bob, gw);
|
|
env.trust (USD (1000), alice, bob);
|
|
env.trust (EUR (1000), alice, bob);
|
|
env (pay (gw, alice, EUR (100)));
|
|
auto const path = STPath ({STPathElement (STPathElement::typeAll,
|
|
EUR.account, EUR.currency, EUR.account)});
|
|
test (env, USD, EUR.issue(), path, tesSUCCESS);
|
|
}
|
|
}
|
|
void testDirectStep ()
|
|
{
|
|
testcase ("Direct Step");
|
|
|
|
using namespace jtx;
|
|
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 USDA = alice["USD"];
|
|
auto const USDB = bob["USD"];
|
|
auto const USDC = carol["USD"];
|
|
auto const USDD = dan["USD"];
|
|
auto const gw = Account ("gw");
|
|
auto const USD = gw["USD"];
|
|
{
|
|
// Pay USD, trivial path
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
|
|
env.fund (XRP (10000), alice, bob, gw);
|
|
env.trust (USD (1000), alice, bob);
|
|
env (pay (gw, alice, USD (100)));
|
|
env (pay (alice, bob, USD (10)), paths (USD));
|
|
env.require (balance (bob, USD (10)));
|
|
}
|
|
{
|
|
// XRP transfer
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
|
|
env.fund (XRP (10000), alice, bob);
|
|
env (pay (alice, bob, XRP (100)));
|
|
env.require (balance (bob, XRP (10000 + 100)));
|
|
env.require (balance (alice, xrpMinusFee (env, 10000 - 100)));
|
|
}
|
|
{
|
|
// Partial payments
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
|
|
env.fund (XRP (10000), alice, bob, gw);
|
|
env.trust (USD (1000), alice, bob);
|
|
env (pay (gw, alice, USD (100)));
|
|
env (pay (alice, bob, USD (110)), paths (USD),
|
|
ter (tecPATH_PARTIAL));
|
|
env.require (balance (bob, USD (0)));
|
|
env (pay (alice, bob, USD (110)), paths (USD),
|
|
txflags (tfPartialPayment));
|
|
env.require (balance (bob, USD (100)));
|
|
}
|
|
{
|
|
// Pay by rippling through accounts, use path finder
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
|
|
env.fund (XRP (10000), alice, bob, carol, dan);
|
|
env.trust (USDA (10), bob);
|
|
env.trust (USDB (10), carol);
|
|
env.trust (USDC (10), dan);
|
|
env (pay (alice, dan, USDC (10)), paths (USDA));
|
|
env.require (
|
|
balance (bob, USDA (10)),
|
|
balance (carol, USDB (10)),
|
|
balance (dan, USDC (10)));
|
|
}
|
|
{
|
|
// Pay by rippling through accounts, specify path
|
|
// and charge a transfer fee
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
|
|
env.fund (XRP (10000), alice, bob, carol, dan);
|
|
env.trust (USDA (10), bob);
|
|
env.trust (USDB (10), alice, carol);
|
|
env.trust (USDC (10), dan);
|
|
env (rate (bob, 1.1));
|
|
|
|
// alice will redeem to bob; a transfer fee will be charged
|
|
env (pay (bob, alice, USDB(6)));
|
|
env (pay (alice, dan, USDC (5)), path (bob, carol),
|
|
sendmax (USDA (6)), txflags (tfNoRippleDirect));
|
|
env.require (balance (dan, USDC (5)));
|
|
env.require (balance (alice, USDB (0.5)));
|
|
}
|
|
{
|
|
// Pay by rippling through accounts, specify path and transfer fee
|
|
// Test that the transfer fee is not charged when alice issues
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
|
|
env.fund (XRP (10000), alice, bob, carol, dan);
|
|
env.trust (USDA (10), bob);
|
|
env.trust (USDB (10), alice, carol);
|
|
env.trust (USDC (10), dan);
|
|
env (rate (bob, 1.1));
|
|
|
|
env (pay (alice, dan, USDC (5)), path (bob, carol),
|
|
sendmax (USDA (6)), txflags (tfNoRippleDirect));
|
|
env.require (balance (dan, USDC (5)));
|
|
env.require (balance (bob, USDA (5)));
|
|
}
|
|
{
|
|
// test best quality path is taken
|
|
// Paths: A->B->D->E ; A->C->D->E
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
|
|
env.fund (XRP (10000), alice, bob, carol, dan, erin);
|
|
env.trust (USDA (10), bob, carol);
|
|
env.trust (USDB (10), dan);
|
|
env.trust (USDC (10), alice, dan);
|
|
env.trust (USDD (20), erin);
|
|
env (rate (bob, 1));
|
|
env (rate (carol, 1.1));
|
|
|
|
// Pay alice so she redeems to carol and a transfer fee is charged
|
|
env (pay (carol, alice, USDC(10)));
|
|
env (pay (alice, erin, USDD (5)), path (carol, dan),
|
|
path (bob, dan), txflags (tfNoRippleDirect));
|
|
|
|
env.require (balance (erin, USDD (5)));
|
|
env.require (balance (dan, USDB (5)));
|
|
env.require (balance (dan, USDC (0)));
|
|
}
|
|
{
|
|
// Limit quality
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
|
|
env.fund (XRP (10000), alice, bob, carol);
|
|
env.trust (USDA (10), bob);
|
|
env.trust (USDB (10), carol);
|
|
|
|
env (pay (alice, carol, USDB (5)), sendmax (USDA (4)),
|
|
txflags (tfLimitQuality | tfPartialPayment), ter (tecPATH_DRY));
|
|
env.require (balance (carol, USDB (0)));
|
|
|
|
env (pay (alice, carol, USDB (5)), sendmax (USDA (4)),
|
|
txflags (tfPartialPayment));
|
|
env.require (balance (carol, USDB (4)));
|
|
}
|
|
}
|
|
|
|
void testLineQuality ()
|
|
{
|
|
testcase ("Line Quality");
|
|
|
|
using namespace jtx;
|
|
auto const alice = Account ("alice");
|
|
auto const bob = Account ("bob");
|
|
auto const carol = Account ("carol");
|
|
auto const dan = Account ("dan");
|
|
auto const USDA = alice["USD"];
|
|
auto const USDB = bob["USD"];
|
|
auto const USDC = carol["USD"];
|
|
auto const USDD = dan["USD"];
|
|
|
|
// Dan -> Bob -> Alice -> Carol; vary bobDanQIn and bobAliceQOut
|
|
for (auto bobDanQIn : {80, 100, 120})
|
|
for (auto bobAliceQOut : {80, 100, 120})
|
|
for (auto const& f : {feature ("nullFeature"), featureFlow})
|
|
{
|
|
if (f != featureFlow && bobDanQIn < 100 && bobAliceQOut < 100)
|
|
continue; // Bug in flow v1
|
|
Env env (*this, features (f));
|
|
env.fund (XRP (10000), alice, bob, carol, dan);
|
|
env (trust (bob, USDD (100)), qualityInPercent (bobDanQIn));
|
|
env (trust (bob, USDA (100)),
|
|
qualityOutPercent (bobAliceQOut));
|
|
env (trust (carol, USDA (100)));
|
|
|
|
env (pay (alice, bob, USDA (100)));
|
|
env.require (balance (bob, USDA (100)));
|
|
env (pay (dan, carol, USDA (10)), path (bob),
|
|
sendmax (USDD (100)), txflags (tfNoRippleDirect));
|
|
env.require (balance (bob, USDA (90)));
|
|
if (bobAliceQOut > bobDanQIn)
|
|
env.require (
|
|
balance (bob, USDD (10.0 * double(bobAliceQOut) /
|
|
double(bobDanQIn))));
|
|
else
|
|
env.require (balance (bob, USDD (10)));
|
|
env.require (balance (carol, USDA (10)));
|
|
}
|
|
|
|
// bob -> alice -> carol; vary carolAliceQIn
|
|
for (auto carolAliceQIn : {80, 100, 120})
|
|
for (auto const& f : {feature ("nullFeature"), featureFlow})
|
|
{
|
|
Env env (*this, features (f));
|
|
env.fund (XRP (10000), alice, bob, carol);
|
|
env (trust (bob, USDA (10)));
|
|
env (trust (carol, USDA (10)), qualityInPercent (carolAliceQIn));
|
|
|
|
env (pay (alice, bob, USDA (10)));
|
|
env.require (balance (bob, USDA (10)));
|
|
env (pay (bob, carol, USDA (5)), sendmax (USDA (10)));
|
|
auto const effectiveQ =
|
|
carolAliceQIn > 100 ? 1.0 : carolAliceQIn / 100.0;
|
|
env.require (balance (bob, USDA (10.0 - 5.0 / effectiveQ)));
|
|
}
|
|
|
|
// bob -> alice -> carol; bobAliceQOut varies.
|
|
for (auto bobAliceQOut : {80, 100, 120})
|
|
for (auto const& f : {feature ("nullFeature"), featureFlow})
|
|
{
|
|
Env env (*this, features (f));
|
|
env.fund (XRP (10000), alice, bob, carol);
|
|
env (trust (bob, USDA (10)), qualityOutPercent (bobAliceQOut));
|
|
env (trust (carol, USDA (10)));
|
|
|
|
env (pay (alice, bob, USDA (10)));
|
|
env.require (balance (bob, USDA (10)));
|
|
env (pay (bob, carol, USDA (5)), sendmax (USDA (5)));
|
|
env.require (balance (carol, USDA (5)));
|
|
env.require (balance (bob, USDA (10-5)));
|
|
}
|
|
}
|
|
|
|
void testBookStep ()
|
|
{
|
|
testcase ("Book Step");
|
|
|
|
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");
|
|
|
|
{
|
|
// simple IOU/IOU offer
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
|
|
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 (50)));
|
|
env (pay (gw, bob, USD (50)));
|
|
|
|
env (offer (bob, BTC (50), USD (50)));
|
|
|
|
env (pay (alice, carol, USD (50)), path (~USD), sendmax (BTC (50)));
|
|
|
|
env.require (balance (alice, BTC (0)));
|
|
env.require (balance (bob, BTC (50)));
|
|
env.require (balance (bob, USD (0)));
|
|
env.require (balance (carol, USD (50)));
|
|
BEAST_EXPECT(!isOffer (env, bob, BTC (50), USD (50)));
|
|
}
|
|
{
|
|
// simple IOU/XRP XRP/IOU offer
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
|
|
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 (50)));
|
|
env (pay (gw, bob, USD (50)));
|
|
|
|
env (offer (bob, BTC (50), XRP (50)));
|
|
env (offer (bob, XRP (50), USD (50)));
|
|
|
|
env (pay (alice, carol, USD (50)), path (~XRP, ~USD),
|
|
sendmax (BTC (50)));
|
|
|
|
env.require (balance (alice, BTC (0)));
|
|
env.require (balance (bob, BTC (50)));
|
|
env.require (balance (bob, USD (0)));
|
|
env.require (balance (carol, USD (50)));
|
|
BEAST_EXPECT(!isOffer (env, bob, XRP (50), USD (50)));
|
|
BEAST_EXPECT(!isOffer (env, bob, BTC (50), XRP (50)));
|
|
}
|
|
{
|
|
// simple XRP -> USD through offer and sendmax
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
|
|
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, bob, USD (50)));
|
|
|
|
env (offer (bob, XRP (50), USD (50)));
|
|
|
|
env (pay (alice, carol, USD (50)), path (~USD),
|
|
sendmax (XRP (50)));
|
|
|
|
env.require (balance (alice, xrpMinusFee (env, 10000 - 50)));
|
|
env.require (balance (bob, xrpMinusFee (env, 10000 + 50)));
|
|
env.require (balance (bob, USD (0)));
|
|
env.require (balance (carol, USD (50)));
|
|
BEAST_EXPECT(!isOffer (env, bob, XRP (50), USD (50)));
|
|
}
|
|
{
|
|
// simple USD -> XRP through offer and sendmax
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
|
|
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, USD (50)));
|
|
|
|
env (offer (bob, USD (50), XRP (50)));
|
|
|
|
env (pay (alice, carol, XRP (50)), path (~XRP),
|
|
sendmax (USD (50)));
|
|
|
|
env.require (balance (alice, USD (0)));
|
|
env.require (balance (bob, xrpMinusFee (env, 10000 - 50)));
|
|
env.require (balance (bob, USD (50)));
|
|
env.require (balance (carol, XRP (10000 + 50)));
|
|
BEAST_EXPECT(!isOffer (env, bob, USD (50), XRP (50)));
|
|
}
|
|
{
|
|
// test unfunded offers are removed when payment succeeds
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
|
|
env.fund (XRP (10000), alice, bob, carol, gw);
|
|
env.trust (USD (1000), alice, bob, carol);
|
|
env.trust (BTC (1000), alice, bob, carol);
|
|
env.trust (EUR (1000), alice, bob, carol);
|
|
|
|
env (pay (gw, alice, BTC (60)));
|
|
env (pay (gw, bob, USD (50)));
|
|
env (pay (gw, bob, EUR (50)));
|
|
|
|
env (offer (bob, BTC (50), USD (50)));
|
|
env (offer (bob, BTC (60), EUR (50)));
|
|
env (offer (bob, EUR (50), USD (50)));
|
|
|
|
// unfund offer
|
|
env (pay (bob, gw, EUR (50)));
|
|
BEAST_EXPECT(isOffer (env, bob, BTC (50), USD (50)));
|
|
BEAST_EXPECT(isOffer (env, bob, BTC (60), EUR (50)));
|
|
BEAST_EXPECT(isOffer (env, bob, EUR (50), USD (50)));
|
|
|
|
env (pay (alice, carol, USD (50)),
|
|
path (~USD), path (~EUR, ~USD),
|
|
sendmax (BTC (60)));
|
|
|
|
env.require (balance (alice, BTC (10)));
|
|
env.require (balance (bob, BTC (50)));
|
|
env.require (balance (bob, USD (0)));
|
|
env.require (balance (bob, EUR (0)));
|
|
env.require (balance (carol, USD (50)));
|
|
// used in the payment
|
|
BEAST_EXPECT(!isOffer (env, bob, BTC (50), USD (50)));
|
|
// found unfunded
|
|
BEAST_EXPECT(!isOffer (env, bob, BTC (60), EUR (50)));
|
|
// unfunded, but should not yet be found unfunded
|
|
BEAST_EXPECT(isOffer (env, bob, EUR (50), USD (50)));
|
|
}
|
|
{
|
|
// test unfunded offers are returned when the payment fails.
|
|
// bob makes two offers: a funded 50 USD for 50 BTC and an unfunded 50
|
|
// EUR for 60 BTC. alice pays carol 61 USD with 61 BTC. alice only
|
|
// has 60 BTC, so the payment will fail. The payment uses two paths:
|
|
// one through bob's funded offer and one through his unfunded
|
|
// offer. When the payment fails `flow` should return the unfunded
|
|
// offer. This test is intentionally similar to the one that removes
|
|
// unfunded offers when the payment succeeds.
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
|
|
env.fund (XRP (10000), alice, bob, carol, gw);
|
|
env.trust (USD (1000), alice, bob, carol);
|
|
env.trust (BTC (1000), alice, bob, carol);
|
|
env.trust (EUR (1000), alice, bob, carol);
|
|
|
|
env (pay (gw, alice, BTC (60)));
|
|
env (pay (gw, bob, USD (50)));
|
|
env (pay (gw, bob, EUR (50)));
|
|
|
|
env (offer (bob, BTC (50), USD (50)));
|
|
env (offer (bob, BTC (60), EUR (50)));
|
|
env (offer (bob, EUR (50), USD (50)));
|
|
|
|
// unfund offer
|
|
env (pay (bob, gw, EUR (50)));
|
|
BEAST_EXPECT(isOffer (env, bob, BTC (50), USD (50)));
|
|
BEAST_EXPECT(isOffer (env, bob, BTC (60), EUR (50)));
|
|
|
|
auto flowJournal = env.app ().logs ().journal ("Flow");
|
|
auto const flowResult = [&]
|
|
{
|
|
STAmount deliver (USD (51));
|
|
STAmount smax (BTC (61));
|
|
PaymentSandbox sb (env.current ().get (), tapNONE);
|
|
STPathSet paths;
|
|
{
|
|
// BTC -> USD
|
|
STPath p1 ({IPE (USD.issue ())});
|
|
paths.push_back (p1);
|
|
// BTC -> EUR -> USD
|
|
STPath p2 ({IPE (EUR.issue ()), IPE (USD.issue ())});
|
|
paths.push_back (p2);
|
|
}
|
|
|
|
return flow (sb, deliver, alice, carol, paths, false, false, true,
|
|
boost::none, smax, flowJournal);
|
|
}();
|
|
|
|
BEAST_EXPECT(flowResult.removableOffers.size () == 1);
|
|
env.app ().openLedger ().modify (
|
|
[&](OpenView& view, beast::Journal j)
|
|
{
|
|
if (flowResult.removableOffers.empty())
|
|
return false;
|
|
Sandbox sb (&view, tapNONE);
|
|
for (auto const& o : flowResult.removableOffers)
|
|
if (auto ok = sb.peek (keylet::offer (o)))
|
|
offerDelete (sb, ok, flowJournal);
|
|
sb.apply (view);
|
|
return true;
|
|
});
|
|
|
|
// used in payment, but since payment failed should be untouched
|
|
BEAST_EXPECT(isOffer (env, bob, BTC (50), USD (50)));
|
|
// found unfunded
|
|
BEAST_EXPECT(!isOffer (env, bob, BTC (60), EUR (50)));
|
|
}
|
|
{
|
|
// Do not produce more in the forward pass than the reverse pass
|
|
// This test uses a path that whose reverse pass will compute a
|
|
// 0.5 USD input required for a 1 EUR output. It sets a sendmax of
|
|
// 0.4 USD, so the payment engine will need to do a forward pass.
|
|
// Without limits, the 0.4 USD would produce 1000 EUR in the forward
|
|
// pass. This test checks that the payment produces 1 EUR, as expected.
|
|
|
|
Env env (*this, features (featureFlow),
|
|
features (featureOwnerPaysFee));
|
|
|
|
auto const closeTime = STAmountSO::soTime2 +
|
|
100 * env.closed ()->info ().closeTimeResolution;
|
|
env.close (closeTime);
|
|
|
|
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 (1000)));
|
|
env (pay (gw, bob, EUR (1000)));
|
|
|
|
env (offer (bob, USD (1), drops (2)), txflags (tfPassive));
|
|
env (offer (bob, drops (1), EUR (1000)), txflags (tfPassive));
|
|
|
|
env (pay (alice, carol, EUR (1)), path (~XRP, ~EUR),
|
|
sendmax (USD (0.4)), txflags (tfNoRippleDirect|tfPartialPayment));
|
|
|
|
env.require (balance (carol, EUR (1)));
|
|
env.require (balance (bob, USD (0.4)));
|
|
env.require (balance (bob, EUR (999)));
|
|
}
|
|
}
|
|
|
|
void testTransferRate ()
|
|
{
|
|
testcase ("Transfer Rate");
|
|
|
|
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");
|
|
|
|
|
|
{
|
|
// Simple payment through a gateway with a
|
|
// transfer rate
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
|
|
env.fund (XRP (10000), alice, bob, carol, gw);
|
|
env(rate(gw, 1.25));
|
|
env.trust (USD (1000), alice, bob, carol);
|
|
env (pay (gw, alice, USD (50)));
|
|
env.require (balance (alice, USD (50)));
|
|
env (pay (alice, bob, USD (40)), sendmax (USD (50)));
|
|
env.require (balance (bob, USD (40)), balance (alice, USD (0)));
|
|
}
|
|
{
|
|
// transfer rate is not charged when issuer is src or dst
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
|
|
env.fund (XRP (10000), alice, bob, carol, gw);
|
|
env(rate(gw, 1.25));
|
|
env.trust (USD (1000), alice, bob, carol);
|
|
env (pay (gw, alice, USD (50)));
|
|
env.require (balance (alice, USD (50)));
|
|
env (pay (alice, gw, USD (40)), sendmax (USD (40)));
|
|
env.require (balance (alice, USD (10)));
|
|
}
|
|
{
|
|
// transfer fee on an offer
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
|
|
env.fund (XRP (10000), alice, bob, carol, gw);
|
|
env(rate(gw, 1.25));
|
|
env.trust (USD (1000), alice, bob, carol);
|
|
env (pay (gw, bob, USD (65)));
|
|
|
|
env (offer (bob, XRP (50), USD (50)));
|
|
|
|
env (pay (alice, carol, USD (50)), path (~USD), sendmax (XRP (50)));
|
|
env.require (
|
|
balance (alice, xrpMinusFee (env, 10000 - 50)),
|
|
balance (bob, USD (2.5)), // owner pays transfer fee
|
|
balance (carol, USD (50)));
|
|
}
|
|
|
|
{
|
|
// Transfer fee two consecutive offers
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
|
|
env.fund (XRP (10000), alice, bob, carol, gw);
|
|
env(rate(gw, 1.25));
|
|
env.trust (USD (1000), alice, bob, carol);
|
|
env.trust (EUR (1000), alice, bob, carol);
|
|
env (pay (gw, bob, USD (50)));
|
|
env (pay (gw, bob, EUR (50)));
|
|
|
|
env (offer (bob, XRP (50), USD (50)));
|
|
env (offer (bob, USD (50), EUR (50)));
|
|
|
|
env (pay (alice, carol, EUR (40)), path (~USD, ~EUR), sendmax (XRP (40)));
|
|
env.require (
|
|
balance (alice, xrpMinusFee (env, 10000 - 40)),
|
|
balance (bob, USD (40)),
|
|
balance (bob, EUR (0)),
|
|
balance (carol, EUR (40)));
|
|
}
|
|
|
|
{
|
|
// First pass through a strand redeems, second pass issues, no offers
|
|
// limiting step is not an endpoint
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
auto const USDA = alice["USD"];
|
|
auto const USDB = bob["USD"];
|
|
|
|
env.fund (XRP (10000), alice, bob, carol, gw);
|
|
env(rate(gw, 1.25));
|
|
env.trust (USD (1000), alice, bob, carol);
|
|
env.trust (USDA (1000), bob);
|
|
env.trust (USDB (1000), gw);
|
|
env (pay (gw, bob, USD (50)));
|
|
// alice -> bob -> gw -> carol. $50 should have transfer fee; $10, no fee
|
|
env (pay (alice, carol, USD(50)), path (bob), sendmax (USDA(60)));
|
|
env.require (
|
|
balance (bob, USD (-10)),
|
|
balance (bob, USDA (60)),
|
|
balance (carol, USD (50)));
|
|
}
|
|
{
|
|
// First pass through a strand redeems, second pass issues, through an offer
|
|
// limiting step is not an endpoint
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
auto const USDA = alice["USD"];
|
|
auto const USDB = bob["USD"];
|
|
Account const dan ("dan");
|
|
|
|
env.fund (XRP (10000), alice, bob, carol, dan, gw);
|
|
env(rate(gw, 1.25));
|
|
env.trust (USD (1000), alice, bob, carol, dan);
|
|
env.trust (EUR (1000), carol, dan);
|
|
env.trust (USDA (1000), bob);
|
|
env.trust (USDB (1000), gw);
|
|
env (pay (gw, bob, USD (50)));
|
|
env (pay (gw, dan, EUR (100)));
|
|
env (offer (dan, USD (100), EUR (100)));
|
|
// alice -> bob -> gw -> carol. $50 should have transfer fee; $10, no fee
|
|
env (pay (alice, carol, EUR (50)), path (bob, gw, ~EUR),
|
|
sendmax (USDA (60)), txflags (tfNoRippleDirect));
|
|
env.require (
|
|
balance (bob, USD (-10)),
|
|
balance (bob, USDA (60)),
|
|
balance (dan, USD (50)),
|
|
balance (dan, EUR (37.5)),
|
|
balance (carol, EUR (50)));
|
|
}
|
|
|
|
{
|
|
// Offer where the owner is also the issuer, owner pays fee
|
|
Env env (*this, features(featureFlow), features(featureOwnerPaysFee));
|
|
|
|
env.fund (XRP (10000), alice, bob, gw);
|
|
env(rate(gw, 1.25));
|
|
env.trust (USD (1000), alice, bob);
|
|
env (offer (gw, XRP (100), USD (100)));
|
|
env (pay (alice, bob, USD (100)),
|
|
sendmax (XRP (100)));
|
|
env.require (
|
|
balance (alice, xrpMinusFee(env, 10000-100)),
|
|
balance (bob, USD (100)));
|
|
}
|
|
{
|
|
// Offer where the owner is also the issuer, sender pays fee
|
|
Env env (*this, features(featureFlow));
|
|
|
|
env.fund (XRP (10000), alice, bob, gw);
|
|
env(rate(gw, 1.25));
|
|
env.trust (USD (1000), alice, bob);
|
|
env (offer (gw, XRP (125), USD (125)));
|
|
env (pay (alice, bob, USD (100)),
|
|
sendmax (XRP (200)));
|
|
env.require (
|
|
balance (alice, xrpMinusFee(env, 10000-125)),
|
|
balance (bob, USD (100)));
|
|
}
|
|
}
|
|
|
|
void
|
|
testFalseDryHelper (jtx::Env& env)
|
|
{
|
|
using namespace jtx;
|
|
|
|
auto const gw = Account ("gateway");
|
|
auto const USD = gw["USD"];
|
|
auto const EUR = gw["EUR"];
|
|
Account const alice ("alice");
|
|
Account const bob ("bob");
|
|
Account const carol ("carol");
|
|
|
|
auto const closeTime = amendmentRIPD1141SoTime() +
|
|
100 * env.closed ()->info ().closeTimeResolution;
|
|
env.close (closeTime);
|
|
|
|
env.fund (XRP(10000), alice, carol, gw);
|
|
env.fund (reserve(env, 5), bob);
|
|
env.trust (USD (1000), alice, bob, carol);
|
|
env.trust (EUR (1000), alice, bob, carol);
|
|
|
|
|
|
env (pay (gw, alice, EUR (50)));
|
|
env (pay (gw, bob, USD (50)));
|
|
|
|
// Bob has _just_ slightly less than 50 xrp available
|
|
// If his owner count changes, he will have more liquidity.
|
|
// This is one error case to test (when Flow is used).
|
|
// Computing the incomming xrp to the XRP/USD offer will require two
|
|
// recursive calls to the EUR/XRP offer. The second call will return
|
|
// tecPATH_DRY, but the entire path should not be marked as dry. This
|
|
// is the second error case to test (when flowV1 is used).
|
|
env (offer (bob, EUR (50), XRP (50)));
|
|
env (offer (bob, XRP (50), USD (50)));
|
|
|
|
env (pay (alice, carol, USD (1000000)), path (~XRP, ~USD),
|
|
sendmax (EUR (500)),
|
|
txflags (tfNoRippleDirect | tfPartialPayment));
|
|
|
|
auto const carolUSD = env.balance(carol, USD).value();
|
|
BEAST_EXPECT(carolUSD > USD (0) && carolUSD < USD (50));
|
|
}
|
|
|
|
void
|
|
testFalseDry ()
|
|
{
|
|
testcase ("falseDryChanges");
|
|
using namespace jtx;
|
|
{
|
|
Env env (*this, features (featureFlow));
|
|
testFalseDryHelper (env);
|
|
}
|
|
{
|
|
Env env (*this);
|
|
testFalseDryHelper (env);
|
|
}
|
|
}
|
|
|
|
void
|
|
testLimitQuality ()
|
|
{
|
|
// Single path with two offers and limit quality. The quality limit is
|
|
// such that the first offer should be taken but the second should not.
|
|
// The total amount delivered should be the sum of the two offers and
|
|
// sendMax should be more than the first offer.
|
|
testcase ("limitQuality");
|
|
using namespace jtx;
|
|
|
|
auto const gw = Account ("gateway");
|
|
auto const USD = gw["USD"];
|
|
Account const alice ("alice");
|
|
Account const bob ("bob");
|
|
Account const carol ("carol");
|
|
|
|
auto const timeDelta = Env{*this}.closed ()->info ().closeTimeResolution;
|
|
|
|
for(auto const& d: {-timeDelta*100, +timeDelta*100}){
|
|
auto const closeTime = amendmentRIPD1141SoTime () + d;
|
|
Env env (*this);
|
|
env.close (closeTime);
|
|
|
|
env.fund (XRP(10000), alice, bob, carol, gw);
|
|
|
|
env.trust (USD(100), alice, bob, carol);
|
|
env (pay (gw, bob, USD (100)));
|
|
env (offer (bob, XRP (50), USD (50)));
|
|
env (offer (bob, XRP (100), USD (50)));
|
|
|
|
auto expectedResult =
|
|
closeTime < amendmentRIPD1141SoTime () ? tecPATH_DRY : tesSUCCESS;
|
|
env (pay (alice, carol, USD (100)), path (~USD), sendmax (XRP (100)),
|
|
txflags (tfNoRippleDirect | tfPartialPayment | tfLimitQuality),
|
|
ter (expectedResult));
|
|
|
|
if (expectedResult == tesSUCCESS)
|
|
env.require (balance (carol, USD (50)));
|
|
}
|
|
}
|
|
|
|
// Helper function that returns the reserve on an account based on
|
|
// the passed in number of owners.
|
|
static XRPAmount reserve(jtx::Env& env, std::uint32_t count)
|
|
{
|
|
return env.current()->fees().accountReserve (count);
|
|
}
|
|
|
|
// 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,
|
|
[&env, &result](std::shared_ptr<SLE const> const& sle)
|
|
{
|
|
if (sle->getType() == ltOFFER)
|
|
result.push_back (sle);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
void
|
|
testSelfPayment1()
|
|
{
|
|
testcase ("Self-payment 1");
|
|
|
|
// In this test case the new flow code mis-computes the amount
|
|
// of money to move. Fortunately the new code's re-execute
|
|
// check catches the problem and throws out the transaction.
|
|
//
|
|
// The old payment code handles the payment correctly.
|
|
using namespace jtx;
|
|
|
|
auto const gw1 = Account ("gw1");
|
|
auto const gw2 = Account ("gw2");
|
|
auto const alice = Account ("alice");
|
|
auto const USD = gw1["USD"];
|
|
auto const EUR = gw2["EUR"];
|
|
|
|
Env env (*this, features (featureFlow));
|
|
|
|
auto const closeTime =
|
|
amendmentRIPD1141SoTime () + 100 * env.closed ()->info ().closeTimeResolution;
|
|
env.close (closeTime);
|
|
|
|
env.fund (XRP (1000000), gw1, gw2);
|
|
env.close ();
|
|
|
|
// The fee that's charged for transactions.
|
|
auto const f = env.current ()->fees ().base;
|
|
|
|
env.fund (reserve (env, 3) + f * 4, alice);
|
|
env.close ();
|
|
|
|
env (trust (alice, USD (2000)));
|
|
env (trust (alice, EUR (2000)));
|
|
env.close ();
|
|
|
|
env (pay (gw1, alice, USD (1)));
|
|
env (pay (gw2, alice, EUR (1000)));
|
|
env.close ();
|
|
|
|
env (offer (alice, USD (500), EUR (600)));
|
|
env.close ();
|
|
|
|
env.require (owners (alice, 3));
|
|
env.require (balance (alice, USD (1)));
|
|
env.require (balance (alice, EUR (1000)));
|
|
|
|
auto 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] == EUR (600));
|
|
BEAST_EXPECT(offer[sfTakerPays] == USD (500));
|
|
}
|
|
|
|
env (pay (alice, alice, EUR (600)), sendmax (USD (500)),
|
|
txflags (tfPartialPayment));
|
|
env.close ();
|
|
|
|
env.require (owners (alice, 3));
|
|
env.require (balance (alice, USD (1)));
|
|
env.require (balance (alice, EUR (1000)));
|
|
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] == EUR (598.8));
|
|
BEAST_EXPECT(offer[sfTakerPays] == USD (499));
|
|
}
|
|
}
|
|
|
|
void
|
|
testSelfPayment2()
|
|
{
|
|
testcase ("Self-payment 2");
|
|
|
|
// In this case the difference between the old payment code and
|
|
// the new is the values left behind in the offer. Not saying either
|
|
// ios ring, they are just different.
|
|
using namespace jtx;
|
|
|
|
auto const gw1 = Account ("gw1");
|
|
auto const gw2 = Account ("gw2");
|
|
auto const alice = Account ("alice");
|
|
auto const USD = gw1["USD"];
|
|
auto const EUR = gw2["EUR"];
|
|
|
|
Env env (*this, features (featureFlow));
|
|
|
|
auto const closeTime =
|
|
amendmentRIPD1141SoTime () + 100 * env.closed ()->info ().closeTimeResolution;
|
|
env.close (closeTime);
|
|
|
|
env.fund (XRP (1000000), gw1, gw2);
|
|
env.close ();
|
|
|
|
// The fee that's charged for transactions.
|
|
auto const f = env.current ()->fees ().base;
|
|
|
|
env.fund (reserve (env, 3) + f * 4, alice);
|
|
env.close ();
|
|
|
|
env (trust (alice, USD (506)));
|
|
env (trust (alice, EUR (606)));
|
|
env.close ();
|
|
|
|
env (pay (gw1, alice, USD (500)));
|
|
env (pay (gw2, alice, EUR (600)));
|
|
env.close ();
|
|
|
|
env (offer (alice, USD (500), EUR (600)));
|
|
env.close ();
|
|
|
|
env.require (owners (alice, 3));
|
|
env.require (balance (alice, USD (500)));
|
|
env.require (balance (alice, EUR (600)));
|
|
|
|
auto 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] == EUR (600));
|
|
BEAST_EXPECT(offer[sfTakerPays] == USD (500));
|
|
}
|
|
|
|
env (pay (alice, alice, EUR (60)), sendmax (USD (50)),
|
|
txflags (tfPartialPayment));
|
|
env.close ();
|
|
|
|
env.require (owners (alice, 3));
|
|
env.require (balance (alice, USD (500)));
|
|
env.require (balance (alice, EUR (600)));
|
|
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] == EUR (594));
|
|
BEAST_EXPECT(offer[sfTakerPays] == USD (495));
|
|
}
|
|
}
|
|
void testSelfFundedXRPEndpoint (bool consumeOffer)
|
|
{
|
|
// Test that the deferred credit table is not bypassed for
|
|
// XRPEndpointSteps. If the account in the first step is sending XRP and
|
|
// that account also owns an offer that receives XRP, it should not be
|
|
// possible for that step to use the XRP received in the offer as part
|
|
// of the payment.
|
|
testcase("Self funded XRPEndpoint");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env(*this, features(featureFlow));
|
|
|
|
// Need new behavior from `accountHolds`
|
|
auto const closeTime = amendmentRIPD1141SoTime() +
|
|
env.closed()->info().closeTimeResolution;
|
|
env.close(closeTime);
|
|
|
|
auto const alice = Account("alice");
|
|
auto const gw = Account("gw");
|
|
auto const USD = gw["USD"];
|
|
|
|
env.fund(XRP(10000), alice, gw);
|
|
env(trust(alice, USD(20)));
|
|
env(pay(gw, alice, USD(10)));
|
|
env(offer(alice, XRP(50000), USD(10)));
|
|
|
|
// Consuming the offer changes the owner count, which could also cause
|
|
// liquidity to decrease in the forward pass
|
|
auto const toSend = consumeOffer ? USD(10) : USD(9);
|
|
env(pay(alice, alice, toSend), path(~USD), sendmax(XRP(20000)),
|
|
txflags(tfPartialPayment | tfNoRippleDirect));
|
|
}
|
|
|
|
void testUnfundedOffer (bool withFix)
|
|
{
|
|
testcase(std::string("Unfunded Offer ") +
|
|
(withFix ? "with fix" : "without fix"));
|
|
|
|
using namespace jtx;
|
|
{
|
|
// Test reverse
|
|
Env env(*this, features(featureFlow));
|
|
auto closeTime = amendmentRIPD1298SoTime();
|
|
if (withFix)
|
|
closeTime += env.closed()->info().closeTimeResolution;
|
|
else
|
|
closeTime -= env.closed()->info().closeTimeResolution;
|
|
env.close(closeTime);
|
|
|
|
auto const alice = Account("alice");
|
|
auto const bob = Account("bob");
|
|
auto const gw = Account("gw");
|
|
auto const USD = gw["USD"];
|
|
|
|
env.fund(XRP(100000), alice, bob, gw);
|
|
env(trust(bob, USD(20)));
|
|
|
|
STAmount tinyAmt1{USD.issue(), 9000000000000000ll, -17, false,
|
|
false, STAmount::unchecked{}};
|
|
STAmount tinyAmt3{USD.issue(), 9000000000000003ll, -17, false,
|
|
false, STAmount::unchecked{}};
|
|
|
|
env(offer(gw, drops(9000000000), tinyAmt3));
|
|
env(pay(alice, bob, tinyAmt1), path(~USD),
|
|
sendmax(drops(9000000000)), txflags(tfNoRippleDirect));
|
|
|
|
if (withFix)
|
|
BEAST_EXPECT(!isOffer(env, gw, XRP(0), USD(0)));
|
|
else
|
|
BEAST_EXPECT(isOffer(env, gw, XRP(0), USD(0)));
|
|
}
|
|
{
|
|
// Test forward
|
|
Env env(*this, features(featureFlow));
|
|
auto closeTime = amendmentRIPD1298SoTime();
|
|
if (withFix)
|
|
closeTime += env.closed()->info().closeTimeResolution;
|
|
else
|
|
closeTime -= env.closed()->info().closeTimeResolution;
|
|
env.close(closeTime);
|
|
|
|
auto const alice = Account("alice");
|
|
auto const bob = Account("bob");
|
|
auto const gw = Account("gw");
|
|
auto const USD = gw["USD"];
|
|
|
|
env.fund(XRP(100000), alice, bob, gw);
|
|
env(trust(alice, USD(20)));
|
|
|
|
STAmount tinyAmt1{USD.issue(), 9000000000000000ll, -17, false,
|
|
false, STAmount::unchecked{}};
|
|
STAmount tinyAmt3{USD.issue(), 9000000000000003ll, -17, false,
|
|
false, STAmount::unchecked{}};
|
|
|
|
env(pay(gw, alice, tinyAmt1));
|
|
|
|
env(offer(gw, tinyAmt3, drops(9000000000)));
|
|
env(pay(alice, bob, drops(9000000000)), path(~XRP),
|
|
sendmax(USD(1)), txflags(tfNoRippleDirect));
|
|
|
|
if (withFix)
|
|
BEAST_EXPECT(!isOffer(env, gw, USD(0), XRP(0)));
|
|
else
|
|
BEAST_EXPECT(isOffer(env, gw, USD(0), XRP(0)));
|
|
}
|
|
}
|
|
|
|
void run() override
|
|
{
|
|
testDirectStep ();
|
|
testLineQuality();
|
|
testBookStep ();
|
|
testTransferRate ();
|
|
testToStrand ();
|
|
testFalseDry();
|
|
testLimitQuality();
|
|
testSelfPayment1();
|
|
testSelfPayment2();
|
|
testSelfFundedXRPEndpoint(false);
|
|
testSelfFundedXRPEndpoint(true);
|
|
testUnfundedOffer(true);
|
|
testUnfundedOffer(false);
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE(Flow,app,ripple);
|
|
|
|
} // test
|
|
} // ripple
|