mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-21 22:30:57 +00:00
fix: Switch SponsorshipSet to use a delta for sfFeeAmount
This commit is contained in:
committed by
Ayaz Salikhov
parent
e290005db5
commit
24b6dad287
@@ -3,13 +3,16 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
@@ -17,36 +20,62 @@
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// Compute the resulting RemainingOwnerCount using signed 64-bit arithmetic to
|
||||
// avoid unsigned wraparound. A missing SLE (object creation) or absent field
|
||||
// counts as zero. Callers handle the out-of-range results: a negative value is
|
||||
// clamped to zero (field absent) and overflow is rejected in preclaim.
|
||||
static std::int64_t
|
||||
totalRemainingOwnerCount(
|
||||
SLE::const_ref sponsorshipSle,
|
||||
std::optional<std::int32_t> const& remainingOwnerCountDelta)
|
||||
{
|
||||
std::uint32_t const currentCount =
|
||||
sponsorshipSle ? (*sponsorshipSle)[~sfRemainingOwnerCount].value_or(0u) : 0u;
|
||||
return static_cast<std::int64_t>(currentCount) + remainingOwnerCountDelta.value_or(0);
|
||||
}
|
||||
|
||||
static bool
|
||||
hasSponsorshipBudget(
|
||||
SLE::const_ref sponsorshipSle,
|
||||
std::optional<STAmount> const& feeAmount,
|
||||
std::optional<std::uint32_t> const& remainingOwnerCount)
|
||||
std::optional<STAmount> const& feeAmountDelta,
|
||||
std::optional<std::int32_t> const& remainingOwnerCountDelta)
|
||||
{
|
||||
// A field the transaction omits keeps whatever the existing object holds,
|
||||
// sfFeeAmountDelta and sfRemainingOwnerCountDelta must be non-negative when creating a new
|
||||
// Sponsorship object.
|
||||
if (!sponsorshipSle)
|
||||
{
|
||||
if (feeAmountDelta.has_value() && *feeAmountDelta <= beast::kZero)
|
||||
return false;
|
||||
|
||||
if (remainingOwnerCountDelta.has_value() && *remainingOwnerCountDelta <= 0)
|
||||
return false;
|
||||
}
|
||||
// If the transaction omits a field, it keeps whatever the existing object holds,
|
||||
// so fall back to the current SLE value when the tx does not set it.
|
||||
bool const hasFeeAmount = feeAmount
|
||||
? *feeAmount > beast::kZero
|
||||
: sponsorshipSle && (*sponsorshipSle)[~sfFeeAmount].value_or(STAmount{0}) > beast::kZero;
|
||||
STAmount const currentFee =
|
||||
sponsorshipSle ? (*sponsorshipSle)[~sfFeeAmount].value_or(STAmount{0}) : STAmount{0};
|
||||
STAmount const newFee = currentFee + feeAmountDelta.value_or(STAmount{0});
|
||||
|
||||
bool const hasRemainingOwnerCount = remainingOwnerCount
|
||||
? *remainingOwnerCount > 0
|
||||
: sponsorshipSle && (*sponsorshipSle)[~sfRemainingOwnerCount].value_or(0) > 0;
|
||||
std::int64_t const newCount =
|
||||
totalRemainingOwnerCount(sponsorshipSle, remainingOwnerCountDelta);
|
||||
|
||||
return hasFeeAmount || hasRemainingOwnerCount;
|
||||
return newFee > beast::kZero || newCount > 0;
|
||||
}
|
||||
|
||||
TxConsequences
|
||||
SponsorshipSet::makeTxConsequences(PreflightContext const& ctx)
|
||||
{
|
||||
auto const feeAmount = ctx.tx[~sfFeeAmount];
|
||||
return TxConsequences{ctx.tx, feeAmount.has_value() ? feeAmount->xrp() : beast::kZero};
|
||||
auto const feeAmount = ctx.tx[~sfFeeAmountDelta];
|
||||
auto const feeAmountDelta = std::max(STAmount{0}, feeAmount.value_or(STAmount{0}));
|
||||
return TxConsequences{ctx.tx, feeAmountDelta.xrp()};
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
@@ -90,8 +119,8 @@ SponsorshipSet::preflight(PreflightContext const& ctx)
|
||||
return temINVALID_FLAG;
|
||||
|
||||
// Transactions deleting `Sponsorship` cannot include modification fields.
|
||||
if (ctx.tx.isFieldPresent(sfFeeAmount) || ctx.tx.isFieldPresent(sfRemainingOwnerCount) ||
|
||||
ctx.tx.isFieldPresent(sfMaxFee))
|
||||
if (ctx.tx.isFieldPresent(sfFeeAmountDelta) ||
|
||||
ctx.tx.isFieldPresent(sfRemainingOwnerCountDelta) || ctx.tx.isFieldPresent(sfMaxFee))
|
||||
return temMALFORMED;
|
||||
}
|
||||
else
|
||||
@@ -101,27 +130,26 @@ SponsorshipSet::preflight(PreflightContext const& ctx)
|
||||
if (account != sponsorID)
|
||||
return temMALFORMED;
|
||||
|
||||
// FeeAmount and MaxFee must be non-negative XRP amounts when present.
|
||||
auto const checkOptionalAmountField = [&](SField const& field) -> NotTEC {
|
||||
if (!ctx.tx.isFieldPresent(field))
|
||||
return tesSUCCESS;
|
||||
// FeeAmountDelta must be a non-zero XRP amount when present.
|
||||
if (auto const feeAmt = ctx.tx[~sfFeeAmountDelta];
|
||||
feeAmt && (!isXRP(*feeAmt) || *feeAmt == beast::kZero))
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
auto const amount = ctx.tx.getFieldAmount(field);
|
||||
// MaxFee must be a non-negative XRP amount when present.
|
||||
if (auto const maxFee = ctx.tx[~sfMaxFee];
|
||||
maxFee && (!isXRP(*maxFee) || *maxFee < beast::kZero))
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (!isXRP(amount))
|
||||
return temBAD_AMOUNT;
|
||||
// RemainingOwnerCountDelta must be a non-zero integer when present.
|
||||
if (auto const remainingOwnerCountDelta = ctx.tx[~sfRemainingOwnerCountDelta];
|
||||
remainingOwnerCountDelta && *remainingOwnerCountDelta == 0)
|
||||
return temINVALID;
|
||||
|
||||
if (amount.xrp() < beast::kZero)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
return tesSUCCESS;
|
||||
};
|
||||
|
||||
if (auto const ret = checkOptionalAmountField(sfFeeAmount); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
if (auto const ret = checkOptionalAmountField(sfMaxFee); !isTesSuccess(ret))
|
||||
return ret;
|
||||
// nothing specified in the tx
|
||||
if (!ctx.tx.isFieldPresent(sfRemainingOwnerCountDelta) &&
|
||||
!ctx.tx.isFieldPresent(sfFeeAmountDelta) && !ctx.tx.isFieldPresent(sfMaxFee) &&
|
||||
((ctx.tx.getFlags() & tfUniversalMask) == 0))
|
||||
return temREDUNDANT;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
@@ -154,12 +182,21 @@ SponsorshipSet::preclaim(PreclaimContext const& ctx)
|
||||
if (ctx.tx.isFlag(tfDeleteObject) && !sponsorshipSle)
|
||||
return tecNO_ENTRY;
|
||||
|
||||
// Reject creating or updating a Sponsorship that would be left with no
|
||||
// budget (neither a positive FeeAmount nor a positive RemainingOwnerCount).
|
||||
// Such an object is unusable yet still consumes the sponsor's reserve.
|
||||
if (!ctx.tx.isFlag(tfDeleteObject) &&
|
||||
!hasSponsorshipBudget(sponsorshipSle, ctx.tx[~sfFeeAmount], ctx.tx[~sfRemainingOwnerCount]))
|
||||
return tecNO_PERMISSION;
|
||||
if (!ctx.tx.isFlag(tfDeleteObject))
|
||||
{
|
||||
// Reject if applying the delta would overflow uint32_t. A negative delta
|
||||
// that underflows is clamped to zero (field absent) rather than erroring.
|
||||
if (totalRemainingOwnerCount(sponsorshipSle, ctx.tx[~sfRemainingOwnerCountDelta]) >
|
||||
static_cast<std::int64_t>(std::numeric_limits<std::uint32_t>::max()))
|
||||
return tecLIMIT_EXCEEDED;
|
||||
|
||||
// Reject creating or updating a Sponsorship that would be left with no
|
||||
// budget (neither a positive FeeAmount nor a positive RemainingOwnerCount).
|
||||
// Such an object is unusable yet still consumes the sponsor's reserve.
|
||||
if (!hasSponsorshipBudget(
|
||||
sponsorshipSle, ctx.tx[~sfFeeAmountDelta], ctx.tx[~sfRemainingOwnerCountDelta]))
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -208,6 +245,91 @@ deleteSponsorship(ApplyView& view, SLE::ref sle, beast::Journal j)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
SponsorshipSet::createSponsorship(
|
||||
Keylet const& sponsorshipKeylet,
|
||||
AccountID const& sponsorID,
|
||||
AccountID const& sponseeID,
|
||||
SLE::ref sponsorAccSle,
|
||||
SLE::ref reserveSponsorAccSle)
|
||||
{
|
||||
auto const feeAmountDelta = ctx_.tx[~sfFeeAmountDelta];
|
||||
auto const maxFee = ctx_.tx[~sfMaxFee];
|
||||
auto const remainingOwnerCountDelta = ctx_.tx[~sfRemainingOwnerCountDelta];
|
||||
|
||||
bool const hasPositiveFeeAmount = feeAmountDelta.has_value() && *feeAmountDelta > beast::kZero;
|
||||
|
||||
// Create a new Sponsorship object between the sponsor and sponsee.
|
||||
auto newSle = std::make_shared<SLE>(sponsorshipKeylet);
|
||||
STAmount sponsorBalanceAfterFee = (*sponsorAccSle)[sfBalance];
|
||||
// sfFeeAmountDelta must be positive if the sponsorship object doesn't exist. This is
|
||||
// checked in preclaim.
|
||||
XRPL_ASSERT(
|
||||
!feeAmountDelta.has_value() || *feeAmountDelta > beast::kZero,
|
||||
"xrpl::SponsorshipSet::doApply : new sponsorship has positive fee amount");
|
||||
|
||||
(*newSle)[sfOwner] = sponsorID;
|
||||
(*newSle)[sfSponsee] = sponseeID;
|
||||
if (feeAmountDelta && feeAmountDelta->xrp() > sponsorBalanceAfterFee.xrp())
|
||||
return tecUNFUNDED;
|
||||
|
||||
if (hasPositiveFeeAmount)
|
||||
sponsorBalanceAfterFee -= *feeAmountDelta;
|
||||
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
sponsorAccSle,
|
||||
sponsorBalanceAfterFee.xrp(),
|
||||
reserveSponsorAccSle,
|
||||
{.ownerCountDelta = 1},
|
||||
ctx_.journal,
|
||||
tecUNFUNDED);
|
||||
!isTesSuccess(ret))
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (hasPositiveFeeAmount)
|
||||
{
|
||||
// New object: FeeAmount starts absent, so deduct and record the full amount
|
||||
(*newSle)[sfFeeAmount] = *feeAmountDelta;
|
||||
(*sponsorAccSle)[sfBalance] -= *feeAmountDelta;
|
||||
}
|
||||
|
||||
if (maxFee && *maxFee > beast::kZero)
|
||||
(*newSle)[sfMaxFee] = *maxFee;
|
||||
if (remainingOwnerCountDelta && *remainingOwnerCountDelta > 0)
|
||||
(*newSle)[sfRemainingOwnerCount] = *remainingOwnerCountDelta;
|
||||
|
||||
std::uint32_t flags = 0;
|
||||
if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForFee))
|
||||
flags |= lsfSponsorshipRequireSignForFee;
|
||||
|
||||
if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForReserve))
|
||||
flags |= lsfSponsorshipRequireSignForReserve;
|
||||
|
||||
(*newSle)[sfFlags] = flags;
|
||||
|
||||
auto const sponsorPage = view().dirInsert(
|
||||
keylet::ownerDir(sponsorID), sponsorshipKeylet, describeOwnerDir(sponsorID));
|
||||
if (!sponsorPage)
|
||||
return tecDIR_FULL; // LCOV_EXCL_LINE
|
||||
(*newSle)[sfOwnerNode] = *sponsorPage;
|
||||
|
||||
auto const sponseePage = view().dirInsert(
|
||||
keylet::ownerDir(sponseeID), sponsorshipKeylet, describeOwnerDir(sponseeID));
|
||||
if (!sponseePage)
|
||||
return tecDIR_FULL; // LCOV_EXCL_LINE
|
||||
(*newSle)[sfSponseeNode] = *sponseePage;
|
||||
|
||||
// NOLINTNEXTLINE(readability-suspicious-call-argument)
|
||||
increaseOwnerCount(view(), sponsorAccSle, reserveSponsorAccSle, 1, ctx_.journal);
|
||||
addSponsorToLedgerEntry(newSle, reserveSponsorAccSle);
|
||||
|
||||
ctx_.view().insert(newSle);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
SponsorshipSet::doApply()
|
||||
{
|
||||
@@ -224,8 +346,8 @@ SponsorshipSet::doApply()
|
||||
if (!ctx_.view().exists(keylet::account(sponseeID)))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const sponsorKeylet = keylet::sponsorship(sponsorID, sponseeID);
|
||||
auto const sponsorshipSle = ctx_.view().peek(sponsorKeylet);
|
||||
auto const sponsorshipKeylet = keylet::sponsorship(sponsorID, sponseeID);
|
||||
auto const sponsorshipSle = ctx_.view().peek(sponsorshipKeylet);
|
||||
|
||||
if (ctx_.tx.isFlag(tfDeleteObject))
|
||||
{
|
||||
@@ -235,11 +357,9 @@ SponsorshipSet::doApply()
|
||||
return deleteSponsorship(ctx_.view(), sponsorshipSle, ctx_.journal);
|
||||
}
|
||||
|
||||
auto const feeAmount = ctx_.tx[~sfFeeAmount];
|
||||
auto const feeAmountDelta = ctx_.tx[~sfFeeAmountDelta];
|
||||
auto const maxFee = ctx_.tx[~sfMaxFee];
|
||||
auto const remainingOwnerCount = ctx_.tx[~sfRemainingOwnerCount];
|
||||
|
||||
bool const hasPositiveFeeAmount = feeAmount.has_value() && *feeAmount > beast::kZero;
|
||||
auto const remainingOwnerCountDelta = ctx_.tx[~sfRemainingOwnerCountDelta];
|
||||
|
||||
auto reserveSponsorAccSle = getTxReserveSponsor(ctx_.getApplyViewContext());
|
||||
if (!reserveSponsorAccSle)
|
||||
@@ -247,24 +367,33 @@ SponsorshipSet::doApply()
|
||||
|
||||
if (!sponsorshipSle)
|
||||
{
|
||||
// Create a new Sponsorship object between the sponsor and sponsee.
|
||||
auto newSle = std::make_shared<SLE>(sponsorKeylet);
|
||||
return createSponsorship(
|
||||
sponsorshipKeylet, sponsorID, sponseeID, sponsorAccSle, *reserveSponsorAccSle);
|
||||
}
|
||||
|
||||
(*newSle)[sfOwner] = sponsorID;
|
||||
(*newSle)[sfSponsee] = sponseeID;
|
||||
if (feeAmount && (*feeAmount).xrp() > (*sponsorAccSle)[sfBalance])
|
||||
// Update the existing Sponsorship object.
|
||||
if (feeAmountDelta)
|
||||
{
|
||||
auto actualDelta = feeAmountDelta.value();
|
||||
auto const currentFee = (*sponsorshipSle)[~sfFeeAmount].valueOr(XRPAmount{0});
|
||||
|
||||
// Clamp negative delta to avoid underflow.
|
||||
if (actualDelta < beast::kZero && -actualDelta > currentFee)
|
||||
actualDelta = -currentFee;
|
||||
// Reject if the sponsor cannot afford the (positive) delta.
|
||||
if (actualDelta > beast::kZero && actualDelta > (*sponsorAccSle)[sfBalance])
|
||||
return tecUNFUNDED;
|
||||
|
||||
STAmount sponsorBalanceAfterFee = (*sponsorAccSle)[sfBalance];
|
||||
if (hasPositiveFeeAmount)
|
||||
sponsorBalanceAfterFee -= *feeAmount;
|
||||
// Move the FeeAmount delta between the sponsor balance and Sponsorship
|
||||
// object.
|
||||
(*sponsorAccSle)[sfBalance] -= actualDelta;
|
||||
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
sponsorAccSle,
|
||||
sponsorBalanceAfterFee.xrp(),
|
||||
(*sponsorAccSle)[sfBalance]->xrp(),
|
||||
*reserveSponsorAccSle,
|
||||
{.ownerCountDelta = 1},
|
||||
{},
|
||||
ctx_.journal,
|
||||
tecUNFUNDED);
|
||||
!isTesSuccess(ret))
|
||||
@@ -272,87 +401,19 @@ SponsorshipSet::doApply()
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (hasPositiveFeeAmount)
|
||||
STAmount const newFee = currentFee + actualDelta;
|
||||
// checked in preclaim
|
||||
XRPL_ASSERT(
|
||||
newFee >= beast::kZero, "xrpl::SponsorshipSet::doApply : new fee is non-negative");
|
||||
if (newFee == beast::kZero)
|
||||
{
|
||||
// New object: FeeAmount starts absent, so deduct and record the full amount
|
||||
(*newSle)[sfFeeAmount] = *feeAmount;
|
||||
(*sponsorAccSle)[sfBalance] -= *feeAmount;
|
||||
sponsorshipSle->makeFieldAbsent(sfFeeAmount);
|
||||
}
|
||||
|
||||
if (maxFee && *maxFee > beast::kZero)
|
||||
(*newSle)[sfMaxFee] = *maxFee;
|
||||
if (remainingOwnerCount && *remainingOwnerCount > 0)
|
||||
(*newSle)[sfRemainingOwnerCount] = *remainingOwnerCount;
|
||||
|
||||
std::uint32_t flags = 0;
|
||||
if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForFee))
|
||||
flags |= lsfSponsorshipRequireSignForFee;
|
||||
|
||||
if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForReserve))
|
||||
flags |= lsfSponsorshipRequireSignForReserve;
|
||||
|
||||
(*newSle)[sfFlags] = flags;
|
||||
|
||||
auto const sponsorPage = view().dirInsert(
|
||||
keylet::ownerDir(sponsorID), sponsorKeylet, describeOwnerDir(sponsorID));
|
||||
if (!sponsorPage)
|
||||
return tecDIR_FULL; // LCOV_EXCL_LINE
|
||||
(*newSle)[sfOwnerNode] = *sponsorPage;
|
||||
|
||||
auto const sponseePage = view().dirInsert(
|
||||
keylet::ownerDir(sponseeID), sponsorKeylet, describeOwnerDir(sponseeID));
|
||||
if (!sponseePage)
|
||||
return tecDIR_FULL; // LCOV_EXCL_LINE
|
||||
(*newSle)[sfSponseeNode] = *sponseePage;
|
||||
|
||||
// NOLINTNEXTLINE(readability-suspicious-call-argument)
|
||||
increaseOwnerCount(view(), sponsorAccSle, *reserveSponsorAccSle, 1, ctx_.journal);
|
||||
addSponsorToLedgerEntry(newSle, *reserveSponsorAccSle);
|
||||
|
||||
ctx_.view().insert(newSle);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// Update the existing Sponsorship object.
|
||||
if (feeAmount)
|
||||
{
|
||||
auto const currentFeeAmount = (*sponsorshipSle)[~sfFeeAmount].valueOr(XRPAmount{0});
|
||||
auto const feeAmountDelta = XRPAmount(*feeAmount - currentFeeAmount);
|
||||
|
||||
if (feeAmountDelta > beast::kZero && feeAmountDelta > (*sponsorAccSle)[sfBalance])
|
||||
return tecUNFUNDED;
|
||||
|
||||
// Move the FeeAmount delta between the sponsor balance and Sponsorship
|
||||
// object.
|
||||
if (feeAmountDelta != beast::kZero)
|
||||
else
|
||||
{
|
||||
STAmount sponsorBalanceAfterFee = (*sponsorAccSle)[sfBalance];
|
||||
sponsorBalanceAfterFee -= feeAmountDelta;
|
||||
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
sponsorAccSle,
|
||||
sponsorBalanceAfterFee.xrp(),
|
||||
*reserveSponsorAccSle,
|
||||
{},
|
||||
ctx_.journal,
|
||||
tecUNFUNDED);
|
||||
!isTesSuccess(ret))
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
(*sponsorAccSle)[sfBalance] -= feeAmountDelta;
|
||||
if (*feeAmount == beast::kZero)
|
||||
{
|
||||
(*sponsorshipSle).makeFieldAbsent(sfFeeAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
(*sponsorshipSle).setFieldAmount(sfFeeAmount, *feeAmount);
|
||||
}
|
||||
ctx_.view().update(sponsorAccSle);
|
||||
(*sponsorshipSle)[sfFeeAmount] = newFee;
|
||||
}
|
||||
ctx_.view().update(sponsorAccSle);
|
||||
}
|
||||
|
||||
if (maxFee)
|
||||
@@ -367,15 +428,21 @@ SponsorshipSet::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
if (remainingOwnerCount)
|
||||
if (remainingOwnerCountDelta)
|
||||
{
|
||||
if (*remainingOwnerCount == 0)
|
||||
std::int64_t const newCount =
|
||||
totalRemainingOwnerCount(sponsorshipSle, remainingOwnerCountDelta);
|
||||
// Overflow is rejected in preclaim; underflow clamps to zero (field absent).
|
||||
XRPL_ASSERT(
|
||||
newCount <= static_cast<std::int64_t>(std::numeric_limits<std::uint32_t>::max()),
|
||||
"xrpl::SponsorshipSet::doApply : RemainingOwnerCount does not overflow");
|
||||
if (newCount <= 0)
|
||||
{
|
||||
sponsorshipSle->makeFieldAbsent(sfRemainingOwnerCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
sponsorshipSle->at(sfRemainingOwnerCount) = *remainingOwnerCount;
|
||||
sponsorshipSle->at(sfRemainingOwnerCount) = static_cast<std::uint32_t>(newCount);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2116,8 +2116,8 @@ class MPToken_test : public beast::unit_test::Suite
|
||||
jv[jss::TransactionType] = jss::SponsorshipSet;
|
||||
jv[jss::Account] = alice.human();
|
||||
jv[sfSponsee.fieldName] = carol.human();
|
||||
jv[sfFeeAmount.fieldName] = mpt.getJson(JsonOptions::Values::None);
|
||||
test(jv, sfFeeAmount.fieldName);
|
||||
jv[sfFeeAmountDelta.fieldName] = mpt.getJson(JsonOptions::Values::None);
|
||||
test(jv, sfFeeAmountDelta.fieldName);
|
||||
}
|
||||
}
|
||||
BEAST_EXPECT(txWithAmounts.empty());
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
@@ -197,10 +198,12 @@ public:
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(temMALFORMED));
|
||||
|
||||
// Invalid feeAmount
|
||||
for (auto const& amt : {XRP(-1), usd(1)})
|
||||
// Invalid FeeAmountDelta
|
||||
for (auto const& amt : {XRP(0), usd(1)})
|
||||
{
|
||||
env(sponsor::set_fee(sponsor, 0, amt), sponsor::SponseeAcc(alice), Ter(temBAD_AMOUNT));
|
||||
env(sponsor::set_fee(sponsor, 0, amt, XRP(1)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(temBAD_AMOUNT));
|
||||
}
|
||||
// Invalid MaxFee
|
||||
for (auto const& amt : {XRP(-1), usd(1)})
|
||||
@@ -209,6 +212,10 @@ public:
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(temBAD_AMOUNT));
|
||||
}
|
||||
// Invalid RemainingOwnerCountDelta
|
||||
env(sponsor::set(sponsor, 0, 0, XRP(2), XRP(1)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(temINVALID));
|
||||
|
||||
// Invalid Delete operation
|
||||
env(sponsor::set_reserve(sponsor, tfDeleteObject, 1),
|
||||
@@ -229,12 +236,15 @@ public:
|
||||
sponsor::CounterpartySponsor(alice),
|
||||
Ter(temMALFORMED));
|
||||
|
||||
// Redundant tx
|
||||
env(sponsor::set(sponsor, 0), sponsor::SponseeAcc(alice), Ter(temREDUNDANT));
|
||||
|
||||
//
|
||||
// preclaim
|
||||
//
|
||||
|
||||
// Invalid Sponsee
|
||||
env(sponsor::set(sponsor, 0), sponsor::SponseeAcc(noFunded), Ter(tecNO_DST));
|
||||
env(sponsor::set(sponsor, 0, 1), sponsor::SponseeAcc(noFunded), Ter(tecNO_DST));
|
||||
env.close();
|
||||
|
||||
// Invalid Sponsor
|
||||
@@ -290,7 +300,7 @@ public:
|
||||
|
||||
// Decreasing feeAmount should succeed (refund, negative delta)
|
||||
adjustAccountXRPBalance(env, sponsor, XRP(500));
|
||||
env(sponsor::set_fee(sponsor, 0, XRP(800)),
|
||||
env(sponsor::set_fee(sponsor, 0, XRP(-200)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(XRP(1)),
|
||||
Ter(tesSUCCESS));
|
||||
@@ -299,7 +309,7 @@ public:
|
||||
|
||||
// Increasing feeAmount within delta budget should succeed
|
||||
adjustAccountXRPBalance(env, sponsor, XRP(500));
|
||||
env(sponsor::set_fee(sponsor, 0, XRP(850)),
|
||||
env(sponsor::set_fee(sponsor, 0, XRP(50)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(XRP(1)),
|
||||
Ter(tesSUCCESS));
|
||||
@@ -308,18 +318,15 @@ public:
|
||||
|
||||
// Increasing feeAmount where delta exceeds balance should fail
|
||||
adjustAccountXRPBalance(env, sponsor, XRP(310));
|
||||
env(sponsor::set_fee(sponsor, 0, XRP(1200)),
|
||||
env(sponsor::set_fee(sponsor, 0, XRP(350)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(XRP(1)),
|
||||
Ter(tecUNFUNDED));
|
||||
env.close();
|
||||
|
||||
// Increasing feeAmount to reach insufficient reserve
|
||||
auto const currentFeeAmount = env.le(keylet::sponsorship(sponsor.id(), alice.id()))
|
||||
->getFieldAmount(sfFeeAmount)
|
||||
.xrp();
|
||||
adjustAccountXRPBalance(env, sponsor, XRP(310));
|
||||
env(sponsor::set_fee(sponsor, 0, currentFeeAmount + XRP(309)),
|
||||
env(sponsor::set_fee(sponsor, 0, XRP(309)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(XRP(1)),
|
||||
Ter(tecUNFUNDED));
|
||||
@@ -543,7 +550,7 @@ public:
|
||||
BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - sle->at(sfFeeAmount) - XRP(1));
|
||||
|
||||
// update sponsorship (decrement)
|
||||
env(sponsor::set(sponsor, 0, 50, XRP(50), XRP(0.5)),
|
||||
env(sponsor::set(sponsor, 0, -50, XRP(-50), XRP(0.5)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(XRP(1)),
|
||||
Ter(tesSUCCESS));
|
||||
@@ -557,7 +564,7 @@ public:
|
||||
BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - sle->at(sfFeeAmount) - XRP(2));
|
||||
|
||||
// update sponsorship (increment)
|
||||
env(sponsor::set(sponsor, 0, 200, XRP(200), XRP(2)),
|
||||
env(sponsor::set(sponsor, 0, 150, XRP(150), XRP(2)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(XRP(1)),
|
||||
Ter(tesSUCCESS));
|
||||
@@ -591,26 +598,32 @@ public:
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice)));
|
||||
|
||||
// Cannot create sponsorship with no fee or reserve budget. MaxFee
|
||||
// and flags do not make a sponsorship object useful by themselves.
|
||||
env(sponsor::set(sponsor, 0), sponsor::SponseeAcc(alice), Ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice)));
|
||||
|
||||
env(sponsor::set_max_fee(sponsor, 0, XRP(1)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice)));
|
||||
|
||||
env(sponsor::set(sponsor, 0, 0, XRP(0), XRP(0)),
|
||||
env(sponsor::set(sponsor, 0, std::nullopt, std::nullopt, XRP(0)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice)));
|
||||
|
||||
// update sponsorship with non-zero value
|
||||
env(sponsor::set(sponsor, 0, 100, XRP(100), XRP(1)),
|
||||
// create sponsorship with negative values
|
||||
env(sponsor::set_reserve(sponsor, 0, -100),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice)));
|
||||
env(sponsor::set_fee(sponsor, 0, XRP(-100)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice)));
|
||||
|
||||
// create sponsorship with non-zero value
|
||||
env(sponsor::set(sponsor, 0, 100, XRP(101), XRP(1)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(XRP(1)));
|
||||
env.close();
|
||||
@@ -618,7 +631,7 @@ public:
|
||||
sle = env.le(keylet::sponsorship(sponsor, alice));
|
||||
BEAST_EXPECT(sle);
|
||||
BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100);
|
||||
BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100));
|
||||
BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(101));
|
||||
BEAST_EXPECT(sle->at(sfMaxFee) == XRP(1));
|
||||
|
||||
// update sponsorship flags
|
||||
@@ -648,7 +661,7 @@ public:
|
||||
lsfSponsorshipRequireSignForReserve);
|
||||
|
||||
// Cannot update sponsorship so both fee and reserve budgets are absent.
|
||||
env(sponsor::set(sponsor, 0, 0, XRP(0), XRP(0)),
|
||||
env(sponsor::set(sponsor, 0, -100, XRP(-101), std::nullopt),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(XRP(1)),
|
||||
Ter(tecNO_PERMISSION));
|
||||
@@ -657,17 +670,17 @@ public:
|
||||
sle = env.le(keylet::sponsorship(sponsor, alice));
|
||||
BEAST_EXPECT(sle);
|
||||
BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100);
|
||||
BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100));
|
||||
BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(101));
|
||||
BEAST_EXPECT(sle->at(sfMaxFee) == XRP(1));
|
||||
}
|
||||
|
||||
{
|
||||
// Removing one budget field while the other remains keeps the
|
||||
// Sponsorship valid. Starting state (from above):
|
||||
// RemainingOwnerCount = 100, FeeAmount = XRP(100).
|
||||
// RemainingOwnerCount = 100, FeeAmount = XRP(101).
|
||||
|
||||
// Remove only FeeAmount (set to 0); RemainingOwnerCount remains.
|
||||
env(sponsor::set_fee(sponsor, 0, XRP(0)),
|
||||
env(sponsor::set_fee(sponsor, 0, XRP(-101)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(XRP(1)),
|
||||
Ter(tesSUCCESS));
|
||||
@@ -686,12 +699,51 @@ public:
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
env(sponsor::set_reserve(sponsor, 0, 0),
|
||||
// A negative FeeAmountDelta larger than the current FeeAmount is
|
||||
// clamped, so only the current FeeAmount is refunded and the field
|
||||
// is removed. RemainingOwnerCount keeps the Sponsorship valid.
|
||||
auto const balanceBefore = env.balance(sponsor);
|
||||
env(sponsor::set_fee(sponsor, 0, XRP(-500)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(XRP(1)),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
sle = env.le(keylet::sponsorship(sponsor, alice));
|
||||
BEAST_EXPECT(sle);
|
||||
BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount));
|
||||
BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100);
|
||||
BEAST_EXPECT(env.balance(sponsor) == balanceBefore + XRP(100) - XRP(1));
|
||||
|
||||
// Restore FeeAmount for the checks below.
|
||||
env(sponsor::set_fee(sponsor, 0, XRP(100)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(XRP(1)),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
env(sponsor::set_reserve(sponsor, 0, -100),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(XRP(1)),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
sle = env.le(keylet::sponsorship(sponsor, alice));
|
||||
BEAST_EXPECT(sle);
|
||||
BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount));
|
||||
BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100));
|
||||
|
||||
// Decreasing FeeAmount below zero must fail with tecNO_PERMISSION
|
||||
// when there is no RemainingOwnerCount (the budget would become
|
||||
// entirely empty). Current state: FeeAmount = XRP(100), no
|
||||
// RemainingOwnerCount.
|
||||
env(sponsor::set_fee(sponsor, 0, XRP(-101)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Fee(XRP(1)),
|
||||
Ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
|
||||
// Confirm that the sponsorship is unchanged.
|
||||
sle = env.le(keylet::sponsorship(sponsor, alice));
|
||||
BEAST_EXPECT(sle);
|
||||
BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount));
|
||||
@@ -748,6 +800,160 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testRemainingOwnerCountOverflow()
|
||||
{
|
||||
testcase("RemainingOwnerCount overflow and underflow clamping");
|
||||
using namespace test::jtx;
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const alice("alice");
|
||||
Account const sponsor("sponsor");
|
||||
env.fund(XRP(10000), alice, sponsor);
|
||||
env.close();
|
||||
|
||||
constexpr std::int32_t kInt32Max = std::numeric_limits<std::int32_t>::max();
|
||||
|
||||
// --- Positive overflow: delta causes count to exceed UINT32_MAX ---
|
||||
{
|
||||
// Create with count = INT32_MAX.
|
||||
env(sponsor::set_reserve(sponsor, 0, kInt32Max),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
BEAST_EXPECT(
|
||||
env.le(keylet::sponsorship(sponsor, alice))->at(sfRemainingOwnerCount) ==
|
||||
static_cast<std::uint32_t>(kInt32Max));
|
||||
|
||||
// Add INT32_MAX again: count = 2 * INT32_MAX = 4294967294 (<= UINT32_MAX, still ok).
|
||||
env(sponsor::set_reserve(sponsor, 0, kInt32Max),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
BEAST_EXPECT(
|
||||
env.le(keylet::sponsorship(sponsor, alice))->at(sfRemainingOwnerCount) ==
|
||||
2u * static_cast<std::uint32_t>(kInt32Max));
|
||||
|
||||
// Adding 2 more pushes count to 4294967296, exceeding UINT32_MAX: reject.
|
||||
env(sponsor::set_reserve(sponsor, 0, 2),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(tecLIMIT_EXCEEDED));
|
||||
env.close();
|
||||
|
||||
// SLE is unchanged.
|
||||
BEAST_EXPECT(
|
||||
env.le(keylet::sponsorship(sponsor, alice))->at(sfRemainingOwnerCount) ==
|
||||
2u * static_cast<std::uint32_t>(kInt32Max));
|
||||
|
||||
env(sponsor::del(sponsor), sponsor::SponseeAcc(alice), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// --- Negative underflow: clamps to 0; fee budget survives ---
|
||||
{
|
||||
// Create with count=10 and a fee budget.
|
||||
env(sponsor::set(sponsor, 0, 10, XRP(100)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
BEAST_EXPECT(
|
||||
env.le(keylet::sponsorship(sponsor, alice))->at(sfRemainingOwnerCount) == 10u);
|
||||
|
||||
// Delta of -20 produces count = -10; clamps to 0 (field absent).
|
||||
env(sponsor::set_reserve(sponsor, 0, -20), sponsor::SponseeAcc(alice), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
auto sle = env.le(keylet::sponsorship(sponsor, alice));
|
||||
BEAST_EXPECT(sle);
|
||||
BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount));
|
||||
BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100));
|
||||
|
||||
env(sponsor::del(sponsor), sponsor::SponseeAcc(alice), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// --- Negative underflow: clamped count=0 with no fee budget → no budget ---
|
||||
{
|
||||
// Create with count=10, no fee.
|
||||
env(sponsor::set_reserve(sponsor, 0, 10), sponsor::SponseeAcc(alice), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
BEAST_EXPECT(
|
||||
env.le(keylet::sponsorship(sponsor, alice))->at(sfRemainingOwnerCount) == 10u);
|
||||
|
||||
// Delta of -20 would clamp count to 0 with no fee → empty budget → tecNO_PERMISSION.
|
||||
env(sponsor::set_reserve(sponsor, 0, -20),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
|
||||
// SLE is unchanged.
|
||||
BEAST_EXPECT(
|
||||
env.le(keylet::sponsorship(sponsor, alice))->at(sfRemainingOwnerCount) == 10u);
|
||||
|
||||
env(sponsor::del(sponsor), sponsor::SponseeAcc(alice), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testConsequences()
|
||||
{
|
||||
testcase("Consequences");
|
||||
using namespace test::jtx;
|
||||
Env env{*this, testableAmendments()};
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
|
||||
Account const alice("alice");
|
||||
Account const sponsor("sponsor");
|
||||
env.memoize(alice);
|
||||
env.memoize(sponsor);
|
||||
|
||||
{
|
||||
// A positive FeeAmountDelta is the maximum XRP the tx can spend.
|
||||
auto const jt = env.jt(
|
||||
sponsor::set_fee(sponsor, 0, XRP(100)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Seq(1),
|
||||
Fee(baseFee));
|
||||
auto const pf =
|
||||
preflight(env.app(), env.current()->rules(), *jt.stx, TapNone, env.journal);
|
||||
BEAST_EXPECT(isTesSuccess(pf.ter));
|
||||
BEAST_EXPECT(!pf.consequences.isBlocker());
|
||||
BEAST_EXPECT(pf.consequences.fee() == drops(baseFee));
|
||||
BEAST_EXPECT(pf.consequences.potentialSpend() == XRP(100));
|
||||
}
|
||||
|
||||
{
|
||||
// A negative FeeAmountDelta withdraws from the sponsorship, so the
|
||||
// transaction cannot spend anything.
|
||||
auto const jt = env.jt(
|
||||
sponsor::set_fee(sponsor, 0, XRP(-100)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Seq(1),
|
||||
Fee(baseFee));
|
||||
auto const pf =
|
||||
preflight(env.app(), env.current()->rules(), *jt.stx, TapNone, env.journal);
|
||||
BEAST_EXPECT(isTesSuccess(pf.ter));
|
||||
BEAST_EXPECT(!pf.consequences.isBlocker());
|
||||
BEAST_EXPECT(pf.consequences.fee() == drops(baseFee));
|
||||
BEAST_EXPECT(pf.consequences.potentialSpend() == XRP(0));
|
||||
}
|
||||
|
||||
{
|
||||
// No FeeAmountDelta at all.
|
||||
auto const jt = env.jt(
|
||||
sponsor::set_reserve(sponsor, 0, 10),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Seq(1),
|
||||
Fee(baseFee));
|
||||
auto const pf =
|
||||
preflight(env.app(), env.current()->rules(), *jt.stx, TapNone, env.journal);
|
||||
BEAST_EXPECT(isTesSuccess(pf.ter));
|
||||
BEAST_EXPECT(!pf.consequences.isBlocker());
|
||||
BEAST_EXPECT(pf.consequences.fee() == drops(baseFee));
|
||||
BEAST_EXPECT(pf.consequences.potentialSpend() == XRP(0));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testPreFundAndCosign()
|
||||
{
|
||||
@@ -810,7 +1016,7 @@ public:
|
||||
Ter(terINSUF_FEE_B));
|
||||
env.close();
|
||||
|
||||
env(sponsor::set_reserve(sponsor, 0, 0), sponsor::SponseeAcc(alice), Ter(tesSUCCESS));
|
||||
env(sponsor::set_reserve(sponsor, 0, -1), sponsor::SponseeAcc(alice), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// reserve insufficient
|
||||
@@ -2090,7 +2296,7 @@ public:
|
||||
XRP(10));
|
||||
|
||||
// clear flag
|
||||
env(sponsor::set_fee(sponsor, tfSponsorshipClearRequireSignForFee, XRP(10)),
|
||||
env(sponsor::set(sponsor, tfSponsorshipClearRequireSignForFee),
|
||||
sponsor::SponseeAcc(alice));
|
||||
env.close();
|
||||
|
||||
@@ -2322,7 +2528,7 @@ public:
|
||||
XRP(10));
|
||||
|
||||
// clear flag
|
||||
env(sponsor::set_fee(sponsor, tfSponsorshipClearRequireSignForFee, XRP(10)),
|
||||
env(sponsor::set(sponsor, tfSponsorshipClearRequireSignForFee),
|
||||
sponsor::SponseeAcc(alice));
|
||||
env.close();
|
||||
|
||||
@@ -4939,7 +5145,7 @@ public:
|
||||
env.close();
|
||||
|
||||
// Create pre-funded sponsorship
|
||||
env(sponsor::set(sponsor, 0, 0, XRP(1)), sponsor::SponseeAcc(alice), Fee(XRP(1)));
|
||||
env(sponsor::set_fee(sponsor, 0, XRP(1)), sponsor::SponseeAcc(alice), Fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
@@ -5443,6 +5649,8 @@ protected:
|
||||
testInvalidSponsorField();
|
||||
|
||||
testSimpleSponsorshipSet();
|
||||
testRemainingOwnerCountOverflow();
|
||||
testConsequences();
|
||||
|
||||
testPreFundAndCosign();
|
||||
testSponsoredFreeTierReserve();
|
||||
|
||||
@@ -21,18 +21,18 @@ namespace xrpl::test::jtx::sponsor {
|
||||
json::Value
|
||||
set(jtx::Account const& account,
|
||||
uint32_t flags,
|
||||
std::optional<uint32_t> const reserveCount,
|
||||
std::optional<STAmount> const feeAmount,
|
||||
std::optional<int32_t> const reserveCountDelta,
|
||||
std::optional<STAmount> const feeAmountDelta,
|
||||
std::optional<STAmount> const maxFee)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::TransactionType] = jss::SponsorshipSet;
|
||||
jv[jss::Account] = account.human();
|
||||
jv[sfFlags.jsonName] = flags;
|
||||
if (reserveCount)
|
||||
jv[sfRemainingOwnerCount.jsonName] = *reserveCount;
|
||||
if (feeAmount)
|
||||
jv[sfFeeAmount.jsonName] = feeAmount->getJson(JsonOptions::Values::None);
|
||||
if (reserveCountDelta)
|
||||
jv[sfRemainingOwnerCountDelta.jsonName] = *reserveCountDelta;
|
||||
if (feeAmountDelta)
|
||||
jv[sfFeeAmountDelta.jsonName] = feeAmountDelta->getJson(JsonOptions::Values::None);
|
||||
if (maxFee)
|
||||
jv[sfMaxFee.jsonName] = maxFee->getJson(JsonOptions::Values::None);
|
||||
return jv;
|
||||
|
||||
@@ -18,24 +18,24 @@ namespace xrpl::test::jtx::sponsor {
|
||||
json::Value
|
||||
set(jtx::Account const& account,
|
||||
std::uint32_t flags,
|
||||
std::optional<std::uint32_t> const reserveCount = std::nullopt,
|
||||
std::optional<STAmount> const feeAmount = std::nullopt,
|
||||
std::optional<std::int32_t> const reserveCountDelta = std::nullopt,
|
||||
std::optional<STAmount> const feeAmountDelta = std::nullopt,
|
||||
std::optional<STAmount> const maxFee = std::nullopt);
|
||||
|
||||
inline json::Value
|
||||
set_fee(
|
||||
jtx::Account const& account,
|
||||
std::uint32_t flags,
|
||||
STAmount feeAmount,
|
||||
STAmount feeAmountDelta,
|
||||
std::optional<STAmount> maxFee = std::nullopt)
|
||||
{
|
||||
return set(account, flags, std::nullopt, std::move(feeAmount), std::move(maxFee));
|
||||
return set(account, flags, std::nullopt, std::move(feeAmountDelta), std::move(maxFee));
|
||||
}
|
||||
|
||||
inline json::Value
|
||||
set_reserve(jtx::Account const& account, std::uint32_t flags, std::uint32_t reserveCount)
|
||||
set_reserve(jtx::Account const& account, std::uint32_t flags, std::int32_t reserveCountDelta)
|
||||
{
|
||||
return set(account, flags, reserveCount);
|
||||
return set(account, flags, reserveCountDelta);
|
||||
}
|
||||
|
||||
inline json::Value
|
||||
|
||||
@@ -31,9 +31,9 @@ TEST(TransactionsSponsorshipSetTests, BuilderSettersRoundTrip)
|
||||
// Transaction-specific field values
|
||||
auto const counterpartySponsorValue = canonical_ACCOUNT();
|
||||
auto const sponseeValue = canonical_ACCOUNT();
|
||||
auto const feeAmountValue = canonical_AMOUNT();
|
||||
auto const feeAmountDeltaValue = canonical_AMOUNT();
|
||||
auto const maxFeeValue = canonical_AMOUNT();
|
||||
auto const remainingOwnerCountValue = canonical_UINT32();
|
||||
auto const remainingOwnerCountDeltaValue = canonical_INT32();
|
||||
|
||||
SponsorshipSetBuilder builder{
|
||||
accountValue,
|
||||
@@ -44,9 +44,9 @@ TEST(TransactionsSponsorshipSetTests, BuilderSettersRoundTrip)
|
||||
// Set optional fields
|
||||
builder.setCounterpartySponsor(counterpartySponsorValue);
|
||||
builder.setSponsee(sponseeValue);
|
||||
builder.setFeeAmount(feeAmountValue);
|
||||
builder.setFeeAmountDelta(feeAmountDeltaValue);
|
||||
builder.setMaxFee(maxFeeValue);
|
||||
builder.setRemainingOwnerCount(remainingOwnerCountValue);
|
||||
builder.setRemainingOwnerCountDelta(remainingOwnerCountDeltaValue);
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
@@ -81,11 +81,11 @@ TEST(TransactionsSponsorshipSetTests, BuilderSettersRoundTrip)
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = feeAmountValue;
|
||||
auto const actualOpt = tx.getFeeAmount();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFeeAmount should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfFeeAmount");
|
||||
EXPECT_TRUE(tx.hasFeeAmount());
|
||||
auto const& expected = feeAmountDeltaValue;
|
||||
auto const actualOpt = tx.getFeeAmountDelta();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFeeAmountDelta should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfFeeAmountDelta");
|
||||
EXPECT_TRUE(tx.hasFeeAmountDelta());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -97,11 +97,11 @@ TEST(TransactionsSponsorshipSetTests, BuilderSettersRoundTrip)
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = remainingOwnerCountValue;
|
||||
auto const actualOpt = tx.getRemainingOwnerCount();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRemainingOwnerCount should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfRemainingOwnerCount");
|
||||
EXPECT_TRUE(tx.hasRemainingOwnerCount());
|
||||
auto const& expected = remainingOwnerCountDeltaValue;
|
||||
auto const actualOpt = tx.getRemainingOwnerCountDelta();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRemainingOwnerCountDelta should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfRemainingOwnerCountDelta");
|
||||
EXPECT_TRUE(tx.hasRemainingOwnerCountDelta());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -122,9 +122,9 @@ TEST(TransactionsSponsorshipSetTests, BuilderFromStTxRoundTrip)
|
||||
// Transaction-specific field values
|
||||
auto const counterpartySponsorValue = canonical_ACCOUNT();
|
||||
auto const sponseeValue = canonical_ACCOUNT();
|
||||
auto const feeAmountValue = canonical_AMOUNT();
|
||||
auto const feeAmountDeltaValue = canonical_AMOUNT();
|
||||
auto const maxFeeValue = canonical_AMOUNT();
|
||||
auto const remainingOwnerCountValue = canonical_UINT32();
|
||||
auto const remainingOwnerCountDeltaValue = canonical_INT32();
|
||||
|
||||
// Build an initial transaction
|
||||
SponsorshipSetBuilder initialBuilder{
|
||||
@@ -135,9 +135,9 @@ TEST(TransactionsSponsorshipSetTests, BuilderFromStTxRoundTrip)
|
||||
|
||||
initialBuilder.setCounterpartySponsor(counterpartySponsorValue);
|
||||
initialBuilder.setSponsee(sponseeValue);
|
||||
initialBuilder.setFeeAmount(feeAmountValue);
|
||||
initialBuilder.setFeeAmountDelta(feeAmountDeltaValue);
|
||||
initialBuilder.setMaxFee(maxFeeValue);
|
||||
initialBuilder.setRemainingOwnerCount(remainingOwnerCountValue);
|
||||
initialBuilder.setRemainingOwnerCountDelta(remainingOwnerCountDeltaValue);
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
@@ -171,10 +171,10 @@ TEST(TransactionsSponsorshipSetTests, BuilderFromStTxRoundTrip)
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = feeAmountValue;
|
||||
auto const actualOpt = rebuiltTx.getFeeAmount();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFeeAmount should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfFeeAmount");
|
||||
auto const& expected = feeAmountDeltaValue;
|
||||
auto const actualOpt = rebuiltTx.getFeeAmountDelta();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFeeAmountDelta should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfFeeAmountDelta");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -185,10 +185,10 @@ TEST(TransactionsSponsorshipSetTests, BuilderFromStTxRoundTrip)
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = remainingOwnerCountValue;
|
||||
auto const actualOpt = rebuiltTx.getRemainingOwnerCount();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRemainingOwnerCount should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfRemainingOwnerCount");
|
||||
auto const& expected = remainingOwnerCountDeltaValue;
|
||||
auto const actualOpt = rebuiltTx.getRemainingOwnerCountDelta();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRemainingOwnerCountDelta should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfRemainingOwnerCountDelta");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -250,12 +250,12 @@ TEST(TransactionsSponsorshipSetTests, OptionalFieldsReturnNullopt)
|
||||
EXPECT_FALSE(tx.getCounterpartySponsor().has_value());
|
||||
EXPECT_FALSE(tx.hasSponsee());
|
||||
EXPECT_FALSE(tx.getSponsee().has_value());
|
||||
EXPECT_FALSE(tx.hasFeeAmount());
|
||||
EXPECT_FALSE(tx.getFeeAmount().has_value());
|
||||
EXPECT_FALSE(tx.hasFeeAmountDelta());
|
||||
EXPECT_FALSE(tx.getFeeAmountDelta().has_value());
|
||||
EXPECT_FALSE(tx.hasMaxFee());
|
||||
EXPECT_FALSE(tx.getMaxFee().has_value());
|
||||
EXPECT_FALSE(tx.hasRemainingOwnerCount());
|
||||
EXPECT_FALSE(tx.getRemainingOwnerCount().has_value());
|
||||
EXPECT_FALSE(tx.hasRemainingOwnerCountDelta());
|
||||
EXPECT_FALSE(tx.getRemainingOwnerCountDelta().has_value());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user