From 24b6dad287dd3307a2ae689e36c9d6c0c29f0fbf Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 29 Jul 2026 14:24:55 -0400 Subject: [PATCH] fix: Switch SponsorshipSet to use a delta for sfFeeAmount --- include/xrpl/protocol/detail/sfields.macro | 2 + .../xrpl/protocol/detail/transactions.macro | 4 +- .../transactions/SponsorshipSet.h | 42 +-- .../tx/transactors/sponsor/SponsorshipSet.h | 13 +- .../tx/transactors/sponsor/SponsorshipSet.cpp | 335 +++++++++++------- src/test/app/MPToken_test.cpp | 4 +- src/test/app/Sponsor_test.cpp | 272 ++++++++++++-- src/test/jtx/impl/sponsor.cpp | 12 +- src/test/jtx/sponsor.h | 12 +- .../transactions/SponsorshipSetTests.cpp | 60 ++-- 10 files changed, 522 insertions(+), 234 deletions(-) diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 56527628c9..29e97b2371 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -238,6 +238,7 @@ TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::kSmdNeedsAsset // int32 TYPED_SFIELD(sfLoanScale, INT32, 1) +TYPED_SFIELD(sfRemainingOwnerCountDelta, INT32, 2) // currency amount (common) TYPED_SFIELD(sfAmount, AMOUNT, 1) @@ -277,6 +278,7 @@ TYPED_SFIELD(sfMinAccountCreateAmount, AMOUNT, 30) TYPED_SFIELD(sfLPTokenBalance, AMOUNT, 31) TYPED_SFIELD(sfFeeAmount, AMOUNT, 32) TYPED_SFIELD(sfMaxFee, AMOUNT, 33) +TYPED_SFIELD(sfFeeAmountDelta, AMOUNT, 34) // variable length (common) TYPED_SFIELD(sfPublicKey, VL, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 6d809f7ab5..1f9603dbae 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1189,9 +1189,9 @@ TRANSACTION(ttSPONSORSHIP_SET, 91, SponsorshipSet, ({ {sfCounterpartySponsor, SoeOptional}, {sfSponsee, SoeOptional}, - {sfFeeAmount, SoeOptional}, + {sfFeeAmountDelta, SoeOptional}, {sfMaxFee, SoeOptional}, - {sfRemainingOwnerCount, SoeOptional}, + {sfRemainingOwnerCountDelta, SoeOptional}, })) /** This system-generated transaction type is used to update the status of the various amendments. diff --git a/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h b/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h index 0124da5e58..dfd12a329f 100644 --- a/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h +++ b/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h @@ -100,29 +100,29 @@ public: } /** - * @brief Get sfFeeAmount (SoeOptional) + * @brief Get sfFeeAmountDelta (SoeOptional) * @return The field value, or std::nullopt if not present. */ [[nodiscard]] protocol_autogen::Optional - getFeeAmount() const + getFeeAmountDelta() const { - if (hasFeeAmount()) + if (hasFeeAmountDelta()) { - return this->tx_->at(sfFeeAmount); + return this->tx_->at(sfFeeAmountDelta); } return std::nullopt; } /** - * @brief Check if sfFeeAmount is present. + * @brief Check if sfFeeAmountDelta is present. * @return True if the field is present, false otherwise. */ [[nodiscard]] bool - hasFeeAmount() const + hasFeeAmountDelta() const { - return this->tx_->isFieldPresent(sfFeeAmount); + return this->tx_->isFieldPresent(sfFeeAmountDelta); } /** @@ -152,29 +152,29 @@ public: } /** - * @brief Get sfRemainingOwnerCount (SoeOptional) + * @brief Get sfRemainingOwnerCountDelta (SoeOptional) * @return The field value, or std::nullopt if not present. */ [[nodiscard]] - protocol_autogen::Optional - getRemainingOwnerCount() const + protocol_autogen::Optional + getRemainingOwnerCountDelta() const { - if (hasRemainingOwnerCount()) + if (hasRemainingOwnerCountDelta()) { - return this->tx_->at(sfRemainingOwnerCount); + return this->tx_->at(sfRemainingOwnerCountDelta); } return std::nullopt; } /** - * @brief Check if sfRemainingOwnerCount is present. + * @brief Check if sfRemainingOwnerCountDelta is present. * @return True if the field is present, false otherwise. */ [[nodiscard]] bool - hasRemainingOwnerCount() const + hasRemainingOwnerCountDelta() const { - return this->tx_->isFieldPresent(sfRemainingOwnerCount); + return this->tx_->isFieldPresent(sfRemainingOwnerCountDelta); } }; @@ -243,13 +243,13 @@ public: } /** - * @brief Set sfFeeAmount (SoeOptional) + * @brief Set sfFeeAmountDelta (SoeOptional) * @return Reference to this builder for method chaining. */ SponsorshipSetBuilder& - setFeeAmount(std::decay_t const& value) + setFeeAmountDelta(std::decay_t const& value) { - object_[sfFeeAmount] = value; + object_[sfFeeAmountDelta] = value; return *this; } @@ -265,13 +265,13 @@ public: } /** - * @brief Set sfRemainingOwnerCount (SoeOptional) + * @brief Set sfRemainingOwnerCountDelta (SoeOptional) * @return Reference to this builder for method chaining. */ SponsorshipSetBuilder& - setRemainingOwnerCount(std::decay_t const& value) + setRemainingOwnerCountDelta(std::decay_t const& value) { - object_[sfRemainingOwnerCount] = value; + object_[sfRemainingOwnerCountDelta] = value; return *this; } diff --git a/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h b/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h index 3310c995ae..1100c5352a 100644 --- a/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h +++ b/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include #include @@ -16,7 +18,7 @@ namespace xrpl { class SponsorshipSet : public Transactor { public: - static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Custom; explicit SponsorshipSet(ApplyContext& ctx) : Transactor(ctx) { @@ -47,6 +49,15 @@ public: XRPAmount fee, ReadView const& view, beast::Journal const& j) override; + +private: + TER + createSponsorship( + Keylet const& sponsorshipKeylet, + AccountID const& sponsorID, + AccountID const& sponseeID, + SLE::ref sponsorAccSle, + SLE::ref reserveSponsorAccSle); }; } // namespace xrpl diff --git a/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp b/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp index 24bfaad2f8..e717c626e4 100644 --- a/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp +++ b/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp @@ -3,13 +3,16 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include +#include #include #include #include @@ -17,36 +20,62 @@ #include #include +#include #include +#include #include #include 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 const& remainingOwnerCountDelta) +{ + std::uint32_t const currentCount = + sponsorshipSle ? (*sponsorshipSle)[~sfRemainingOwnerCount].value_or(0u) : 0u; + return static_cast(currentCount) + remainingOwnerCountDelta.value_or(0); +} + static bool hasSponsorshipBudget( SLE::const_ref sponsorshipSle, - std::optional const& feeAmount, - std::optional const& remainingOwnerCount) + std::optional const& feeAmountDelta, + std::optional 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::numeric_limits::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(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(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::numeric_limits::max()), + "xrpl::SponsorshipSet::doApply : RemainingOwnerCount does not overflow"); + if (newCount <= 0) { sponsorshipSle->makeFieldAbsent(sfRemainingOwnerCount); } else { - sponsorshipSle->at(sfRemainingOwnerCount) = *remainingOwnerCount; + sponsorshipSle->at(sfRemainingOwnerCount) = static_cast(newCount); } } diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index f3f967af81..c9adce0305 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -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()); diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index 1012a7d0b6..5ddc3aa6a8 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -58,6 +58,7 @@ #include #include +#include #include #include #include @@ -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::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(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(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(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(); diff --git a/src/test/jtx/impl/sponsor.cpp b/src/test/jtx/impl/sponsor.cpp index cdf68800f5..453ccebcb9 100644 --- a/src/test/jtx/impl/sponsor.cpp +++ b/src/test/jtx/impl/sponsor.cpp @@ -21,18 +21,18 @@ namespace xrpl::test::jtx::sponsor { json::Value set(jtx::Account const& account, uint32_t flags, - std::optional const reserveCount, - std::optional const feeAmount, + std::optional const reserveCountDelta, + std::optional const feeAmountDelta, std::optional 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; diff --git a/src/test/jtx/sponsor.h b/src/test/jtx/sponsor.h index 43d55d7246..f87a13c462 100644 --- a/src/test/jtx/sponsor.h +++ b/src/test/jtx/sponsor.h @@ -18,24 +18,24 @@ namespace xrpl::test::jtx::sponsor { json::Value set(jtx::Account const& account, std::uint32_t flags, - std::optional const reserveCount = std::nullopt, - std::optional const feeAmount = std::nullopt, + std::optional const reserveCountDelta = std::nullopt, + std::optional const feeAmountDelta = std::nullopt, std::optional const maxFee = std::nullopt); inline json::Value set_fee( jtx::Account const& account, std::uint32_t flags, - STAmount feeAmount, + STAmount feeAmountDelta, std::optional 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 diff --git a/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipSetTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipSetTests.cpp index dce8cfca3f..c5bc41c6e6 100644 --- a/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipSetTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipSetTests.cpp @@ -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()); } }