mirror of
https://github.com/Xahau/xahaud.git
synced 2025-11-04 18:55:49 +00:00
Compare commits
14 Commits
fix-github
...
service_fe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b58c3a644 | ||
|
|
0c4f5680e6 | ||
|
|
f0222d9fa6 | ||
|
|
baffb7c2ae | ||
|
|
891a9c0b81 | ||
|
|
efa5800384 | ||
|
|
96b78b1c8a | ||
|
|
f19e03842b | ||
|
|
e57160913e | ||
|
|
515c4c14bd | ||
|
|
2b8efc398d | ||
|
|
01bd0cc4ac | ||
|
|
6fe06d857c | ||
|
|
51764e1b4e |
@@ -749,6 +749,7 @@ if (tests)
|
||||
src/test/app/Regression_test.cpp
|
||||
src/test/app/Remit_test.cpp
|
||||
src/test/app/SHAMapStore_test.cpp
|
||||
src/test/app/ServiceFee_test.cpp
|
||||
src/test/app/SetAuth_test.cpp
|
||||
src/test/app/SetRegularKey_test.cpp
|
||||
src/test/app/SetTrust_test.cpp
|
||||
|
||||
@@ -88,6 +88,69 @@ preflight0(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
checkServiceFee(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.tx.isFieldPresent(sfServiceFee))
|
||||
return tesSUCCESS;
|
||||
|
||||
if (!ctx.rules.enabled(featureServiceFee))
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
<< "ServiceFee: Service fee feature is not enabled.";
|
||||
return temDISABLED;
|
||||
}
|
||||
|
||||
STObject const& obj = const_cast<ripple::STTx&>(ctx.tx)
|
||||
.getField(sfServiceFee)
|
||||
.downcast<STObject>();
|
||||
|
||||
// This should be enforced by template but doesn't hurt to
|
||||
// defensively check it here.
|
||||
if (!obj.isFieldPresent(sfDestination) || !obj.isFieldPresent(sfAmount) ||
|
||||
obj.getCount() != 2)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "ServiceFee: Malformed: Destination and Amount "
|
||||
"fields are required.";
|
||||
return temINVALID;
|
||||
}
|
||||
|
||||
if (ctx.tx.getAccountID(sfAccount) == obj.getAccountID(sfDestination))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "ServiceFee: Malformed: Destination may not be "
|
||||
"the same as the source account.";
|
||||
return temDST_IS_SRC;
|
||||
}
|
||||
|
||||
auto const amount = obj.getFieldAmount(sfAmount);
|
||||
|
||||
if (!isXRP(amount))
|
||||
{
|
||||
if (!isLegalNet(amount))
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
<< "ServiceFee: Malformed: Amount must be a valid net amount.";
|
||||
return temBAD_AMOUNT;
|
||||
}
|
||||
|
||||
if (isBadCurrency(amount))
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
<< "ServiceFee: Malformed: Currency is not allowed.";
|
||||
return temBAD_CURRENCY;
|
||||
}
|
||||
}
|
||||
|
||||
if (amount <= beast::zero)
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
<< "ServiceFee: Malformed: Amount must be a positive value.";
|
||||
return temBAD_AMOUNT;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Performs early sanity checks on the account and fee fields */
|
||||
NotTEC
|
||||
preflight1(PreflightContext const& ctx)
|
||||
@@ -165,6 +228,9 @@ preflight1(PreflightContext const& ctx)
|
||||
ctx.tx.isFieldPresent(sfAccountTxnID))
|
||||
return temINVALID;
|
||||
|
||||
if (auto const ret = checkServiceFee(ctx); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -1894,6 +1960,178 @@ Transactor::operator()()
|
||||
applied = isTecClaim(result);
|
||||
}
|
||||
|
||||
if (applied && view().rules().enabled(featureServiceFee) &&
|
||||
ctx_.tx.isFieldPresent(sfServiceFee))
|
||||
do
|
||||
{
|
||||
// Service fee is processed on a best-effort basis without affecting
|
||||
// tx application. The reason is that the client completely controls
|
||||
// the service fee that it submits with the user's txn, and
|
||||
// therefore is already completely aware of the user's capacity to
|
||||
// pay the fee and therefore enforcement logic is unnecessary
|
||||
// chain-side.
|
||||
|
||||
STObject const& obj = const_cast<ripple::STTx&>(ctx_.tx)
|
||||
.getField(sfServiceFee)
|
||||
.downcast<STObject>();
|
||||
auto const src = ctx_.tx.getAccountID(sfAccount);
|
||||
auto const dst = obj.getAccountID(sfDestination);
|
||||
auto const amt = obj.getFieldAmount(sfAmount);
|
||||
|
||||
// check if the source exists
|
||||
auto const& sleSrc = view().read(keylet::account(src));
|
||||
if (!sleSrc)
|
||||
{
|
||||
// this can happen if the account was just deleted
|
||||
JLOG(j_.debug()) << "service fee not applied because source "
|
||||
<< src << " does not exist.";
|
||||
break;
|
||||
}
|
||||
|
||||
// check if the destination exists
|
||||
// service fee cannot be used to create accounts.
|
||||
if (!view().exists(keylet::account(dst)))
|
||||
{
|
||||
JLOG(j_.debug())
|
||||
<< "service fee not applied because destination " << dst
|
||||
<< " does not exist.";
|
||||
break;
|
||||
}
|
||||
|
||||
if (isXRP(amt))
|
||||
{
|
||||
// check if there's enough left in the sender's account
|
||||
auto srcBal = sleSrc->getFieldAmount(sfBalance);
|
||||
|
||||
// service fee will only be delivered if the account
|
||||
// contains adequate balance to cover reserves, otherwise
|
||||
// it is disregarded
|
||||
auto after = srcBal - amt - fee;
|
||||
if (after < view().fees().accountReserve(
|
||||
sleSrc->getFieldU32(sfOwnerCount)))
|
||||
{
|
||||
JLOG(j_.debug())
|
||||
<< "service fee not applied because source " << src
|
||||
<< " cannot pay it (native).";
|
||||
break;
|
||||
}
|
||||
|
||||
PaymentSandbox pv(&view());
|
||||
auto res = accountSend(pv, src, dst, amt, j_, false);
|
||||
if (isTesSuccess(res))
|
||||
{
|
||||
pv.apply(ctx_.rawView());
|
||||
break;
|
||||
}
|
||||
|
||||
JLOG(j_.warn()) << "service fee (native) not applied because "
|
||||
"accountSend failed.";
|
||||
break;
|
||||
}
|
||||
|
||||
// issued currency
|
||||
|
||||
// service fee cannot be used to create trustlines,
|
||||
// so a line must already exist and the currency must
|
||||
// be able to be xfer'd to it
|
||||
|
||||
auto const issuer = amt.getIssuer();
|
||||
if (issuer != src && !view().exists(keylet::line(src, amt.issue())))
|
||||
{
|
||||
JLOG(j_.debug())
|
||||
<< "service fee not applied because source " << src
|
||||
<< " has no trustline for currency: " << amt.getCurrency()
|
||||
<< " issued by: " << toBase58(issuer) << ".";
|
||||
break;
|
||||
}
|
||||
|
||||
if (issuer != dst && !view().exists(keylet::line(dst, amt.issue())))
|
||||
{
|
||||
JLOG(j_.debug())
|
||||
<< "service fee not applied because destination " << dst
|
||||
<< " has no trustline for currency: " << amt.getCurrency()
|
||||
<< " issued by: " << toBase58(issuer) << ".";
|
||||
break;
|
||||
}
|
||||
|
||||
if (dst != issuer && src != issuer)
|
||||
{
|
||||
TER const res =
|
||||
trustTransferAllowed(view(), {src, dst}, amt.issue(), j_);
|
||||
if (!isTesSuccess(res))
|
||||
{
|
||||
JLOG(j_.debug())
|
||||
<< "service fee not applied because destination " << dst
|
||||
<< " trust transfer not allowed for currency: "
|
||||
<< amt.getCurrency()
|
||||
<< " issued by: " << toBase58(issuer) << ".";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (src != issuer)
|
||||
{
|
||||
Keylet const srcLine =
|
||||
keylet::line(src, issuer, amt.getCurrency());
|
||||
auto const sleSrcLine = view().read(srcLine);
|
||||
STAmount srcBalance = src < amt.issue().account
|
||||
? (*sleSrcLine)[sfBalance]
|
||||
: -(*sleSrcLine)[sfBalance];
|
||||
// TODO: xferRate
|
||||
if (srcBalance < amt)
|
||||
{
|
||||
JLOG(j_.debug())
|
||||
<< "service fee not applied because source " << src
|
||||
<< " has insufficient funds for currency: "
|
||||
<< amt.getCurrency()
|
||||
<< " issued by: " << toBase58(issuer) << ".";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (dst != issuer)
|
||||
{
|
||||
Keylet const dstLine =
|
||||
keylet::line(dst, issuer, amt.getCurrency());
|
||||
auto const sleDstLine = view().read(dstLine);
|
||||
STAmount dstLimit = dst < amt.issue().account
|
||||
? (*sleDstLine)[sfLowLimit]
|
||||
: (*sleDstLine)[sfHighLimit];
|
||||
if (accountFunds(view(), dst, amt, fhZERO_IF_FROZEN, j_) + amt >
|
||||
dstLimit)
|
||||
{
|
||||
JLOG(j_.debug())
|
||||
<< "service fee not applied because destination " << dst
|
||||
<< " has insufficient trustline limit for "
|
||||
"currency: "
|
||||
<< amt.getCurrency()
|
||||
<< " issued by: " << toBase58(issuer) << ".";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// action the transfer
|
||||
{
|
||||
PaymentSandbox pv(&view());
|
||||
|
||||
STAmount saActual;
|
||||
auto res = accountSend(pv, src, dst, amt, j_, false);
|
||||
if (isTesSuccess(res))
|
||||
{
|
||||
pv.apply(ctx_.rawView());
|
||||
break;
|
||||
}
|
||||
|
||||
JLOG(j_.warn())
|
||||
<< "service fee not sent from " << src << " to " << dst
|
||||
<< " for " << amt.getCurrency() << " issued by "
|
||||
<< toBase58(issuer) << " because "
|
||||
<< "accountSend() failed with code " << res << ".";
|
||||
break;
|
||||
}
|
||||
|
||||
} while (0);
|
||||
|
||||
if (applied)
|
||||
{
|
||||
// Check invariants: if `tecINVARIANT_FAILED` is not returned, we can
|
||||
@@ -2042,12 +2280,12 @@ Transactor::operator()()
|
||||
|
||||
if (applied)
|
||||
{
|
||||
// Transaction succeeded fully or (retries are not allowed and the
|
||||
// transaction could claim a fee)
|
||||
// Transaction succeeded fully or (retries are not allowed and
|
||||
// the transaction could claim a fee)
|
||||
|
||||
// The transactor and invariant checkers guarantee that this will
|
||||
// *never* trigger but if it, somehow, happens, don't allow a tx
|
||||
// that charges a negative fee.
|
||||
// The transactor and invariant checkers guarantee that this
|
||||
// will *never* trigger but if it, somehow, happens, don't allow
|
||||
// a tx that charges a negative fee.
|
||||
if (fee < beast::zero)
|
||||
Throw<std::logic_error>("fee charged is negative!");
|
||||
|
||||
@@ -2058,7 +2296,8 @@ Transactor::operator()()
|
||||
if (!view().open() && fee != beast::zero)
|
||||
ctx_.destroyXRP(fee);
|
||||
|
||||
// Once we call apply, we will no longer be able to look at view()
|
||||
// Once we call apply, we will no longer be able to look at
|
||||
// view()
|
||||
ctx_.apply(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -422,6 +422,13 @@ transferXRP(
|
||||
STAmount const& amount,
|
||||
beast::Journal j);
|
||||
|
||||
/** Check if the account lacks required authorization.
|
||||
* Return tecNO_AUTH or tecNO_LINE if it does
|
||||
* and tesSUCCESS otherwise.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
requireAuth(ReadView const& view, Issue const& issue, AccountID const& account);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
//
|
||||
|
||||
@@ -1583,4 +1583,24 @@ transferXRP(
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
requireAuth(ReadView const& view, Issue const& issue, AccountID const& account)
|
||||
{
|
||||
if (isXRP(issue) || issue.account == account)
|
||||
return tesSUCCESS;
|
||||
if (auto const issuerAccount = view.read(keylet::account(issue.account));
|
||||
issuerAccount && (*issuerAccount)[sfFlags] & lsfRequireAuth)
|
||||
{
|
||||
if (auto const trustLine =
|
||||
view.read(keylet::line(account, issue.account, issue.currency)))
|
||||
return ((*trustLine)[sfFlags] &
|
||||
((account > issue.account) ? lsfLowAuth : lsfHighAuth))
|
||||
? tesSUCCESS
|
||||
: TER{tecNO_AUTH};
|
||||
return TER{tecNO_LINE};
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace detail {
|
||||
// Feature.cpp. Because it's only used to reserve storage, and determine how
|
||||
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
|
||||
// the actual number of amendments. A LogicError on startup will verify this.
|
||||
static constexpr std::size_t numFeatures = 76;
|
||||
static constexpr std::size_t numFeatures = 77;
|
||||
|
||||
/** Amendments that this server supports and the default voting behavior.
|
||||
Whether they are enabled depends on the Rules defined in the validated
|
||||
@@ -364,6 +364,7 @@ extern uint256 const fix240911;
|
||||
extern uint256 const fixFloatDivide;
|
||||
extern uint256 const fixReduceImport;
|
||||
extern uint256 const fixXahauV3;
|
||||
extern uint256 const featureServiceFee;
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
|
||||
@@ -608,6 +608,7 @@ extern SField const sfMemos;
|
||||
extern SField const sfNFTokens;
|
||||
extern SField const sfHooks;
|
||||
extern SField const sfGenesisMint;
|
||||
extern SField const sfServiceFee;
|
||||
|
||||
// array of objects (uncommon)
|
||||
extern SField const sfMajorities;
|
||||
|
||||
@@ -470,6 +470,7 @@ REGISTER_FIX (fix240911, Supported::yes, VoteBehavior::De
|
||||
REGISTER_FIX (fixFloatDivide, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FIX (fixReduceImport, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FIX (fixXahauV3, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FEATURE(ServiceFee, Supported::yes, VoteBehavior::DefaultNo);
|
||||
|
||||
// The following amendments are obsolete, but must remain supported
|
||||
// because they could potentially get enabled.
|
||||
|
||||
@@ -157,6 +157,13 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{sfDigest, soeOPTIONAL},
|
||||
{sfFlags, soeOPTIONAL},
|
||||
});
|
||||
|
||||
add(sfServiceFee.jsonName.c_str(),
|
||||
sfServiceFee.getCode(),
|
||||
{
|
||||
{sfAmount, soeREQUIRED},
|
||||
{sfDestination, soeREQUIRED},
|
||||
});
|
||||
}
|
||||
|
||||
InnerObjectFormats const&
|
||||
|
||||
@@ -350,6 +350,7 @@ CONSTRUCT_UNTYPED_SFIELD(sfImportVLKey, "ImportVLKey", OBJECT,
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfHookEmission, "HookEmission", OBJECT, 93);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfMintURIToken, "MintURIToken", OBJECT, 92);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfAmountEntry, "AmountEntry", OBJECT, 91);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfServiceFee, "ServiceFee", OBJECT, 90);
|
||||
|
||||
// array of objects
|
||||
// ARRAY/1 is reserved for end of array
|
||||
|
||||
@@ -44,6 +44,7 @@ TxFormats::TxFormats()
|
||||
{sfNetworkID, soeOPTIONAL},
|
||||
{sfHookParameters, soeOPTIONAL},
|
||||
{sfOperationLimit, soeOPTIONAL},
|
||||
{sfServiceFee, soeOPTIONAL},
|
||||
};
|
||||
|
||||
add(jss::AccountSet,
|
||||
|
||||
@@ -126,6 +126,7 @@ JSS(UNLReport); // transaction type.
|
||||
JSS(SettleDelay); // in: TransactionSign
|
||||
JSS(SendMax); // in: TransactionSign
|
||||
JSS(Sequence); // in/out: TransactionSign; field.
|
||||
JSS(ServiceFee); // field.
|
||||
JSS(SetFlag); // field.
|
||||
JSS(SetRegularKey); // transaction type.
|
||||
JSS(SetHook); // transaction type.
|
||||
|
||||
617
src/test/app/ServiceFee_test.cpp
Normal file
617
src/test/app/ServiceFee_test.cpp
Normal file
@@ -0,0 +1,617 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL-Labs
|
||||
|
||||
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/basics/chrono.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <test/jtx.h>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
struct ServiceFee_test : public beast::unit_test::suite
|
||||
{
|
||||
static STAmount
|
||||
lineBalance(
|
||||
jtx::Env const& env,
|
||||
jtx::Account const& account,
|
||||
jtx::Account const& gw,
|
||||
jtx::IOU const& iou)
|
||||
{
|
||||
auto const sle = env.le(keylet::line(account, gw, iou.currency));
|
||||
if (sle && sle->isFieldPresent(sfBalance))
|
||||
return (*sle)[sfBalance];
|
||||
return STAmount(iou, 0);
|
||||
}
|
||||
|
||||
void
|
||||
testEnabled(FeatureBitset features)
|
||||
{
|
||||
testcase("enabled");
|
||||
using namespace jtx;
|
||||
using namespace std::literals::chrono_literals;
|
||||
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
auto const carol = Account("carol");
|
||||
for (bool const withSFee : {true, false})
|
||||
{
|
||||
auto const amend =
|
||||
withSFee ? features : features - featureServiceFee;
|
||||
Env env{*this, amend};
|
||||
auto const feeDrops = env.current()->fees().base;
|
||||
env.fund(XRP(1000), alice, bob, carol);
|
||||
|
||||
auto const preAlice = env.balance(alice);
|
||||
auto const preBob = env.balance(bob);
|
||||
auto const preCarol = env.balance(carol);
|
||||
|
||||
auto const result = withSFee ? ter(tesSUCCESS) : ter(temDISABLED);
|
||||
env(pay(alice, bob, XRP(10)),
|
||||
fee(feeDrops),
|
||||
sfee(XRP(1), carol),
|
||||
result);
|
||||
env.close();
|
||||
|
||||
auto const postAlice =
|
||||
withSFee ? preAlice - feeDrops - XRP(10) - XRP(1) : preAlice;
|
||||
auto const postBob = withSFee ? preBob + XRP(10) : preBob;
|
||||
auto const postCarol = withSFee ? preCarol + XRP(1) : preCarol;
|
||||
BEAST_EXPECT(env.balance(alice) == postAlice);
|
||||
BEAST_EXPECT(env.balance(bob) == postBob);
|
||||
BEAST_EXPECT(env.balance(carol) == postCarol);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testInvalid(FeatureBitset features)
|
||||
{
|
||||
testcase("invalid");
|
||||
using namespace jtx;
|
||||
using namespace std::literals::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"];
|
||||
|
||||
// malformed inner object. No Amount // TEMPLATE ERROR
|
||||
// malformed inner object. No Destination // TEMPLATE ERROR
|
||||
// skipping self service-fee
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const amt = XRP(10);
|
||||
auto const sfeeAmt = XRP(1);
|
||||
env(pay(alice, bob, amt), sfee(sfeeAmt, alice), ter(temDST_IS_SRC));
|
||||
env.close();
|
||||
}
|
||||
// skipping non-positive service-fee
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(1000), alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
auto const amt = XRP(10);
|
||||
auto const sfeeAmt = XRP(-1);
|
||||
env(pay(alice, bob, amt), sfee(sfeeAmt, carol), ter(temBAD_AMOUNT));
|
||||
env.close();
|
||||
}
|
||||
// source does not exist.
|
||||
{
|
||||
// TODO
|
||||
} // destination does not exist.
|
||||
{
|
||||
Env env{*this, features};
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const preAlice = env.balance(alice);
|
||||
auto const amt = XRP(10);
|
||||
auto const sfeeAmt = XRP(1);
|
||||
env(pay(alice, bob, amt), sfee(sfeeAmt, carol));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(alice) == preAlice - amt - baseFee);
|
||||
}
|
||||
// insufficient reserve
|
||||
{
|
||||
// TODO
|
||||
} // no trustline (source)
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(1000), alice, bob, carol, gw);
|
||||
env.close();
|
||||
env.trust(USD(100000), carol);
|
||||
env.close();
|
||||
env(pay(gw, carol, USD(10000)));
|
||||
env.close();
|
||||
|
||||
auto const preAlice = env.balance(alice, USD);
|
||||
auto const preCarol = env.balance(carol, USD);
|
||||
BEAST_EXPECT(preAlice == USD(0));
|
||||
|
||||
auto const sfeeAmt = USD(1);
|
||||
env(pay(alice, bob, XRP(100)), sfee(sfeeAmt, carol));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(alice, USD) == preAlice);
|
||||
BEAST_EXPECT(env.balance(carol, USD) == preCarol);
|
||||
}
|
||||
// insufficient trustline balance (source)
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(1000), alice, bob, carol, gw);
|
||||
env.close();
|
||||
env.trust(USD(10), alice, carol);
|
||||
env.close();
|
||||
env(pay(gw, alice, USD(1)));
|
||||
env(pay(gw, carol, USD(1)));
|
||||
env.close();
|
||||
|
||||
auto const preAlice = env.balance(alice, USD);
|
||||
auto const preCarol = env.balance(carol, USD);
|
||||
auto const sfeeAmt = USD(10);
|
||||
env(pay(alice, bob, XRP(100)), sfee(sfeeAmt, carol));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(alice, USD) == preAlice);
|
||||
BEAST_EXPECT(env.balance(carol, USD) == preCarol);
|
||||
}
|
||||
// no trustline (destination)
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(1000), alice, bob, carol, gw);
|
||||
env.close();
|
||||
env.trust(USD(100000), alice);
|
||||
env.close();
|
||||
env(pay(gw, alice, USD(10000)));
|
||||
env.close();
|
||||
|
||||
auto const preAlice = env.balance(alice, USD);
|
||||
auto const preCarol = env.balance(carol, USD);
|
||||
auto const sfeeAmt = USD(1);
|
||||
env(pay(alice, bob, XRP(100)), sfee(sfeeAmt, carol));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(alice, USD) == preAlice);
|
||||
BEAST_EXPECT(env.balance(carol, USD) == preCarol);
|
||||
}
|
||||
// insufficient trustline limit (destination)
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(1000), alice, bob, carol, gw);
|
||||
env.close();
|
||||
env.trust(USD(10), alice);
|
||||
env.trust(USD(1), carol);
|
||||
env.close();
|
||||
env(pay(gw, alice, USD(10)));
|
||||
env.close();
|
||||
|
||||
auto const preAlice = env.balance(alice, USD);
|
||||
auto const preCarol = env.balance(carol, USD);
|
||||
auto const sfeeAmt = USD(10);
|
||||
env(pay(alice, bob, XRP(100)), sfee(sfeeAmt, carol));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(alice, USD) == preAlice);
|
||||
BEAST_EXPECT(env.balance(carol, USD) == preCarol);
|
||||
}
|
||||
// accountSend() failed // INTERNAL ERROR
|
||||
}
|
||||
|
||||
void
|
||||
testRippleState(FeatureBitset features)
|
||||
{
|
||||
testcase("ripple_state");
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals;
|
||||
|
||||
struct TestAccountData
|
||||
{
|
||||
Account src;
|
||||
Account dst;
|
||||
Account gw;
|
||||
bool hasTrustline;
|
||||
bool negative;
|
||||
};
|
||||
|
||||
std::array<TestAccountData, 8> tests = {{
|
||||
// src > dst && src > issuer && dst no trustline
|
||||
{Account("alice2"), Account("bob0"), Account{"gw0"}, false, true},
|
||||
// src < dst && src < issuer && dst no trustline
|
||||
{Account("carol0"), Account("dan1"), Account{"gw1"}, false, false},
|
||||
// dst > src && dst > issuer && dst no trustline
|
||||
{Account("dan1"), Account("alice2"), Account{"gw0"}, false, true},
|
||||
// dst < src && dst < issuer && dst no trustline
|
||||
{Account("bob0"), Account("carol0"), Account{"gw1"}, false, false},
|
||||
// src > dst && src > issuer && dst has trustline
|
||||
{Account("alice2"), Account("bob0"), Account{"gw0"}, true, true},
|
||||
// src < dst && src < issuer && dst has trustline
|
||||
{Account("carol0"), Account("dan1"), Account{"gw1"}, true, false},
|
||||
// dst > src && dst > issuer && dst has trustline
|
||||
{Account("dan1"), Account("alice2"), Account{"gw0"}, true, true},
|
||||
// dst < src && dst < issuer && dst has trustline
|
||||
{Account("bob0"), Account("carol0"), Account{"gw1"}, true, false},
|
||||
}};
|
||||
|
||||
for (auto const& t : tests)
|
||||
{
|
||||
Env env{*this, features};
|
||||
auto const carol = Account("carol");
|
||||
auto const USD = t.gw["USD"];
|
||||
env.fund(XRP(5000), t.src, t.dst, t.gw, carol);
|
||||
env.close();
|
||||
if (t.hasTrustline)
|
||||
env.trust(USD(100000), t.src, t.dst, carol);
|
||||
else
|
||||
env.trust(USD(100000), t.src, t.dst);
|
||||
env.close();
|
||||
|
||||
env(pay(t.gw, t.src, USD(10000)));
|
||||
env(pay(t.gw, t.dst, USD(10000)));
|
||||
if (t.hasTrustline)
|
||||
env(pay(t.gw, carol, USD(10000)));
|
||||
env.close();
|
||||
|
||||
auto const delta = USD(100);
|
||||
auto const sfeeAmt = USD(1);
|
||||
auto const preSrc = lineBalance(env, t.src, t.gw, USD);
|
||||
auto const preDst = lineBalance(env, t.dst, t.gw, USD);
|
||||
auto const preCarol = lineBalance(env, carol, t.gw, USD);
|
||||
env(pay(t.src, t.dst, delta), sfee(sfeeAmt, carol));
|
||||
env.close();
|
||||
|
||||
auto const appliedSFee = t.hasTrustline ? sfeeAmt : USD(0);
|
||||
auto const postSrc = t.negative ? (preSrc + delta + appliedSFee)
|
||||
: (preSrc - delta - appliedSFee);
|
||||
auto const postDst =
|
||||
t.negative ? (preDst - delta) : (preDst + delta);
|
||||
auto const postCarol = t.negative ? (preCarol - appliedSFee)
|
||||
: (preCarol + appliedSFee);
|
||||
BEAST_EXPECT(lineBalance(env, t.src, t.gw, USD) == postSrc);
|
||||
BEAST_EXPECT(lineBalance(env, t.dst, t.gw, USD) == postDst);
|
||||
BEAST_EXPECT(lineBalance(env, carol, t.gw, USD) == postCarol);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testGateway(FeatureBitset features)
|
||||
{
|
||||
testcase("gateway");
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals;
|
||||
|
||||
struct TestAccountData
|
||||
{
|
||||
Account acct;
|
||||
Account gw;
|
||||
bool hasTrustline;
|
||||
bool negative;
|
||||
};
|
||||
|
||||
std::array<TestAccountData, 4> tests = {{
|
||||
// acct no trustline
|
||||
// acct > issuer
|
||||
{Account("alice2"), Account{"gw0"}, false, true},
|
||||
// acct < issuer
|
||||
{Account("carol0"), Account{"gw1"}, false, false},
|
||||
|
||||
// acct has trustline
|
||||
// acct > issuer
|
||||
{Account("alice2"), Account{"gw0"}, true, true},
|
||||
// acct < issuer
|
||||
{Account("carol0"), Account{"gw1"}, true, false},
|
||||
}};
|
||||
|
||||
// test gateway is source
|
||||
for (auto const& t : tests)
|
||||
{
|
||||
Env env{*this, features};
|
||||
auto const carol = Account("carol");
|
||||
auto const USD = t.gw["USD"];
|
||||
env.fund(XRP(5000), t.acct, t.gw, carol);
|
||||
env.trust(USD(100000), carol);
|
||||
env(pay(t.gw, carol, USD(10000)));
|
||||
env.close();
|
||||
|
||||
if (t.hasTrustline)
|
||||
{
|
||||
env.trust(USD(100000), t.acct);
|
||||
env(pay(t.gw, t.acct, USD(10000)));
|
||||
env.close();
|
||||
}
|
||||
|
||||
auto const preAcct = lineBalance(env, t.acct, t.gw, USD);
|
||||
auto const preCarol = lineBalance(env, carol, t.gw, USD);
|
||||
|
||||
auto const delta = USD(100);
|
||||
auto const sfeeAmt = USD(1);
|
||||
env(pay(t.gw, carol, delta), sfee(sfeeAmt, t.acct));
|
||||
env.close();
|
||||
|
||||
auto const appliedSFee = t.hasTrustline ? sfeeAmt : USD(0);
|
||||
// Receiver of service fee
|
||||
auto const postAcct =
|
||||
t.negative ? (preAcct - appliedSFee) : (preAcct + appliedSFee);
|
||||
// Receiver of payment
|
||||
auto const postCarol =
|
||||
t.negative ? (preCarol - delta) : (preCarol + delta);
|
||||
BEAST_EXPECT(lineBalance(env, t.acct, t.gw, USD) == postAcct);
|
||||
BEAST_EXPECT(lineBalance(env, carol, t.gw, USD) == postCarol);
|
||||
BEAST_EXPECT(lineBalance(env, t.gw, t.acct, USD) == postAcct);
|
||||
BEAST_EXPECT(lineBalance(env, t.gw, carol, USD) == postCarol);
|
||||
}
|
||||
|
||||
// test gateway is destination
|
||||
for (auto const& t : tests)
|
||||
{
|
||||
Env env{*this, features};
|
||||
auto const USD = t.gw["USD"];
|
||||
env.fund(XRP(5000), t.acct, t.gw);
|
||||
env.trust(USD(100000), t.acct);
|
||||
env(pay(t.gw, t.acct, USD(10000)));
|
||||
env.close();
|
||||
|
||||
auto const preAcct = lineBalance(env, t.acct, t.gw, USD);
|
||||
auto const delta = USD(100);
|
||||
auto const sfeeAmt = USD(1);
|
||||
env(pay(t.acct, t.gw, delta), sfee(sfeeAmt, t.gw));
|
||||
env.close();
|
||||
|
||||
// Sender of Payment & Fee
|
||||
auto const postAcct = t.negative ? (preAcct + delta + sfeeAmt)
|
||||
: (preAcct - delta - sfeeAmt);
|
||||
BEAST_EXPECT(lineBalance(env, t.acct, t.gw, USD) == postAcct);
|
||||
BEAST_EXPECT(lineBalance(env, t.gw, t.acct, USD) == postAcct);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testRequireAuth(FeatureBitset features)
|
||||
{
|
||||
testcase("require_auth");
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals;
|
||||
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
auto const carol = Account("carol");
|
||||
auto const gw = Account{"gateway"};
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
auto const aliceUSD = alice["USD"];
|
||||
auto const bobUSD = bob["USD"];
|
||||
auto const carolUSD = carol["USD"];
|
||||
|
||||
// test asfRequireAuth
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(1000), alice, bob, carol, gw);
|
||||
env(fset(gw, asfRequireAuth));
|
||||
env.close();
|
||||
env(trust(gw, carolUSD(10000)), txflags(tfSetfAuth));
|
||||
env(trust(carol, USD(10000)));
|
||||
env(trust(gw, bobUSD(10000)), txflags(tfSetfAuth));
|
||||
env(trust(bob, USD(10000)));
|
||||
env.close();
|
||||
env(pay(gw, carol, USD(1000)));
|
||||
env(pay(gw, bob, USD(1000)));
|
||||
env.close();
|
||||
|
||||
{
|
||||
// Alice does not receive service fee because she is not
|
||||
// authorized
|
||||
auto const preAlice = env.balance(alice, USD);
|
||||
auto const sfeeAmt = USD(1);
|
||||
env(pay(bob, carol, XRP(100)), sfee(sfeeAmt, alice));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(alice, USD) == preAlice);
|
||||
}
|
||||
|
||||
{
|
||||
env(trust(gw, aliceUSD(10000)), txflags(tfSetfAuth));
|
||||
env(trust(alice, USD(10000)));
|
||||
env.close();
|
||||
env(pay(gw, alice, USD(1000)));
|
||||
env.close();
|
||||
|
||||
// Alice now receives service fee because she is authorized
|
||||
auto const preAlice = env.balance(alice, USD);
|
||||
auto const sfeeAmt = USD(1);
|
||||
env(pay(bob, carol, XRP(100)), sfee(sfeeAmt, alice));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(alice, USD) == preAlice + sfeeAmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFreeze(FeatureBitset features)
|
||||
{
|
||||
testcase("freeze");
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals;
|
||||
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
auto const carol = Account("carol");
|
||||
auto const gw = Account{"gateway"};
|
||||
auto const USD = gw["USD"];
|
||||
// test Global Freeze
|
||||
{
|
||||
// setup env
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(1000), alice, bob, carol, gw);
|
||||
env.close();
|
||||
env.trust(USD(100000), alice, bob, carol);
|
||||
env.close();
|
||||
env(pay(gw, alice, USD(1000)));
|
||||
env(pay(gw, bob, USD(1000)));
|
||||
env(pay(gw, carol, USD(1000)));
|
||||
env.close();
|
||||
env(fset(gw, asfGlobalFreeze));
|
||||
env.close();
|
||||
|
||||
{
|
||||
// carol cannot receive because of global freeze
|
||||
auto const preCarol = env.balance(carol, USD);
|
||||
auto const sfeeAmt = USD(1);
|
||||
env(pay(alice, bob, XRP(100)), sfee(sfeeAmt, carol));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(carol, USD) == preCarol);
|
||||
}
|
||||
|
||||
{
|
||||
// clear global freeze
|
||||
env(fclear(gw, asfGlobalFreeze));
|
||||
env.close();
|
||||
|
||||
// carol can receive because global freeze is cleared
|
||||
auto const preCarol = env.balance(carol, USD);
|
||||
auto const sfeeAmt = USD(1);
|
||||
env(pay(alice, bob, XRP(100)), sfee(sfeeAmt, carol));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(carol, USD) == preCarol + sfeeAmt);
|
||||
}
|
||||
}
|
||||
|
||||
// test Individual Freeze
|
||||
{
|
||||
// Env Setup
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(1000), alice, bob, carol, gw);
|
||||
env.close();
|
||||
env.trust(USD(100000), alice, bob, carol);
|
||||
env.close();
|
||||
env(pay(gw, alice, USD(1000)));
|
||||
env(pay(gw, bob, USD(1000)));
|
||||
env(pay(gw, carol, USD(1000)));
|
||||
env.close();
|
||||
|
||||
// set freeze on carol trustline
|
||||
env(trust(gw, USD(10000), carol, tfSetFreeze));
|
||||
env.close();
|
||||
|
||||
{
|
||||
auto const preCarol = env.balance(carol, USD);
|
||||
auto const sfeeAmt = USD(1);
|
||||
env(pay(alice, bob, XRP(100)), sfee(sfeeAmt, carol));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(carol, USD) == preCarol);
|
||||
}
|
||||
|
||||
{
|
||||
// clear freeze on carol trustline
|
||||
env(trust(gw, USD(10000), carol, tfClearFreeze));
|
||||
env.close();
|
||||
|
||||
auto const preCarol = env.balance(carol, USD);
|
||||
auto const sfeeAmt = USD(1);
|
||||
env(pay(alice, bob, XRP(100)), sfee(sfeeAmt, carol));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(carol, USD) == preCarol + sfeeAmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testTransferRate(FeatureBitset features)
|
||||
{
|
||||
testcase("transfer_rate");
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals;
|
||||
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
auto const carol = Account("carol");
|
||||
auto const gw = Account{"gateway"};
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
// test rate
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(10000), alice, bob, carol, gw);
|
||||
env(rate(gw, 1.25));
|
||||
env.close();
|
||||
env.trust(USD(100000), alice, carol);
|
||||
env.close();
|
||||
env(pay(gw, alice, USD(10000)));
|
||||
env(pay(gw, carol, USD(10000)));
|
||||
env.close();
|
||||
|
||||
auto const preAlice = env.balance(alice, USD);
|
||||
auto const preCarol = env.balance(carol, USD);
|
||||
auto const sfeeAmt = USD(1);
|
||||
env(pay(alice, bob, XRP(100)), sfee(sfeeAmt, carol));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(alice, USD) == preAlice - sfeeAmt);
|
||||
BEAST_EXPECT(
|
||||
env.balance(carol, USD) == preCarol + sfeeAmt - USD(0.20));
|
||||
}
|
||||
|
||||
// test issuer doesnt pay own rate
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(10000), alice, carol, gw);
|
||||
env(rate(gw, 1.25));
|
||||
env.close();
|
||||
env.trust(USD(100000), carol);
|
||||
env.close();
|
||||
env(pay(gw, carol, USD(10000)));
|
||||
env.close();
|
||||
|
||||
auto const preGwC = -lineBalance(env, carol, gw, USD);
|
||||
auto const preCarol = env.balance(carol, USD);
|
||||
auto const sfeeAmt = USD(1);
|
||||
env(pay(gw, alice, XRP(100)), sfee(sfeeAmt, carol));
|
||||
env.close();
|
||||
BEAST_EXPECT(-lineBalance(env, carol, gw, USD) == preGwC + sfeeAmt);
|
||||
BEAST_EXPECT(env.balance(carol, USD) == preCarol + sfeeAmt);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithFeats(FeatureBitset features)
|
||||
{
|
||||
testEnabled(features);
|
||||
testInvalid(features);
|
||||
testRippleState(features);
|
||||
testGateway(features);
|
||||
testRequireAuth(features);
|
||||
testFreeze(features);
|
||||
testTransferRate(features);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
auto const sa = supported_amendments();
|
||||
testWithFeats(sa);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ServiceFee, app, ripple);
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -61,6 +61,23 @@ public:
|
||||
operator()(Env&, JTx& jt) const;
|
||||
};
|
||||
|
||||
/** Set the service fee on a JTx. */
|
||||
class sfee
|
||||
{
|
||||
private:
|
||||
STAmount amount_;
|
||||
Account dest_;
|
||||
|
||||
public:
|
||||
explicit sfee(STAmount const& amount, Account const& destination)
|
||||
: amount_(amount), dest_(destination)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
operator()(Env&, JTx& jtx) const;
|
||||
};
|
||||
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
|
||||
@@ -34,6 +34,14 @@ fee::operator()(Env&, JTx& jt) const
|
||||
jt[jss::Fee] = amount_->getJson(JsonOptions::none);
|
||||
}
|
||||
|
||||
void
|
||||
sfee::operator()(Env&, JTx& jt) const
|
||||
{
|
||||
jt.jv[jss::ServiceFee] = Json::objectValue;
|
||||
jt.jv[jss::ServiceFee][jss::Amount] = amount_.getJson(JsonOptions::none);
|
||||
jt.jv[jss::ServiceFee][jss::Destination] = dest_.human();
|
||||
}
|
||||
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
|
||||
Reference in New Issue
Block a user