initial version of feature service fee, uncompiled untested

This commit is contained in:
Richard Holland
2025-01-26 21:25:43 +11:00
parent d17f7151ab
commit 51764e1b4e
7 changed files with 170 additions and 23 deletions

View File

@@ -2040,32 +2040,167 @@ Transactor::operator()()
result = tecOVERSIZE;
}
if (applied)
{
// Transaction succeeded fully or (retries are not allowed and the
// transaction could claim a fee)
if (view().rules().enabled(featureServiceFee) && applied &&
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.
// 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!");
STObject const& obj = const_cast<ripple::STTx&>(ctx_.tx)
.getField(sfServiceFee)
.downcast<STObject>();
// Charge whatever fee they specified. The fee has already been
// deducted from the balance of the account that issued the
// transaction. We just need to account for it in the ledger
// header.
if (!view().open() && fee != beast::zero)
ctx_.destroyXRP(fee);
// 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(j_.warn())
<< "service fee not applied - malformed inner object.";
break;
}
// Once we call apply, we will no longer be able to look at view()
ctx_.apply(result);
}
auto const src = ctx_.tx.getAccountID(sfAccount);
auto const dst = obj.getAccountID(sfDestination);
JLOG(j_.trace()) << (applied ? "applied" : "not applied")
<< transToken(result);
auto const amt = obj.getFieldAmount(sfAmount);
return {result, applied};
}
// sanity check fields
if (src == dst)
{
JLOG(j_.trace())
<< "skipping self service-fee on " << src << ".";
break;
}
if (amt <= beast::zero)
{
JLOG(j_.trace())
<< "skipping non-positive service-fee from " << src << ".";
break;
}
// 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_.trace()) << "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_.trace())
<< "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;
if (after < view().fees().accountReserve(
sleSrc->getFieldU32(sfOwnerCount)))
{
JLOG(j_.trace())
<< "service fee not applied because source " << src
<< " cannot pay it (native).";
break;
}
// action the transfer
if (TER const ter{
view().transferXRP(view(), src, dst, amt, j_))
!isTesSuccess(ter))
{
JLOG(j_.warn())
<< "service fee error transferring " << amt << " from "
<< src << " to " << dst << " error: " << ter << ".";
}
break;
}
// execution to here means issued currency service fee
// 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& sleLine = view().peek(keylet::line(dst, amt.getIssuer(), amt.getCurrency()));
if (!sleLine && amt.getIssuer() != dst)
{
JLOG(j_.trace())
<< "service fee not applied because destination " << dst
<< " has no trustline for currency: "
<< amt.getCurrency()
<< " issued by: " << amt.getIssuer() << ".";
break;
}
// action the transfer
{
PaymentSandbox pv(&view());
auto res = accountSend(pv, src, dst, amt, j_);
if (res == tesSUCCESS)
{
pv.apply(ctx_.rawView());
break;
}
JLOG(j_.trace())
<< "service fee not sent from " << src << " to " << dst
<< " for " << amt.getCurrency() " issued by "
<< amt.getIssuer() " because "
<< "accountSend() failed with code " << res << ".";
}
}
while (0)
;
if (applied)
{
// 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.
if (fee < beast::zero)
Throw<std::logic_error>("fee charged is negative!");
// Charge whatever fee they specified. The fee has already been
// deducted from the balance of the account that issued the
// transaction. We just need to account for it in the ledger
// header.
if (!view().open() && fee != beast::zero)
ctx_.destroyXRP(fee);
// Once we call apply, we will no longer be able to look at
// view()
ctx_.apply(result);
}
JLOG(j_.trace())
<< (applied ? "applied" : "not applied") << transToken(result);
return {result, applied};
}
} // namespace ripple

View File

@@ -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

View File

@@ -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;

View File

@@ -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.

View File

@@ -157,6 +157,13 @@ InnerObjectFormats::InnerObjectFormats()
{sfDigest, soeOPTIONAL},
{sfFlags, soeOPTIONAL},
});
add(sfServiceFee.jsonName.c_str(),
sfServiceFee.getCode(),
{
{sfAmount, soeREQUIRED},
{sfDestination, soeREQUIRED},
});
}
InnerObjectFormats const&

View File

@@ -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

View File

@@ -44,6 +44,7 @@ TxFormats::TxFormats()
{sfNetworkID, soeOPTIONAL},
{sfHookParameters, soeOPTIONAL},
{sfOperationLimit, soeOPTIONAL},
{sfServiceFee, soeOPTIONAL},
};
add(jss::AccountSet,