mirror of
				https://github.com/Xahau/xahaud.git
				synced 2025-11-04 10:45:50 +00:00 
			
		
		
		
	Compare commits
	
		
			14 Commits
		
	
	
		
			849d447a20
			...
			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