Transfer fee changes:

An offer owner pays the transfer fee and only change a transfer fee
when transfering an IOU (as the old code does).
This commit is contained in:
seelabs
2016-04-21 07:17:40 -04:00
committed by Howard Hinnant
parent d7a778ce6a
commit c549c9dff0
12 changed files with 436 additions and 134 deletions

View File

@@ -62,6 +62,7 @@ flow (
STPathSet const& paths,
bool defaultPaths,
bool partialPayment,
bool ownerPaysTransferFee,
boost::optional<Quality> const& limitQuality,
boost::optional<STAmount> const& sendMax,
beast::Journal j)
@@ -83,7 +84,7 @@ flow (
// convert the paths to a collection of strands. Each strand is the collection
// of account->account steps and book steps that may be used in this payment.
auto sr = toStrands (sb, src, dst, dstIssue, sendMaxIssue, paths,
defaultPaths, j);
defaultPaths, ownerPaysTransferFee, j);
if (sr.first != tesSUCCESS)
{

View File

@@ -51,6 +51,7 @@ flow (PaymentSandbox& view,
STPathSet const& paths,
bool defaultPaths,
bool partialPayment,
bool ownerPaysTransferFee,
boost::optional<Quality> const& limitQuality,
boost::optional<STAmount> const& sendMax,
beast::Journal j);

View File

@@ -133,9 +133,11 @@ RippleCalc::Output RippleCalc::rippleCalculate (
try
{
bool const ownerPaysTransferFee =
view.rules ().enabled (featureOwnerPaysFee, config.features);
flowV2Out = flow (flowV2SB, saDstAmountReq, uSrcAccountID,
uDstAccountID, spsPaths, defaultPaths, partialPayment,
limitQuality, sendMax, j);
ownerPaysTransferFee, limitQuality, sendMax, j);
}
catch (std::exception& e)
{
@@ -144,25 +146,28 @@ RippleCalc::Output RippleCalc::rippleCalculate (
Throw();
}
if (callFlowV2 && callFlowV1 &&
(flowV2Out.result () != flowV1Out.result () ||
(flowV2Out.result () == tesSUCCESS &&
(flowV2Out.actualAmountIn != flowV1Out.actualAmountIn ||
flowV2Out.actualAmountOut != flowV1Out.actualAmountOut))))
if (j.debug())
{
JLOG (j.trace()) <<
"Mismatch: New Flow and RippleCalc" <<
" Old actualIn: " << flowV1Out.actualAmountIn <<
" New actualIn: " << flowV2Out.actualAmountIn <<
" Old actualOut: " << flowV1Out.actualAmountOut <<
" New actualOut: " << flowV2Out.actualAmountOut <<
" Old result: " << flowV1Out.result () <<
" New result: " << flowV2Out.result();
}
else
{
JLOG (j.trace()) << "Match: New Flow and RippleCalc";
auto logResult = [&] (std::string const& algoName, Output const& result)
{
j.debug() << "RippleCalc Result> " <<
" actualIn: " << result.actualAmountIn <<
", actualOut: " << result.actualAmountOut <<
", result: " << result.result () <<
", dstAmtReq: " << saDstAmountReq <<
", sendMax: " << saMaxAmountReq <<
", algo: " << algoName;
};
if (callFlowV1)
{
logResult ("V1", flowV1Out);
}
if (callFlowV2)
{
logResult ("V2", flowV2Out);
}
}
JLOG (j.trace()) << "Using old flow: " << useFlowV1Output;
}

View File

@@ -52,6 +52,9 @@ private:
Book book_;
AccountID strandSrc_;
AccountID strandDst_;
// Charge transfer fees whan the prev step redeems
Step const* const prevStep_ = nullptr;
bool const ownerPaysTransferFee_;
beast::Journal j_;
struct Cache
@@ -59,7 +62,6 @@ private:
TIn in;
TOut out;
Cache () = default;
Cache (TIn const& in_, TOut const& out_)
: in (in_), out (out_)
{
@@ -73,10 +75,14 @@ public:
Issue const& out,
AccountID const& strandSrc,
AccountID const& strandDst,
Step const* prevStep,
bool ownerPaysTransferFee,
beast::Journal j)
: book_ (in, out)
, strandSrc_ (strandSrc)
, strandDst_ (strandDst)
, prevStep_ (prevStep)
, ownerPaysTransferFee_ (ownerPaysTransferFee)
, j_ (j)
{
}
@@ -102,6 +108,12 @@ public:
return EitherAmount (cache_->out);
}
bool
redeems (ReadView const& sb, bool fwd) const override
{
return !ownerPaysTransferFee_;
}
boost::optional<Book>
bookStepBook () const override
{
@@ -147,7 +159,8 @@ private:
void consumeOffer (PaymentSandbox& sb,
TOffer<TIn, TOut>& offer,
TAmounts<TIn, TOut> const& ofrAmt,
TAmounts<TIn, TOut> const& stepAmt) const;
TAmounts<TIn, TOut> const& stepAmt,
TOut const& ownerGives) const;
std::string logString () const override
{
@@ -178,7 +191,9 @@ static
void limitStepIn (Quality const& ofrQ,
TAmounts<TIn, TOut>& ofrAmt,
TAmounts<TIn, TOut>& stpAmt,
TOut& ownerGives,
std::uint32_t transferRateIn,
std::uint32_t transferRateOut,
TIn const& limit)
{
if (limit < stpAmt.in)
@@ -188,6 +203,8 @@ void limitStepIn (Quality const& ofrQ,
stpAmt.in, QUALITY_ONE, transferRateIn, /*roundUp*/ false);
ofrAmt = ofrQ.ceil_in (ofrAmt, inLmt);
stpAmt.out = ofrAmt.out;
ownerGives = mulRatio (
ofrAmt.out, transferRateOut, QUALITY_ONE, /*roundUp*/ false);
}
}
@@ -197,13 +214,17 @@ static
void limitStepOut (Quality const& ofrQ,
TAmounts<TIn, TOut>& ofrAmt,
TAmounts<TIn, TOut>& stpAmt,
TOut& ownerGives,
std::uint32_t transferRateIn,
std::uint32_t transferRateOut,
TOut const& limit)
{
if (limit < stpAmt.out)
{
stpAmt.out = limit;
ofrAmt = ofrQ.ceil_out (ofrAmt, limit);
ownerGives = mulRatio (
stpAmt.out, transferRateOut, QUALITY_ONE, /*roundUp*/ false);
ofrAmt = ofrQ.ceil_out (ofrAmt, stpAmt.out);
stpAmt.in = mulRatio (
ofrAmt.in, transferRateIn, QUALITY_ONE, /*roundUp*/ true);
}
@@ -225,18 +246,28 @@ forEachOffer (
Book const& book,
AccountID const& src,
AccountID const& dst,
bool prevStepRedeems,
bool ownerPaysTransferFee,
Callback& callback,
std::uint32_t limit,
beast::Journal j)
{
// Charge the offer owner, not the sender
// Charge a fee even if the owner is the same as the issuer
// (the old code does not charge a fee)
// Calculate amount that goes to the taker and the amount charged the offer owner
auto transferRate = [&](AccountID const& id)->std::uint32_t
{
if (isXRP (id) || id == src || id == dst)
if (isXRP (id) || id == dst)
return QUALITY_ONE;
return rippleTransferRate (sb, id);
};
std::uint32_t const trIn = transferRate (book.in.account);
std::uint32_t const trIn =
prevStepRedeems ? transferRate (book.in.account) : QUALITY_ONE;
// Always charge the transfer fee, even if the owner is the issuer
std::uint32_t const trOut =
ownerPaysTransferFee ? transferRate (book.out.account) : QUALITY_ONE;
typename FlowOfferStream<TAmtIn, TAmtOut>::StepCounter counter (limit, j);
FlowOfferStream<TAmtIn, TAmtOut> offers (
@@ -251,16 +282,31 @@ forEachOffer (
else if (*ofrQ != offer.quality ())
break;
auto const funds = offers.ownerFunds ();
auto ofrAmt = offer.amount ();
auto stpAmt = make_Amounts (
mulRatio (ofrAmt.in, trIn, QUALITY_ONE, /*roundUp*/ true),
ofrAmt.out);
// owner pays the transfer fee
auto ownerGives =
mulRatio (ofrAmt.out, trOut, QUALITY_ONE, /*roundUp*/ false);
if (funds < stpAmt.out)
limitStepOut (*ofrQ, ofrAmt, stpAmt, trIn, funds);
auto const funds =
(offer.owner () == offer.issueOut ().account)
? ownerGives // Offer owner is issuer; they have unlimited funds
: offers.ownerFunds ();
if (!callback (offer, ofrAmt, stpAmt, trIn))
if (funds < ownerGives)
{
// We already know offer.owner()!=offer.issueOut().account
ownerGives = funds;
stpAmt.out = mulRatio (
ownerGives, QUALITY_ONE, trOut, /*roundUp*/ false);
ofrAmt = ofrQ->ceil_out (ofrAmt, stpAmt.out);
stpAmt.in = mulRatio (
ofrAmt.in, trIn, QUALITY_ONE, /*roundUp*/ true);
}
if (!callback (offer, ofrAmt, stpAmt, ownerGives, trIn, trOut))
break;
}
@@ -272,7 +318,8 @@ void BookStep<TIn, TOut>::consumeOffer (
PaymentSandbox& sb,
TOffer<TIn, TOut>& offer,
TAmounts<TIn, TOut> const& ofrAmt,
TAmounts<TIn, TOut> const& stepAmt) const
TAmounts<TIn, TOut> const& stepAmt,
TOut const& ownerGives) const
{
// The offer owner gets the ofrAmt. The difference between ofrAmt and stepAmt
// is a transfer fee that goes to book_.in.account
@@ -283,9 +330,11 @@ void BookStep<TIn, TOut>::consumeOffer (
Throw<FlowException> (dr);
}
// The offer owner pays `ownerGives`. The difference between ownerGives and
// stepAmt is a transfer fee that goes to book_.out.account
{
auto const cr = accountSend (sb, offer.owner (), book_.out.account,
toSTAmount (stepAmt.out, book_.out), j_);
toSTAmount (ownerGives, book_.out), j_);
if (cr != tesSUCCESS)
Throw<FlowException> (cr);
}
@@ -330,7 +379,9 @@ BookStep<TIn, TOut>::revImp (
[&](TOffer<TIn, TOut>& offer,
TAmounts<TIn, TOut> const& ofrAmt,
TAmounts<TIn, TOut> const& stpAmt,
std::uint32_t transferRateIn) mutable -> bool
TOut const& ownerGives,
std::uint32_t transferRateIn,
std::uint32_t transferRateOut) mutable -> bool
{
if (remainingOut <= beast::zero)
return false;
@@ -341,7 +392,7 @@ BookStep<TIn, TOut>::revImp (
savedOuts.insert(stpAmt.out);
result = TAmounts<TIn, TOut>(sum (savedIns), sum(savedOuts));
remainingOut = out - result.out;
this->consumeOffer (sb, offer, ofrAmt, stpAmt);
this->consumeOffer (sb, offer, ofrAmt, stpAmt, ownerGives);
// return true b/c even if the payment is satisfied,
// we need to consume the offer
return true;
@@ -350,22 +401,24 @@ BookStep<TIn, TOut>::revImp (
{
auto ofrAdjAmt = ofrAmt;
auto stpAdjAmt = stpAmt;
limitStepOut (
offer.quality (), ofrAdjAmt, stpAdjAmt, transferRateIn, remainingOut);
auto ownerGivesAdj = ownerGives;
limitStepOut (offer.quality (), ofrAdjAmt, stpAdjAmt, ownerGivesAdj,
transferRateIn, transferRateOut, remainingOut);
remainingOut = beast::zero;
savedIns.insert (stpAdjAmt.in);
savedOuts.insert (remainingOut);
result.in = sum(savedIns);
result.out = out;
this->consumeOffer (sb, offer, ofrAdjAmt, stpAdjAmt);
this->consumeOffer (sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
return false;
}
};
{
auto const r = forEachOffer<TIn, TOut> (
sb, afView, book_,
strandSrc_, strandDst_, eachOffer, maxOffersToConsume_, j_);
auto const prevStepRedeems = prevStep_ && prevStep_->redeems (sb, false);
auto const r = forEachOffer<TIn, TOut> (sb, afView, book_, strandSrc_,
strandDst_, prevStepRedeems, ownerPaysTransferFee_, eachOffer,
maxOffersToConsume_, j_);
boost::container::flat_set<uint256> toRm = std::move(std::get<0>(r));
std::uint32_t const offersConsumed = std::get<1>(r);
ofrsToRm.insert (boost::container::ordered_unique_range_t{},
@@ -427,42 +480,93 @@ BookStep<TIn, TOut>::fwdImp (
[&](TOffer<TIn, TOut>& offer,
TAmounts<TIn, TOut> const& ofrAmt,
TAmounts<TIn, TOut> const& stpAmt,
std::uint32_t transferRateIn) mutable -> bool
TOut const& ownerGives,
std::uint32_t transferRateIn,
std::uint32_t transferRateOut) mutable -> bool
{
assert(cache_);
if (remainingIn <= beast::zero)
return false;
bool processMore = true;
auto ofrAdjAmt = ofrAmt;
auto stpAdjAmt = stpAmt;
auto ownerGivesAdj = ownerGives;
typename boost::container::flat_multiset<TOut>::const_iterator lastOut;
if (stpAmt.in <= remainingIn)
{
savedIns.insert(stpAmt.in);
savedOuts.insert(stpAmt.out);
lastOut = savedOuts.insert(stpAmt.out);
result = TAmounts<TIn, TOut>(sum (savedIns), sum(savedOuts));
remainingIn = in - result.in;
this->consumeOffer (sb, offer, ofrAmt, stpAmt);
// return true b/c even if the payment is satisfied,
// we need to consume the offer
return true;
// consume the offer even if stepAmt.in == remainingIn
processMore = true;
}
else
{
auto ofrAdjAmt = ofrAmt;
auto stpAdjAmt = stpAmt;
limitStepIn (
offer.quality (), ofrAdjAmt, stpAdjAmt, transferRateIn, remainingIn);
limitStepIn (offer.quality (), ofrAdjAmt, stpAdjAmt, ownerGivesAdj,
transferRateIn, transferRateOut, remainingIn);
savedIns.insert (remainingIn);
savedOuts.insert (stpAdjAmt.out);
remainingIn = beast::zero;
lastOut = savedOuts.insert (stpAdjAmt.out);
result.out = sum (savedOuts);
result.in = in;
this->consumeOffer (sb, offer, ofrAdjAmt, stpAdjAmt);
return false;
processMore = false;
}
if (result.out > cache_->out && result.in <= cache_->in)
{
// The step produced more output in the forward pass than the
// reverse pass while consuming the same input (or less). If we
// compute the input required to produce the cached output
// (produced in the reverse step) and the input is equal to
// the input consumed in the forward step, then consume the
// input provided in the forward step and produce the output
// requested from the reverse step.
auto const lastOutAmt = *lastOut;
savedOuts.erase(lastOut);
auto const remainingOut = cache_->out - sum (savedOuts);
auto ofrAdjAmtRev = ofrAmt;
auto stpAdjAmtRev = stpAmt;
auto ownerGivesAdjRev = ownerGives;
limitStepOut (offer.quality (), ofrAdjAmtRev, stpAdjAmtRev,
ownerGivesAdjRev, transferRateIn, transferRateOut,
remainingOut);
if (stpAdjAmtRev.in == remainingIn)
{
result.in = in;
result.out = cache_->out;
savedIns.clear();
savedIns.insert(result.in);
savedOuts.clear();
savedOuts.insert(result.out);
ofrAdjAmt = ofrAdjAmtRev;
stpAdjAmt.in = remainingIn;
stpAdjAmt.out = remainingOut;
ownerGivesAdj = ownerGivesAdjRev;
}
else
{
// This is (likely) a problem case, and wil be caught
// with later checks
savedOuts.insert (lastOutAmt);
}
}
remainingIn = in - result.in;
this->consumeOffer (sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
return processMore;
};
{
auto const r = forEachOffer<TIn, TOut> (
sb, afView, book_,
strandSrc_, strandDst_, eachOffer, maxOffersToConsume_, j_);
auto const prevStepRedeems = prevStep_ && prevStep_->redeems (sb, true);
auto const r = forEachOffer<TIn, TOut> (sb, afView, book_, strandSrc_,
strandDst_, prevStepRedeems, ownerPaysTransferFee_, eachOffer,
maxOffersToConsume_, j_);
boost::container::flat_set<uint256> toRm = std::move(std::get<0>(r));
std::uint32_t const offersConsumed = std::get<1>(r);
ofrsToRm.insert (boost::container::ordered_unique_range_t{},
@@ -607,7 +711,8 @@ make_BookStepHelper (
Issue const& out)
{
auto r = std::make_unique<BookStep<TIn, TOut>> (
in, out, ctx.strandSrc, ctx.strandDst, ctx.j);
in, out, ctx.strandSrc, ctx.strandDst, ctx.prevStep,
ctx.ownerPaysTransferFee, ctx.j);
auto ter = r->check (ctx);
if (ter != tesSUCCESS)
return {ter, nullptr};

View File

@@ -39,9 +39,10 @@ class DirectStepI : public StepImp<IOUAmount, IOUAmount, DirectStepI>
AccountID src_;
AccountID dst_;
Currency currency_;
// Transfer fees are never charged when sending directly to or from an
// issuing account
bool noTransferFee_;
// Charge transfer fees when the prev step redeems
Step const* const prevStep_ = nullptr;
beast::Journal j_;
struct Cache
@@ -49,15 +50,17 @@ class DirectStepI : public StepImp<IOUAmount, IOUAmount, DirectStepI>
IOUAmount in;
IOUAmount srcToDst;
IOUAmount out;
bool srcRedeems;
Cache () = default;
Cache (
IOUAmount const& in_,
IOUAmount const& srcToDst_,
IOUAmount const& out_)
IOUAmount const& out_,
bool srcRedeems_)
: in(in_)
, srcToDst(srcToDst_)
, out(out_)
, srcRedeems(srcRedeems_)
{}
};
@@ -67,18 +70,19 @@ class DirectStepI : public StepImp<IOUAmount, IOUAmount, DirectStepI>
std::pair <std::uint32_t, std::uint32_t>
qualities (
PaymentSandbox& sb,
bool srcRedeems) const;
bool srcRedeems,
bool fwd) const;
public:
DirectStepI (
AccountID const& src,
AccountID const& dst,
Currency const& c,
bool noTransferFee,
Step const* prevStep,
beast::Journal j)
:src_(src)
, dst_(dst)
, currency_ (c)
, noTransferFee_ (noTransferFee)
, prevStep_ (prevStep)
, j_ (j) {}
AccountID const& src () const
@@ -115,6 +119,9 @@ class DirectStepI : public StepImp<IOUAmount, IOUAmount, DirectStepI>
return src_;
}
bool
redeems (ReadView const& sb, bool fwd) const override;
std::pair<IOUAmount, IOUAmount>
revImp (
PaymentSandbox& sb,
@@ -142,14 +149,14 @@ class DirectStepI : public StepImp<IOUAmount, IOUAmount, DirectStepI>
void setCacheLimiting (
IOUAmount const& fwdIn,
IOUAmount const& fwdSrcToDst,
IOUAmount const& fwdOut);
IOUAmount const& fwdOut,
bool srcRedeems);
friend bool operator==(DirectStepI const& lhs, DirectStepI const& rhs)
{
return lhs.src_ == rhs.src_ &&
lhs.dst_ == rhs.dst_ &&
lhs.currency_ == rhs.currency_ &&
lhs.noTransferFee_ == rhs.noTransferFee_;
lhs.currency_ == rhs.currency_;
}
friend bool operator!=(DirectStepI const& lhs, DirectStepI const& rhs)
@@ -199,6 +206,25 @@ maxFlow (
return {creditLimit2 (sb, dst, src, cur) + srcOwed, false};
}
bool
DirectStepI::redeems (ReadView const& sb, bool fwd) const
{
if (!fwd)
{
auto const srcOwed = creditBalance2 (sb, dst_, src_, currency_);
return srcOwed.signum () > 0;
}
else
{
if (!cache_)
{
assert (0);
return false;
}
return cache_->srcRedeems;
}
}
std::pair<IOUAmount, IOUAmount>
DirectStepI::revImp (
PaymentSandbox& sb,
@@ -214,7 +240,7 @@ DirectStepI::revImp (
maxFlow (sb, src_, dst_, currency_);
std::uint32_t srcQOut, dstQIn;
std::tie (srcQOut, dstQIn) = qualities (sb, srcRedeems);
std::tie (srcQOut, dstQIn) = qualities (sb, srcRedeems, false);
Issue const srcToDstIss (currency_, srcRedeems ? dst_ : src_);
@@ -232,7 +258,8 @@ DirectStepI::revImp (
cache_.emplace (
IOUAmount (beast::zero),
IOUAmount (beast::zero),
IOUAmount (beast::zero));
IOUAmount (beast::zero),
srcRedeems);
return {beast::zero, beast::zero};
}
@@ -244,7 +271,7 @@ DirectStepI::revImp (
IOUAmount const in = mulRatio (
srcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true);
cache_.emplace (in, srcToDst, out);
cache_.emplace (in, srcToDst, out, srcRedeems);
rippleCredit (sb,
src_, dst_, toSTAmount (srcToDst, srcToDstIss),
/*checkIssuer*/ true, j_);
@@ -262,7 +289,7 @@ DirectStepI::revImp (
maxSrcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true);
IOUAmount const actualOut = mulRatio (
maxSrcToDst, dstQIn, QUALITY_ONE, /*roundUp*/ false);
cache_.emplace (in, maxSrcToDst, actualOut);
cache_.emplace (in, maxSrcToDst, actualOut, srcRedeems);
rippleCredit (sb,
src_, dst_, toSTAmount (maxSrcToDst, srcToDstIss),
/*checkIssuer*/ true, j_);
@@ -276,14 +303,15 @@ DirectStepI::revImp (
}
// The forward pass should never have more liquidity than the reverse
// pass. But sometime rounding differences cause the forward pass to
// pass. But sometimes rounding differences cause the forward pass to
// deliver more liquidity. Use the cached values from the reverse pass
// to prevent this.
void
DirectStepI::setCacheLimiting (
IOUAmount const& fwdIn,
IOUAmount const& fwdSrcToDst,
IOUAmount const& fwdOut)
IOUAmount const& fwdOut,
bool srcRedeems)
{
if (cache_->in < fwdIn)
{
@@ -305,7 +333,7 @@ DirectStepI::setCacheLimiting (
<< " cacheSrcToDst: " << to_string (cache_->srcToDst)
<< " fwdOut: " << to_string (fwdOut)
<< " cacheOut: " << to_string (cache_->out);
cache_.emplace (fwdIn, fwdSrcToDst, fwdOut);
cache_.emplace (fwdIn, fwdSrcToDst, fwdOut, srcRedeems);
return;
}
}
@@ -315,6 +343,7 @@ DirectStepI::setCacheLimiting (
cache_->srcToDst = fwdSrcToDst;
if (fwdOut < cache_->out)
cache_->out = fwdOut;
cache_->srcRedeems = srcRedeems;
};
std::pair<IOUAmount, IOUAmount>
@@ -332,7 +361,7 @@ DirectStepI::fwdImp (
maxFlow (sb, src_, dst_, currency_);
std::uint32_t srcQOut, dstQIn;
std::tie (srcQOut, dstQIn) = qualities (sb, srcRedeems);
std::tie (srcQOut, dstQIn) = qualities (sb, srcRedeems, true);
Issue const srcToDstIss (currency_, srcRedeems ? dst_ : src_);
@@ -350,7 +379,8 @@ DirectStepI::fwdImp (
cache_.emplace (
IOUAmount (beast::zero),
IOUAmount (beast::zero),
IOUAmount (beast::zero));
IOUAmount (beast::zero),
srcRedeems);
return {beast::zero, beast::zero};
}
@@ -361,7 +391,7 @@ DirectStepI::fwdImp (
{
IOUAmount const out = mulRatio (
srcToDst, dstQIn, QUALITY_ONE, /*roundUp*/ false);
setCacheLimiting (in, srcToDst, out);
setCacheLimiting (in, srcToDst, out, srcRedeems);
rippleCredit (sb,
src_, dst_, toSTAmount (cache_->srcToDst, srcToDstIss),
/*checkIssuer*/ true, j_);
@@ -379,7 +409,7 @@ DirectStepI::fwdImp (
maxSrcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true);
IOUAmount const out = mulRatio (
maxSrcToDst, dstQIn, QUALITY_ONE, /*roundUp*/ false);
setCacheLimiting (actualIn, maxSrcToDst, out);
setCacheLimiting (actualIn, maxSrcToDst, out, srcRedeems);
rippleCredit (sb,
src_, dst_, toSTAmount (cache_->srcToDst, srcToDstIss),
/*checkIssuer*/ true, j_);
@@ -499,7 +529,8 @@ quality (
std::pair<std::uint32_t, std::uint32_t>
DirectStepI::qualities (
PaymentSandbox& sb,
bool srcRedeems) const
bool srcRedeems,
bool fwd) const
{
if (srcRedeems)
{
@@ -511,9 +542,10 @@ DirectStepI::qualities (
}
else
{
// Charge a transfer rate when issuing, unless this is the first step.
// Charge a transfer rate when issuing and previous step redeems
auto const prevStepRedeems = prevStep_ && prevStep_->redeems (sb, fwd);
std::uint32_t const srcQOut =
noTransferFee_ ? QUALITY_ONE : rippleTransferRate (sb, src_);
prevStepRedeems ? rippleTransferRate (sb, src_) : QUALITY_ONE;
return std::make_pair(
srcQOut,
quality ( // dst quality in
@@ -655,8 +687,9 @@ make_DirectStepI (
AccountID const& dst,
Currency const& c)
{
// Only charge a transfer fee if the previous step redeems
auto r = std::make_unique<DirectStepI> (
src, dst, c, /* noTransferFee */ ctx.isFirst, ctx.j);
src, dst, c, ctx.prevStep, ctx.j);
auto ter = r->check (ctx);
if (ter != tesSUCCESS)
return {ter, nullptr};

View File

@@ -139,6 +139,7 @@ toStrand (
Issue const& deliver,
boost::optional<Issue> const& sendMaxIssue,
STPath const& path,
bool ownerPaysTransferFee,
beast::Journal j)
{
if (isXRP (src))
@@ -233,7 +234,7 @@ toStrand (
auto ctx = [&](bool isLast = false)
{
return StrandContext{view, result, strandSrc, strandDst, isLast,
seenDirectIssues, seenBookOuts, j};
ownerPaysTransferFee, seenDirectIssues, seenBookOuts, j};
};
for (int i = 0; i < pes.size () - 1; ++i)
@@ -359,7 +360,7 @@ toStrand (
result.emplace_back (std::move (s.second));
else
{
JLOG (j.warn()) << "toStep failed";
JLOG (j.debug()) << "toStep failed: " << s.first;
return {s.first, Strand{}};
}
}
@@ -376,6 +377,7 @@ toStrands (
boost::optional<Issue> const& sendMax,
STPathSet const& paths,
bool addDefaultPath,
bool ownerPaysTransferFee,
beast::Journal j)
{
std::vector<Strand> result;
@@ -391,7 +393,8 @@ toStrands (
if (addDefaultPath)
{
auto sp = toStrand (view, src, dst, deliver, sendMax, STPath(), j);
auto sp = toStrand (
view, src, dst, deliver, sendMax, STPath (), ownerPaysTransferFee, j);
auto const ter = sp.first;
auto& strand = sp.second;
@@ -407,7 +410,10 @@ toStrands (
JLOG (j.trace()) << "toStrand failed";
Throw<FlowException> (tefEXCEPTION, "toStrand returned tes & empty strand");
}
insert(std::move(strand));
else
{
insert(std::move(strand));
}
}
else if (paths.empty ())
{
@@ -419,7 +425,8 @@ toStrands (
TER lastFailTer = tesSUCCESS;
for (auto const& p : paths)
{
auto sp = toStrand (view, src, dst, deliver, sendMax, p, j);
auto sp = toStrand (
view, src, dst, deliver, sendMax, p, ownerPaysTransferFee, j);
auto ter = sp.first;
auto& strand = sp.second;
@@ -436,9 +443,10 @@ toStrands (
JLOG (j.trace()) << "toStrand failed";
Throw<FlowException> (tefEXCEPTION, "toStrand returned tes & empty strand");
}
if (ter == tesSUCCESS)
else
{
insert(std::move(strand));
}
}
if (result.empty ())
@@ -455,6 +463,7 @@ StrandContext::StrandContext (
AccountID strandSrc_,
AccountID strandDst_,
bool isLast_,
bool ownerPaysTransferFee_,
std::array<boost::container::flat_set<Issue>, 2>& seenDirectIssues_,
boost::container::flat_set<Issue>& seenBookOuts_,
beast::Journal j_)
@@ -463,6 +472,7 @@ StrandContext::StrandContext (
, strandDst (strandDst_)
, isFirst (strand_.empty ())
, isLast (isLast_)
, ownerPaysTransferFee (ownerPaysTransferFee_)
, strandSize (strand_.size ())
, prevStep (!strand_.empty () ? strand_.back ().get ()
: nullptr)

View File

@@ -120,6 +120,18 @@ public:
return boost::none;
}
/**
If this step is a DirectStepI and the src redeems to the dst, return true,
otherwise return false.
If this step is a BookStep, return false if the owner pays the transfer fee,
otherwise return true.
*/
virtual bool
redeems (ReadView const& sb, bool fwd) const
{
return false;
}
/**
If this step is a BookStep, return the book.
*/
@@ -226,6 +238,7 @@ toStrand (
Issue const& deliver,
boost::optional<Issue> const& sendMaxIssue,
STPath const& path,
bool ownerPaysTransferFee,
beast::Journal j);
/**
@@ -253,6 +266,7 @@ toStrands (ReadView const& sb,
boost::optional<Issue> const& sendMax,
STPathSet const& paths,
bool addDefaultPath,
bool ownerPaysTransferFee,
beast::Journal j);
template <class TIn, class TOut, class TDerived>
@@ -338,6 +352,7 @@ struct StrandContext
AccountID const strandDst;
bool const isFirst;
bool const isLast = false;
bool ownerPaysTransferFee;
size_t const strandSize;
// The previous step in the strand. Needed to check the no ripple constraint
Step const* const prevStep = nullptr;
@@ -357,6 +372,7 @@ struct StrandContext
AccountID strandSrc_,
AccountID strandDst_,
bool isLast_,
bool ownerPaysTransferFee_,
std::array<boost::container::flat_set<Issue>, 2>& seenDirectIssues_,
boost::container::flat_set<Issue>& seenBookOuts_,
beast::Journal j);

View File

@@ -138,7 +138,7 @@ flow (
limitStepOut = r.second;
if (strand[i]->dry (r.second) ||
get<TInAmt> (r.first) != get<TInAmt> (*maxIn))
get<TInAmt> (r.first) != *maxIn)
{
// Something is very wrong
// throwing out the sandbox can only increase liquidity

View File

@@ -182,7 +182,7 @@ struct Flow_test : public beast::unit_test::suite
TER expTer, auto&&... expSteps)
{
auto r = toStrand (*env.current (), alice, bob,
deliver, sendMaxIssue, path, env.app ().logs ().journal ("Flow"));
deliver, sendMaxIssue, path, true, env.app ().logs ().journal ("Flow"));
expect (r.first == expTer);
if (sizeof...(expSteps))
expect (equal (
@@ -190,7 +190,7 @@ struct Flow_test : public beast::unit_test::suite
};
{
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, carol, gw);
test (env, USD, boost::none, STPath(), terNO_LINE);
@@ -236,20 +236,20 @@ struct Flow_test : public beast::unit_test::suite
{
// The root account can't be the dst
auto r = toStrand (*env.current (), alice,
xrpAccount (), XRP, USD.issue (), STPath (), flowJournal);
xrpAccount (), XRP, USD.issue (), STPath (), true, flowJournal);
expect (r.first == temBAD_PATH);
}
{
// The root account can't be the src
auto r =
toStrand (*env.current (), xrpAccount (),
alice, XRP, boost::none, STPath (), flowJournal);
alice, XRP, boost::none, STPath (), true, flowJournal);
expect (r.first == temBAD_PATH);
}
{
// The root account can't be the src
auto r = toStrand (*env.current (),
noAccount (), bob, USD, boost::none, STPath (), flowJournal);
noAccount (), bob, USD, boost::none, STPath (), true, flowJournal);
expect (r.first == terNO_ACCOUNT);
}
}
@@ -279,7 +279,7 @@ struct Flow_test : public beast::unit_test::suite
// cannot have more than one offer with the same output issue
using namespace jtx;
Env env (*this, features (featureFlowV2));
Env env (*this, features (featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, carol, gw);
env.trust (USD (10000), alice, bob, carol);
@@ -299,7 +299,7 @@ struct Flow_test : public beast::unit_test::suite
}
{
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, noripple (gw));
env.trust (USD (1000), alice, bob);
env (pay (gw, alice, USD (100)));
@@ -308,7 +308,7 @@ struct Flow_test : public beast::unit_test::suite
{
// check global freeze
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, gw);
env.trust (USD (1000), alice, bob);
env (pay (gw, alice, USD (100)));
@@ -333,7 +333,7 @@ struct Flow_test : public beast::unit_test::suite
}
{
// Freeze between gw and alice
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, gw);
env.trust (USD (1000), alice, bob);
env (pay (gw, alice, USD (100)));
@@ -346,7 +346,7 @@ struct Flow_test : public beast::unit_test::suite
// check no auth
// An account may require authorization to receive IOUs from an
// issuer
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, gw);
env (fset (gw, asfRequireAuth));
env.trust (USD (1000), alice, bob);
@@ -359,7 +359,7 @@ struct Flow_test : public beast::unit_test::suite
// Check pure issue redeem still works
auto r = toStrand (*env.current (), alice, gw, USD,
boost::none, STPath (), env.app ().logs ().journal ("Flow"));
boost::none, STPath (), true, env.app ().logs ().journal ("Flow"));
expect (r.first == tesSUCCESS);
expect (equal (r.second, D{alice, gw, usdC}));
}
@@ -382,7 +382,7 @@ struct Flow_test : public beast::unit_test::suite
auto const USD = gw["USD"];
{
// Pay USD, trivial path
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, gw);
env.trust (USD (1000), alice, bob);
@@ -392,7 +392,7 @@ struct Flow_test : public beast::unit_test::suite
}
{
// XRP transfer
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob);
env (pay (alice, bob, XRP (100)));
@@ -401,7 +401,7 @@ struct Flow_test : public beast::unit_test::suite
}
{
// Partial payments
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, gw);
env.trust (USD (1000), alice, bob);
@@ -415,7 +415,7 @@ struct Flow_test : public beast::unit_test::suite
}
{
// Pay by rippling through accounts, use path finder
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, carol, dan);
env.trust (USDA (10), bob);
@@ -430,32 +430,52 @@ struct Flow_test : public beast::unit_test::suite
{
// Pay by rippling through accounts, specify path
// and charge a transfer fee
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, carol, dan);
env.trust (USDA (10), bob);
env.trust (USDB (10), carol);
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(featureFlowV2), 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));
sendmax (USDA (6)), txflags (tfNoRippleDirect));
env.require (balance (dan, USDC (5)));
env.require (balance (bob, USDA (5.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(featureFlowV2));
Env env (*this, features(featureFlowV2), 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), 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));
@@ -465,7 +485,7 @@ struct Flow_test : public beast::unit_test::suite
}
{
// Limit quality
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, carol);
env.trust (USDA (10), bob);
@@ -497,7 +517,7 @@ struct Flow_test : public beast::unit_test::suite
{
// simple IOU/IOU offer
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, carol, gw);
env.trust (USD (1000), alice, bob, carol);
@@ -518,7 +538,7 @@ struct Flow_test : public beast::unit_test::suite
}
{
// simple IOU/XRP XRP/IOU offer
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, carol, gw);
env.trust (USD (1000), alice, bob, carol);
@@ -542,7 +562,7 @@ struct Flow_test : public beast::unit_test::suite
}
{
// simple XRP -> USD through offer and sendmax
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, carol, gw);
env.trust (USD (1000), alice, bob, carol);
@@ -563,7 +583,7 @@ struct Flow_test : public beast::unit_test::suite
}
{
// simple USD -> XRP through offer and sendmax
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, carol, gw);
env.trust (USD (1000), alice, bob, carol);
@@ -584,7 +604,7 @@ struct Flow_test : public beast::unit_test::suite
}
{
// test unfunded offers are removed when payment succeeds
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, carol, gw);
env.trust (USD (1000), alice, bob, carol);
@@ -630,7 +650,7 @@ struct Flow_test : public beast::unit_test::suite
// 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(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, carol, gw);
env.trust (USD (1000), alice, bob, carol);
@@ -666,7 +686,7 @@ struct Flow_test : public beast::unit_test::suite
paths.push_back (p2);
}
return flow (sb, deliver, alice, carol, paths, false, false,
return flow (sb, deliver, alice, carol, paths, false, false, true,
boost::none, smax, flowJournal);
}();
@@ -689,6 +709,38 @@ struct Flow_test : public beast::unit_test::suite
// found unfunded
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 (featureFlowV2),
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 ()
@@ -709,7 +761,7 @@ struct Flow_test : public beast::unit_test::suite
{
// Simple payment through a gateway with a
// transfer rate
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, carol, gw);
env(rate(gw, 1.25));
@@ -721,7 +773,7 @@ struct Flow_test : public beast::unit_test::suite
}
{
// transfer rate is not charged when issuer is src or dst
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, carol, gw);
env(rate(gw, 1.25));
@@ -733,25 +785,25 @@ struct Flow_test : public beast::unit_test::suite
}
{
// transfer fee on an offer
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), 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 (50)));
env (pay (gw, bob, USD (65)));
env (offer (bob, XRP (50), USD (50)));
env (pay (alice, carol, USD (40)), path (~USD), sendmax (XRP (50)));
env (pay (alice, carol, USD (50)), path (~USD), sendmax (XRP (50)));
env.require (
balance (alice, xrpMinusFee (env, 10000 - 50)),
balance (bob, USD (0)),
balance (carol, USD (40)));
balance (bob, USD (2.5)), // owner pays transfer fee
balance (carol, USD (50)));
}
{
// Transfer fee two consecutive offers
Env env (*this, features(featureFlowV2));
Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee));
env.fund (XRP (10000), alice, bob, carol, gw);
env(rate(gw, 1.25));
@@ -763,12 +815,89 @@ struct Flow_test : public beast::unit_test::suite
env (offer (bob, XRP (50), USD (50)));
env (offer (bob, USD (50), EUR (50)));
env (pay (alice, carol, EUR (32)), path (~USD, ~EUR), sendmax (XRP (50)));
env (pay (alice, carol, EUR (40)), path (~USD, ~EUR), sendmax (XRP (40)));
env.require (
balance (alice, xrpMinusFee (env, 10000 - 50)),
balance (alice, xrpMinusFee (env, 10000 - 40)),
balance (bob, USD (40)),
balance (bob, EUR (50 - 40)),
balance (carol, EUR (32)));
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(featureFlowV2), 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(featureFlowV2), 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(featureFlowV2), 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(featureFlowV2));
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)));
}
}

View File

@@ -34,8 +34,8 @@ namespace ripple {
NetClock::time_point const& flowV2SoTime ()
{
using namespace std::chrono_literals;
// Mon March 28, 2016 10:00:00am PST
static NetClock::time_point const soTime{512503200s};
// Wed May 25, 2016 10:00:00am PDT
static NetClock::time_point const soTime{517510800s};
return soTime;
}

View File

@@ -40,6 +40,7 @@ extern uint256 const featureSusPay;
extern uint256 const featureTrustSetAuth;
extern uint256 const featureFeeEscalation;
extern uint256 const featureFlowV2;
extern uint256 const featureOwnerPaysFee;
} // ripple

View File

@@ -51,5 +51,6 @@ uint256 const featureSusPay = feature("SusPay");
uint256 const featureTrustSetAuth = feature("TrustSetAuth");
uint256 const featureFeeEscalation = feature("FeeEscalation");
uint256 const featureFlowV2 = feature("FlowV2");
uint256 const featureOwnerPaysFee = feature("OwnerPaysFee");
} // ripple