Merge remote-tracking branch 'origin/develop' into tapanito/invariant-improvement

This commit is contained in:
Vito
2026-07-23 14:55:37 +02:00
1329 changed files with 78097 additions and 28516 deletions

View File

@@ -1,6 +1,5 @@
#include <xrpl/tx/Transactor.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
@@ -18,6 +17,7 @@
#include <xrpl/ledger/helpers/NFTokenHelpers.h>
#include <xrpl/ledger/helpers/OfferHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -43,6 +43,7 @@
#include <xrpl/tx/applySteps.h>
#include <xrpl/tx/invariants/CheckInvariants.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <functional>
@@ -56,7 +57,9 @@
namespace xrpl {
/** Performs early sanity checks on the txid */
/**
* Performs early sanity checks on the txid
*/
NotTEC
preflight0(PreflightContext const& ctx, std::uint32_t flagMask)
{
@@ -111,7 +114,8 @@ preflight0(PreflightContext const& ctx, std::uint32_t flagMask)
namespace detail {
/** Checks the validity of the transactor signing key.
/**
* Checks the validity of the transactor signing key.
*
* Normally called from preflight1.
*/
@@ -169,7 +173,61 @@ preflightCheckSimulateKeys(ApplyFlags flags, STObject const& sigObject, beast::J
} // namespace detail
/** Performs early sanity checks on the account and fee fields */
static NotTEC
preflight1Sponsor(PreflightContext const& ctx)
{
bool const hasSponsor = ctx.tx.isFieldPresent(sfSponsor);
bool const hasSponsorFlags = ctx.tx.isFieldPresent(sfSponsorFlags);
bool const hasSponsorSig = ctx.tx.isFieldPresent(sfSponsorSignature);
if ((hasSponsor || hasSponsorFlags || hasSponsorSig) && !ctx.rules.enabled(featureSponsor))
return temDISABLED;
if (hasSponsor != hasSponsorFlags)
{
JLOG(ctx.j.debug()) << "preflight1: sponsor and sponsor flags mismatch";
return temINVALID_FLAG;
}
if (hasSponsorSig && (!hasSponsor || !hasSponsorFlags))
{
JLOG(ctx.j.debug()) << "preflight1: sponsor signature without sponsor definition";
return temMALFORMED;
}
if (hasSponsorFlags)
{
auto const sponsorFlags = ctx.tx.getFieldU32(sfSponsorFlags);
if (((sponsorFlags & spfSponsorFlagMask) != 0u) || sponsorFlags == 0)
{
JLOG(ctx.j.debug()) << "preflight1: invalid sponsor flags";
return temINVALID_FLAG;
}
// Reserve sponsorship is only permitted for an explicit allow-list of
// transaction types, for v1. All other tx types reject spfSponsorReserve here.
if (isReserveSponsored(ctx.tx))
{
if (!isReserveSponsorAllowed(ctx.tx.getTxnType()))
{
JLOG(ctx.j.debug())
<< "preflight1: spfSponsorReserve not allowed for this transaction type";
return temINVALID_FLAG;
}
}
}
if (hasSponsor && ctx.tx.getAccountID(sfSponsor) == ctx.tx.getAccountID(sfAccount))
{
JLOG(ctx.j.debug()) << "preflight1: Sponsor account cannot be the same as the account";
return temMALFORMED;
}
return tesSUCCESS;
}
/**
* Performs early sanity checks on the account and fee fields
*/
NotTEC
Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask)
{
@@ -222,18 +280,25 @@ Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask)
if (ctx.tx.getSeqProxy().isTicket() && ctx.tx.isFieldPresent(sfAccountTxnID))
return temINVALID;
if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatch))
if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatchV1_1))
return temINVALID_FLAG;
XRPL_ASSERT(
ctx.tx.isFlag(tfInnerBatchTxn) == ctx.parentBatchId.has_value() ||
!ctx.rules.enabled(featureBatch),
"Inner batch transaction must have a parent batch ID.");
// Reject if the inner batch flag and parentBatchId are inconsistent.
// A standalone tx with tfInnerBatchTxn but no parentBatchId is an
// attack attempt. A tx with parentBatchId but without tfInnerBatchTxn
// is a programming error.
if (ctx.tx.isFlag(tfInnerBatchTxn) != ctx.parentBatchId.has_value())
return temINVALID_INNER_BATCH;
if (auto const ter = preflight1Sponsor(ctx); !isTesSuccess(ter))
return ter;
return tesSUCCESS;
}
/** Checks whether the signature appears valid */
/**
* Checks whether the signature appears valid
*/
NotTEC
Transactor::preflight2(PreflightContext const& ctx)
{
@@ -244,15 +309,19 @@ Transactor::preflight2(PreflightContext const& ctx)
return *ret;
}
// It should be impossible for the InnerBatchTxn flag to be set without
// featureBatch being enabled
XRPL_ASSERT_PARTS(
!ctx.tx.isFlag(tfInnerBatchTxn) || ctx.rules.enabled(featureBatch),
"xrpl::Transactor::preflight2",
"InnerBatch flag only set if feature enabled");
// Skip signature check on batch inner transactions
if (ctx.tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatch))
// Skip the signature check on batch inner transactions. preflight1 already
// enforces both conditions; re-checking them as defense in depth guarantees
// we never return success (and so skip signature validation) for an inner
// transaction unless the amendment is enabled and it really sits inside a
// batch.
if (ctx.tx.isFlag(tfInnerBatchTxn))
{
if (!ctx.rules.enabled(featureBatchV1_1))
return temINVALID_FLAG;
if (!ctx.parentBatchId.has_value())
return temINVALID_INNER_BATCH;
return tesSUCCESS;
}
// Do not add any checks after this point that are relevant for
// batch inner transactions. They will be skipped.
@@ -339,6 +408,44 @@ Transactor::checkPermission(
return tesSUCCESS;
}
NotTEC
Transactor::checkSponsor(ReadView const& view, STTx const& tx)
{
if (!tx.isFieldPresent(sfSponsor))
return tesSUCCESS;
// Reserve sponsorship with permissioned delegation is disallowed.
if (tx.isFieldPresent(sfDelegate) && isReserveSponsored(tx))
return temINVALID;
if (!view.exists(keylet::account(tx.getAccountID(sfSponsor))))
return terNO_ACCOUNT;
// Skip Sponsorship existence checks if the sponsor has signed the transaction - this
// transaction is valid regardless of the Sponsorship object.
// The use of the Sponsorship object is properly handled in
// getFeePayer/checkReserve/increaseOwnerCount/decreaseOwnerCount.
if (tx.isFieldPresent(sfSponsorSignature))
return tesSUCCESS;
// If the transaction contains sfDelegate, the Sponsorship object should be
// between the sponsor and the delegate.
auto const sponsorshipSle =
view.read(keylet::sponsorship(tx.getAccountID(sfSponsor), tx.getInitiator()));
// sponsorship object missing for pre-funded (no co-signing) tx
if (!sponsorshipSle)
return terNO_PERMISSION;
if (isFeeSponsored(tx) && sponsorshipSle->isFlag(lsfSponsorshipRequireSignForFee))
return terNO_PERMISSION;
if (isReserveSponsored(tx) && sponsorshipSle->isFlag(lsfSponsorshipRequireSignForReserve))
return terNO_PERMISSION;
return tesSUCCESS;
}
XRPAmount
Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
{
@@ -347,6 +454,7 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
// The computation has two parts:
// * The base fee, which is the same for most transactions.
// * The additional cost of each multisignature on the transaction.
// * The additional cost of each multisignature on the sponsor.
XRPAmount const baseFee = view.fees().base;
// Each signer adds one more baseFee to the minimum required fee
@@ -354,7 +462,24 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
std::size_t const signerCount =
tx.isFieldPresent(sfSigners) ? tx.getFieldArray(sfSigners).size() : 0;
return baseFee + (signerCount * baseFee);
std::size_t sponsorSignerCount = 0;
if (tx.isFieldPresent(sfSponsorSignature))
{
auto const sponsorObj = tx.getFieldObject(sfSponsorSignature);
if (sponsorObj.isFieldPresent(sfSigners))
sponsorSignerCount += sponsorObj.getFieldArray(sfSigners).size();
}
return baseFee + ((signerCount + sponsorSignerCount) * baseFee);
}
XRPAmount
Transactor::calculateBaseFee(
ReadView const& view,
STTx const& tx,
std::uint32_t extraBaseFeeMultiplier)
{
return calculateBaseFee(view, tx) + view.fees().base * extraBaseFeeMultiplier;
}
// Returns the fee in fee units, not scaled for load.
@@ -424,12 +549,51 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
if (feePaid == beast::kZero)
return tesSUCCESS;
auto const id = ctx.tx.getFeePayer();
auto const sle = ctx.view.read(keylet::account(id));
if (!sle)
return terNO_ACCOUNT;
auto const feePayer = getFeePayer(ctx.view, ctx.tx);
auto const payerSle = ctx.view.read(feePayer.keylet);
auto const balance = (*sle)[sfBalance].xrp();
if (!payerSle)
{
if (feePayer.type == FeePayerType::SponsorPreFunded)
{
// Sanity check: already checked in checkSponsor
return tefINTERNAL; // LCOV_EXCL_LINE
}
return terNO_ACCOUNT;
}
XRPAmount maxSpendable = beast::kZero;
if (feePayer.type == FeePayerType::SponsorPreFunded)
{
if (payerSle->getType() != ltSPONSORSHIP)
return tefINTERNAL; // LCOV_EXCL_LINE
if (payerSle->isFieldPresent(feePayer.balanceField))
maxSpendable = payerSle->getFieldAmount(feePayer.balanceField).xrp();
if (payerSle->isFieldPresent(sfMaxFee))
{
auto const cap = payerSle->getFieldAmount(sfMaxFee).xrp();
maxSpendable = std::min(maxSpendable, cap);
}
}
else
{
if (payerSle->getType() != ltACCOUNT_ROOT)
return tefINTERNAL; // LCOV_EXCL_LINE
if (feePayer.type == FeePayerType::SponsorCoSigned)
{
auto const sponsorReserve = accountReserve(ctx.view, payerSle, ctx.j);
maxSpendable = payerSle->getFieldAmount(sfBalance).xrp() - sponsorReserve;
}
else
{
maxSpendable = payerSle->getFieldAmount(feePayer.balanceField).xrp();
}
}
// NOTE: Because preclaim evaluates against a static readview, it
// does not reflect fee deductions from other transactions paid by
@@ -438,12 +602,12 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
// transactions, this check may pass optimistically.
// The fee shortfall will be handled by the Transactor::reset mechanism,
// which caps the fee to the remaining actual balance.
if (balance < feePaid)
if (maxSpendable < feePaid)
{
JLOG(ctx.j.trace()) << "Insufficient balance:" << " balance=" << to_string(balance)
JLOG(ctx.j.trace()) << "Insufficient balance:" << " balance=" << to_string(maxSpendable)
<< " paid=" << to_string(feePaid);
if ((balance > beast::kZero) && !ctx.view.open())
if ((maxSpendable > beast::kZero) && !ctx.view.open())
{
// Closed ledger, non-zero balance, less than fee
return tecINSUFF_FEE;
@@ -460,16 +624,72 @@ Transactor::payFee()
{
auto const feePaid = ctx_.tx[sfFee].xrp();
auto const feePayer = ctx_.tx.getFeePayer();
auto const sle = view().peek(keylet::account(feePayer));
auto const feePayer = getFeePayer(view(), ctx_.tx);
auto const sle = view().peek(feePayer.keylet);
JLOG(j_.trace()) << "Fee payer: " + to_string(feePayer.id);
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
// Deduct the fee, so it's not available during the transaction.
// Will only write the account back if the transaction succeeds.
sle->setFieldAmount(sfBalance, sle->getFieldAmount(sfBalance) - feePaid);
if (feePayer != accountID_)
view().update(sle); // done in `apply()` for the account
if (feePaid == beast::kZero)
return tesSUCCESS;
XRPAmount balance = beast::kZero;
if (sle->isFieldPresent(feePayer.balanceField))
{
balance = sle->getFieldAmount(feePayer.balanceField).xrp();
}
else if (feePayer.balanceField != sfFeeAmount)
{
return tefINTERNAL; // LCOV_EXCL_LINE
}
// A co-signed sponsor pays the fee out of its own account balance, but must
// never be charged into its account reserve, and a pre-funded sponsorship's
// fee is capped by sfMaxFee. Mirror the spendable amount computed in
// checkFee() so both limits are enforced on the apply path too.
XRPAmount spendable = balance;
if (feePayer.type == FeePayerType::SponsorCoSigned)
{
auto const sponsorReserve = accountReserve(view(), sle, j_);
// max(balance - reserve, 0) with overflow handling
spendable = balance > sponsorReserve ? balance - sponsorReserve : beast::kZero;
}
else if (feePayer.type == FeePayerType::SponsorPreFunded && sle->isFieldPresent(sfMaxFee))
{
auto const cap = sle->getFieldAmount(sfMaxFee).xrp();
spendable = std::min(spendable, cap);
}
// Only sponsor fee-payers reject here on insufficient funds. For an
// ordinary account, the fee falls through and is capped by reset(), which
// caps to the account's balance. That capping is wrong for sponsors: a
// co-signed sponsor would be charged into its own reserve, and a prefunded
// sponsorship's fee amount should be rejected rather than partially spent.
if (feePaid > spendable &&
(feePayer.type == FeePayerType::SponsorPreFunded ||
feePayer.type == FeePayerType::SponsorCoSigned))
{
if ((spendable > beast::kZero) && !view().open())
return tecINSUFF_FEE;
return terINSUF_FEE_B;
}
auto const feeAmountAfter = balance - feePaid;
if (feeAmountAfter == beast::kZero && feePayer.balanceField == sfFeeAmount)
{
// Because ltSponsorship.sfFeeAmount is soeOptional
sle->makeFieldAbsent(feePayer.balanceField);
}
else
{
sle->setFieldAmount(feePayer.balanceField, feeAmountAfter);
}
view().update(sle);
// VFALCO Should we call view().rawDestroyXRP() here as well?
return tesSUCCESS;
@@ -529,7 +749,7 @@ Transactor::checkSeqProxy(ReadView const& view, STTx const& tx, beast::Journal j
}
// Transaction can never succeed if the Ticket is not in the ledger.
if (!view.exists(keylet::kTicket(id, tSeqProx)))
if (!view.exists(keylet::ticket(id, tSeqProx)))
{
JLOG(j.trace()) << "applyTransaction: ticket already used or never created "
<< "a_seq=" << aSeq << " t_seq=" << tSeqProx;
@@ -594,7 +814,7 @@ Transactor::ticketDelete(
{
// Delete the Ticket, adjust the account root ticket count, and
// reduce the owner count.
SLE::pointer const sleTicket = view.peek(keylet::kTicket(ticketIndex));
SLE::pointer const sleTicket = view.peek(keylet::ticket(ticketIndex));
if (!sleTicket)
{
// LCOV_EXCL_START
@@ -643,7 +863,7 @@ Transactor::ticketDelete(
}
// Update the Ticket owner's reserve.
adjustOwnerCount(view, sleAccount, -1, j);
decreaseOwnerCountForObject(view, sleAccount, sleTicket, 1, j);
// Remove Ticket from ledger.
view.erase(sleTicket);
@@ -700,23 +920,26 @@ Transactor::checkSign(
std::optional<uint256 const> const& parentBatchId,
AccountID const& idAccount,
STObject const& sigObject,
beast::Journal const j)
beast::Journal const j,
bool permitUncreatedAccount)
{
{
auto const sle = view.read(keylet::account(idAccount));
if (view.rules().enabled(featureLendingProtocol) && isPseudoAccount(sle))
if ((view.rules().enabled(featureLendingProtocol) ||
view.rules().enabled(featureBatchV1_1) || view.rules().enabled(fixCleanup3_3_0)) &&
isPseudoAccount(sle))
{
// Pseudo-accounts can't sign transactions. This check is gated on
// the Lending Protocol amendment because that's the project it was
// added under, and it doesn't justify another amendment
// Pseudo-accounts can't sign transactions. This check is gated on a
// few different amendments so that it takes effect as soon as any of
// them is activated.
return tefBAD_AUTH;
}
}
auto const pkSigner = sigObject.getFieldVL(sfSigningPubKey);
// Ignore signature check on batch inner transactions
if (parentBatchId && view.rules().enabled(featureBatch))
if (parentBatchId && view.rules().enabled(featureBatchV1_1))
{
// Defensive Check: These values are also checked in Batch::preflight
if (sigObject.isFieldPresent(sfTxnSignature) || !pkSigner.empty() ||
@@ -734,6 +957,21 @@ Transactor::checkSign(
return tesSUCCESS;
}
if (sigObject.isFieldPresent(sfSponsorSignature))
{
// Co-signed sponsorship
// Sanity check: already checked in preflight1
if (!sigObject.isFieldPresent(sfSponsor))
return tefINTERNAL; // LCOV_EXCL_LINE
auto const sponsorID = sigObject.getAccountID(sfSponsor);
auto const sponsorSignature = sigObject.getFieldObject(sfSponsorSignature);
if (auto const ret = checkSign(view, flags, std::nullopt, sponsorID, sponsorSignature, j);
!isTesSuccess(ret))
return ret;
}
// If the pk is empty and not simulate or simulate and signers,
// then we must be multi-signing.
if (sigObject.isFieldPresent(sfSigners))
@@ -754,7 +992,16 @@ Transactor::checkSign(
auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner)));
auto const sleAccount = view.read(keylet::account(idAccount));
if (!sleAccount)
return terNO_ACCOUNT;
{
// An account that does not exist yet can only be authorized by its own
// master key, and only where an un-created signer is permitted (a batch
// whose earlier inner creates the account). Otherwise it cannot sign.
if (!permitUncreatedAccount)
return terNO_ACCOUNT;
if (idAccount != idSigner)
return tefBAD_AUTH;
return tesSUCCESS;
}
return checkSingleSign(view, idSigner, idAccount, sleAccount, j);
}
@@ -767,50 +1014,6 @@ Transactor::checkSign(PreclaimContext const& ctx)
return checkSign(ctx.view, ctx.flags, ctx.parentBatchId, idAccount, ctx.tx, ctx.j);
}
NotTEC
Transactor::checkBatchSign(PreclaimContext const& ctx)
{
NotTEC ret = tesSUCCESS;
STArray const& signers{ctx.tx.getFieldArray(sfBatchSigners)};
for (auto const& signer : signers)
{
auto const idAccount = signer.getAccountID(sfAccount);
Blob const& pkSigner = signer.getFieldVL(sfSigningPubKey);
if (pkSigner.empty())
{
if (ret = checkMultiSign(ctx.view, ctx.flags, idAccount, signer, ctx.j);
!isTesSuccess(ret))
return ret;
}
else
{
// LCOV_EXCL_START
if (!publicKeyType(makeSlice(pkSigner)))
return tefBAD_AUTH;
// LCOV_EXCL_STOP
auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner)));
auto const sleAccount = ctx.view.read(keylet::account(idAccount));
// A batch can include transactions from an un-created account ONLY
// when the account master key is the signer
if (!sleAccount)
{
if (idAccount != idSigner)
return tefBAD_AUTH;
return tesSUCCESS;
}
if (ret = checkSingleSign(ctx.view, idSigner, idAccount, sleAccount, ctx.j);
!isTesSuccess(ret))
return ret;
}
}
return ret;
}
NotTEC
Transactor::checkSingleSign(
ReadView const& view,
@@ -852,7 +1055,7 @@ Transactor::checkMultiSign(
beast::Journal const j)
{
// Get id's SignerList and Quorum.
STLedgerEntry::const_pointer const sleAccountSigners = view.read(keylet::signers(id));
STLedgerEntry::const_pointer const sleAccountSigners = view.read(keylet::signerList(id));
// If the signer list doesn't exist the account is not multi-signing.
if (!sleAccountSigners)
{
@@ -1032,7 +1235,7 @@ removeExpiredNFTokenOffers(
for (auto const& index : offers)
{
if (auto const offer = view.peek(keylet::nftoffer(index)))
if (auto const offer = view.peek(keylet::nftokenOffer(index)))
{
nft::deleteTokenOffer(view, offer);
if (++removed == kExpiredOfferRemoveLimit)
@@ -1081,10 +1284,11 @@ removeDeletedTrustLines(
}
}
/** Reset the context, discarding any changes made and adjust the fee.
@param fee The transaction fee to be charged.
@return A pair containing the transaction result and the actual fee charged.
/**
* Reset the context, discarding any changes made and adjust the fee.
*
* @param fee The transaction fee to be charged.
* @return A pair containing the transaction result and the actual fee charged.
*/
std::pair<TER, XRPAmount>
Transactor::reset(XRPAmount fee)
@@ -1098,22 +1302,49 @@ Transactor::reset(XRPAmount fee)
if (!txnAcct)
return {tefINTERNAL, beast::kZero};
auto const payerSle = view().peek(keylet::account(ctx_.tx.getFeePayer()));
auto const feePayer = getFeePayer(view(), ctx_.tx);
auto const payerSle = view().peek(feePayer.keylet);
if (!payerSle)
return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE
auto const balance = payerSle->getFieldAmount(sfBalance).xrp();
XRPAmount balance = beast::kZero;
if (payerSle->isFieldPresent(feePayer.balanceField))
{
balance = payerSle->getFieldAmount(feePayer.balanceField).xrp();
}
else if (feePayer.balanceField != sfFeeAmount)
{
return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE
}
if (feePayer.type == FeePayerType::SponsorPreFunded && payerSle->isFieldPresent(sfMaxFee))
{
auto const cap = payerSle->getFieldAmount(sfMaxFee).xrp();
fee = std::min(fee, cap);
}
// A co-signed sponsor must never be charged into its own account reserve,
// so the fee is capped to the balance above the reserve rather than to the
// full balance.
XRPAmount spendable = balance;
if (feePayer.type == FeePayerType::SponsorCoSigned)
{
auto const sponsorReserve = accountReserve(view(), payerSle, j_);
// max(balance - reserve, 0) with overflow handling
spendable = balance > sponsorReserve ? balance - sponsorReserve : beast::kZero;
}
// balance should have already been checked in checkFee / preFlight.
XRPL_ASSERT(
balance != beast::kZero && (!view().open() || balance >= fee),
(fee == beast::kZero || balance != beast::kZero) && (!view().open() || balance >= fee),
"xrpl::Transactor::reset : valid balance");
// We retry/reject the transaction if the account balance is zero or
// we're applying against an open ledger and the balance is less than
// the fee
if (fee > balance)
fee = balance;
if (fee > spendable)
fee = spendable;
// Since we reset the context, we need to charge the fee and update
// the account's sequence number (or consume the Ticket) again.
@@ -1121,7 +1352,17 @@ Transactor::reset(XRPAmount fee)
// If for some reason we are unable to consume the ticket or sequence
// then the ledger is corrupted. Rather than make things worse we
// reject the transaction.
payerSle->setFieldAmount(sfBalance, balance - fee);
auto const feeAmountAfter = balance - fee;
if (feeAmountAfter == beast::kZero && feePayer.balanceField == sfFeeAmount)
{
// Because ltSponsorship.sfFeeAmount is soeOptional
payerSle->makeFieldAbsent(feePayer.balanceField);
}
else
{
payerSle->setFieldAmount(feePayer.balanceField, feeAmountAfter);
}
TER const ter{consumeSeqProxy(txnAcct)};
XRPL_ASSERT(isTesSuccess(ter), "xrpl::Transactor::reset : result is tesSUCCESS");
@@ -1135,6 +1376,48 @@ Transactor::reset(XRPAmount fee)
return {ter, fee};
}
FeePayer
Transactor::getFeePayer(ReadView const& view, STTx const& tx)
{
if (tx.isFieldPresent(sfSponsor) && isFeeSponsored(tx))
{
auto const sponsorID = tx.getAccountID(sfSponsor);
auto const sponseeID = tx.getInitiator();
auto const sponsorshipKeylet = keylet::sponsorship(sponsorID, sponseeID);
// if pre-funded sponsorship exists, prefer it
if (view.exists(sponsorshipKeylet))
{
// pre funded
return FeePayer{
.id = sponsorID,
.keylet = sponsorshipKeylet,
.balanceField = sfFeeAmount,
.type = FeePayerType::SponsorPreFunded};
}
// Checked in Transactor::checkSponsor
XRPL_ASSERT(
tx.isFieldPresent(sfSponsorSignature),
"xrpl::getFeePayer has sponsor signature without a sponsorship object");
// co-signed
return FeePayer{
.id = sponsorID,
.keylet = keylet::account(sponsorID),
.balanceField = sfBalance,
.type = FeePayerType::SponsorCoSigned};
}
AccountID const payerID = tx.getInitiator();
auto const payerAccountKeylet = keylet::account(payerID);
auto const payerType =
tx.isFieldPresent(sfDelegate) ? FeePayerType::Delegate : FeePayerType::Account;
return FeePayer{
.id = payerID, .keylet = payerAccountKeylet, .balanceField = sfBalance, .type = payerType};
}
// The sole purpose of this function is to provide a convenient, named
// location to set a breakpoint, to be used when replaying transactions.
void