diff --git a/cspell.config.yaml b/cspell.config.yaml index 0d38c4be7b0..b13e79779b2 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -271,6 +271,8 @@ words: - sles - soci - socidb + - sponsee + - sponsees - SRPMS - sslws - statsd diff --git a/include/xrpl/ledger/helpers/AccountRootHelpers.h b/include/xrpl/ledger/helpers/AccountRootHelpers.h index cf6082d5333..e19fe23393b 100644 --- a/include/xrpl/ledger/helpers/AccountRootHelpers.h +++ b/include/xrpl/ledger/helpers/AccountRootHelpers.h @@ -33,9 +33,96 @@ isGlobalFrozen(ReadView const& view, AccountID const& issuer); [[nodiscard]] XRPAmount xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, beast::Journal j); +/** Returns the account reserve, in drops. + Actual owner count can be adjusted by delta in ownerCountAdj + The reserve is calculated as + (ownerCount + "sponsoring object count" - "sponsored object count" + additionalOwnerCount) * + increment + (1 if not sponsored account + sponsoringAccountCount) * "reserve base" +*/ +[[nodiscard]] XRPAmount +accountReserve( + ReadView const& view, + SLE::const_ref sle, + beast::Journal j, + std::int32_t ownerCountAdj = 0, + std::int32_t reserveCountAdj = 0); + +[[nodiscard]] inline XRPAmount +accountReserve( + ReadView const& view, + AccountID const& id, + beast::Journal j, + std::int32_t ownerCountAdj = 0, + std::int32_t reserveCountAdj = 0) +{ + return accountReserve(view, view.read(keylet::account(id)), j, ownerCountAdj, reserveCountAdj); +} + +XRPAmount +baseAccountReserve(ReadView const& view, std::int32_t ownerCount); + +[[nodiscard]] TER +checkInsufficientReserve( + ReadView const& view, + STTx const& tx, + SLE::const_ref accSle, + STAmount const& accBalance, + SLE::const_ref sponsorSle, + std::int32_t ownerCountDelta, + std::int32_t reserveCountDelta = 0, + beast::Journal j = beast::Journal{beast::Journal::getNullSink()}); + +std::uint32_t +ownerCount( + ReadView const& view, + SLE::const_ref sle, + beast::Journal j, + std::int32_t ownerCountAdj = 0); + /** Adjust the owner count up or down. */ void -adjustOwnerCount(ApplyView& view, SLE::ref sle, std::int32_t amount, beast::Journal j); +adjustOwnerCount( + ApplyView& view, + SLE::ref accountSle, + SLE::ref sponsorSle, + std::int32_t amount, + beast::Journal j = beast::Journal{beast::Journal::getNullSink()}); + +inline void +adjustOwnerCount( + ApplyView& view, + AccountID const& account, + std::optional const& sponsor, + std::int32_t amount, + beast::Journal j = beast::Journal{beast::Journal::getNullSink()}) +{ + adjustOwnerCount( + view, + view.peek(keylet::account(account)), + sponsor ? view.peek(keylet::account(*sponsor)) : SLE::pointer(), + amount, + j); +} + +void +adjustOwnerCountObj( + ApplyView& view, + SLE::ref accountSle, + SLE::ref objectSle, + std::int32_t amount, + beast::Journal j = beast::Journal{beast::Journal::getNullSink()}); + +inline void +adjustOwnerCountObj( + ApplyView& view, + AccountID const& account, + SLE::ref objectSle, + std::int32_t amount, + beast::Journal j = beast::Journal{beast::Journal::getNullSink()}) +{ + SLE::ref accountSle = view.peek(keylet::account(account)); + adjustOwnerCountObj(view, accountSle, objectSle, amount, j); +} /** Returns IOU issuer transfer fee as Rate. Rate specifies * the fee as fractions of 1 billion. For example, 1% transfer rate @@ -71,7 +158,7 @@ getPseudoAccountFields(); - null pointer */ [[nodiscard]] bool -isPseudoAccount(SLE::const_pointer sleAcct, std::set const& pseudoFieldFilter = {}); +isPseudoAccount(SLE::const_ref sleAcct, std::set const& pseudoFieldFilter = {}); /** Convenience overload that reads the account from the view. */ [[nodiscard]] inline bool diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h index bdb83230ebf..bd27f172232 100644 --- a/include/xrpl/ledger/helpers/EscrowHelpers.h +++ b/include/xrpl/ledger/helpers/EscrowHelpers.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,7 @@ template TER escrowUnlockApplyHelper( ApplyView& view, + STTx const& tx, Rate lockedRate, SLE::ref sleDest, STAmount const& xrpBalance, @@ -31,6 +33,7 @@ template <> inline TER escrowUnlockApplyHelper( ApplyView& view, + STTx const& tx, Rate lockedRate, SLE::ref sleDest, STAmount const& xrpBalance, @@ -56,8 +59,13 @@ escrowUnlockApplyHelper( if (!view.exists(trustLineKey) && createAsset) { // Can the account cover the trust line's reserve? - if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; - xrpBalance < view.fees().accountReserve(ownerCount + 1)) + auto const sponsorSle = getTxReserveSponsor(view, tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + + if (auto const ret = + checkInsufficientReserve(view, tx, sleDest, xrpBalance, *sponsorSle, 1, 0, journal); + !isTesSuccess(ret)) { JLOG(journal.trace()) << "Trust line does not exist. " "Insufficient reserve to create line."; @@ -84,6 +92,7 @@ escrowUnlockApplyHelper( Issue(currency, receiver), // limit of zero 0, // quality in 0, // quality out + *sponsorSle, // sponsor journal); // journal !isTesSuccess(ter)) { @@ -161,6 +170,7 @@ template <> inline TER escrowUnlockApplyHelper( ApplyView& view, + STTx const& tx, Rate lockedRate, SLE::ref sleDest, STAmount const& xrpBalance, @@ -176,24 +186,31 @@ escrowUnlockApplyHelper( auto const mptID = amount.get().getMptID(); auto const issuanceKey = keylet::mptIssuance(mptID); - if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && createAsset && !receiverIssuer) + auto const mptKeylet = keylet::mptoken(issuanceKey.key, receiver); + if (!view.exists(mptKeylet) && createAsset && !receiverIssuer) { - if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; - xrpBalance < view.fees().accountReserve(ownerCount + 1)) - { - return tecINSUFFICIENT_RESERVE; - } + auto const sponsorSle = getTxReserveSponsor(view, tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ter = createMPToken(view, mptID, receiver, 0); !isTesSuccess(ter)) + if (auto const ret = + checkInsufficientReserve(view, tx, sleDest, xrpBalance, *sponsorSle, 1, 0, journal); + !isTesSuccess(ret)) + return ret; + + if (auto const ter = createMPToken(view, mptID, receiver, *sponsorSle, 0); + !isTesSuccess(ter)) { return ter; // LCOV_EXCL_LINE } // update owner count. - adjustOwnerCount(view, sleDest, 1, journal); + adjustOwnerCount(view, sleDest, *sponsorSle, 1, journal); + auto mptSle = view.peek(mptKeylet); + addSponsorToLedgerEntry(mptSle, *sponsorSle); } - if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && !receiverIssuer) + if (!view.exists(mptKeylet) && !receiverIssuer) return tecNO_PERMISSION; auto const xferRate = transferRate(view, amount); diff --git a/include/xrpl/ledger/helpers/MPTokenHelpers.h b/include/xrpl/ledger/helpers/MPTokenHelpers.h index c709badab86..a5fc8d8472d 100644 --- a/include/xrpl/ledger/helpers/MPTokenHelpers.h +++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h @@ -72,6 +72,7 @@ canAddHolding(ReadView const& view, MPTIssue const& mptIssue); [[nodiscard]] TER authorizeMPToken( ApplyView& view, + STTx const& tx, XRPAmount const& priorBalance, MPTID const& mptIssuanceID, AccountID const& account, @@ -103,6 +104,7 @@ requireAuth( [[nodiscard]] TER enforceMPTokenAuthorization( ApplyView& view, + STTx const& tx, MPTID const& mptIssuanceID, AccountID const& account, XRPAmount const& priorBalance, @@ -189,6 +191,7 @@ canMPTTradeAndTransfer( [[nodiscard]] TER addEmptyHolding( ApplyView& view, + STTx const& tx, AccountID const& accountID, XRPAmount priorBalance, MPTIssue const& mptIssue, @@ -197,6 +200,7 @@ addEmptyHolding( [[nodiscard]] TER removeEmptyHolding( ApplyView& view, + STTx const& tx, AccountID const& accountID, MPTIssue const& mptIssue, beast::Journal journal); @@ -228,6 +232,7 @@ createMPToken( ApplyView& view, MPTID const& mptIssuanceID, AccountID const& account, + SLE::ref sponsorSle, std::uint32_t const flags); TER @@ -235,6 +240,7 @@ checkCreateMPT( xrpl::ApplyView& view, xrpl::MPTIssue const& mptIssue, xrpl::AccountID const& holder, + SLE::ref sponsorSle, beast::Journal j); //------------------------------------------------------------------------------ diff --git a/include/xrpl/ledger/helpers/NFTokenHelpers.h b/include/xrpl/ledger/helpers/NFTokenHelpers.h index 362cfe5a8c4..86036fd023e 100644 --- a/include/xrpl/ledger/helpers/NFTokenHelpers.h +++ b/include/xrpl/ledger/helpers/NFTokenHelpers.h @@ -39,7 +39,7 @@ findTokenAndPage(ApplyView& view, AccountID const& owner, uint256 const& nftoken /** Insert the token in the owner's token directory. */ TER -insertToken(ApplyView& view, AccountID owner, STObject&& nft); +insertToken(ApplyView& view, STTx const& tx, AccountID owner, SLE::ref sponsorSle, STObject&& nft); /** Remove the token from the owner's token directory. */ TER @@ -107,6 +107,7 @@ tokenOfferCreatePreclaim( TER tokenOfferCreateApply( ApplyView& view, + STTx const& tx, AccountID const& acctID, STAmount const& amount, std::optional const& dest, diff --git a/include/xrpl/ledger/helpers/RippleStateHelpers.h b/include/xrpl/ledger/helpers/RippleStateHelpers.h index 3aaaa541fdc..afd42624d13 100644 --- a/include/xrpl/ledger/helpers/RippleStateHelpers.h +++ b/include/xrpl/ledger/helpers/RippleStateHelpers.h @@ -149,6 +149,7 @@ trustCreate( // Issuer should be the account being set. std::uint32_t uQualityIn, std::uint32_t uQualityOut, + SLE::ref sponsorSle, beast::Journal j); [[nodiscard]] TER @@ -229,6 +230,7 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc [[nodiscard]] TER addEmptyHolding( ApplyView& view, + STTx const& tx, AccountID const& accountID, XRPAmount priorBalance, Issue const& issue, diff --git a/include/xrpl/ledger/helpers/SponsorHelpers.h b/include/xrpl/ledger/helpers/SponsorHelpers.h new file mode 100644 index 00000000000..525f109484b --- /dev/null +++ b/include/xrpl/ledger/helpers/SponsorHelpers.h @@ -0,0 +1,137 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +inline bool +isReserveSponsored(STTx const& tx) +{ + return (tx.getFieldU32(sfSponsorFlags) & spfSponsorReserve) != 0u; +} + +inline bool +isSponsorReserveCoSigning(STTx const& tx) +{ + if (!tx.isFieldPresent(sfSponsorSignature)) + return false; + return isReserveSponsored(tx); +} + +inline std::optional +getTxReserveSponsorAccountID(STTx const& tx) +{ + if (tx.isFieldPresent(sfSponsor) && isReserveSponsored(tx)) + { + return tx.getAccountID(sfSponsor); + } + return {}; +} + +inline std::expected +getTxReserveSponsor(ApplyView& view, STTx const& tx) +{ + auto const sponsorID = getTxReserveSponsorAccountID(tx); + if (sponsorID) + { + auto sle = view.peek(keylet::account(*sponsorID)); + + // already checked in Transactor::checkSponsor + if (!sle) + return std::unexpected(tecINTERNAL); + return sle; + } + return SLE::pointer(); +} + +inline std::expected +getTxReserveSponsor(ReadView const& view, STTx const& tx) +{ + auto const sponsorID = getTxReserveSponsorAccountID(tx); + if (sponsorID) + { + auto sle = view.read(keylet::account(*sponsorID)); + + // already checked in Transactor::checkSponsor + if (!sle) + return std::unexpected(tecINTERNAL); + return sle; + } + return SLE::pointer(); +} + +inline std::optional +getLedgerEntryReserveSponsorAccountID(SLE::const_ref sle, SF_ACCOUNT const& field = sfSponsor) +{ + if (sle->isFieldPresent(field)) + return sle->getAccountID(field); + return {}; +} + +inline SLE::pointer +getLedgerEntryReserveSponsor( + ApplyView& view, + SLE::const_ref sle, + SF_ACCOUNT const& field = sfSponsor) +{ + auto const sponsorID = getLedgerEntryReserveSponsorAccountID(sle, field); + if (sponsorID) + return view.peek(keylet::account(*sponsorID)); + return {}; +} + +inline SLE::const_pointer +getLedgerEntryReserveSponsor( + ReadView const& view, + SLE::const_ref sle, + SF_ACCOUNT const& field = sfSponsor) +{ + auto const sponsorID = getLedgerEntryReserveSponsorAccountID(sle, field); + if (sponsorID) + return view.read(keylet::account(*sponsorID)); + return {}; +} + +inline void +addSponsorToLedgerEntry( + SLE::ref sle, + SLE::const_ref sponsorSle, + SF_ACCOUNT const& field = sfSponsor) +{ + XRPL_ASSERT( + (sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) || + (sle->getType() != ltRIPPLE_STATE && field == sfSponsor), + "addSponsorToLedgerEntry : Invalid field to the LedgerEntry"); + if (sponsorSle) + sle->setAccountID(field, sponsorSle->getAccountID(sfAccount)); +} + +inline void +removeSponsorFromLedgerEntry(SLE::ref sle, SF_ACCOUNT const& field = sfSponsor) +{ + XRPL_ASSERT( + (sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) || + (sle->getType() != ltRIPPLE_STATE && field == sfSponsor), + "removeSponsorFromLedgerEntry : Invalid field to the LedgerEntry"); + if (sle->isFieldPresent(field)) + sle->makeFieldAbsent(field); +} + +// namespace sponsor +// { +// // Accessing the ledger to check if provided sponsor is valid. +// [[nodiscard]] TER +// valid(ReadView const& view, STTx const& tx, beast::Journal j) +// { +// } +// } + +} // namespace xrpl diff --git a/include/xrpl/ledger/helpers/TokenHelpers.h b/include/xrpl/ledger/helpers/TokenHelpers.h index f736e51d285..8cb5890bef3 100644 --- a/include/xrpl/ledger/helpers/TokenHelpers.h +++ b/include/xrpl/ledger/helpers/TokenHelpers.h @@ -231,6 +231,7 @@ canAddHolding(ReadView const& view, Asset const& asset); [[nodiscard]] TER addEmptyHolding( ApplyView& view, + STTx const& tx, AccountID const& accountID, XRPAmount priorBalance, Asset const& asset, @@ -239,6 +240,7 @@ addEmptyHolding( [[nodiscard]] TER removeEmptyHolding( ApplyView& view, + STTx const& tx, AccountID const& accountID, Asset const& asset, beast::Journal journal); @@ -299,6 +301,7 @@ accountSend( AccountID const& to, STAmount const& saAmount, beast::Journal j, + SLE::ref sponsorSle = {}, WaiveTransferFee waiveFee = WaiveTransferFee::No, AllowMPTOverflow allowOverflow = AllowMPTOverflow::No); @@ -316,6 +319,7 @@ accountSendMulti( Asset const& asset, MultiplePaymentDestinations const& receivers, beast::Journal j, + SLE::ref sponsorSle, WaiveTransferFee waiveFee = WaiveTransferFee::No); [[nodiscard]] TER diff --git a/include/xrpl/protocol/Fees.h b/include/xrpl/protocol/Fees.h index 14bcc068bf3..c79a7060f99 100644 --- a/include/xrpl/protocol/Fees.h +++ b/include/xrpl/protocol/Fees.h @@ -33,17 +33,6 @@ struct Fees : base(base), reserve(reserve), increment(increment) { } - - /** Returns the account reserve given the owner count, in drops. - - The reserve is calculated as the reserve base plus - the reserve increment times the number of increments. - */ - [[nodiscard]] XRPAmount - accountReserve(std::size_t ownerCount) const - { - return reserve + ownerCount * increment; - } }; } // namespace xrpl diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 887a208ec61..0da050b9e8b 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -151,6 +151,10 @@ static TicketT const kTicket{}; Keylet signers(AccountID const& account) noexcept; +/** A Sponsorship */ +Keylet +sponsor(AccountID const& sponsor, AccountID const& sponsee) noexcept; + /** A Check */ /** @{ */ Keylet diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 99d5d818f14..412dd781593 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -203,7 +203,11 @@ enum LedgerEntryType : std::uint16_t { LEDGER_OBJECT(Loan, \ LSF_FLAG(lsfLoanDefault, 0x00010000) \ LSF_FLAG(lsfLoanImpaired, 0x00020000) \ - LSF_FLAG(lsfLoanOverpayment, 0x00040000)) /* True, loan allows overpayments */ + LSF_FLAG(lsfLoanOverpayment, 0x00040000)) /* True, loan allows overpayments */ \ + \ + LEDGER_OBJECT(Sponsorship, \ + LSF_FLAG(lsfSponsorshipRequireSignForFee, 0x00010000) \ + LSF_FLAG(lsfSponsorshipRequireSignForReserve, 0x00020000)) // clang-format on diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index c89610f3544..bbe7da7861d 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -220,6 +220,7 @@ enum TERcodes : TERUnderlyingType { // create a pseudo-account terNO_DELEGATE_PERMISSION, // Delegate does not have permission terLOCKED, // MPT is locked + terNO_SPONSORSHIP, // No sponsorship found }; //------------------------------------------------------------------------------ @@ -358,6 +359,7 @@ enum TECcodes : TERUnderlyingType { tecLIMIT_EXCEEDED = 195, tecPSEUDO_ACCOUNT = 196, tecPRECISION_LOSS = 197, + tecNO_SPONSOR_PERMISSION = 198, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 4652cc1bf09..f300c253ca6 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -102,7 +102,8 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal; TRANSACTION(Payment, \ TF_FLAG(tfNoRippleDirect, 0x00010000) \ TF_FLAG(tfPartialPayment, 0x00020000) \ - TF_FLAG(tfLimitQuality, 0x00040000), \ + TF_FLAG(tfLimitQuality, 0x00040000) \ + TF_FLAG(tfSponsorCreatedAccount, 0x00080000), \ MASK_ADJ(0)) \ \ TRANSACTION(TrustSet, \ @@ -214,6 +215,20 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal; TF_FLAG(tfLoanDefault, 0x00010000) \ TF_FLAG(tfLoanImpair, 0x00020000) \ TF_FLAG(tfLoanUnimpair, 0x00040000), \ + MASK_ADJ(0)) \ + \ + TRANSACTION(SponsorshipSet, \ + TF_FLAG(tfSponsorshipSetRequireSignForFee, 0x00010000) \ + TF_FLAG(tfSponsorshipClearRequireSignForFee, 0x00020000) \ + TF_FLAG(tfSponsorshipSetRequireSignForReserve, 0x00040000) \ + TF_FLAG(tfSponsorshipClearRequireSignForReserve, 0x00080000) \ + TF_FLAG(tfDeleteObject, 0x00100000), \ + MASK_ADJ(0)) \ + \ + TRANSACTION(SponsorshipTransfer, \ + TF_FLAG(tfSponsorshipEnd, 0x00000001) \ + TF_FLAG(tfSponsorshipCreate, 0x00000002) \ + TF_FLAG(tfSponsorshipReassign, 0x00000004), \ MASK_ADJ(0)) // clang-format on @@ -338,6 +353,9 @@ getAllTxFlags() inline constexpr FlagValue tfMPTPaymentMask = ~(tfUniversal | tfPartialPayment); inline constexpr FlagValue tfTrustSetPermissionMask = ~(tfUniversal | tfSetfAuth | tfSetFreeze | tfClearFreeze); +inline constexpr FlagValue tfSponsorshipSetPermissionMask = + ~(tfUniversal | tfSponsorshipSetRequireSignForFee | tfSponsorshipSetRequireSignForReserve | + tfSponsorshipClearRequireSignForFee | tfSponsorshipClearRequireSignForReserve); // MPTokenIssuanceCreate MutableFlags: // Indicating specific fields or flags may be changed after issuance. @@ -445,6 +463,35 @@ getAsfFlagMap() #pragma pop_macro("ACCOUNTSET_FLAG_TO_MAP") #pragma pop_macro("ACCOUNTSET_FLAGS") +#pragma push_macro("SPONSOR_FLAGS") +#pragma push_macro("SPONSOR_FLAG_TO_VALUE") +#pragma push_macro("SPONSOR_FLAG_TO_MAP") + +// Sponsor Flag values +#define SPONSOR_FLAGS(SPF_FLAG) \ + SPF_FLAG(spfSponsorFee, 1) \ + SPF_FLAG(spfSponsorReserve, 2) + +#define SPONSOR_FLAG_TO_VALUE(name, value) inline constexpr FlagValue name = value; +#define SPONSOR_FLAG_TO_MAP(name, value) {#name, value}, + +SPONSOR_FLAGS(SPONSOR_FLAG_TO_VALUE) + +inline std::map const& +getspfFlagMap() +{ + static std::map const flags = {SPONSOR_FLAGS(SPONSOR_FLAG_TO_MAP)}; + return flags; +} + +#undef SPONSOR_FLAG_TO_VALUE +#undef SPONSOR_FLAG_TO_MAP +#undef SPONSOR_FLAGS + +#pragma pop_macro("SPONSOR_FLAG_TO_VALUE") +#pragma pop_macro("SPONSOR_FLAG_TO_MAP") +#pragma pop_macro("SPONSOR_FLAGS") + } // namespace xrpl // NOLINTEND(readability-identifier-naming) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index d3500ab144f..f6850919538 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -15,6 +15,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. +XRPL_FEATURE(Sponsor, Supported::No, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_3_0, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_2_0, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(MPTokensV2, Supported::No, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 632038a9c5b..7c534067a04 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -150,6 +150,9 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({ {sfAMMID, SoeOptional}, // pseudo-account designator {sfVaultID, SoeOptional}, // pseudo-account designator {sfLoanBrokerID, SoeOptional}, // pseudo-account designator + {sfSponsoredOwnerCount, SoeDefault}, + {sfSponsoringOwnerCount, SoeDefault}, + {sfSponsoringAccountCount,SoeDefault}, })) /** A ledger object which contains a list of object identifiers. @@ -286,6 +289,8 @@ LEDGER_ENTRY(ltRIPPLE_STATE, 0x0072, RippleState, state, ({ {sfHighNode, SoeOptional}, {sfHighQualityIn, SoeOptional}, {sfHighQualityOut, SoeOptional}, + {sfHighSponsor, SoeOptional}, + {sfLowSponsor, SoeOptional}, })) /** The ledger object which lists the network's fee settings. @@ -607,5 +612,20 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfLoanScale, SoeDefault}, })) +/** A ledger object representing a sponsorship. + \sa keylet::sponsor + */ +LEDGER_ENTRY(ltSPONSORSHIP, 0x0090, Sponsorship, sponsorship, ({ + {sfPreviousTxnID, SoeRequired}, + {sfPreviousTxnLgrSeq, SoeRequired}, + {sfOwner, SoeRequired}, + {sfSponsee, SoeRequired}, + {sfFeeAmount, SoeOptional}, + {sfMaxFee, SoeOptional}, + {sfReserveCount, SoeDefault}, + {sfOwnerNode, SoeRequired}, + {sfSponseeNode, SoeRequired}, +})) + #undef EXPAND #undef LEDGER_ENTRY_DUPLICATE diff --git a/include/xrpl/protocol/detail/permissions.macro b/include/xrpl/protocol/detail/permissions.macro index 729861a0132..6f31ced50bf 100644 --- a/include/xrpl/protocol/detail/permissions.macro +++ b/include/xrpl/protocol/detail/permissions.macro @@ -47,3 +47,9 @@ PERMISSION(MPTokenIssuanceLock, ttMPTOKEN_ISSUANCE_SET, 65547) /** This permission grants the delegated account the ability to unlock MPToken. */ PERMISSION(MPTokenIssuanceUnlock, ttMPTOKEN_ISSUANCE_SET, 65548) + +/** This permission grants the delegated account the ability to set SponsorFee. */ +PERMISSION(SponsorFee, ttSPONSORSHIP_SET, 65549) + +/** This permission grants the delegated account the ability to set SponsorReserve. */ +PERMISSION(SponsorReserve, ttSPONSORSHIP_SET, 65550) diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 01bb4fc4805..4870d448c7e 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -113,6 +113,11 @@ TYPED_SFIELD(sfInterestRate, UINT32, 65) // 1/10 basis points (bi TYPED_SFIELD(sfLateInterestRate, UINT32, 66) // 1/10 basis points (bips) TYPED_SFIELD(sfCloseInterestRate, UINT32, 67) // 1/10 basis points (bips) TYPED_SFIELD(sfOverpaymentInterestRate, UINT32, 68) // 1/10 basis points (bips) +TYPED_SFIELD(sfSponsoredOwnerCount, UINT32, 69) +TYPED_SFIELD(sfSponsoringOwnerCount, UINT32, 70) +TYPED_SFIELD(sfSponsoringAccountCount, UINT32, 71) +TYPED_SFIELD(sfReserveCount, UINT32, 72) +TYPED_SFIELD(sfSponsorFlags, UINT32, 73) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -146,6 +151,7 @@ TYPED_SFIELD(sfSubjectNode, UINT64, 28) TYPED_SFIELD(sfLockedAmount, UINT64, 29, SField::kSmdBaseTen|SField::kSmdDefault) TYPED_SFIELD(sfVaultNode, UINT64, 30) TYPED_SFIELD(sfLoanBrokerNode, UINT64, 31) +TYPED_SFIELD(sfSponseeNode, UINT64, 32) // 128-bit TYPED_SFIELD(sfEmailHash, UINT128, 1) @@ -206,6 +212,7 @@ TYPED_SFIELD(sfLoanBrokerID, UINT256, 37, SField::kSmdPseudoAccount | SField::kSmdDefault) TYPED_SFIELD(sfLoanID, UINT256, 38) TYPED_SFIELD(sfReferenceHolding, UINT256, 39) +TYPED_SFIELD(sfObjectID, UINT256, 40) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) @@ -265,6 +272,8 @@ TYPED_SFIELD(sfPrice, AMOUNT, 28) TYPED_SFIELD(sfSignatureReward, AMOUNT, 29) TYPED_SFIELD(sfMinAccountCreateAmount, AMOUNT, 30) TYPED_SFIELD(sfLPTokenBalance, AMOUNT, 31) +TYPED_SFIELD(sfFeeAmount, AMOUNT, 32) +TYPED_SFIELD(sfMaxFee, AMOUNT, 33) // variable length (common) TYPED_SFIELD(sfPublicKey, VL, 1) @@ -325,6 +334,11 @@ TYPED_SFIELD(sfIssuingChainDoor, ACCOUNT, 23) TYPED_SFIELD(sfSubject, ACCOUNT, 24) TYPED_SFIELD(sfBorrower, ACCOUNT, 25) TYPED_SFIELD(sfCounterparty, ACCOUNT, 26) +TYPED_SFIELD(sfSponsor, ACCOUNT, 27) +TYPED_SFIELD(sfHighSponsor, ACCOUNT, 28) +TYPED_SFIELD(sfLowSponsor, ACCOUNT, 29) +TYPED_SFIELD(sfCounterpartySponsor, ACCOUNT, 30) +TYPED_SFIELD(sfSponsee, ACCOUNT, 31) // vector of 256-bit TYPED_SFIELD(sfIndexes, VECTOR256, 1, SField::kSmdNever) @@ -389,6 +403,7 @@ UNTYPED_SFIELD(sfRawTransaction, OBJECT, 34) UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35) UNTYPED_SFIELD(sfBook, OBJECT, 36) UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 37, SField::kSmdDefault, SField::kNotSigning) +UNTYPED_SFIELD(sfSponsorSignature, OBJECT, 38, SField::kSmdDefault, SField::kNotSigning) // array of objects (common) // ARRAY/1 is reserved for end of array diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 450e2558cce..73e5173876b 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1077,6 +1077,35 @@ TRANSACTION(ttLOAN_PAY, 84, LoanPay, {sfAmount, SoeRequired, SoeMptSupported}, })) +/** This transaction transfer sponsorship */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttSPONSORSHIP_TRANSFER, 85, SponsorshipTransfer, + Delegation::Delegable, + featureSponsor, + NoPriv, + ({ + {sfObjectID, SoeOptional}, + {sfSponsee, SoeOptional}, +})) + +/** This transaction create sponsorship object */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttSPONSORSHIP_SET, 86, SponsorshipSet, + Delegation::Delegable, + featureSponsor, + NoPriv, + ({ + {sfCounterpartySponsor, SoeOptional}, + {sfSponsee, SoeOptional}, + {sfFeeAmount, SoeOptional}, + {sfMaxFee, SoeOptional}, + {sfReserveCount, SoeOptional}, +})) + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 8a2a1125427..d1a57124cc0 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -552,6 +552,9 @@ JSS(source_account); // in: PathRequest, RipplePathFind JSS(source_amount); // in: PathRequest, RipplePathFind JSS(source_currencies); // in: PathRequest, RipplePathFind JSS(source_tag); // out: AccountChannels +JSS(sponsee); // in: LedgerEntry +JSS(sponsor); // in: LedgerEntry +JSS(sponsored); // in: AccountObjects JSS(stand_alone); // out: NetworkOPs JSS(standard_deviation); // out: get_aggregate_price JSS(start); // in: TxHistory diff --git a/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h b/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h index f9a12a027f2..b442e77fe0f 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h +++ b/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h @@ -518,6 +518,78 @@ class AccountRoot : public LedgerEntryBase { return this->sle_->isFieldPresent(sfLoanBrokerID); } + + /** + * @brief Get sfSponsoredOwnerCount (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getSponsoredOwnerCount() const + { + if (hasSponsoredOwnerCount()) + return this->sle_->at(sfSponsoredOwnerCount); + return std::nullopt; + } + + /** + * @brief Check if sfSponsoredOwnerCount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasSponsoredOwnerCount() const + { + return this->sle_->isFieldPresent(sfSponsoredOwnerCount); + } + + /** + * @brief Get sfSponsoringOwnerCount (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getSponsoringOwnerCount() const + { + if (hasSponsoringOwnerCount()) + return this->sle_->at(sfSponsoringOwnerCount); + return std::nullopt; + } + + /** + * @brief Check if sfSponsoringOwnerCount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasSponsoringOwnerCount() const + { + return this->sle_->isFieldPresent(sfSponsoringOwnerCount); + } + + /** + * @brief Get sfSponsoringAccountCount (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getSponsoringAccountCount() const + { + if (hasSponsoringAccountCount()) + return this->sle_->at(sfSponsoringAccountCount); + return std::nullopt; + } + + /** + * @brief Check if sfSponsoringAccountCount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasSponsoringAccountCount() const + { + return this->sle_->isFieldPresent(sfSponsoringAccountCount); + } }; /** @@ -819,6 +891,39 @@ class AccountRootBuilder : public LedgerEntryBuilderBase return *this; } + /** + * @brief Set sfSponsoredOwnerCount (SoeDefault) + * @return Reference to this builder for method chaining. + */ + AccountRootBuilder& + setSponsoredOwnerCount(std::decay_t const& value) + { + object_[sfSponsoredOwnerCount] = value; + return *this; + } + + /** + * @brief Set sfSponsoringOwnerCount (SoeDefault) + * @return Reference to this builder for method chaining. + */ + AccountRootBuilder& + setSponsoringOwnerCount(std::decay_t const& value) + { + object_[sfSponsoringOwnerCount] = value; + return *this; + } + + /** + * @brief Set sfSponsoringAccountCount (SoeDefault) + * @return Reference to this builder for method chaining. + */ + AccountRootBuilder& + setSponsoringAccountCount(std::decay_t const& value) + { + object_[sfSponsoringAccountCount] = value; + return *this; + } + /** * @brief Build and return the completed AccountRoot wrapper. * @param index The ledger entry index. diff --git a/include/xrpl/protocol_autogen/ledger_entries/RippleState.h b/include/xrpl/protocol_autogen/ledger_entries/RippleState.h index e8debfe7924..388a062ec12 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/RippleState.h +++ b/include/xrpl/protocol_autogen/ledger_entries/RippleState.h @@ -243,6 +243,54 @@ class RippleState : public LedgerEntryBase { return this->sle_->isFieldPresent(sfHighQualityOut); } + + /** + * @brief Get sfHighSponsor (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getHighSponsor() const + { + if (hasHighSponsor()) + return this->sle_->at(sfHighSponsor); + return std::nullopt; + } + + /** + * @brief Check if sfHighSponsor is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasHighSponsor() const + { + return this->sle_->isFieldPresent(sfHighSponsor); + } + + /** + * @brief Get sfLowSponsor (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getLowSponsor() const + { + if (hasLowSponsor()) + return this->sle_->at(sfLowSponsor); + return std::nullopt; + } + + /** + * @brief Check if sfLowSponsor is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasLowSponsor() const + { + return this->sle_->isFieldPresent(sfLowSponsor); + } }; /** @@ -410,6 +458,28 @@ class RippleStateBuilder : public LedgerEntryBuilderBase return *this; } + /** + * @brief Set sfHighSponsor (SoeOptional) + * @return Reference to this builder for method chaining. + */ + RippleStateBuilder& + setHighSponsor(std::decay_t const& value) + { + object_[sfHighSponsor] = value; + return *this; + } + + /** + * @brief Set sfLowSponsor (SoeOptional) + * @return Reference to this builder for method chaining. + */ + RippleStateBuilder& + setLowSponsor(std::decay_t const& value) + { + object_[sfLowSponsor] = value; + return *this; + } + /** * @brief Build and return the completed RippleState wrapper. * @param index The ledger entry index. diff --git a/include/xrpl/protocol_autogen/ledger_entries/Sponsorship.h b/include/xrpl/protocol_autogen/ledger_entries/Sponsorship.h new file mode 100644 index 00000000000..96b46a6aab3 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Sponsorship.h @@ -0,0 +1,344 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::ledger_entries { + +class SponsorshipBuilder; + +/** + * @brief Ledger Entry: Sponsorship + * + * Type: ltSPONSORSHIP (0x0090) + * RPC Name: sponsorship + * + * Immutable wrapper around SLE providing type-safe field access. + * Use SponsorshipBuilder to construct new ledger entries. + */ +class Sponsorship : public LedgerEntryBase +{ +public: + static constexpr LedgerEntryType entryType = ltSPONSORSHIP; + + /** + * @brief Construct a Sponsorship ledger entry wrapper from an existing SLE object. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + explicit Sponsorship(SLE::const_pointer sle) + : LedgerEntryBase(std::move(sle)) + { + // Verify ledger entry type + if (sle_->getType() != entryType) + { + throw std::runtime_error("Invalid ledger entry type for Sponsorship"); + } + } + + // Ledger entry-specific field getters + + /** + * @brief Get sfPreviousTxnID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getPreviousTxnID() const + { + return this->sle_->at(sfPreviousTxnID); + } + + /** + * @brief Get sfPreviousTxnLgrSeq (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getPreviousTxnLgrSeq() const + { + return this->sle_->at(sfPreviousTxnLgrSeq); + } + + /** + * @brief Get sfOwner (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getOwner() const + { + return this->sle_->at(sfOwner); + } + + /** + * @brief Get sfSponsee (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getSponsee() const + { + return this->sle_->at(sfSponsee); + } + + /** + * @brief Get sfFeeAmount (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getFeeAmount() const + { + if (hasFeeAmount()) + return this->sle_->at(sfFeeAmount); + return std::nullopt; + } + + /** + * @brief Check if sfFeeAmount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasFeeAmount() const + { + return this->sle_->isFieldPresent(sfFeeAmount); + } + + /** + * @brief Get sfMaxFee (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getMaxFee() const + { + if (hasMaxFee()) + return this->sle_->at(sfMaxFee); + return std::nullopt; + } + + /** + * @brief Check if sfMaxFee is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasMaxFee() const + { + return this->sle_->isFieldPresent(sfMaxFee); + } + + /** + * @brief Get sfReserveCount (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getReserveCount() const + { + if (hasReserveCount()) + return this->sle_->at(sfReserveCount); + return std::nullopt; + } + + /** + * @brief Check if sfReserveCount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasReserveCount() const + { + return this->sle_->isFieldPresent(sfReserveCount); + } + + /** + * @brief Get sfOwnerNode (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getOwnerNode() const + { + return this->sle_->at(sfOwnerNode); + } + + /** + * @brief Get sfSponseeNode (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getSponseeNode() const + { + return this->sle_->at(sfSponseeNode); + } +}; + +/** + * @brief Builder for Sponsorship ledger entries. + * + * Provides a fluent interface for constructing ledger entries with method chaining. + * Uses STObject internally for flexible ledger entry construction. + * Inherits common field setters from LedgerEntryBuilderBase. + */ +class SponsorshipBuilder : public LedgerEntryBuilderBase +{ +public: + /** + * @brief Construct a new SponsorshipBuilder with required fields. + * @param previousTxnID The sfPreviousTxnID field value. + * @param previousTxnLgrSeq The sfPreviousTxnLgrSeq field value. + * @param owner The sfOwner field value. + * @param sponsee The sfSponsee field value. + * @param ownerNode The sfOwnerNode field value. + * @param sponseeNode The sfSponseeNode field value. + */ + SponsorshipBuilder(std::decay_t const& previousTxnID,std::decay_t const& previousTxnLgrSeq,std::decay_t const& owner,std::decay_t const& sponsee,std::decay_t const& ownerNode,std::decay_t const& sponseeNode) + : LedgerEntryBuilderBase(ltSPONSORSHIP) + { + setPreviousTxnID(previousTxnID); + setPreviousTxnLgrSeq(previousTxnLgrSeq); + setOwner(owner); + setSponsee(sponsee); + setOwnerNode(ownerNode); + setSponseeNode(sponseeNode); + } + + /** + * @brief Construct a SponsorshipBuilder from an existing SLE object. + * @param sle The existing ledger entry to copy from. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + SponsorshipBuilder(SLE::const_pointer sle) + { + if (sle->at(sfLedgerEntryType) != ltSPONSORSHIP) + { + throw std::runtime_error("Invalid ledger entry type for Sponsorship"); + } + object_ = *sle; + } + + /** @brief Ledger entry-specific field setters */ + + /** + * @brief Set sfPreviousTxnID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setPreviousTxnID(std::decay_t const& value) + { + object_[sfPreviousTxnID] = value; + return *this; + } + + /** + * @brief Set sfPreviousTxnLgrSeq (SoeRequired) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setPreviousTxnLgrSeq(std::decay_t const& value) + { + object_[sfPreviousTxnLgrSeq] = value; + return *this; + } + + /** + * @brief Set sfOwner (SoeRequired) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setOwner(std::decay_t const& value) + { + object_[sfOwner] = value; + return *this; + } + + /** + * @brief Set sfSponsee (SoeRequired) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setSponsee(std::decay_t const& value) + { + object_[sfSponsee] = value; + return *this; + } + + /** + * @brief Set sfFeeAmount (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setFeeAmount(std::decay_t const& value) + { + object_[sfFeeAmount] = value; + return *this; + } + + /** + * @brief Set sfMaxFee (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setMaxFee(std::decay_t const& value) + { + object_[sfMaxFee] = value; + return *this; + } + + /** + * @brief Set sfReserveCount (SoeDefault) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setReserveCount(std::decay_t const& value) + { + object_[sfReserveCount] = value; + return *this; + } + + /** + * @brief Set sfOwnerNode (SoeRequired) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setOwnerNode(std::decay_t const& value) + { + object_[sfOwnerNode] = value; + return *this; + } + + /** + * @brief Set sfSponseeNode (SoeRequired) + * @return Reference to this builder for method chaining. + */ + SponsorshipBuilder& + setSponseeNode(std::decay_t const& value) + { + object_[sfSponseeNode] = value; + return *this; + } + + /** + * @brief Build and return the completed Sponsorship wrapper. + * @param index The ledger entry index. + * @return The constructed ledger entry wrapper. + */ + Sponsorship + build(uint256 const& index) + { + return Sponsorship{std::make_shared(std::move(object_), index)}; + } +}; + +} // namespace xrpl::ledger_entries diff --git a/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h b/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h new file mode 100644 index 00000000000..0b64104e618 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h @@ -0,0 +1,290 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class SponsorshipSetBuilder; + +/** + * @brief Transaction: SponsorshipSet + * + * Type: ttSPONSORSHIP_SET (86) + * Delegable: Delegation::Delegable + * Amendment: featureSponsor + * Privileges: NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use SponsorshipSetBuilder to construct new transactions. + */ +class SponsorshipSet : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttSPONSORSHIP_SET; + + /** + * @brief Construct a SponsorshipSet transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit SponsorshipSet(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for SponsorshipSet"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfCounterpartySponsor (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getCounterpartySponsor() const + { + if (hasCounterpartySponsor()) + { + return this->tx_->at(sfCounterpartySponsor); + } + return std::nullopt; + } + + /** + * @brief Check if sfCounterpartySponsor is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCounterpartySponsor() const + { + return this->tx_->isFieldPresent(sfCounterpartySponsor); + } + + /** + * @brief Get sfSponsee (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getSponsee() const + { + if (hasSponsee()) + { + return this->tx_->at(sfSponsee); + } + return std::nullopt; + } + + /** + * @brief Check if sfSponsee is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasSponsee() const + { + return this->tx_->isFieldPresent(sfSponsee); + } + + /** + * @brief Get sfFeeAmount (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getFeeAmount() const + { + if (hasFeeAmount()) + { + return this->tx_->at(sfFeeAmount); + } + return std::nullopt; + } + + /** + * @brief Check if sfFeeAmount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasFeeAmount() const + { + return this->tx_->isFieldPresent(sfFeeAmount); + } + + /** + * @brief Get sfMaxFee (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getMaxFee() const + { + if (hasMaxFee()) + { + return this->tx_->at(sfMaxFee); + } + return std::nullopt; + } + + /** + * @brief Check if sfMaxFee is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasMaxFee() const + { + return this->tx_->isFieldPresent(sfMaxFee); + } + + /** + * @brief Get sfReserveCount (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getReserveCount() const + { + if (hasReserveCount()) + { + return this->tx_->at(sfReserveCount); + } + return std::nullopt; + } + + /** + * @brief Check if sfReserveCount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasReserveCount() const + { + return this->tx_->isFieldPresent(sfReserveCount); + } +}; + +/** + * @brief Builder for SponsorshipSet transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class SponsorshipSetBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new SponsorshipSetBuilder with required fields. + * @param account The account initiating the transaction. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + SponsorshipSetBuilder(SF_ACCOUNT::type::value_type account, + std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttSPONSORSHIP_SET, account, sequence, fee) + { + } + + /** + * @brief Construct a SponsorshipSetBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + SponsorshipSetBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttSPONSORSHIP_SET) + { + throw std::runtime_error("Invalid transaction type for SponsorshipSetBuilder"); + } + object_ = *tx; + } + + /** @brief Transaction-specific field setters */ + + /** + * @brief Set sfCounterpartySponsor (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipSetBuilder& + setCounterpartySponsor(std::decay_t const& value) + { + object_[sfCounterpartySponsor] = value; + return *this; + } + + /** + * @brief Set sfSponsee (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipSetBuilder& + setSponsee(std::decay_t const& value) + { + object_[sfSponsee] = value; + return *this; + } + + /** + * @brief Set sfFeeAmount (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipSetBuilder& + setFeeAmount(std::decay_t const& value) + { + object_[sfFeeAmount] = value; + return *this; + } + + /** + * @brief Set sfMaxFee (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipSetBuilder& + setMaxFee(std::decay_t const& value) + { + object_[sfMaxFee] = value; + return *this; + } + + /** + * @brief Set sfReserveCount (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipSetBuilder& + setReserveCount(std::decay_t const& value) + { + object_[sfReserveCount] = value; + return *this; + } + + /** + * @brief Build and return the SponsorshipSet wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + SponsorshipSet + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return SponsorshipSet{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h b/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h new file mode 100644 index 00000000000..8ac43071ee8 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h @@ -0,0 +1,179 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class SponsorshipTransferBuilder; + +/** + * @brief Transaction: SponsorshipTransfer + * + * Type: ttSPONSORSHIP_TRANSFER (85) + * Delegable: Delegation::Delegable + * Amendment: featureSponsor + * Privileges: NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use SponsorshipTransferBuilder to construct new transactions. + */ +class SponsorshipTransfer : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttSPONSORSHIP_TRANSFER; + + /** + * @brief Construct a SponsorshipTransfer transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit SponsorshipTransfer(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for SponsorshipTransfer"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfObjectID (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getObjectID() const + { + if (hasObjectID()) + { + return this->tx_->at(sfObjectID); + } + return std::nullopt; + } + + /** + * @brief Check if sfObjectID is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasObjectID() const + { + return this->tx_->isFieldPresent(sfObjectID); + } + + /** + * @brief Get sfSponsee (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getSponsee() const + { + if (hasSponsee()) + { + return this->tx_->at(sfSponsee); + } + return std::nullopt; + } + + /** + * @brief Check if sfSponsee is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasSponsee() const + { + return this->tx_->isFieldPresent(sfSponsee); + } +}; + +/** + * @brief Builder for SponsorshipTransfer transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class SponsorshipTransferBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new SponsorshipTransferBuilder with required fields. + * @param account The account initiating the transaction. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + SponsorshipTransferBuilder(SF_ACCOUNT::type::value_type account, + std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttSPONSORSHIP_TRANSFER, account, sequence, fee) + { + } + + /** + * @brief Construct a SponsorshipTransferBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + SponsorshipTransferBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttSPONSORSHIP_TRANSFER) + { + throw std::runtime_error("Invalid transaction type for SponsorshipTransferBuilder"); + } + object_ = *tx; + } + + /** @brief Transaction-specific field setters */ + + /** + * @brief Set sfObjectID (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipTransferBuilder& + setObjectID(std::decay_t const& value) + { + object_[sfObjectID] = value; + return *this; + } + + /** + * @brief Set sfSponsee (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SponsorshipTransferBuilder& + setSponsee(std::decay_t const& value) + { + object_[sfSponsee] = value; + return *this; + } + + /** + * @brief Build and return the SponsorshipTransfer wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + SponsorshipTransfer + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return SponsorshipTransfer{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/tx/Transactor.h b/include/xrpl/tx/Transactor.h index 86b1e856b33..d419e561283 100644 --- a/include/xrpl/tx/Transactor.h +++ b/include/xrpl/tx/Transactor.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -108,6 +109,20 @@ struct PreflightResult; // Needed for preflight specialization class Change; +enum class FeePayerType { + Account, + Delegate, + SponsorCoSigned, + SponsorPreFunded, +}; + +struct FeePayer +{ + Keylet entry; + SF_AMOUNT const& balanceField; + FeePayerType type{FeePayerType::Account}; +}; + class Transactor { protected: @@ -224,6 +239,9 @@ class Transactor static NotTEC checkPermission(ReadView const& view, STTx const& tx); + + static NotTEC + checkSponsor(ReadView const& view, STTx const& tx); ///////////////////////////////////////////////////// // Interface used by AccountDelete @@ -356,6 +374,9 @@ class Transactor std::pair reset(XRPAmount fee); + static FeePayer + getFeePayer(ReadView const& view, STTx const& tx); + TER consumeSeqProxy(SLE::pointer const& sleAccount); TER diff --git a/include/xrpl/tx/invariants/InvariantCheck.h b/include/xrpl/tx/invariants/InvariantCheck.h index 93780627262..3ed02fda67f 100644 --- a/include/xrpl/tx/invariants/InvariantCheck.h +++ b/include/xrpl/tx/invariants/InvariantCheck.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -415,7 +416,9 @@ using InvariantChecks = std::tuple< ValidVault, ValidMPTPayment, ValidAmounts, - ValidMPTTransfer>; + ValidMPTTransfer, + SponsorshipOwnerCountsMatch, + SponsorshipAccountCountMatchesField>; /** * @brief get a tuple of all invariant checks diff --git a/include/xrpl/tx/invariants/SponsorshipInvariant.h b/include/xrpl/tx/invariants/SponsorshipInvariant.h new file mode 100644 index 00000000000..8bae9d8d479 --- /dev/null +++ b/include/xrpl/tx/invariants/SponsorshipInvariant.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace xrpl { + +/** + * @brief Invariant: Sponsored owner counts are balanced. + * + * The following check is made for every transaction: + * - The sum of all per-account deltas of `sfSponsoredOwnerCount` equals + * the sum of all per-account deltas of `sfSponsoringOwnerCount`. + * - Account OwnerCount must be greater than or equal to SponsoredOwnerCount. + */ +class SponsorshipOwnerCountsMatch +{ + std::int64_t deltaSponsoredOwnerCount_ = 0; + std::int64_t deltaSponsoringOwnerCount_ = 0; + std::int64_t deltaSponsoredObjectOwnerCount_ = 0; + std::uint64_t invalidOwnerCountLessThanSponsoredOwnerCount_ = 0; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + [[nodiscard]] bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const; +}; + +/** + * @brief Invariant: Sponsoring account relationships tracked consistently. + * + * The following check is made for every transaction: + * - The net delta of `sfSponsoringAccountCount` across all accounts equals + * the net delta of the count of ltACCOUNT_ROOT entries having + * `sfSponsor` present (presence transitions only: add/remove). + */ +class SponsorshipAccountCountMatchesField +{ + std::int64_t deltaSponsoringAccountCount_ = 0; + std::int64_t deltaSponsorFieldPresence_ = 0; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + [[nodiscard]] bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/paths/AMMOffer.h b/include/xrpl/tx/paths/AMMOffer.h index 21c45c36a3c..4292d67015d 100644 --- a/include/xrpl/tx/paths/AMMOffer.h +++ b/include/xrpl/tx/paths/AMMOffer.h @@ -104,7 +104,10 @@ class AMMOffer send(Args&&... args) { return accountSend( - std::forward(args)..., WaiveTransferFee::Yes, AllowMPTOverflow::Yes); + std::forward(args)..., + SLE::pointer(), + WaiveTransferFee::Yes, + AllowMPTOverflow::Yes); } [[nodiscard]] bool diff --git a/include/xrpl/tx/paths/Offer.h b/include/xrpl/tx/paths/Offer.h index 2dab5bcebf7..f7b8fcc608b 100644 --- a/include/xrpl/tx/paths/Offer.h +++ b/include/xrpl/tx/paths/Offer.h @@ -224,7 +224,8 @@ template TER TOffer::send(Args&&... args) { - return accountSend(std::forward(args)..., WaiveTransferFee::No, AllowMPTOverflow::Yes); + return accountSend( + std::forward(args)..., SLE::pointer(), WaiveTransferFee::No, AllowMPTOverflow::Yes); } template diff --git a/include/xrpl/tx/transactors/dex/AMMWithdraw.h b/include/xrpl/tx/transactors/dex/AMMWithdraw.h index 6e88320eae7..1973968adb9 100644 --- a/include/xrpl/tx/transactors/dex/AMMWithdraw.h +++ b/include/xrpl/tx/transactors/dex/AMMWithdraw.h @@ -100,6 +100,7 @@ class AMMWithdraw : public Transactor static std::tuple> equalWithdrawTokens( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const account, AccountID const& ammAccount, @@ -134,6 +135,7 @@ class AMMWithdraw : public Transactor static std::tuple> withdraw( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, AccountID const& account, @@ -177,6 +179,7 @@ class AMMWithdraw : public Transactor std::pair withdraw( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -202,6 +205,7 @@ class AMMWithdraw : public Transactor std::pair equalWithdrawTokens( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -227,6 +231,7 @@ class AMMWithdraw : public Transactor std::pair equalWithdrawLimit( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -249,6 +254,7 @@ class AMMWithdraw : public Transactor std::pair singleWithdraw( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -270,6 +276,7 @@ class AMMWithdraw : public Transactor std::pair singleWithdrawTokens( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -292,6 +299,7 @@ class AMMWithdraw : public Transactor std::pair singleWithdrawEPrice( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, diff --git a/include/xrpl/tx/transactors/oracle/OracleSet.h b/include/xrpl/tx/transactors/oracle/OracleSet.h index 831c11b8c4f..51ed7be3dcd 100644 --- a/include/xrpl/tx/transactors/oracle/OracleSet.h +++ b/include/xrpl/tx/transactors/oracle/OracleSet.h @@ -22,6 +22,12 @@ class OracleSet : public Transactor { } + static uint32_t + calculateOracleReserve(std::size_t count) + { + return count > 5 ? 2 : 1; + } + static NotTEC preflight(PreflightContext const& ctx); diff --git a/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h b/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h new file mode 100644 index 00000000000..3eaeba6392a --- /dev/null +++ b/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h @@ -0,0 +1,49 @@ +#pragma once + +#include + +namespace xrpl { + +class SponsorshipSet : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit SponsorshipSet(ApplyContext& ctx) : Transactor(ctx) + { + } + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static NotTEC + checkPermission(ReadView const& view, STTx const& tx); + + static TER + preclaim(PreclaimContext const& ctx); + + static TER + deleteSponsorship(ApplyView& view, SLE::ref sle, beast::Journal j); + + TER + doApply() override; + + void + visitInvariantEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/sponsor/SponsorshipTransfer.h b/include/xrpl/tx/transactors/sponsor/SponsorshipTransfer.h new file mode 100644 index 00000000000..a473e5008df --- /dev/null +++ b/include/xrpl/tx/transactors/sponsor/SponsorshipTransfer.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +namespace xrpl { + +class SponsorshipTransfer : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit SponsorshipTransfer(ApplyContext& ctx) : Transactor(ctx) + { + } + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h index d946587e324..13c3c1f3d1c 100644 --- a/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h @@ -63,7 +63,7 @@ class MPTokenIssuanceCreate : public Transactor beast::Journal const& j) override; static std::expected - create(ApplyView& view, beast::Journal journal, MPTCreateArgs const& args); + create(ApplyView& view, STTx const& tx, beast::Journal journal, MPTCreateArgs const& args); }; } // namespace xrpl diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index fdd7998609e..7bc99edc02e 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -443,7 +444,7 @@ doWithdraw( // Create trust line or MPToken for the receiving account if (dstAcct == senderAcct) { - if (auto const ter = addEmptyHolding(view, senderAcct, priorBalance, amount.asset(), j); + if (auto const ter = addEmptyHolding(view, tx, senderAcct, priorBalance, amount.asset(), j); !isTesSuccess(ter) && ter != tecDUPLICATE) return ter; } @@ -469,9 +470,13 @@ doWithdraw( // LCOV_EXCL_STOP } + auto const sponsorSle = getTxReserveSponsor(view, tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + // Move the funds directly from the broker's pseudo-account to the // dstAcct - return accountSend(view, sourceAcct, dstAcct, amount, j, WaiveTransferFee::Yes); + return accountSend(view, sourceAcct, dstAcct, amount, j, *sponsorSle, WaiveTransferFee::Yes); } TER diff --git a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp index 1c4acc7bc49..2d0573f36e8 100644 --- a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp +++ b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -15,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -83,6 +85,109 @@ confineOwnerCount( return adjusted; } +static std::uint32_t +ownerCountHlp( + ReadView const& view, + SLE::const_ref sle, + std::int32_t adjustment, + bool reportConfine, + beast::Journal j) +{ + AccountID const id = sle->getAccountID(sfAccount); + std::uint32_t const savedCount = sle->at(sfOwnerCount); + std::uint32_t const hookedCount = view.ownerCountHook(id, savedCount); + + std::uint32_t const sponsoredCount = sle->at(sfSponsoredOwnerCount); + std::uint32_t const sponsoringCount = sle->at(sfSponsoringOwnerCount); + + if (hookedCount < sponsoredCount) + { + Throw( + "xrpl::ownerCountHlp : OwnerCount must be greater than or equal to " + "SponsoredOwnerCount"); + } + + std::int64_t deltaCount = + static_cast(adjustment) - sponsoredCount + sponsoringCount; + if (deltaCount > std::numeric_limits::max()) + { + deltaCount = std::numeric_limits::max(); + JLOG(j.fatal()) << "Account " << id << " delta count exceeds max, " + << "adjustment: " << adjustment << ", sponsoredCount: " << sponsoredCount + << ", sponsoringOwnerCount: " << sponsoringCount; + } + else if (deltaCount < std::numeric_limits::min()) + { + deltaCount = std::numeric_limits::min(); + JLOG(j.fatal()) << "Account " << id << " delta count exceeds min, " + << "adjustment: " << adjustment << ", sponsoredCount: " << sponsoredCount + << ", sponsoringCount: " << sponsoringCount; + } + + std::uint32_t const confinedCount = reportConfine + ? confineOwnerCount(hookedCount, deltaCount, id, j) + : confineOwnerCount(hookedCount, deltaCount); + + return confinedCount; +} + +static std::uint32_t +reserveCountHlp(SLE::const_ref sle, std::int32_t adjustment, beast::Journal j) +{ + bool const isSponsored = sle->isFieldPresent(sfSponsor); + std::uint32_t const sponsoringCount = sle->getFieldU32(sfSponsoringAccountCount); + std::uint32_t const reserveCount = (isSponsored ? 0 : 1) + sponsoringCount; + + std::uint32_t adjusted{reserveCount + adjustment}; + if (adjustment > 0) + { + // Overflow is well defined on unsigned + if (adjusted < reserveCount) + { + JLOG(j.fatal()) << "Reserve count exceeds max!"; + adjusted = std::numeric_limits::max(); + } + } + else + { + // Underflow is well defined on unsigned + if (adjusted > reserveCount) + { + JLOG(j.fatal()) << "Reserve count set below 0!"; + adjusted = 0; + } + } + return adjusted; +} + +static inline XRPAmount +baseReserveHlp(ReadView const& view, std::uint32_t ownerCount, std::uint32_t reserveCount) +{ + auto const& fees = view.fees(); + return (fees.reserve * reserveCount) + (fees.increment * ownerCount); +} + +static XRPAmount +reserveHlp( + ReadView const& view, + SLE::const_ref sle, + std::uint32_t ownerCount, + std::uint32_t reserveCount) +{ + // Pseudo-accounts have no reserve requirement + if (isPseudoAccount(sle)) + return XRPAmount(0); + + auto const reserve = baseReserveHlp(view, ownerCount, reserveCount); + return reserve; +} + +std::uint32_t +ownerCount(ReadView const& view, SLE::const_ref sle, beast::Journal j, std::int32_t adjustment) +{ + return ownerCountHlp(view, sle, adjustment, true, j); +} + XRPAmount xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, beast::Journal j) { @@ -90,13 +195,9 @@ xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, if (sle == nullptr) return beast::kZero; - // Return balance minus reserve - std::uint32_t const ownerCount = - confineOwnerCount(view.ownerCountHook(id, sle->getFieldU32(sfOwnerCount)), ownerCountAdj); - - // Pseudo-accounts have no reserve requirement - auto const reserve = - isPseudoAccount(sle) ? XRPAmount{0} : view.fees().accountReserve(ownerCount); + std::uint32_t const ownerCount = ownerCountHlp(view, sle, ownerCountAdj, false, j); + std::uint32_t const reserveCount = reserveCountHlp(sle, 0, j); + auto const reserve = reserveHlp(view, sle, ownerCount, reserveCount); auto const fullBalance = sle->getFieldAmount(sfBalance); @@ -124,20 +225,158 @@ transferRate(ReadView const& view, AccountID const& issuer) return kParityRate; } +static void +adjustOwnerCountHlp( + ApplyView& view, + SLE::ref sle, + SF_UINT32 const& sfield, + AccountID const& accID, + std::int32_t adjustment, + beast::Journal j, + bool callHook = true) +{ + std::uint32_t const current = sle->at(sfield); + std::uint32_t const adjusted = confineOwnerCount(current, adjustment, accID, j); + if (callHook) + view.adjustOwnerCountHook(accID, current, adjusted); + sle->at(sfield) = adjusted; + view.update(sle); +} + void -adjustOwnerCount(ApplyView& view, SLE::ref sle, std::int32_t amount, beast::Journal j) +adjustOwnerCount( + ApplyView& view, + SLE::ref accountSle, + SLE::ref sponsorSle, + std::int32_t adjustment, + beast::Journal j) { - if (!sle) + if (!accountSle) + Throw("xrpl::adjustOwnerCount : valid account sle"); + + auto const sleType = accountSle->getType(); + bool const validType = sponsorSle ? sleType == ltACCOUNT_ROOT + : sleType == ltLOAN_BROKER || sleType == ltACCOUNT_ROOT; + if (!validType) + Throw("xrpl::adjustOwnerCount : valid account sle type"); + + XRPL_ASSERT(adjustment, "xrpl::adjustOwnerCount : nonzero adjustment input"); + if (adjustment == 0) return; - XRPL_ASSERT(amount, "xrpl::adjustOwnerCount : nonzero amount input"); - std::uint32_t const current{sle->getFieldU32(sfOwnerCount)}; - AccountID const id = (*sle)[sfAccount]; - std::uint32_t const adjusted = confineOwnerCount(current, amount, id, j); - view.adjustOwnerCountHook(id, current, adjusted); - sle->at(sfOwnerCount) = adjusted; - view.update(sle); + + auto const accountID = accountSle->getAccountID(sfAccount); + if (sponsorSle) + { + if (sponsorSle->getType() != ltACCOUNT_ROOT) + Throw("xrpl::adjustOwnerCount : valid sponsor sle type"); + auto const sponsorID = sponsorSle->getAccountID(sfAccount); + + adjustOwnerCountHlp(view, accountSle, sfSponsoredOwnerCount, accountID, adjustment, j); + adjustOwnerCountHlp(view, sponsorSle, sfSponsoringOwnerCount, sponsorID, adjustment, j); + + auto sponsorObjSle = view.peek(keylet::sponsor(sponsorID, accountID)); + if (sponsorObjSle && adjustment > 0) + { + // update the pre-funded ReserveCount on Sponsorship ledger object + // Reserve count moves opposite to adjustment: +adjustment => consume reserve (-), + adjustOwnerCountHlp( + view, sponsorObjSle, sfReserveCount, sponsorID, -adjustment, j, false); + } + } + adjustOwnerCountHlp(view, accountSle, sfOwnerCount, accountID, adjustment, j); +} + +void +adjustOwnerCountObj( + ApplyView& view, + SLE::ref accountSle, + SLE::ref objectSle, + std::int32_t amount, + beast::Journal j) +{ + if (!objectSle) + Throw("xrpl::adjustOwnerCount : valid object sle"); + if (objectSle->getType() == ltACCOUNT_ROOT) + Throw("xrpl::adjustOwnerCount : valid object sle type"); + + SLE::ref sponsorSle = getLedgerEntryReserveSponsor(view, objectSle); + adjustOwnerCount(view, accountSle, sponsorSle, amount, j); } +XRPAmount +accountReserve( + ReadView const& view, + SLE::const_ref sle, + beast::Journal j, + std::int32_t ownerCountAdj, + std::int32_t reserveCountAdj) +{ + if (!sle) + Throw("xrpl::accountReserve : valid sle"); + if (sle->getType() != ltACCOUNT_ROOT) + Throw("xrpl::accountReserve : valid sle type"); + + std::uint32_t const ownerCount = ownerCountHlp(view, sle, ownerCountAdj, true, j); + std::uint32_t const reserveCount = reserveCountHlp(sle, reserveCountAdj, j); + + return reserveHlp(view, sle, ownerCount, reserveCount); +} + +XRPAmount +baseAccountReserve(ReadView const& view, std::int32_t ownerCount) +{ + auto const reserve = baseReserveHlp(view, ownerCount, 1); + return reserve; +} + +TER +checkInsufficientReserve( + ReadView const& view, + STTx const& tx, + SLE::const_ref accSle, + STAmount const& accBalance, + SLE::const_ref sponsorSle, + std::int32_t ownerCountDelta, + std::int32_t reserveCountDelta, + beast::Journal j) +{ + if (sponsorSle) + { + auto const isCoSigning = isSponsorReserveCoSigning(tx); + + auto const sle = view.read( + keylet::sponsor(sponsorSle->getAccountID(sfAccount), accSle->getAccountID(sfAccount))); + + // prefunded sponsor should have a sponsorship entry + if (!isCoSigning && !sle) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (sle) + { + auto const ownerCountAllowed = sle->getFieldU32(sfReserveCount); + if (ownerCountAllowed < ownerCountDelta) + return tecINSUFFICIENT_RESERVE; + } + + auto const sponsorBalance = sponsorSle->getFieldAmount(sfBalance); + STAmount const sponsorReserve = + accountReserve(view, sponsorSle, j, ownerCountDelta, reserveCountDelta); + + if (sponsorBalance < sponsorReserve) + return tecINSUFFICIENT_RESERVE; + } + else + { + STAmount const reserve = + accountReserve(view, accSle, j, ownerCountDelta, reserveCountDelta); + if (accBalance < reserve) + return tecINSUFFICIENT_RESERVE; + } + return tesSUCCESS; +} + +// ---------------------------------------------------- + AccountID pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey) { @@ -188,7 +427,7 @@ getPseudoAccountFields() } [[nodiscard]] bool -isPseudoAccount(SLE::const_pointer sleAcct, std::set const& pseudoFieldFilter) +isPseudoAccount(SLE::const_ref sleAcct, std::set const& pseudoFieldFilter) { auto const& fields = getPseudoAccountFields(); diff --git a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp index ca5876f88a4..1f6faeef860 100644 --- a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp +++ b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp @@ -97,7 +97,7 @@ deleteSLE(ApplyView& view, SLE::ref sleCredential, beast::Journal j) } if (isOwner) - adjustOwnerCount(view, sleAccount, -1, j); + adjustOwnerCountObj(view, sleAccount, sleCredential, -1, j); return tesSUCCESS; }; diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index 8b3385471d2..23269f710a2 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -11,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -126,6 +127,7 @@ canAddHolding(ReadView const& view, MPTIssue const& mptIssue) [[nodiscard]] TER addEmptyHolding( ApplyView& view, + STTx const& tx, AccountID const& accountID, XRPAmount priorBalance, MPTIssue const& mptIssue, @@ -142,12 +144,13 @@ addEmptyHolding( if (accountID == mptIssue.getIssuer()) return tesSUCCESS; - return authorizeMPToken(view, priorBalance, mptID, accountID, journal); + return authorizeMPToken(view, tx, priorBalance, mptID, accountID, journal); } [[nodiscard]] TER authorizeMPToken( ApplyView& view, + STTx const& tx, XRPAmount const& priorBalance, MPTID const& mptIssuanceID, AccountID const& account, @@ -180,7 +183,7 @@ authorizeMPToken( keylet::ownerDir(account), (*sleMpt)[sfOwnerNode], sleMpt->key(), false)) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleAcct, -1, journal); + adjustOwnerCountObj(view, sleAcct, sleMpt, -1, journal); view.erase(sleMpt); return tesSUCCESS; @@ -190,18 +193,27 @@ authorizeMPToken( // - add the new mptokenKey to the owner directory // - create the MPToken object for the holder + auto const sponsorSle = getTxReserveSponsor(view, tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + + auto const isSponsoredAndPreFunded = *sponsorSle && !isSponsorReserveCoSigning(tx); + // The reserve that is required to create the MPToken. Note // that although the reserve increases with every item // an account owns, in the case of MPTokens we only // *enforce* a reserve if the user owns more than two // items. This is similar to the reserve requirements of trust lines. - std::uint32_t const uOwnerCount = sleAcct->getFieldU32(sfOwnerCount); - XRPAmount const reserveCreate( - (uOwnerCount < 2) ? XRPAmount(beast::kZero) - : view.fees().accountReserve(uOwnerCount + 1)); - - if (priorBalance < reserveCreate) - return tecINSUFFICIENT_RESERVE; + // If PreFunded Sponsor, it must be checked whether sufficient + // ReserveCount exists. + if (ownerCount(view, *sponsorSle ? *sponsorSle : sleAcct, journal) >= 2 || + isSponsoredAndPreFunded) + { + if (auto const ret = checkInsufficientReserve( + view, tx, sleAcct, priorBalance, *sponsorSle, 1, 0, journal); + !isTesSuccess(ret)) + return ret; + } // Defensive check before we attempt to create MPToken for the issuer auto const mpt = view.read(keylet::mptIssuance(mptIssuanceID)); @@ -225,7 +237,8 @@ authorizeMPToken( view.insert(mptoken); // Update owner count. - adjustOwnerCount(view, sleAcct, 1, journal); + adjustOwnerCount(view, sleAcct, *sponsorSle, 1, journal); + addSponsorToLedgerEntry(mptoken, *sponsorSle); return tesSUCCESS; } @@ -270,6 +283,7 @@ authorizeMPToken( [[nodiscard]] TER removeEmptyHolding( ApplyView& view, + STTx const& tx, AccountID const& accountID, MPTIssue const& mptIssue, beast::Journal journal) @@ -292,6 +306,7 @@ removeEmptyHolding( return authorizeMPToken( view, + tx, {}, // priorBalance mptID, accountID, @@ -401,6 +416,7 @@ requireAuth( [[nodiscard]] TER enforceMPTokenAuthorization( ApplyView& view, + STTx const& tx, MPTID const& mptIssuanceID, AccountID const& account, XRPAmount const& priorBalance, // for MPToken authorization @@ -482,6 +498,7 @@ enforceMPTokenAuthorization( "xrpl::enforceMPTokenAuthorization : new MPToken for domain"); if (auto const err = authorizeMPToken( view, + tx, priorBalance, // priorBalance mptIssuanceID, // mptIssuanceID account, // account @@ -896,6 +913,7 @@ createMPToken( ApplyView& view, MPTID const& mptIssuanceID, AccountID const& account, + SLE::ref sponsorSle, std::uint32_t const flags) { auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); @@ -912,6 +930,9 @@ createMPToken( (*mptoken)[sfFlags] = flags; (*mptoken)[sfOwnerNode] = *ownerNode; + if (sponsorSle) + addSponsorToLedgerEntry(mptoken, sponsorSle); + view.insert(mptoken); return tesSUCCESS; @@ -922,6 +943,7 @@ checkCreateMPT( xrpl::ApplyView& view, xrpl::MPTIssue const& mptIssue, xrpl::AccountID const& holder, + SLE::ref sponsorSle, beast::Journal j) { if (mptIssue.getIssuer() == holder) @@ -931,7 +953,7 @@ checkCreateMPT( auto const mptokenID = keylet::mptoken(mptIssuanceID.key, holder); if (!view.exists(mptokenID)) { - if (auto const err = createMPToken(view, mptIssue.getMptID(), holder, 0); + if (auto const err = createMPToken(view, mptIssue.getMptID(), holder, sponsorSle, 0); !isTesSuccess(err)) { return err; @@ -941,7 +963,8 @@ checkCreateMPT( { return tecINTERNAL; } - adjustOwnerCount(view, sleAcct, 1, j); + + adjustOwnerCount(view, sleAcct, sponsorSle, 1, j); } return tesSUCCESS; } diff --git a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp index 5b467db7966..c77f49c8649 100644 --- a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp @@ -4,12 +4,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -21,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -32,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -67,12 +71,15 @@ locatePage(ApplyView& view, AccountID const& owner, uint256 const& id) Keylet(ltNFTOKEN_PAGE, view.succ(first.key, last.key.next()).value_or(last.key))); } -static SLE::pointer +static std::expected getPageForToken( ApplyView& view, + STTx const& tx, AccountID const& owner, + SLE::ref sponsorSle, uint256 const& id, - std::function const& createCallback) + std::function const& + createCallback) { auto const base = keylet::nftpageMin(owner); auto const first = keylet::nftpage(base, id); @@ -91,7 +98,9 @@ getPageForToken( cp = std::make_shared(last); cp->setFieldArray(sfNFTokens, arr); view.insert(cp); - createCallback(view, owner); + + if (auto const ret = createCallback(view, tx, cp, owner, sponsorSle); !isTesSuccess(ret)) + return std::unexpected(ret); return cp; } @@ -204,7 +213,8 @@ getPageForToken( cp->setFieldH256(sfPreviousPageMin, np->key()); view.update(cp); - createCallback(view, owner); + if (auto const ret = createCallback(view, tx, np, owner, sponsorSle); ret != tesSUCCESS) + return std::unexpected(ret); return (first.key < np->key()) ? np : cp; } @@ -260,37 +270,55 @@ changeTokenURI( /** Insert the token in the owner's token directory. */ TER -insertToken(ApplyView& view, AccountID owner, STObject&& nft) +insertToken(ApplyView& view, STTx const& tx, AccountID owner, SLE::ref sponsorSle, STObject&& nft) { XRPL_ASSERT(nft.isFieldPresent(sfNFTokenID), "xrpl::nft::insertToken : has NFT token"); // First, we need to locate the page the NFT belongs to, creating it // if necessary. This operation may fail if it is impossible to insert // the NFT. - SLE::pointer const page = - getPageForToken(view, owner, nft[sfNFTokenID], [](ApplyView& view, AccountID const& owner) { - adjustOwnerCount( - view, - view.peek(keylet::account(owner)), - 1, - beast::Journal{beast::Journal::getNullSink()}); - }); + auto createCallback = [](ApplyView& view, + STTx const& tx, + std::shared_ptr const& newPage, + AccountID const& owner, + SLE::ref sponsorSle) -> TER { + if (isReserveSponsored(tx)) + { + auto const ownerSle = view.read(keylet::account(owner)); + auto const ownerBalance = ownerSle->getFieldAmount(sfBalance); + if (auto const ret = + checkInsufficientReserve(view, tx, ownerSle, ownerBalance, sponsorSle, 1); + !isTesSuccess(ret)) + return ret; + } - if (!page) + adjustOwnerCount(view, view.peek(keylet::account(owner)), sponsorSle, 1); + + addSponsorToLedgerEntry(newPage, sponsorSle); + return tesSUCCESS; + }; + + auto const page = + getPageForToken(view, tx, owner, sponsorSle, nft[sfNFTokenID], createCallback); + + if (!page.has_value()) + return page.error(); + + if (!(*page)) return tecNO_SUITABLE_NFTOKEN_PAGE; { - auto arr = page->getFieldArray(sfNFTokens); + auto arr = (*page)->getFieldArray(sfNFTokens); arr.pushBack(std::move(nft)); arr.sort([](STObject const& o1, STObject const& o2) { return compareTokens(o1.getFieldH256(sfNFTokenID), o2.getFieldH256(sfNFTokenID)); }); - page->setFieldArray(sfNFTokens, arr); + (*page)->setFieldArray(sfNFTokens, arr); } - view.update(page); + view.update((*page)); return tesSUCCESS; } @@ -411,22 +439,11 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S curr->setFieldArray(sfNFTokens, arr); view.update(curr); - int cnt = 0; - if (prev && mergePages(view, prev, curr)) - cnt--; + adjustOwnerCountObj(view, owner, prev, -1); if (next && mergePages(view, curr, next)) - cnt--; - - if (cnt != 0) - { - adjustOwnerCount( - view, - view.peek(keylet::account(owner)), - cnt, - beast::Journal{beast::Journal::getNullSink()}); - } + adjustOwnerCountObj(view, owner, curr, -1); return tesSUCCESS; } @@ -460,11 +477,7 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S curr->makeFieldAbsent(sfPreviousPageMin); } - adjustOwnerCount( - view, - view.peek(keylet::account(owner)), - -1, - beast::Journal{beast::Journal::getNullSink()}); + adjustOwnerCountObj(view, owner, prev, -1); view.update(curr); view.erase(prev); @@ -500,9 +513,9 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S view.update(next); } - view.erase(curr); + adjustOwnerCountObj(view, owner, curr, -1); - int cnt = 1; + view.erase(curr); // Since we're here, try to consolidate the previous and current pages // of the page we removed (if any) into one. mergePages() _should_ @@ -517,13 +530,9 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S view, view.peek(Keylet(ltNFTOKEN_PAGE, prev->key())), view.peek(Keylet(ltNFTOKEN_PAGE, next->key())))) - cnt++; - - adjustOwnerCount( - view, - view.peek(keylet::account(owner)), - -1 * cnt, - beast::Journal{beast::Journal::getNullSink()}); + { + adjustOwnerCountObj(view, owner, prev, -1); + } return tesSUCCESS; } @@ -639,8 +648,7 @@ deleteTokenOffer(ApplyView& view, SLE::ref offer) false)) return false; - adjustOwnerCount( - view, view.peek(keylet::account(owner)), -1, beast::Journal{beast::Journal::getNullSink()}); + adjustOwnerCountObj(view, owner, offer, -1); view.erase(offer); return true; @@ -745,7 +753,7 @@ repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner) { Throw( "NFTokenPage directory for " + to_string(owner) + - " cannot be repaired. Unexpected link problem."); + " cannot be repaired. std::unexpected link problem."); } newPrev->at(sfNextPageMin) = nextPage->key(); view.update(newPrev); @@ -919,6 +927,7 @@ tokenOfferCreatePreclaim( TER tokenOfferCreateApply( ApplyView& view, + STTx const& tx, AccountID const& acctID, STAmount const& amount, std::optional const& dest, @@ -930,9 +939,14 @@ tokenOfferCreateApply( std::uint32_t txFlags) { Keylet const acctKeylet = keylet::account(acctID); - if (auto const acct = view.read(acctKeylet); - priorBalance < view.fees().accountReserve((*acct)[sfOwnerCount] + 1)) - return tecINSUFFICIENT_RESERVE; + auto const acct = view.read(acctKeylet); + auto const sponsorSle = getTxReserveSponsor(view, tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + if (auto const ret = + checkInsufficientReserve(view, tx, acct, priorBalance, *sponsorSle, 1, 0, j); + !isTesSuccess(ret)) + return ret; auto const offerID = keylet::nftoffer(acctID, seqProxy.value()); @@ -979,11 +993,13 @@ tokenOfferCreateApply( if (dest) (*offer)[sfDestination] = *dest; + addSponsorToLedgerEntry(offer, *sponsorSle); + view.insert(offer); } // Update owner count. - adjustOwnerCount(view, view.peek(acctKeylet), 1, j); + adjustOwnerCount(view, view.peek(acctKeylet), *sponsorSle, 1, j); return tesSUCCESS; } diff --git a/src/libxrpl/ledger/helpers/OfferHelpers.cpp b/src/libxrpl/ledger/helpers/OfferHelpers.cpp index 52498701439..b7b52e82b93 100644 --- a/src/libxrpl/ledger/helpers/OfferHelpers.cpp +++ b/src/libxrpl/ledger/helpers/OfferHelpers.cpp @@ -55,7 +55,7 @@ offerDelete(ApplyView& view, SLE::ref sle, beast::Journal j) } } - adjustOwnerCount(view, view.peek(keylet::account(owner)), -1, j); + adjustOwnerCountObj(view, owner, sle, -1, j); view.erase(sle); diff --git a/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp index 03b68e88607..a3b194586b8 100644 --- a/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp +++ b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp @@ -58,7 +58,7 @@ closeChannel(SLE::ref slep, ApplyView& view, uint256 const& key, beast::Journal XRPL_ASSERT( (*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount"); (*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance]; - adjustOwnerCount(view, sle, -1, j); + adjustOwnerCountObj(view, sle, slep, -1, j); view.update(sle); // Remove PayChan from ledger diff --git a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp index 7ed7ab8fc42..c715894145e 100644 --- a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp +++ b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +31,7 @@ #include #include #include +#include namespace xrpl { @@ -195,6 +198,7 @@ trustCreate( // Issuer should be the account being set. std::uint32_t uQualityIn, std::uint32_t uQualityOut, + SLE::ref sponsorSle, beast::Journal j) { JLOG(j.trace()) << "trustCreate: " << to_string(uSrcAccountID) << ", " @@ -281,7 +285,9 @@ trustCreate( } sleRippleState->setFieldU32(sfFlags, uFlags); - adjustOwnerCount(view, sleAccount, 1, j); + adjustOwnerCount(view, sleAccount, sponsorSle, 1, j); + + addSponsorToLedgerEntry(sleRippleState, sponsorSle, bSetHigh ? sfHighSponsor : sfLowSponsor); // ONLY: Create ripple balance. sleRippleState->setFieldAmount(sfBalance, bSetHigh ? -saBalance : saBalance); @@ -317,6 +323,9 @@ trustDelete( return tefBAD_LEDGER; // LCOV_EXCL_LINE } + removeSponsorFromLedgerEntry(sleRippleState, sfHighSponsor); + removeSponsorFromLedgerEntry(sleRippleState, sfLowSponsor); + JLOG(j.trace()) << "trustDelete: Deleting ripple line: state"; view.erase(sleRippleState); @@ -369,11 +378,15 @@ updateTrustLine( { // VFALCO Where is the line being deleted? // Clear the reserve of the sender, possibly delete the line! - adjustOwnerCount(view, sle, -1, j); + auto const currentSponsor = + getLedgerEntryReserveSponsor(view, state, !bSenderHigh ? sfLowSponsor : sfHighSponsor); + adjustOwnerCount(view, sle, currentSponsor, -1, j); // Clear reserve flag. state->clearFlag(senderReserveFlag); + removeSponsorFromLedgerEntry(state, !bSenderHigh ? sfLowSponsor : sfHighSponsor); + // Balance is zero, receiver reserve is clear. if (!after && !state->isFlag(receiverReserveFlag)) return true; @@ -472,6 +485,7 @@ issueIOU( limit, 0, 0, + {}, j); } @@ -621,6 +635,7 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc TER addEmptyHolding( ApplyView& view, + STTx const& tx, AccountID const& accountID, XRPAmount priorBalance, Issue const& issue, @@ -649,9 +664,19 @@ addEmptyHolding( if (view.read(index)) return tecDUPLICATE; + SLE::pointer sponsorSle; + if (!isPseudoAccount(sleDst)) + { + auto sle = getTxReserveSponsor(view, tx); + if (!sle) + return sle.error(); // LCOV_EXCL_LINE + sponsorSle = std::move(*sle); + } + // Can the account cover the trust line reserve ? - std::uint32_t const ownerCount = sleDst->at(sfOwnerCount); - if (priorBalance < view.fees().accountReserve(ownerCount + 1)) + if (auto const ret = + checkInsufficientReserve(view, tx, sleDst, priorBalance, sponsorSle, 1, 0, journal); + !isTesSuccess(ret)) return tecNO_LINE_INSUF_RESERVE; return trustCreate( @@ -669,6 +694,7 @@ addEmptyHolding( /*saLimit=*/STAmount{Issue{currency, dstId}}, /*uQualityIn=*/0, /*uQualityOut=*/0, + sponsorSle, journal); } @@ -710,11 +736,14 @@ removeEmptyHolding( if (!sleLowAccount) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleLowAccount, -1, journal); + auto const currentLowSponsor = getLedgerEntryReserveSponsor(view, line, sfLowSponsor); + + adjustOwnerCount(view, sleLowAccount, currentLowSponsor, -1, journal); // It's not really necessary to clear the reserve flag, since the line // is about to be deleted, but this will make the metadata reflect an // accurate state at the time of deletion. line->clearFlag(lsfLowReserve); + removeSponsorFromLedgerEntry(line, sfLowSponsor); } if (line->isFlag(lsfHighReserve)) @@ -724,11 +753,14 @@ removeEmptyHolding( if (!sleHighAccount) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleHighAccount, -1, journal); + auto const currentHighSponsor = getLedgerEntryReserveSponsor(view, line, sfHighSponsor); + + adjustOwnerCount(view, sleHighAccount, currentHighSponsor, -1, journal); // It's not really necessary to clear the reserve flag, since the line // is about to be deleted, but this will make the metadata reflect an // accurate state at the time of deletion. line->clearFlag(lsfHighReserve); + removeSponsorFromLedgerEntry(line, sfHighSponsor); } return trustDelete( @@ -768,6 +800,9 @@ deleteAMMTrustLine( if (ammAccountID && (low != *ammAccountID && high != *ammAccountID)) return terNO_AMM; + auto const sponsorSle = + getLedgerEntryReserveSponsor(view, sleState, !ammLow ? sfLowSponsor : sfHighSponsor); + if (auto const ter = trustDelete(view, sleState, low, high, j); !isTesSuccess(ter)) { JLOG(j.error()) << "deleteAMMTrustLine: failed to delete the trustline."; @@ -778,7 +813,7 @@ deleteAMMTrustLine( if (!sleState->isFlag(uFlags)) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, !ammLow ? sleLow : sleHigh, -1, j); + adjustOwnerCount(view, !ammLow ? sleLow : sleHigh, sponsorSle, -1, j); return tesSUCCESS; } diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp index 71914568681..beb5413bc0e 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -6,9 +6,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -22,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -35,12 +38,6 @@ namespace xrpl { // Forward declaration for function that remains in View.h/cpp -bool -isLPTokenFrozen( - ReadView const& view, - AccountID const& account, - Asset const& asset, - Asset const& asset2); //------------------------------------------------------------------------------ // @@ -478,6 +475,7 @@ canAddHolding(ReadView const& view, Asset const& asset) TER addEmptyHolding( ApplyView& view, + STTx const& tx, AccountID const& accountID, XRPAmount priorBalance, Asset const& asset, @@ -485,7 +483,7 @@ addEmptyHolding( { return std::visit( [&](TIss const& issue) -> TER { - return addEmptyHolding(view, accountID, priorBalance, issue, journal); + return addEmptyHolding(view, tx, accountID, priorBalance, issue, journal); }, asset.value()); } @@ -493,13 +491,21 @@ addEmptyHolding( TER removeEmptyHolding( ApplyView& view, + STTx const& tx, AccountID const& accountID, Asset const& asset, beast::Journal journal) { return std::visit( [&](TIss const& issue) -> TER { - return removeEmptyHolding(view, accountID, issue, journal); + if constexpr (std::is_same_v) + { + return removeEmptyHolding(view, accountID, issue, journal); + } + else + { + return removeEmptyHolding(view, tx, accountID, issue, journal); + } }, asset.value()); } @@ -553,6 +559,7 @@ directSendNoFeeIOU( AccountID const& uReceiverID, STAmount const& saAmount, bool bCheckIssuer, + SLE::ref sponsorSle, beast::Journal j) { AccountID const& issuer = saAmount.getIssuer(); @@ -623,7 +630,12 @@ directSendNoFeeIOU( // Sender quality out is 0. { // Clear the reserve of the sender, possibly delete the line! - adjustOwnerCount(view, view.peek(keylet::account(uSenderID)), -1, j); + auto const currentSponsor = getLedgerEntryReserveSponsor( + view, sleRippleState, !bSenderHigh ? sfLowSponsor : sfHighSponsor); + adjustOwnerCount(view, view.peek(keylet::account(uSenderID)), currentSponsor, -1, j); + + removeSponsorFromLedgerEntry( + sleRippleState, !bSenderHigh ? sfLowSponsor : sfHighSponsor); // Clear reserve flag. sleRippleState->clearFlag(senderReserveFlag); @@ -686,6 +698,7 @@ directSendNoFeeIOU( saReceiverLimit, 0, 0, + sponsorSle, j); } @@ -700,6 +713,7 @@ directSendNoLimitIOU( STAmount const& saAmount, STAmount& saActual, beast::Journal j, + SLE::ref sponsorSle, WaiveTransferFee waiveFee) { auto const& issuer = saAmount.getIssuer(); @@ -712,7 +726,8 @@ directSendNoLimitIOU( if (uSenderID == issuer || uReceiverID == issuer || issuer == noAccount()) { // Direct send: redeeming IOUs and/or sending own IOUs. - auto const ter = directSendNoFeeIOU(view, uSenderID, uReceiverID, saAmount, false, j); + auto const ter = + directSendNoFeeIOU(view, uSenderID, uReceiverID, saAmount, false, sponsorSle, j); if (!isTesSuccess(ter)) return ter; saActual = saAmount; @@ -730,10 +745,12 @@ directSendNoLimitIOU( << to_string(uReceiverID) << " : deliver=" << saAmount.getFullText() << " cost=" << saActual.getFullText(); - TER terResult = directSendNoFeeIOU(view, issuer, uReceiverID, saAmount, true, j); + TER terResult = directSendNoFeeIOU(view, issuer, uReceiverID, saAmount, true, sponsorSle, j); if (tesSUCCESS == terResult) - terResult = directSendNoFeeIOU(view, uSenderID, issuer, saActual, true, j); + { + terResult = directSendNoFeeIOU(view, uSenderID, issuer, saActual, true, sponsorSle, j); + } return terResult; } @@ -749,6 +766,7 @@ directSendNoLimitMultiIOU( MultiplePaymentDestinations const& receivers, STAmount& actual, beast::Journal j, + SLE::ref sponsorSle, WaiveTransferFee waiveFee) { auto const& issuer = issue.getIssuer(); @@ -776,7 +794,8 @@ directSendNoLimitMultiIOU( if (senderID == issuer || receiverID == issuer || issuer == noAccount()) { // Direct send: redeeming IOUs and/or sending own IOUs. - if (auto const ter = directSendNoFeeIOU(view, senderID, receiverID, amount, false, j)) + if (auto const ter = + directSendNoFeeIOU(view, senderID, receiverID, amount, false, sponsorSle, j)) return ter; actual += amount; // Do not add amount to takeFromSender, because directSendNoFeeIOU took @@ -799,14 +818,15 @@ directSendNoLimitMultiIOU( << to_string(receiverID) << " : deliver=" << amount.getFullText() << " cost=" << actual.getFullText(); - if (TER const terResult = directSendNoFeeIOU(view, issuer, receiverID, amount, true, j)) + if (TER const terResult = + directSendNoFeeIOU(view, issuer, receiverID, amount, true, sponsorSle, j)) return terResult; } if (senderID != issuer && takeFromSender) { if (TER const terResult = - directSendNoFeeIOU(view, senderID, issuer, takeFromSender, true, j)) + directSendNoFeeIOU(view, senderID, issuer, takeFromSender, true, sponsorSle, j)) return terResult; } @@ -820,6 +840,7 @@ accountSendIOU( AccountID const& uReceiverID, STAmount const& saAmount, beast::Journal j, + SLE::ref sponsorSle, WaiveTransferFee waiveFee) { if (view.rules().enabled(fixAMMv1_1)) @@ -851,7 +872,8 @@ accountSendIOU( JLOG(j.trace()) << "accountSendIOU: " << to_string(uSenderID) << " -> " << to_string(uReceiverID) << " : " << saAmount.getFullText(); - return directSendNoLimitIOU(view, uSenderID, uReceiverID, saAmount, saActual, j, waiveFee); + return directSendNoLimitIOU( + view, uSenderID, uReceiverID, saAmount, saActual, j, sponsorSle, waiveFee); } /* XRP send which does not check reserve and can do pure adjustment. @@ -937,6 +959,7 @@ accountSendMultiIOU( Issue const& issue, MultiplePaymentDestinations const& receivers, beast::Journal j, + SLE::ref sponsorSle, WaiveTransferFee waiveFee) { XRPL_ASSERT_PARTS( @@ -948,7 +971,8 @@ accountSendMultiIOU( JLOG(j.trace()) << "accountSendMultiIOU: " << to_string(senderID) << " sending " << receivers.size() << " IOUs"; - return directSendNoLimitMultiIOU(view, senderID, issue, receivers, actual, j, waiveFee); + return directSendNoLimitMultiIOU( + view, senderID, issue, receivers, actual, j, sponsorSle, waiveFee); } /* XRP send which does not check reserve and can do pure adjustment. @@ -1380,7 +1404,7 @@ directSendNoFee( { return saAmount.asset().visit( [&](Issue const&) { - return directSendNoFeeIOU(view, uSenderID, uReceiverID, saAmount, bCheckIssuer, j); + return directSendNoFeeIOU(view, uSenderID, uReceiverID, saAmount, bCheckIssuer, {}, j); }, [&](MPTIssue const&) { XRPL_ASSERT(!bCheckIssuer, "xrpl::directSendNoFee : not checking issuer"); @@ -1395,12 +1419,13 @@ accountSend( AccountID const& uReceiverID, STAmount const& saAmount, beast::Journal j, + SLE::ref sponsorSle, WaiveTransferFee waiveFee, AllowMPTOverflow allowOverflow) { return saAmount.asset().visit( [&](Issue const&) { - return accountSendIOU(view, uSenderID, uReceiverID, saAmount, j, waiveFee); + return accountSendIOU(view, uSenderID, uReceiverID, saAmount, j, sponsorSle, waiveFee); }, [&](MPTIssue const&) { return accountSendMPT( @@ -1415,13 +1440,14 @@ accountSendMulti( Asset const& asset, MultiplePaymentDestinations const& receivers, beast::Journal j, + SLE::ref sponsorSle, WaiveTransferFee waiveFee) { XRPL_ASSERT_PARTS( receivers.size() > 1, "xrpl::accountSendMulti", "multiple recipients provided"); return asset.visit( [&](Issue const& issue) { - return accountSendMultiIOU(view, senderID, issue, receivers, j, waiveFee); + return accountSendMultiIOU(view, senderID, issue, receivers, j, sponsorSle, waiveFee); }, [&](MPTIssue const& issue) { return accountSendMultiMPT(view, senderID, issue, receivers, j, waiveFee); diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index ae29bd32975..6974b205685 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -84,6 +84,7 @@ enum class LedgerNameSpace : std::uint16_t { Vault = 'V', LoanBroker = 'l', // lower-case L Loan = 'L', + Sponsorship = '>', // No longer used or supported. Left here to reserve the space to avoid accidental reuse. Contract [[deprecated]] = 'c', @@ -318,6 +319,12 @@ signers(AccountID const& account) noexcept return signers(account, 0); } +Keylet +sponsor(AccountID const& sponsor, AccountID const& sponsee) noexcept +{ + return {ltSPONSORSHIP, indexHash(LedgerNameSpace::Sponsorship, sponsor, sponsee)}; +} + Keylet check(AccountID const& id, std::uint32_t seq) noexcept { diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index 66b2822a421..0bdb217771c 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -160,6 +160,14 @@ InnerObjectFormats::InnerObjectFormats() {sfTxnSignature, SoeOptional}, {sfSigners, SoeOptional}, }); + + add(sfSponsorSignature.jsonName.cStr(), + sfSponsorSignature.getCode(), + { + {sfSigningPubKey, SoeOptional}, + {sfTxnSignature, SoeOptional}, + {sfSigners, SoeOptional}, + }); } InnerObjectFormats const& diff --git a/src/libxrpl/protocol/LedgerFormats.cpp b/src/libxrpl/protocol/LedgerFormats.cpp index 8b91bb79309..a29ea492664 100644 --- a/src/libxrpl/protocol/LedgerFormats.cpp +++ b/src/libxrpl/protocol/LedgerFormats.cpp @@ -15,6 +15,7 @@ LedgerFormats::getCommonFields() {sfLedgerIndex, SoeOptional}, {sfLedgerEntryType, SoeRequired}, {sfFlags, SoeRequired}, + {sfSponsor, SoeOptional}, }; return kCommonFields; } diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 55f0ea12893..4bcf579926d 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -279,6 +279,14 @@ STTx::checkSign(Rules const& rules) const if (auto const ret = checkSign(rules, counterSig); !ret) return std::unexpected("Counterparty: " + ret.error()); } + + if (isFieldPresent(sfSponsorSignature)) + { + auto const sponsorSignatureObj = getFieldObject(sfSponsorSignature); + if (auto const ret = checkSign(rules, sponsorSignatureObj); !ret) + return std::unexpected("Sponsor: " + ret.error()); + } + return {}; } diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 6b8dfc68113..f7663d0f571 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -106,6 +106,7 @@ transResults() MAKE_ERROR(tecLIMIT_EXCEEDED, "Limit exceeded."), MAKE_ERROR(tecPSEUDO_ACCOUNT, "This operation is not allowed against a pseudo-account."), MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."), + MAKE_ERROR(tecNO_SPONSOR_PERMISSION, "Sponsor has not authorized this transaction."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), @@ -216,6 +217,7 @@ transResults() MAKE_ERROR(terADDRESS_COLLISION, "Failed to allocate an unique account address."), MAKE_ERROR(terNO_DELEGATE_PERMISSION, "Delegated account lacks permission to perform this transaction."), MAKE_ERROR(terLOCKED, "Fund is locked."), + MAKE_ERROR(terNO_SPONSORSHIP, "No sponsorship found."), MAKE_ERROR(tesSUCCESS, "The transaction was applied. Only final in a validated ledger."), }; diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index b926bdf0e55..e4d4c4b03c6 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -30,6 +30,9 @@ TxFormats::getCommonFields() {sfSigners, SoeOptional}, // submit_multisigned {sfNetworkID, SoeOptional}, {sfDelegate, SoeOptional}, + {sfSponsor, SoeOptional}, + {sfSponsorFlags, SoeOptional}, + {sfSponsorSignature, SoeOptional}, }; return kCommonFields; } diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index aa7b81c0157..dd643537ecc 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,7 @@ #include #include +#include #include #include #include @@ -164,6 +166,62 @@ preflightCheckSimulateKeys(ApplyFlags flags, STObject const& sigObject, beast::J } // namespace detail +static NotTEC +preflight1Sponsor(PreflightContext const& ctx, AccountID const& id) +{ + 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 (hasSponsorFlags && + ((ctx.tx.getFieldU32(sfSponsorFlags) & ~(spfSponsorFee | spfSponsorReserve)) != 0u)) + { + JLOG(ctx.j.debug()) << "preflight1: invalid sponsor flags"; + return temINVALID_FLAG; + } + + if (!hasSponsor) + { + if (hasSponsorFlags) + { + JLOG(ctx.j.debug()) << "preflight1: sponsor flags without sponsor definition"; + return temINVALID_FLAG; + } + + if (hasSponsorSig) + { + JLOG(ctx.j.debug()) << "preflight1: sponsor signature without sponsor definition"; + return temMALFORMED; + } + } + else if (hasSponsorFlags) + { + auto const sponsorFlags = ctx.tx.getFieldU32(sfSponsorFlags); + if (((sponsorFlags & ~(spfSponsorFee | spfSponsorReserve)) != 0u) || sponsorFlags == 0) + { + JLOG(ctx.j.debug()) << "preflight1: invalid sponsor flags"; + return temINVALID_FLAG; + } + } + else + { + JLOG(ctx.j.debug()) << "preflight1: no sponsor flags"; + return temINVALID_FLAG; + } + + if (hasSponsor && ctx.tx.getAccountID(sfSponsor) == id) + { + JLOG(ctx.j.debug()) << "preflight1: Sponsor account cannot be the " + "same as the transaction originator"; + return temMALFORMED; + } + + return tesSUCCESS; +} + /** Performs early sanity checks on the account and fee fields */ NotTEC Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask) @@ -215,6 +273,9 @@ Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask) !ctx.rules.enabled(featureBatch), "Inner batch transaction must have a parent batch ID."); + if (auto const ter = preflight1Sponsor(ctx, id); !isTesSuccess(ter)) + return ter; + return tesSUCCESS; } @@ -310,6 +371,40 @@ Transactor::checkPermission(ReadView const& view, STTx const& tx) return checkTxPermission(sle, tx); } +NotTEC +Transactor::checkSponsor(ReadView const& view, STTx const& tx) +{ + if (!tx.isFieldPresent(sfSponsor)) + return tesSUCCESS; + + if (auto const sponsorSle = getTxReserveSponsor(view, tx); !sponsorSle) + return terNO_ACCOUNT; + + auto const hasSponsorSignature = tx.isFieldPresent(sfSponsorSignature); + + if (hasSponsorSignature) + return tesSUCCESS; + + auto const sponsorshipSle = + view.read(keylet::sponsor(tx.getAccountID(sfSponsor), tx.getAccountID(sfAccount))); + + // sponsorship object missing for pre-funded tx + if (!sponsorshipSle) + return terNO_SPONSORSHIP; + + auto const sponsorFlags = tx.getFieldU32(sfSponsorFlags); + + if (((sponsorFlags & spfSponsorFee) != 0u) && + sponsorshipSle->isFlag(lsfSponsorshipRequireSignForFee)) + return terNO_SPONSORSHIP; + + if (((sponsorFlags & spfSponsorReserve) != 0u) && + sponsorshipSle->isFlag(lsfSponsorshipRequireSignForReserve)) + return terNO_SPONSORSHIP; + + return tesSUCCESS; +} + XRPAmount Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) { @@ -318,6 +413,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 @@ -325,7 +421,15 @@ 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); + sponsorSignerCount += + sponsorObj.isFieldPresent(sfSigners) ? sponsorObj.getFieldArray(sfSigners).size() : 0; + } + + return baseFee + ((signerCount + sponsorSignerCount) * baseFee); } // Returns the fee in fee units, not scaled for load. @@ -395,12 +499,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) + auto const feePayer = getFeePayer(ctx.view, ctx.tx); + auto const payerSle = ctx.view.read(feePayer.entry); + + 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(); - auto const balance = (*sle)[sfBalance].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) + { + STAmount const sponsorReserve = accountReserve(ctx.view, payerSle, ctx.j); + maxSpendable = payerSle->getFieldAmount(sfBalance).xrp() - sponsorReserve.xrp(); + } + 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 @@ -409,12 +552,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; @@ -431,16 +574,27 @@ 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.entry); + + JLOG(j_.trace()) << "Fee payer: " + to_string(feePayer.entry.key); + 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 + auto const feeAmountAfter = sle->getFieldAmount(feePayer.balanceField) - 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; @@ -614,7 +768,7 @@ Transactor::ticketDelete( } // Update the Ticket owner's reserve. - adjustOwnerCount(view, sleAccount, -1, j); + adjustOwnerCountObj(view, sleAccount, sleTicket, -1, j); // Remove Ticket from ledger. view.erase(sleTicket); @@ -705,6 +859,22 @@ 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 sponsorAccountID = sigObject.getAccountID(sfSponsor); + auto const sponsorSignature = sigObject.getFieldObject(sfSponsorSignature); + if (auto const ret = + checkSign(view, flags, std::nullopt, sponsorAccountID, 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)) @@ -1089,11 +1259,19 @@ 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.entry); + if (!payerSle) return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE - auto const balance = payerSle->getFieldAmount(sfBalance).xrp(); + auto const balance = payerSle->getFieldAmount(feePayer.balanceField).xrp(); + + if (feePayer.type == FeePayerType::SponsorPreFunded && payerSle->isFieldPresent(sfMaxFee)) + { + auto const cap = payerSle->getFieldAmount(sfMaxFee).xrp(); + fee = std::min(fee, cap); + } // balance should have already been checked in checkFee / preFlight. XRPL_ASSERT( @@ -1112,7 +1290,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"); @@ -1126,6 +1314,40 @@ Transactor::reset(XRPAmount fee) return {ter, fee}; } +FeePayer +Transactor::getFeePayer(ReadView const& view, STTx const& tx) +{ + if (tx.isFieldPresent(sfSponsor) && ((tx.getFieldU32(sfSponsorFlags) & spfSponsorFee) != 0u)) + { + auto const sponsorAccountID = tx.getAccountID(sfSponsor); + auto const sponseeAccountID = tx.getAccountID(sfAccount); + auto const hasSponsorSignature = tx.isFieldPresent(sfSponsorSignature); + auto const sponsorshipKeylet = keylet::sponsor(sponsorAccountID, sponseeAccountID); + + // if pre-funded sponsorship exists, prefer it + if (hasSponsorSignature && !view.exists(sponsorshipKeylet)) + { + // co-signed + return FeePayer{ + .entry = keylet::account(sponsorAccountID), + .balanceField = sfBalance, + .type = FeePayerType::SponsorCoSigned}; + } + + // pre funded + return FeePayer{ + .entry = sponsorshipKeylet, + .balanceField = sfFeeAmount, + .type = FeePayerType::SponsorPreFunded}; + } + + auto const payerAccountKeylet = keylet::account(tx.getFeePayer()); + auto const payerType = + tx.isFieldPresent(sfDelegate) ? FeePayerType::Delegate : FeePayerType::Account; + + return FeePayer{.entry = 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 diff --git a/src/libxrpl/tx/applySteps.cpp b/src/libxrpl/tx/applySteps.cpp index 336bb2004b8..3691fbd4342 100644 --- a/src/libxrpl/tx/applySteps.cpp +++ b/src/libxrpl/tx/applySteps.cpp @@ -181,6 +181,9 @@ invokePreclaim(PreclaimContext const& ctx) if (NotTEC const result = T::checkPriorTxAndLastLedger(ctx)) return result; + if (NotTEC const result = T::checkSponsor(ctx.view, ctx.tx)) + return result; + if (NotTEC const result = T::checkPermission(ctx.view, ctx.tx)) return result; diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp index b4a533905c4..8fc50fa3776 100644 --- a/src/libxrpl/tx/invariants/InvariantCheck.cpp +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp @@ -130,6 +130,10 @@ XRPNotCreated::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref a if (isXRP((*before)[sfAmount])) drops_ -= (*before)[sfAmount].xrp().drops(); break; + case ltSPONSORSHIP: + if (before->isFieldPresent(sfFeeAmount)) + drops_ -= (*before)[sfFeeAmount].xrp().drops(); + break; default: break; } @@ -150,6 +154,10 @@ XRPNotCreated::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref a if (!isDelete && isXRP((*after)[sfAmount])) drops_ += (*after)[sfAmount].xrp().drops(); break; + case ltSPONSORSHIP: + if (!isDelete && after->isFieldPresent(sfFeeAmount)) + drops_ += (*after)[sfFeeAmount].xrp().drops(); + break; default: break; } @@ -503,6 +511,20 @@ AccountRootsDeletedClean::finalize( if (enforce) return false; } + // An account should not be deleted with sponsorship fields + if (after->isFieldPresent(sfSponsoredOwnerCount) || + after->isFieldPresent(sfSponsoringOwnerCount) || + after->isFieldPresent(sfSponsoringAccountCount) || after->isFieldPresent(sfSponsor)) + { + JLOG(j.fatal()) << "Invariant failed: account deletion left " + "behind a sponsorship field"; + XRPL_ASSERT( + enforce, + "xrpl::AccountRootsDeletedClean::finalize : " + "deleted account has no sponsorship fields"); + if (enforce) + return false; + } // Simple types for (auto const& [keyletfunc, _1, _2] : kDirectAccountKeylets) { @@ -869,8 +891,10 @@ ValidPseudoAccounts::visitEntry(bool isDelete, SLE::const_ref before, SLE::const // 1. Exactly one of the pseudo-account fields is set. // 2. The sequence number is not changed. // 3. The lsfDisableMaster, lsfDefaultRipple, and lsfDepositAuth - // flags are set. + // flags are set. // 4. The RegularKey is not set. + // 5. The SponsoredOwnerCount, SponsoringOwnerCount, SponsoringAccountCount, Sponsor + // fields are not set. { std::vector const& fields = getPseudoAccountFields(); @@ -897,6 +921,12 @@ ValidPseudoAccounts::visitEntry(bool isDelete, SLE::const_ref before, SLE::const { errors_.emplace_back("pseudo-account has a regular key"); } + if (after->isFieldPresent(sfSponsoredOwnerCount) || + after->isFieldPresent(sfSponsoringOwnerCount) || after->isFieldPresent(sfSponsor) || + after->isFieldPresent(sfSponsoringAccountCount)) + { + errors_.emplace_back("pseudo-account has a sponsorship field"); + } } } } diff --git a/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp b/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp index 544a3af2dc7..90397db3c6a 100644 --- a/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp +++ b/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp @@ -150,6 +150,18 @@ ValidPermissionedDomain::finalize( } return true; } + case ttSPONSORSHIP_TRANSFER: { + if (sleStatus_.empty()) + return true; + + if (sleStatus_[0].isDelete) + { + JLOG(j.fatal()) << "Invariant failed: domain object " + "deleted by SponsorshipTransfer"; + return false; + } + return check(sleStatus_[0], j); + } default: { if (!sleStatus_.empty()) { diff --git a/src/libxrpl/tx/invariants/SponsorshipInvariant.cpp b/src/libxrpl/tx/invariants/SponsorshipInvariant.cpp new file mode 100644 index 00000000000..41d55e90195 --- /dev/null +++ b/src/libxrpl/tx/invariants/SponsorshipInvariant.cpp @@ -0,0 +1,174 @@ +#include +// +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +// Add new sponsorship-related invariants implementations +void +SponsorshipOwnerCountsMatch::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + auto getSponsored = [](std::shared_ptr const& sle) -> std::uint32_t { + if (sle && sle->getType() == ltACCOUNT_ROOT) + return sle->getFieldU32(sfSponsoredOwnerCount); + return 0; + }; + auto getSponsoring = [](std::shared_ptr const& sle) -> std::uint32_t { + if (sle && sle->getType() == ltACCOUNT_ROOT) + return sle->getFieldU32(sfSponsoringOwnerCount); + return 0; + }; + + auto getOwnerCount = [](std::shared_ptr const& sle) -> std::uint32_t { + if (sle && sle->getType() == ltACCOUNT_ROOT) + return sle->getFieldU32(sfOwnerCount); + return 0; + }; + + auto getSponsoredObjectOwnerCount = + [&](std::shared_ptr const& sle) -> std::uint32_t { + if (!sle) + return 0; + switch (sle->getType()) + { + case ltACCOUNT_ROOT: { + return 0; + } + case ltRIPPLE_STATE: { + uint32_t ownerCount = 0; + if (sle->isFieldPresent(sfHighSponsor)) + ownerCount++; + if (sle->isFieldPresent(sfLowSponsor)) + ownerCount++; + return ownerCount; + } + case ltORACLE: { + if (!sle->isFieldPresent(sfSponsor)) + return 0; + auto const priceDataSeries = sle->getFieldArray(sfPriceDataSeries); + return OracleSet::calculateOracleReserve(priceDataSeries.size()); + } + case ltVAULT: { + if (!sle->isFieldPresent(sfSponsor)) + return 0; + return 2; + } + default: { + if (sle->isFieldPresent(sfSponsor)) + return 1; + return 0; + } + } + }; + + std::int64_t const beforeSponsored = getSponsored(before); + std::int64_t const afterSponsored = getSponsored(after); + std::int64_t const beforeSponsoring = getSponsoring(before); + std::int64_t const afterSponsoring = getSponsoring(after); + + std::int64_t const beforeSponsoredObjectOwnerCount = getSponsoredObjectOwnerCount(before); + std::int64_t const afterSponsoredObjectOwnerCount = + isDelete ? 0 : getSponsoredObjectOwnerCount(after); + + deltaSponsoredOwnerCount_ += (afterSponsored - beforeSponsored); + deltaSponsoringOwnerCount_ += (afterSponsoring - beforeSponsoring); + + deltaSponsoredObjectOwnerCount_ += + (afterSponsoredObjectOwnerCount - beforeSponsoredObjectOwnerCount); + + if (getOwnerCount(after) < getSponsored(after)) + invalidOwnerCountLessThanSponsoredOwnerCount_ += 1; +} + +bool +SponsorshipOwnerCountsMatch::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) const +{ + if (deltaSponsoredOwnerCount_ != deltaSponsoringOwnerCount_) + { + JLOG(j.fatal()) << "Invariant failed: SponsoredOwnerCount does not " + "equal SponsoringOwnerCount delta."; + return false; + } + + if (deltaSponsoredObjectOwnerCount_ != deltaSponsoredOwnerCount_) + { + JLOG(j.fatal()) << "Invariant failed: SponsoredObjectOwnerCount does not " + "equal SponsoredOwnerCount delta."; + return false; + } + + if (invalidOwnerCountLessThanSponsoredOwnerCount_ > 0) + { + JLOG(j.fatal()) + << "Invariant failed: OwnerCount must be greater than or equal to SponsoredOwnerCount."; + return false; + } + + return true; +} + +void +SponsorshipAccountCountMatchesField::visitEntry( + bool, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + auto getSponsoringAccountCount = [](std::shared_ptr const& sle) -> std::uint32_t { + if (sle && sle->getType() == ltACCOUNT_ROOT) + return sle->getFieldU32(sfSponsoringAccountCount); + return 0; + }; + + auto hasSponsorField = [](std::shared_ptr const& sle) -> bool { + return sle && sle->getType() == ltACCOUNT_ROOT && sle->isFieldPresent(sfSponsor); + }; + + std::int64_t const beforeCount = getSponsoringAccountCount(before); + std::int64_t const afterCount = getSponsoringAccountCount(after); + deltaSponsoringAccountCount_ += (afterCount - beforeCount); + + int const beforePresent = hasSponsorField(before) ? 1 : 0; + int const afterPresent = hasSponsorField(after) ? 1 : 0; + deltaSponsorFieldPresence_ += (afterPresent - beforePresent); +} + +bool +SponsorshipAccountCountMatchesField::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) const +{ + if (deltaSponsoringAccountCount_ != deltaSponsorFieldPresence_) + { + JLOG(j.fatal()) << "Invariant failed: Net delta of SponsoringAccountCount does not " + "match net delta of sfSponsor presence."; + return false; + } + + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/paths/BookStep.cpp b/src/libxrpl/tx/paths/BookStep.cpp index 5cc2a987b86..6f31de8805f 100644 --- a/src/libxrpl/tx/paths/BookStep.cpp +++ b/src/libxrpl/tx/paths/BookStep.cpp @@ -731,7 +731,7 @@ BookStep::forEachOffer( // Create MPToken for the offer's owner. No need to check // for the reserve since the offer is removed if it is consumed. // Therefore, the owner count remains the same. - if (auto const err = checkCreateMPT(sb, assetIn.get(), owner, j_); + if (auto const err = checkCreateMPT(sb, assetIn.get(), owner, {}, j_); !isTesSuccess(err)) { return true; diff --git a/src/libxrpl/tx/paths/MPTEndpointStep.cpp b/src/libxrpl/tx/paths/MPTEndpointStep.cpp index 7dcd6d92417..7a2a0f33615 100644 --- a/src/libxrpl/tx/paths/MPTEndpointStep.cpp +++ b/src/libxrpl/tx/paths/MPTEndpointStep.cpp @@ -410,7 +410,8 @@ MPTEndpointOfferCrossingStep::checkCreateMPT(ApplyView& view, xrpl::DebtDirectio // for the reserve since the offer doesn't go on the books // if crossed. Insufficient reserve is allowed if the offer // crossed. See CreateOffer::applyGuts() for reserve check. - if (auto const err = xrpl::checkCreateMPT(view, mptIssue_, dst_, j_); !isTesSuccess(err)) + if (auto const err = xrpl::checkCreateMPT(view, mptIssue_, dst_, {}, j_); + !isTesSuccess(err)) { JLOG(j_.trace()) << "MPTEndpointStep::checkCreateMPT: failed create MPT"; resetCache(srcDebtDir); diff --git a/src/libxrpl/tx/transactors/Sponsor/SponsorshipSet.cpp b/src/libxrpl/tx/transactors/Sponsor/SponsorshipSet.cpp new file mode 100644 index 00000000000..8529ebd59bc --- /dev/null +++ b/src/libxrpl/tx/transactors/Sponsor/SponsorshipSet.cpp @@ -0,0 +1,413 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl { + +std::uint32_t +SponsorshipSet::getFlagsMask(PreflightContext const& ctx) +{ + return tfSponsorshipSetMask; +} + +NotTEC +SponsorshipSet::preflight(PreflightContext const& ctx) +{ + if (ctx.tx.isFlag(tfSponsorshipSetRequireSignForFee) && + ctx.tx.isFlag(tfSponsorshipClearRequireSignForFee)) + return temINVALID_FLAG; + if (ctx.tx.isFlag(tfSponsorshipSetRequireSignForReserve) && + ctx.tx.isFlag(tfSponsorshipClearRequireSignForReserve)) + return temINVALID_FLAG; + + auto const account = ctx.tx.getAccountID(sfAccount); + bool const hasSponsor = ctx.tx.isFieldPresent(sfCounterpartySponsor); + bool const hasSponsee = ctx.tx.isFieldPresent(sfSponsee); + + // The transaction must specify either Sponsor or Sponsee, but not both. + if (hasSponsor == hasSponsee) + return temMALFORMED; + + auto const sponsorAccountID = ctx.tx[~sfCounterpartySponsor].value_or(account); + auto const sponseeAccountID = ctx.tx[~sfSponsee].value_or(account); + + if (sponsorAccountID == sponseeAccountID) + return temMALFORMED; + + if (ctx.tx.isFlag(tfDeleteObject)) + { + // can not combine with any modification flags when deleting + constexpr std::uint32_t kModifyFlags = tfSponsorshipSetRequireSignForFee | + tfSponsorshipSetRequireSignForReserve | tfSponsorshipClearRequireSignForFee | + tfSponsorshipClearRequireSignForReserve; + + if ((ctx.tx.getFlags() & kModifyFlags) != 0u) + return temINVALID_FLAG; + + // can not include these fields when deleting + if (ctx.tx.isFieldPresent(sfFeeAmount) || ctx.tx.isFieldPresent(sfReserveCount) || + ctx.tx.isFieldPresent(sfMaxFee)) + return temMALFORMED; + } + else + { + // although both Sponsor and Sponsee can delete, + // only the Sponsor can create or update sponsorship. + if (account != sponsorAccountID) + return temMALFORMED; + + // Check FeeAmount and MaxFee + auto const checkOptionalAmountField = [&](SField const& field) -> NotTEC { + if (!ctx.tx.isFieldPresent(field)) + return tesSUCCESS; + + auto const amount = ctx.tx.getFieldAmount(field); + + if (!isXRP(amount)) + return temBAD_AMOUNT; + + if (amount.xrp() < XRPAmount{0}) + 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; + } + + return tesSUCCESS; +} + +NotTEC +SponsorshipSet::checkPermission(ReadView const& view, STTx const& tx) +{ + auto const delegate = tx[~sfDelegate]; + if (!delegate) + return tesSUCCESS; + + auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate); + auto const sle = view.read(delegateKey); + + if (!sle) + return terNO_DELEGATE_PERMISSION; + + if (checkTxPermission(sle, tx) == tesSUCCESS) + return tesSUCCESS; + + auto const txFlags = tx.getFlags(); + + // this is added in case more flags will be added for SponsorshipSet + // in the future. Currently unreachable. + if ((txFlags & tfSponsorshipSetPermissionMask) != 0u) + return terNO_DELEGATE_PERMISSION; + + std::unordered_set granularPermissions; + loadGranularPermission(sle, ttSPONSORSHIP_SET, granularPermissions); + + auto const sponsoringFee = tx.isFieldPresent(sfFeeAmount) || tx.isFieldPresent(sfMaxFee) || + ((txFlags & (tfSponsorshipSetRequireSignForFee | tfSponsorshipClearRequireSignForFee)) != + 0u); + auto const sponsoringReserve = tx.isFieldPresent(sfReserveCount) || + ((txFlags & + (tfSponsorshipSetRequireSignForReserve | tfSponsorshipClearRequireSignForReserve)) != 0u); + + if (sponsoringFee && !granularPermissions.contains(SponsorFee)) + return terNO_DELEGATE_PERMISSION; + + if (sponsoringReserve && !granularPermissions.contains(SponsorReserve)) + return terNO_DELEGATE_PERMISSION; + + return tesSUCCESS; +} + +TER +SponsorshipSet::preclaim(PreclaimContext const& ctx) +{ + auto const sponsorAccountID = ctx.tx[~sfCounterpartySponsor].value_or(ctx.tx[sfAccount]); + auto const sponseeAccountID = ctx.tx[~sfSponsee].value_or(ctx.tx[sfAccount]); + + if (sponseeAccountID == sponsorAccountID) + return tecINTERNAL; // LCOV_EXCL_LINE + + // check Sponsor + auto const sponsorAccSle = ctx.view.read(keylet::account(sponsorAccountID)); + if (!sponsorAccSle) + return tecNO_DST; + + // check Sponsee + auto const sponseeSle = ctx.view.read(keylet::account(sponseeAccountID)); + if (!sponseeSle) + return tecNO_DST; + + // Pseudo accounts cannot be sponsors or sponsees + if (isPseudoAccount(sponsorAccSle) || isPseudoAccount(sponseeSle)) + return tecNO_PERMISSION; + + // check if object exists + auto const sponsorObjSle = ctx.view.read(keylet::sponsor(sponsorAccountID, sponseeAccountID)); + + if (ctx.tx.isFlag(tfDeleteObject) && !sponsorObjSle) + return tecNO_ENTRY; + + return tesSUCCESS; +} + +TER +SponsorshipSet::deleteSponsorship(ApplyView& view, SLE::ref sle, beast::Journal j) +{ + if (!sle) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const sponsorAccountID = (*sle)[sfOwner]; + auto const sponseeAccountID = (*sle)[sfSponsee]; + + // The reserve for the Sponsorship object is held by the sponsor (Owner). + auto sponsorAccSle = view.peek(keylet::account(sponsorAccountID)); + if (!sponsorAccSle) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (!view.dirRemove(keylet::ownerDir(sponsorAccountID), (*sle)[sfOwnerNode], sle->key(), false)) + { + // LCOV_EXCL_START + JLOG(j.fatal()) << "Unable to delete Sponsorship from sponsor."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + if (!view.dirRemove( + keylet::ownerDir(sponseeAccountID), (*sle)[sfSponseeNode], sle->key(), false)) + { + // LCOV_EXCL_START + JLOG(j.fatal()) << "Unable to delete Sponsorship from sponsee."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + + adjustOwnerCountObj(view, sponsorAccSle, sle, -1, j); + + // transfer feeAmount back to the sponsor + if (sle->isFieldPresent(sfFeeAmount)) + (*sponsorAccSle)[sfBalance] += sle->getFieldAmount(sfFeeAmount); + + view.erase(sle); + + return tesSUCCESS; +} + +TER +SponsorshipSet::doApply() +{ + auto const sponsorAccountID = ctx_.tx[~sfCounterpartySponsor].value_or(accountID_); + auto const sponseeAccountID = ctx_.tx[~sfSponsee].value_or(accountID_); + + if (sponseeAccountID == sponsorAccountID) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const sponsorAccSle = ctx_.view().peek(keylet::account(sponsorAccountID)); + if (!sponsorAccSle) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (!ctx_.view().exists(keylet::account(sponseeAccountID))) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const sponsorKeylet = keylet::sponsor(sponsorAccountID, sponseeAccountID); + auto const sponsorObjSle = ctx_.view().peek(sponsorKeylet); + + if (ctx_.tx.isFlag(tfDeleteObject)) + { + // Delete + if (!sponsorObjSle) + return tecINTERNAL; // LCOV_EXCL_LINE + + return deleteSponsorship(ctx_.view(), sponsorObjSle, ctx_.journal); + } + + auto const feeAmount = ctx_.tx[~sfFeeAmount]; + auto const maxFee = ctx_.tx[~sfMaxFee]; + auto const reserveCount = ctx_.tx[~sfReserveCount]; + + auto reserveSponsorAccSle = getTxReserveSponsor(view(), ctx_.tx); + if (!reserveSponsorAccSle) + return reserveSponsorAccSle.error(); // LCOV_EXCL_LINE + + if (!sponsorObjSle) + { + // Create + auto newSle = std::make_shared(sponsorKeylet); + + (*newSle)[sfOwner] = sponsorAccountID; + (*newSle)[sfSponsee] = sponseeAccountID; + if (feeAmount && (*feeAmount).xrp() > (*sponsorAccSle)[sfBalance]) + return tecUNFUNDED; + + if (feeAmount && *feeAmount > XRPAmount(0)) + { + (*sponsorAccSle)[sfBalance] -= *feeAmount; + (*newSle)[sfFeeAmount] = *feeAmount; + } + + if (auto const ret = checkInsufficientReserve( + ctx_.view(), + ctx_.tx, + sponsorAccSle, + STAmount{(*sponsorAccSle)[sfBalance]}.xrp(), + *reserveSponsorAccSle, + 1, + 0, + ctx_.journal); + !isTesSuccess(ret)) + return tecUNFUNDED; + + if (maxFee && *maxFee > XRPAmount(0)) + (*newSle)[sfMaxFee] = *maxFee; + if (reserveCount && *reserveCount > 0) + (*newSle)[sfReserveCount] = *reserveCount; + + auto 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(sponsorAccountID), sponsorKeylet, describeOwnerDir(sponsorAccountID)); + if (!sponsorPage) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*newSle)[sfOwnerNode] = *sponsorPage; + + auto const sponseePage = view().dirInsert( + keylet::ownerDir(sponseeAccountID), sponsorKeylet, describeOwnerDir(sponseeAccountID)); + if (!sponseePage) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*newSle)[sfSponseeNode] = *sponseePage; + + // NOLINTNEXTLINE(readability-suspicious-call-argument) + adjustOwnerCount(view(), sponsorAccSle, *reserveSponsorAccSle, 1, ctx_.journal); + addSponsorToLedgerEntry(newSle, *reserveSponsorAccSle); + + ctx_.view().insert(newSle); + return tesSUCCESS; + } + + // Update + if (feeAmount) + { + auto const currentFeeAmount = (*sponsorObjSle)[~sfFeeAmount].valueOr(XRPAmount(0)); + auto feeAmountDelta = XRPAmount(*feeAmount - currentFeeAmount); + + if (feeAmountDelta > beast::kZero && feeAmountDelta > (*sponsorAccSle)[sfBalance]) + return tecUNFUNDED; + + // transfer feeAmount to ledger entry + if (feeAmountDelta != beast::kZero) + { + (*sponsorAccSle)[sfBalance] -= feeAmountDelta; + + if (*feeAmount == XRPAmount(0)) + { + (*sponsorObjSle).makeFieldAbsent(sfFeeAmount); + } + else + { + (*sponsorObjSle).setFieldAmount(sfFeeAmount, *feeAmount); + } + + if (auto const ret = checkInsufficientReserve( + ctx_.view(), + ctx_.tx, + sponsorAccSle, + STAmount{(*sponsorAccSle)[sfBalance]}.xrp(), + *reserveSponsorAccSle, + 0, + 0, + ctx_.journal); + !isTesSuccess(ret)) + return tecUNFUNDED; + } + } + + if (maxFee) + { + if (*maxFee == XRPAmount(0)) + { + (*sponsorObjSle).makeFieldAbsent(sfMaxFee); + } + else + { + (*sponsorObjSle)[sfMaxFee] = *maxFee; + } + } + + if (reserveCount) + sponsorObjSle->at(sfReserveCount) = *reserveCount; + + // update Flags + auto flags = sponsorObjSle->getFieldU32(sfFlags); + if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForFee)) + flags |= lsfSponsorshipRequireSignForFee; + + if (ctx_.tx.isFlag(tfSponsorshipClearRequireSignForFee)) + flags &= ~lsfSponsorshipRequireSignForFee; + + if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForReserve)) + flags |= lsfSponsorshipRequireSignForReserve; + + if (ctx_.tx.isFlag(tfSponsorshipClearRequireSignForReserve)) + flags &= ~lsfSponsorshipRequireSignForReserve; + + if (flags != (*sponsorObjSle)[sfFlags]) + (*sponsorObjSle)[sfFlags] = flags; + + view().update(sponsorObjSle); + + return tesSUCCESS; +} + +void +SponsorshipSet::visitInvariantEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&) +{ +} + +bool +SponsorshipSet::finalizeInvariants( + STTx const&, + TER, + XRPAmount, + ReadView const&, + beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp b/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp new file mode 100644 index 00000000000..24eb54e632a --- /dev/null +++ b/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp @@ -0,0 +1,645 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace xrpl { + +static std::optional +applyCountDelta(std::uint32_t current, std::int64_t delta) +{ + std::int64_t const next = static_cast(current) + delta; + if (next < 0 || next > std::numeric_limits::max()) + return std::nullopt; + return static_cast(next); +} + +std::uint32_t +SponsorshipTransfer::getFlagsMask(PreflightContext const& ctx) +{ + return tfSponsorshipTransferMask; +} + +NotTEC +SponsorshipTransfer::preflight(PreflightContext const& ctx) +{ + auto const flags = ctx.tx.getFlags(); + auto const flagsSet = flags & ~(tfSponsorshipTransferMask | tfUniversal); + if (std::popcount(flagsSet) != 1) + { + JLOG(ctx.j.debug()) << "preflight: Only one SponsorshipTransfer flag can be set per tx."; + return temINVALID_FLAG; + } + + if (ctx.tx.isFlag(tfSponsorshipCreate)) + { + if (!isReserveSponsored(ctx.tx)) + { + JLOG(ctx.j.debug()) + << "preflight: spfSponsorReserve should be set when creating sponsorship"; + return temINVALID_FLAG; + } + if (ctx.tx.isFieldPresent(sfSponsee)) + { + JLOG(ctx.j.debug()) + << "preflight: sfSponsee should be available only when ending sponsorship"; + return temMALFORMED; + } + } + if (ctx.tx.isFlag(tfSponsorshipReassign)) + { + if (!isReserveSponsored(ctx.tx)) + { + JLOG(ctx.j.debug()) + << "preflight: spfSponsorReserve should be set when reassigning sponsorship"; + return temINVALID_FLAG; + } + if (ctx.tx.isFieldPresent(sfSponsee)) + { + JLOG(ctx.j.debug()) + << "preflight: sfSponsee should not be set when reassigning sponsorship"; + return temMALFORMED; + } + } + if (ctx.tx.isFlag(tfSponsorshipEnd)) + { + if (isReserveSponsored(ctx.tx)) + { + JLOG(ctx.j.debug()) + << "preflight: spfSponsorReserve should not be set when ending sponsorship"; + return temINVALID_FLAG; + } + + if (ctx.tx.isFieldPresent(sfSponsee)) + { + if (ctx.tx.getAccountID(sfSponsee) == ctx.tx.getAccountID(sfAccount)) + { + JLOG(ctx.j.debug()) << "preflight: sfSponsee should not be the same as the account"; + return temMALFORMED; + } + } + } + + // When an account sponsoring, sfSponsorSignature must be provided + auto const newSponsor = getTxReserveSponsorAccountID(ctx.tx); + bool const isObjectSponsor = ctx.tx.isFieldPresent(sfObjectID); + + // both sfSponsor and sfObjectID are provided + bool const isNewAccountSponsor = newSponsor && !isObjectSponsor; + + if (isNewAccountSponsor && !ctx.tx.isFieldPresent(sfSponsorSignature)) + { + JLOG(ctx.j.debug()) << "preflight: sponsoring an account needs co-signing sponsor"; + return temMALFORMED; + } + + return tesSUCCESS; +} + +template +inline std::optional +getLedgerEntryOwner(ReadView const& view, T const& sle, AccountID const& account) +{ + switch (sle->getType()) + { + case ltNFTOKEN_OFFER: + case ltORACLE: + case ltPERMISSIONED_DOMAIN: + case ltVAULT: + case ltLOAN_BROKER: + return sle->getAccountID(sfOwner); + case ltCHECK: + case ltDID: + case ltTICKET: + case ltOFFER: + case ltXCHAIN_OWNED_CLAIM_ID: + case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID: + case ltESCROW: + case ltPAYCHAN: + case ltMPTOKEN: + case ltDELEGATE: + case ltBRIDGE: + case ltDEPOSIT_PREAUTH: + return sle->getAccountID(sfAccount); + case ltMPTOKEN_ISSUANCE: + return sle->getAccountID(sfIssuer); + case ltLOAN: + return sle->getAccountID(sfBorrower); + case ltSIGNER_LIST: { + auto const signerList = view.read(keylet::signers(account)); + if (!signerList) + return std::nullopt; + if (signerList->key() == sle->key()) + return account; + return std::nullopt; + } + case ltCREDENTIAL: { + if (sle->isFlag(lsfAccepted)) + return sle->getAccountID(sfSubject); + return sle->getAccountID(sfIssuer); + } + case ltNFTOKEN_PAGE: { + // the upper 20 bytes of the index of ltNFTokenPage are the Owner's + // AccountID + uint256 const& key = sle->key(); + return AccountID::fromVoid(key.data()); + } + case ltRIPPLE_STATE: { + if (sle->isFlag(lsfHighReserve)) + { + auto const highAccount = sle->getFieldAmount(sfHighLimit).getIssuer(); + if (highAccount == account) + return highAccount; + } + if (sle->isFlag(lsfLowReserve)) + { + auto const lowAccount = sle->getFieldAmount(sfLowLimit).getIssuer(); + if (lowAccount == account) + return lowAccount; + } + return std::nullopt; + } + case ltACCOUNT_ROOT: { + // AccountRoot is not supported for object sponsorship + return std::nullopt; + } + case ltNEGATIVE_UNL: + case ltDIR_NODE: + case ltAMENDMENTS: + case ltLEDGER_HASHES: + case ltFEE_SETTINGS: + case ltAMM: + return std::nullopt; + default: + return std::nullopt; + }; +} + +template +inline std::uint32_t +getLedgerEntryOwnerCount(T const& sle) +{ + switch (sle->getType()) + { + case ltORACLE: { + return OracleSet::calculateOracleReserve(sle->getFieldArray(sfPriceDataSeries).size()); + } + default: + return 1; + } +}; + +template +inline SF_ACCOUNT const& +getLedgerEntrySponsorField(T const& sle, AccountID const& owner) +{ + switch (sle->getType()) + { + case ltRIPPLE_STATE: { + if (sle->isFlag(lsfHighReserve)) + { + auto const highAccount = sle->getFieldAmount(sfHighLimit).getIssuer(); + if (highAccount == owner) + return sfHighSponsor; + } + if (sle->isFlag(lsfLowReserve)) + { + auto const lowAccount = sle->getFieldAmount(sfLowLimit).getIssuer(); + if (lowAccount == owner) + return sfLowSponsor; + } + // LCOV_EXCL_START + UNREACHABLE("Should not happen. Owner should be checked before calling this function."); + return sfSponsor; + // LCOV_EXCL_STOP + } + default: + return sfSponsor; + } +}; + +TER +SponsorshipTransfer::preclaim(PreclaimContext const& ctx) +{ + auto const index = ctx.tx[~sfObjectID]; + auto const newSponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); + if (!newSponsorSle) + return newSponsorSle.error(); // LCOV_EXCL_LINE + + bool const isObjectSponsor = index != std::nullopt; + + auto const account = ctx.tx[sfAccount]; + + auto const sponseeAccountID = ctx.tx[~sfSponsee].value_or(account); + auto const sponseeSle = ctx.view.read(keylet::account(sponseeAccountID)); + if (!sponseeSle) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (isObjectSponsor) + { + auto const sle = ctx.view.read(keylet::unchecked(*index)); + if (!sle) + return tecNO_ENTRY; + + auto const ownerCountDelta = getLedgerEntryOwnerCount(sle); + + auto const owner = getLedgerEntryOwner(ctx.view, sle, sponseeAccountID); + if (!owner || owner != sponseeAccountID) + return tecNO_PERMISSION; + + auto const& sponsorField = getLedgerEntrySponsorField(sle, *owner); + + if (ctx.tx.isFlag(tfSponsorshipCreate)) + { + if (!*newSponsorSle) + return tecNO_PERMISSION; + + // check object is not sponsored yet + if (sle->isFieldPresent(sponsorField)) + return tecNO_PERMISSION; + } + else if (ctx.tx.isFlag(tfSponsorshipReassign)) + { + if (!*newSponsorSle) + return tecNO_PERMISSION; + + // check object is already ctx.sponsored + if (!sle->isFieldPresent(sponsorField)) + return tecNO_PERMISSION; + } + else if (ctx.tx.isFlag(tfSponsorshipEnd)) + { + if (*newSponsorSle) + return tecNO_PERMISSION; + + // check object is sponsored + if (!sle->isFieldPresent(sponsorField)) + return tecNO_PERMISSION; + + // only the sponsor or sponsee can end sponsorship + auto const sponsor = sle->getAccountID(sponsorField); + if (account != sponsor && account != sponseeAccountID) + return tecNO_PERMISSION; + } + + // check new sponsor have sufficient balance + // NOLINTNEXTLINE(readability-suspicious-call-argument) + if (auto const ter = checkInsufficientReserve( + ctx.view, + ctx.tx, + sponseeSle, + sponseeSle->getFieldAmount(sfBalance), + *newSponsorSle, + ownerCountDelta, + 0, + ctx.j); + !isTesSuccess(ter)) + return ter; + } + else + { + if (ctx.tx.isFlag(tfSponsorshipCreate)) + { + if (!*newSponsorSle) + return tecNO_PERMISSION; + + // check account is not sponsored yet + if (sponseeSle->isFieldPresent(sfSponsor)) + return tecNO_PERMISSION; + } + else if (ctx.tx.isFlag(tfSponsorshipReassign)) + { + if (!*newSponsorSle) + return tecNO_PERMISSION; + + // check account is already sponsored + if (!sponseeSle->isFieldPresent(sfSponsor)) + return tecNO_PERMISSION; + } + else if (ctx.tx.isFlag(tfSponsorshipEnd)) + { + if (*newSponsorSle) + return tecNO_PERMISSION; + + // check account is sponsored + if (!sponseeSle->isFieldPresent(sfSponsor)) + return tecNO_PERMISSION; + + // only the sponsor or sponsee can end sponsorship + auto const sponsor = sponseeSle->getAccountID(sfSponsor); + if (account != sponsor && account != sponseeAccountID) + return tecNO_PERMISSION; + } + + // check account have sufficient balance + // In the case of removing an account sponsor, accSle should have no sfSponsor set + // (AccountReserve = 0). However, by setting accountCountDelta = 1 here, we are able to + // calculate the actual required Account Reserve. + // NOLINTNEXTLINE(readability-suspicious-call-argument) + if (auto const ter = checkInsufficientReserve( + ctx.view, + ctx.tx, + sponseeSle, + sponseeSle->getFieldAmount(sfBalance), + *newSponsorSle, + 0, + 1, + ctx.j); + !isTesSuccess(ter)) + return ter; + } + + return tesSUCCESS; +} + +static TER +reduceReserveCount( + ApplyView& view, + AccountID const& account, + AccountID const& sponsor, + int64_t delta) +{ + if (delta == 0) + return tesSUCCESS; + if (delta > 0) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const sponsorKeylet = keylet::sponsor(sponsor, account); + auto const sponsorSle = view.peek(sponsorKeylet); + if (!sponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const afterReserveCount = applyCountDelta(sponsorSle->getFieldU32(sfReserveCount), delta); + if (!afterReserveCount) + { + // already checked in preclaim() + UNREACHABLE("xrpl::reduceReserveCount : invalid reserve count"); + return tefINTERNAL; // LCOV_EXCL_LINE + } + + sponsorSle->at(sfReserveCount) = *afterReserveCount; + view.update(sponsorSle); + return tesSUCCESS; +} + +TER +SponsorshipTransfer::doApply() +{ + auto const& tx = ctx_.tx; + + auto const index = tx[~sfObjectID]; + bool const isObjectSponsor = index != std::nullopt; + + auto const sponseeAccountID = tx[~sfSponsee].value_or(accountID_); + auto const sponseeSle = view().peek(keylet::account(sponseeAccountID)); + if (!sponseeSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const setSponsorFieldU32 = + [] [[nodiscard]] (auto const& sle, auto const& field, auto const& delta) -> TER { + auto const newValue = applyCountDelta(sle->getFieldU32(field), delta); + if (!newValue) + { + UNREACHABLE("xrpl::SponsorshipTransfer::doApply : Invalid sponsor field value"); + return tecINTERNAL; // LCOV_EXCL_LINE + } + + sle->at(field) = *newValue; + return tesSUCCESS; + }; + + if (isObjectSponsor) + { + auto const hasSignature = tx.isFieldPresent(sfSponsorSignature); + + // transfer object sponsor + auto const objSle = view().peek(keylet::unchecked(*index)); + if (!objSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const ownerAccountID = getLedgerEntryOwner(view(), objSle, sponseeAccountID); + if (!ownerAccountID) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const ownerSle = view().peek(keylet::account(*ownerAccountID)); + if (!ownerSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + std::int64_t const ownerCountDelta = getLedgerEntryOwnerCount(objSle); + + auto const& sponsorField = getLedgerEntrySponsorField(objSle, *ownerAccountID); + + if (ctx_.tx.isFlag(tfSponsorshipCreate)) + { + auto const newSponsorAccountID = tx.getAccountID(sfSponsor); + XRPL_ASSERT(!!newSponsorAccountID, "New sponsor is required when creating sponsorship"); + + // update owner's sponsored count + if (auto const ter = + setSponsorFieldU32(ownerSle, sfSponsoredOwnerCount, ownerCountDelta); + !isTesSuccess(ter)) + return ter; + view().update(ownerSle); + + // increment new sponsor's sponsoring count + auto const newSponsorSle = view().peek(keylet::account(newSponsorAccountID)); + if (!newSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + if (auto const ter = + setSponsorFieldU32(newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); + !isTesSuccess(ter)) + return ter; + view().update(newSponsorSle); + + // set new sponsor to object + objSle->setAccountID(sponsorField, newSponsorAccountID); + view().update(objSle); + + if (!hasSignature) + { + // use ReserveCount for pre-funded sponsoring + if (auto const ter = reduceReserveCount( + view(), sponseeAccountID, newSponsorAccountID, -ownerCountDelta); + !isTesSuccess(ter)) + return ter; + } + } + else if (ctx_.tx.isFlag(tfSponsorshipReassign)) + { + auto const newSponsorAccountID = tx.getAccountID(sfSponsor); + XRPL_ASSERT( + !!newSponsorAccountID, "New sponsor is required when reassigning sponsorship"); + + auto const oldSponsorAccountID = objSle->getAccountID(sponsorField); + XRPL_ASSERT( + !!oldSponsorAccountID, "Old sponsor is required when reassigning sponsorship"); + + // decrement old sponsor's sponsoring count + auto const oldSponsorSle = view().peek(keylet::account(oldSponsorAccountID)); + if (!oldSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + if (auto const ter = + setSponsorFieldU32(oldSponsorSle, sfSponsoringOwnerCount, -ownerCountDelta); + !isTesSuccess(ter)) + return ter; + view().update(oldSponsorSle); + + // increment new sponsor's sponsoring count + auto const newSponsorSle = view().peek(keylet::account(newSponsorAccountID)); + if (!newSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + if (auto const ter = + setSponsorFieldU32(newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); + !isTesSuccess(ter)) + return ter; + view().update(newSponsorSle); + + // set new sponsor to object + objSle->setAccountID(sponsorField, newSponsorAccountID); + view().update(objSle); + + if (!hasSignature) + { + // use ReserveCount for pre-funded sponsoring + if (auto const ter = reduceReserveCount( + view(), sponseeAccountID, newSponsorAccountID, -ownerCountDelta); + !isTesSuccess(ter)) + return ter; + } + } + else if (ctx_.tx.isFlag(tfSponsorshipEnd)) + { + auto const oldSponsorAccountID = objSle->getAccountID(sponsorField); + XRPL_ASSERT(!!oldSponsorAccountID, "Old sponsor is required when ending sponsorship"); + + auto const oldSponsorSle = view().peek(keylet::account(oldSponsorAccountID)); + if (!oldSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + // decrement sponsored count + if (auto const ter = + setSponsorFieldU32(sponseeSle, sfSponsoredOwnerCount, -ownerCountDelta); + !isTesSuccess(ter)) + return ter; + view().update(sponseeSle); + + // decrement old sponsoring count + if (auto const ter = + setSponsorFieldU32(oldSponsorSle, sfSponsoringOwnerCount, -ownerCountDelta); + !isTesSuccess(ter)) + return ter; + view().update(oldSponsorSle); + + // remove sponsor from object + objSle->makeFieldAbsent(sponsorField); + view().update(objSle); + } + } + else + { + if (ctx_.tx.isFlag(tfSponsorshipCreate)) + { + // create account sponsor + // increment new sponsoring count + auto const newSponsorAccountID = tx.getAccountID(sfSponsor); + auto const newSponsorSle = view().peek(keylet::account(newSponsorAccountID)); + if (!newSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + if (auto const ter = setSponsorFieldU32(newSponsorSle, sfSponsoringAccountCount, 1); + !isTesSuccess(ter)) + return ter; + view().update(newSponsorSle); + + // set new sponsor to account + sponseeSle->setAccountID(sfSponsor, newSponsorAccountID); + view().update(sponseeSle); + } + else if (ctx_.tx.isFlag(tfSponsorshipReassign)) + { + // reassign account sponsor + // increment new sponsoring count + auto const newSponsorAccountID = tx.getAccountID(sfSponsor); + auto const newSponsorSle = view().peek(keylet::account(newSponsorAccountID)); + if (!newSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + if (auto const ter = setSponsorFieldU32(newSponsorSle, sfSponsoringAccountCount, 1); + !isTesSuccess(ter)) + return ter; + view().update(newSponsorSle); + + // decrement old sponsoring count + auto const oldSponsor = sponseeSle->getAccountID(sfSponsor); + auto const oldSponsorSle = view().peek(keylet::account(oldSponsor)); + if (!oldSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + if (auto const ter = setSponsorFieldU32(oldSponsorSle, sfSponsoringAccountCount, -1); + !isTesSuccess(ter)) + return ter; + view().update(oldSponsorSle); + + // set new sponsor to account + sponseeSle->setAccountID(sfSponsor, newSponsorAccountID); + view().update(sponseeSle); + } + else if (ctx_.tx.isFlag(tfSponsorshipEnd)) + { + // dissolve account sponsor + auto const oldSponsorAccountID = sponseeSle->getAccountID(sfSponsor); + sponseeSle->makeFieldAbsent(sfSponsor); + view().update(sponseeSle); + + // decrement account sponsoring count + auto const oldSponsorSle = view().peek(keylet::account(oldSponsorAccountID)); + if (!oldSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + if (auto const ter = setSponsorFieldU32(oldSponsorSle, sfSponsoringAccountCount, -1); + !isTesSuccess(ter)) + return ter; + view().update(oldSponsorSle); + } + } + + return tesSUCCESS; +} + +void +SponsorshipTransfer::visitInvariantEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&) +{ +} + +bool +SponsorshipTransfer::finalizeInvariants( + STTx const&, + TER, + XRPAmount, + ReadView const&, + beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/account/AccountDelete.cpp b/src/libxrpl/tx/transactors/account/AccountDelete.cpp index 833f1c8b250..f3d8b4a5c6c 100644 --- a/src/libxrpl/tx/transactors/account/AccountDelete.cpp +++ b/src/libxrpl/tx/transactors/account/AccountDelete.cpp @@ -30,12 +30,12 @@ #include #include #include +#include #include #include namespace xrpl { - bool AccountDelete::checkExtraFeatures(PreflightContext const& ctx) { @@ -186,11 +186,23 @@ removeDelegateFromLedger( return DelegateSet::deleteDelegate(view, sleDel, j); } -// Return nullptr if the LedgerEntryType represents an obligation that can't -// be deleted. Otherwise return the pointer to the function that can delete -// the non-obligation +TER +removeSponsorshipFromLedger( + ServiceRegistry&, + ApplyView& view, + AccountID const&, + uint256 const&, + SLE::ref sleDel, + beast::Journal j) +{ + return SponsorshipSet::deleteSponsorship(view, sleDel, j); +} + +// Return nullptr if the object represents an obligation that can't be deleted +// during deletion of account. Otherwise return the pointer to the function +// that can delete the non-obligation. DeleterFuncPtr -nonObligationDeleter(LedgerEntryType t) +nonObligationDeleter(LedgerEntryType t, SLE::const_ref sleItem, AccountID const& account) { switch (t) { @@ -212,6 +224,12 @@ nonObligationDeleter(LedgerEntryType t) return removeCredentialFromLedger; case ltDELEGATE: return removeDelegateFromLedger; + case ltSPONSORSHIP: + // A Sponsorship lives in both the sponsor's (Owner) and the + // sponsee's owner directories, but it is an obligation only for the + // sponsor, who holds its reserve. The sponsee must remain free to + // delete its account. + return (*sleItem)[sfOwner] == account ? nullptr : removeSponsorshipFromLedger; default: return nullptr; } @@ -268,6 +286,15 @@ AccountDelete::preclaim(PreclaimContext const& ctx) if (cp) return tecHAS_OBLIGATIONS; + if (sleAccount->isFieldPresent(sfSponsor)) + { + if (dst != sleAccount->getAccountID(sfSponsor)) + return tecNO_SPONSOR_PERMISSION; + } + if (sleAccount->isFieldPresent(sfSponsoringOwnerCount) || + sleAccount->isFieldPresent(sfSponsoringAccountCount)) + return tecHAS_OBLIGATIONS; + // We don't allow an account to be deleted if its sequence number // is within 256 of the current ledger. This prevents replay of old // transactions if this account is resurrected after it is deleted. @@ -327,7 +354,7 @@ AccountDelete::preclaim(PreclaimContext const& ctx) LedgerEntryType const nodeType{safeCast((*sleItem)[sfLedgerEntryType])}; - if (nonObligationDeleter(nodeType) == nullptr) + if (nonObligationDeleter(nodeType, sleItem, account) == nullptr) return tecHAS_OBLIGATIONS; // We found a deletable directory entry. Count it. If we find too @@ -368,7 +395,7 @@ AccountDelete::doApply() [&](LedgerEntryType nodeType, uint256 const& dirEntry, SLE::pointer& sleItem) -> std::pair { - if (auto deleter = nonObligationDeleter(nodeType)) + if (auto deleter = nonObligationDeleter(nodeType, sleItem, accountID_)) { TER const result{deleter(ctx_.registry, view(), accountID_, dirEntry, sleItem, j_)}; @@ -388,12 +415,41 @@ AccountDelete::doApply() if (!isTesSuccess(ter)) return ter; + if (src->isFieldPresent(sfSponsoredOwnerCount)) + return tefINTERNAL; // LCOV_EXCL_LINE + // Transfer any XRP remaining after the fee is paid to the destination: auto const remainingBalance = src->getFieldAmount(sfBalance).xrp(); (*dst)[sfBalance] = (*dst)[sfBalance] + remainingBalance; (*src)[sfBalance] = (*src)[sfBalance] - remainingBalance; ctx_.deliver(remainingBalance); + if (src->isFieldPresent(sfSponsor)) + { + auto const sponsorAccountID = src->getAccountID(sfSponsor); + auto sponsorSle = view().peek(keylet::account(sponsorAccountID)); + + if (!sponsorSle || !sponsorSle->isFieldPresent(sfSponsoringAccountCount)) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const sponsoringAccountCount = sponsorSle->getFieldU32(sfSponsoringAccountCount); + + if (sponsoringAccountCount == 0) + { + // sanity check + // Since sfSponsoringAccountCount is set to soeDEFAULT, the field will not be + // present with a value of 0. + return tefINTERNAL; // LCOV_EXCL_LINE + } + sponsorSle->at(sfSponsoringAccountCount) = sponsoringAccountCount - 1; + view().update(sponsorSle); + + // Following line might look redundant, but without it, sfSponsor + // would end up remaining in after-ltAccountRoot during the + // InvariantCheck. + src->makeFieldAbsent(sfSponsor); + } + XRPL_ASSERT( (*src)[sfBalance] == XRPAmount(0), "xrpl::AccountDelete::doApply : source balance is zero"); diff --git a/src/libxrpl/tx/transactors/account/SignerListSet.cpp b/src/libxrpl/tx/transactors/account/SignerListSet.cpp index cedb9ace78e..d92194900cd 100644 --- a/src/libxrpl/tx/transactors/account/SignerListSet.cpp +++ b/src/libxrpl/tx/transactors/account/SignerListSet.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -215,8 +216,8 @@ removeSignersFromLedger( // LCOV_EXCL_STOP } - adjustOwnerCount( - view, view.peek(accountKeylet), removeFromOwnerCount, registry.getJournal("View")); + adjustOwnerCountObj( + view, view.peek(accountKeylet), signers, removeFromOwnerCount, registry.getJournal("View")); view.erase(signers); @@ -315,19 +316,26 @@ SignerListSet::replaceSignerList() if (!sle) return tefINTERNAL; // LCOV_EXCL_LINE - // Compute new reserve. Verify the account has funds to meet the reserve. - std::uint32_t const oldOwnerCount{(*sle)[sfOwnerCount]}; - static constexpr int kAddedOwnerCount = 1; std::uint32_t const flags{lsfOneOwnerCount}; - XRPAmount const newReserve{view().fees().accountReserve(oldOwnerCount + kAddedOwnerCount)}; - // We check the reserve against the starting balance because we want to // allow dipping into the reserve to pay fees. This behavior is consistent // with TicketCreate. - if (preFeeBalance_ < newReserve) - return tecINSUFFICIENT_RESERVE; + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + if (auto const ret = checkInsufficientReserve( + ctx_.view(), + ctx_.tx, + sle, + preFeeBalance_, + *sponsorSle, + kAddedOwnerCount, + 0, + ctx_.journal); + !isTesSuccess(ret)) + return ret; // Everything's ducky. Add the ltSIGNER_LIST to the ledger. auto signerList = std::make_shared(signerListKeylet); @@ -349,7 +357,8 @@ SignerListSet::replaceSignerList() // If we succeeded, the new entry counts against the // creator's reserve. - adjustOwnerCount(view(), sle, kAddedOwnerCount, viewJ); + adjustOwnerCount(view(), sle, *sponsorSle, kAddedOwnerCount, viewJ); + addSponsorToLedgerEntry(signerList, *sponsorSle); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp index 273beaea0f2..8229f9fd7b6 100644 --- a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp +++ b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -436,8 +437,7 @@ transferHelper( return tecINTERNAL; // LCOV_EXCL_LINE { - auto const ownerCount = sleSrc->getFieldU32(sfOwnerCount); - auto const reserve = psb.fees().accountReserve(ownerCount); + auto const reserve = accountReserve(psb, sleSrc, j, 0, 0); auto const availableBalance = [&]() -> STAmount { STAmount curBal = (*sleSrc)[sfBalance]; @@ -726,10 +726,10 @@ finalizeClaimHelper( return result; } + adjustOwnerCountObj(outerSb, sleOwner, sleClaimID, -1, j); + // Remove the claim id from the ledger outerSb.erase(sleClaimID); - - adjustOwnerCount(outerSb, sleOwner, -1, j); } } @@ -948,6 +948,7 @@ TER applyCreateAccountAttestations( ApplyView& view, RawView& rawView, + STTx const& tx, TIter attBegin, TIter attEnd, AccountID const& doorAccount, @@ -1029,10 +1030,11 @@ applyCreateAccountAttestations( // Check reserve auto const balance = (*sleDoor)[sfBalance]; - auto const reserve = psb.fees().accountReserve((*sleDoor)[sfOwnerCount] + 1); - - if (balance < reserve) - return std::unexpected(tecINSUFFICIENT_RESERVE); + // Don't sponsor door account objects in transactions not sent by the door account + // itself + if (auto const ret = checkInsufficientReserve(psb, tx, sleDoor, balance, {}, 1, 0, j); + !isTesSuccess(ret)) + return std::unexpected(ret); // tecINSUFFICIENT_RESERVE } std::vector atts; @@ -1137,8 +1139,9 @@ applyCreateAccountAttestations( if (!sleDoor) return tecINTERNAL; // LCOV_EXCL_LINE - // Reserve was already checked - adjustOwnerCount(psb, sleDoor, 1, j); + // Don't sponsor door account objects in transactions not sent by the door account + // itself + adjustOwnerCount(psb, sleDoor, {}, 1, j); psb.insert(createdSleClaimID); psb.update(sleDoor); } @@ -1311,6 +1314,7 @@ attestationDoApply(ApplyContext& ctx) return applyCreateAccountAttestations( ctx.view(), ctx.rawView(), + ctx.tx, &*att, &*att + 1, thisDoor, @@ -1436,10 +1440,13 @@ XChainCreateBridge::preclaim(PreclaimContext const& ctx) return terNO_ACCOUNT; auto const balance = (*sleAcc)[sfBalance]; - auto const reserve = ctx.view.fees().accountReserve((*sleAcc)[sfOwnerCount] + 1); - - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; + auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + if (auto const ret = checkInsufficientReserve( + ctx.view, ctx.tx, sleAcc, balance, *sponsorSle, 1, 0, ctx.j); + !isTesSuccess(ret)) + return ret; } return tesSUCCESS; @@ -1481,7 +1488,11 @@ XChainCreateBridge::doApply() (*sleBridge)[sfOwnerNode] = *page; } - adjustOwnerCount(ctx_.view(), sleAcct, 1, ctx_.journal); + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + adjustOwnerCount(ctx_.view(), sleAcct, *sponsorSle, 1, ctx_.journal); + addSponsorToLedgerEntry(sleBridge, *sponsorSle); ctx_.view().insert(sleBridge); ctx_.view().update(sleAcct); @@ -1984,10 +1995,13 @@ XChainCreateClaimID::preclaim(PreclaimContext const& ctx) return terNO_ACCOUNT; auto const balance = (*sleAcc)[sfBalance]; - auto const reserve = ctx.view.fees().accountReserve((*sleAcc)[sfOwnerCount] + 1); - - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; + auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + if (auto const ret = checkInsufficientReserve( + ctx.view, ctx.tx, sleAcc, balance, *sponsorSle, 1, 0, ctx.j); + !isTesSuccess(ret)) + return ret; } return tesSUCCESS; @@ -2043,7 +2057,11 @@ XChainCreateClaimID::doApply() (*sleClaimID)[sfOwnerNode] = *page; } - adjustOwnerCount(ctx_.view(), sleAcct, 1, ctx_.journal); + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + adjustOwnerCount(ctx_.view(), sleAcct, *sponsorSle, 1, ctx_.journal); + addSponsorToLedgerEntry(sleClaimID, *sponsorSle); ctx_.view().insert(sleClaimID); ctx_.view().update(sleBridge); diff --git a/src/libxrpl/tx/transactors/check/CheckCancel.cpp b/src/libxrpl/tx/transactors/check/CheckCancel.cpp index d9667721916..e7bd2b23a27 100644 --- a/src/libxrpl/tx/transactors/check/CheckCancel.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCancel.cpp @@ -91,8 +91,7 @@ CheckCancel::doApply() } // If we succeeded, update the check owner's reserve. - auto const sleSrc = view().peek(keylet::account(srcId)); - adjustOwnerCount(view(), sleSrc, -1, viewJ); + adjustOwnerCountObj(view(), srcId, sleCheck, -1, viewJ); // Remove check from ledger. view().erase(sleCheck); diff --git a/src/libxrpl/tx/transactors/check/CheckCash.cpp b/src/libxrpl/tx/transactors/check/CheckCash.cpp index c5813ae42d1..ab46f186f10 100644 --- a/src/libxrpl/tx/transactors/check/CheckCash.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCash.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -30,7 +31,7 @@ #include #include -#include +#include #include namespace xrpl { @@ -175,7 +176,7 @@ CheckCash::preclaim(PreclaimContext const& ctx) // once the check is cashed, since the check's reserve will no // longer be required. So, if we're dealing in XRP, we add one // reserve's worth to the available funds. - if (value.native()) + if (value.native() && !sleCheck->isFieldPresent(sfSponsor)) availableFunds += XRPAmount{ctx.view.fees().increment}; if (value > availableFunds) @@ -307,6 +308,8 @@ CheckCash::doApply() // LCOV_EXCL_STOP } + auto const sponsorCheckSle = getLedgerEntryReserveSponsor(psb, sleCheck); + // Preclaim already checked that source has at least the requested // funds. // @@ -335,7 +338,7 @@ CheckCash::doApply() // from src's directory, we allow them to send that additional // incremental reserve amount in the transfer. Hence the -1 // argument. - STAmount const srcLiquid{xrpLiquid(psb, srcId, -1, viewJ)}; + STAmount const srcLiquid{xrpLiquid(psb, srcId, sponsorCheckSle ? 0 : -1, viewJ)}; // Now, how much do they need in order to be successful? STAmount const xrpDeliver{ @@ -385,14 +388,19 @@ CheckCash::doApply() STAmount const flowDeliver{ optDeliverMin ? maxDeliverMin() : ctx_.tx.getFieldAmount(sfAmount)}; + auto const sponsorSle = getTxReserveSponsor(psb, ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + // Check reserve. Return destination account SLE if enough reserve, // otherwise return nullptr. auto checkReserve = [&]() -> SLE::pointer { auto sleDst = psb.peek(keylet::account(accountID_)); // Can the account cover the trust line's or MPT reserve? - if (std::uint32_t const ownerCount = {sleDst->at(sfOwnerCount)}; - preFeeBalance_ < psb.fees().accountReserve(ownerCount + 1)) + if (auto const ret = checkInsufficientReserve( + psb, ctx_.tx, sleDst, preFeeBalance_, *sponsorSle, 1, 0, j_); + !isTesSuccess(ret)) { JLOG(j_.trace()) << "Trust line does not exist. " "Insufficient reserve to create line."; @@ -449,6 +457,7 @@ CheckCash::doApply() Issue(currency, accountID_), // limit of zero 0, // quality in 0, // quality out + *sponsorSle, // sponsor viewJ); // journal !isTesSuccess(ter)) { @@ -495,7 +504,8 @@ CheckCash::doApply() if (sleDst == nullptr) return tecINSUFFICIENT_RESERVE; - if (auto const err = checkCreateMPT(psb, mptID, accountID_, j_); + if (auto const err = + checkCreateMPT(psb, mptID, accountID_, *sponsorSle, j_); !isTesSuccess(err)) { return err; @@ -581,7 +591,8 @@ CheckCash::doApply() } // If we succeeded, update the check owner's reserve. - adjustOwnerCount(psb, psb.peek(keylet::account(srcId)), -1, viewJ); + + adjustOwnerCount(psb, psb.peek(keylet::account(srcId)), sponsorCheckSle, -1, viewJ); // Remove check from ledger. psb.erase(sleCheck); diff --git a/src/libxrpl/tx/transactors/check/CheckCreate.cpp b/src/libxrpl/tx/transactors/check/CheckCreate.cpp index ff82912a1f6..e9f3e175c64 100644 --- a/src/libxrpl/tx/transactors/check/CheckCreate.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCreate.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -194,13 +195,13 @@ CheckCreate::doApply() // A check counts against the reserve of the issuing account, but we // check the starting balance because we want to allow dipping into the // reserve to pay fees. - { - STAmount const reserve{view().fees().accountReserve(sle->getFieldU32(sfOwnerCount) + 1)}; - - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; - } - + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + if (auto const ret = checkInsufficientReserve( + view(), ctx_.tx, sle, preFeeBalance_, *sponsorSle, 1, 0, ctx_.journal); + !isTesSuccess(ret)) + return ret; // Note that we use the value from the sequence or ticket as the // Check sequence. For more explanation see comments in SeqProxy.h. std::uint32_t const seq = ctx_.tx.getSeqValue(); @@ -253,7 +254,9 @@ CheckCreate::doApply() sleCheck->setFieldU64(sfOwnerNode, *page); } // If we succeeded, the new entry counts against the creator's reserve. - adjustOwnerCount(view(), sle, 1, viewJ); + + adjustOwnerCount(view(), sle, *sponsorSle, 1, viewJ); + addSponsorToLedgerEntry(sleCheck, *sponsorSle); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp index e0ebdd893ab..32563a536e9 100644 --- a/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp +++ b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -93,12 +94,13 @@ CredentialAccept::doApply() if (!sleSubject || !sleIssuer) return tefINTERNAL; // LCOV_EXCL_LINE - { - STAmount const reserve{ - view().fees().accountReserve(sleSubject->getFieldU32(sfOwnerCount) + 1)}; - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; - } + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + if (auto const ret = checkInsufficientReserve( + view(), ctx_.tx, sleSubject, preFeeBalance_, *sponsorSle, 1, 0, ctx_.journal); + !isTesSuccess(ret)) + return ret; auto const credType(ctx_.tx[sfCredentialType]); Keylet const credentialKey = keylet::credential(accountID_, issuer, credType); @@ -117,8 +119,10 @@ CredentialAccept::doApply() sleCred->setFieldU32(sfFlags, lsfAccepted); view().update(sleCred); - adjustOwnerCount(view(), sleIssuer, -1, j_); - adjustOwnerCount(view(), sleSubject, 1, j_); + adjustOwnerCountObj(view(), sleIssuer, sleCred, -1, j_); + removeSponsorFromLedgerEntry(sleCred); + adjustOwnerCount(view(), sleSubject, *sponsorSle, 1, j_); + addSponsorToLedgerEntry(sleCred, *sponsorSle); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp index acca408a95f..90cfbb5ca91 100644 --- a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp +++ b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp @@ -7,6 +7,7 @@ #include #include // IWYU pragma: keep #include +#include #include #include #include @@ -130,12 +131,13 @@ CredentialCreate::doApply() if (!sleIssuer) return tefINTERNAL; // LCOV_EXCL_LINE - { - STAmount const reserve{ - view().fees().accountReserve(sleIssuer->getFieldU32(sfOwnerCount) + 1)}; - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; - } + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + if (auto const ret = checkInsufficientReserve( + view(), ctx_.tx, sleIssuer, preFeeBalance_, *sponsorSle, 1, 0, ctx_.journal); + !isTesSuccess(ret)) + return ret; sleCred->setAccountID(sfSubject, subject); sleCred->setAccountID(sfIssuer, accountID_); @@ -153,7 +155,8 @@ CredentialCreate::doApply() return tecDIR_FULL; sleCred->setFieldU64(sfIssuerNode, *page); - adjustOwnerCount(view(), sleIssuer, 1, j_); + adjustOwnerCount(view(), sleIssuer, *sponsorSle, 1, j_); + addSponsorToLedgerEntry(sleCred, *sponsorSle); } if (subject == accountID_) diff --git a/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp index 82fe88aa9fe..ff58e9a2c83 100644 --- a/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp +++ b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -94,11 +95,13 @@ DelegateSet::doApply() if (permissions.empty()) return tecINTERNAL; // LCOV_EXCL_LINE - STAmount const reserve{ - ctx_.view().fees().accountReserve(sleOwner->getFieldU32(sfOwnerCount) + 1)}; - - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + if (auto const ret = checkInsufficientReserve( + view(), ctx_.tx, sleOwner, preFeeBalance_, *sponsorSle, 1, 0, ctx_.journal); + !isTesSuccess(ret)) + return ret; sle = std::make_shared(delegateKey); sle->setAccountID(sfAccount, accountID_); @@ -126,7 +129,8 @@ DelegateSet::doApply() (*sle)[sfDestinationNode] = *destPage; ctx_.view().insert(sle); - adjustOwnerCount(ctx_.view(), sleOwner, 1, ctx_.journal); + adjustOwnerCount(ctx_.view(), sleOwner, *sponsorSle, 1, ctx_.journal); + addSponsorToLedgerEntry(sle, *sponsorSle); return tesSUCCESS; } @@ -166,7 +170,7 @@ DelegateSet::deleteDelegate(ApplyView& view, SLE::ref sle, beast::Journal j) if (!sleOwner) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleOwner, -1, j); + adjustOwnerCountObj(view, sleOwner, sle, -1, j); view.erase(sle); diff --git a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp index b94e97e931a..27d0da57fdd 100644 --- a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp @@ -225,6 +225,7 @@ AMMClawback::applyGuts(Sandbox& sb) std::tie(result, newLPTokenBalance, amountWithdraw, amount2Withdraw) = AMMWithdraw::equalWithdrawTokens( sb, + ctx_.tx, *ammSle, holder, ammAccount, @@ -312,6 +313,7 @@ AMMClawback::equalWithdrawMatchingOneAmount( // tfee is actually not used, so pass tfee as 0. return AMMWithdraw::equalWithdrawTokens( sb, + ctx_.tx, ammSle, holder, ammAccount, @@ -345,6 +347,7 @@ AMMClawback::equalWithdrawMatchingOneAmount( return AMMWithdraw::withdraw( sb, + ctx_.tx, ammSle, ammAccount, holder, @@ -365,6 +368,7 @@ AMMClawback::equalWithdrawMatchingOneAmount( // tfee is actually not used, so pass tfee as 0. return AMMWithdraw::withdraw( sb, + ctx_.tx, ammSle, ammAccount, holder, diff --git a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp index 9c1a5cfacbc..bde57f75df8 100644 --- a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -147,15 +148,43 @@ AMMCreate::preclaim(PreclaimContext const& ctx) return terNO_RIPPLE; } - // Check the reserve for LPToken trustline - STAmount const xrpBalance = xrpLiquid(ctx.view, accountID, 1, ctx.j); - // Insufficient reserve - if (xrpBalance <= beast::kZero) + if (ctx.view.rules().enabled(featureSponsor)) { - JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves"; - return tecINSUF_RESERVE_LINE; + auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + + // Check the reserve for LPToken trustline + // Insufficient reserve + auto const accountSle = ctx.view.read(keylet::account(accountID)); + if (auto const ret = checkInsufficientReserve( + ctx.view, + ctx.tx, + accountSle, + accountSle->getFieldAmount(sfBalance), + *sponsorSle, + 1, + 0, + ctx.j); + !isTesSuccess(ret)) + { + JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves"; + return tecINSUF_RESERVE_LINE; + } + } + else + { + STAmount const xrpBalance = xrpLiquid(ctx.view, accountID, 1, ctx.j); + // Insufficient reserve + if (xrpBalance <= beast::kZero) + { + JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves"; + return tecINSUF_RESERVE_LINE; + } } + auto const ownerCountAdj = isReserveSponsored(ctx.tx) ? 0 : 1; + STAmount const xrpBalance = xrpLiquid(ctx.view, accountID, ownerCountAdj, ctx.j); auto insufficientBalance = [&](STAmount const& amount) { if (isXRP(amount)) return xrpBalance < amount; @@ -294,7 +323,11 @@ applyCreate(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Jou sb.insert(ammSle); // Send LPT to LP. - auto res = accountSend(sb, accountId, account, lpTokens, ctx.journal); + auto const sponsorSle = getTxReserveSponsor(sb, ctx.tx); + if (!sponsorSle) + return {sponsorSle.error(), false}; // LCOV_EXCL_LINE + + auto res = accountSend(sb, accountId, account, lpTokens, ctx.journal, *sponsorSle); if (!isTesSuccess(res)) { JLOG(j.debug()) << "AMM Instance: failed to send LPT " << lpTokens; @@ -315,17 +348,30 @@ applyCreate(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Jou return err; } - if (auto const err = createMPToken(sb, mptID, accountId, flags); !isTesSuccess(err)) + if (auto const err = createMPToken(sb, mptID, accountId, {}, flags); + !isTesSuccess(err)) return err; // Don't adjust AMM owner count. // It's irrelevant for pseudo-account like AMM. return accountSend( - sb, account, accountId, amount, ctx.journal, WaiveTransferFee::Yes); + sb, + account, + accountId, + amount, + ctx.journal, + {}, // don't sponsor for AMM Trustline + WaiveTransferFee::Yes); }, // Set AMM flag on AMM trustline [&](Issue const& issue) -> TER { if (auto const res = accountSend( - sb, account, accountId, amount, ctx.journal, WaiveTransferFee::Yes)) + sb, + account, + accountId, + amount, + ctx.journal, + {}, // don't sponsor for AMM Trustline + WaiveTransferFee::Yes)) return res; // Set AMM flag on AMM trustline if (!isXRP(amount)) diff --git a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp index 91858e3cd7e..9db26c99200 100644 --- a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -234,11 +235,33 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) // Adjust the reserve if LP doesn't have LPToken trustline auto const sle = ctx.view.read(keylet::line(accountID, lpIssue.account, lpIssue.currency)); - if (xrpLiquid(ctx.view, accountID, !sle, ctx.j) >= deposit) - return TER(tesSUCCESS); - if (sle) - return tecUNFUNDED_AMM; - return tecINSUF_RESERVE_LINE; + + auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + auto const accountSle = ctx.view.read(keylet::account(accountID)); + auto const reserveAdj = (*sponsorSle || sle) ? 0 : 1; + + if (xrpLiquid(ctx.view, accountID, reserveAdj, ctx.j) < deposit) + { + if (sle) + return tecUNFUNDED_AMM; + return tecINSUF_RESERVE_LINE; + } + + if (auto const ret = checkInsufficientReserve( + ctx.view, + ctx.tx, + accountSle, + accountSle->getFieldAmount(sfBalance) - deposit, + *sponsorSle, + 1, + !sle, + ctx.j); + *sponsorSle && !isTesSuccess(ret)) + return tecINSUF_RESERVE_LINE; + + return tesSUCCESS; } return accountFunds( ctx.view, @@ -359,12 +382,37 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) // We checked above but need to check again if depositing IOU only. if (ammLPHolds(ctx.view, *ammSle, accountID, ctx.j) == beast::kZero) { - STAmount const xrpBalance = xrpLiquid(ctx.view, accountID, 1, ctx.j); - // Insufficient reserve - if (xrpBalance <= beast::kZero) + if (ctx.view.rules().enabled(featureSponsor)) + { + auto const accountSle = ctx.view.read(keylet::account(accountID)); + auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + // Insufficient reserve + if (auto const ret = checkInsufficientReserve( + ctx.view, + ctx.tx, + accountSle, + accountSle->getFieldAmount(sfBalance), + *sponsorSle, + 1, + 0, + ctx.j); + !isTesSuccess(ret)) + { + JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves"; + return tecINSUF_RESERVE_LINE; + } + } + else { - JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves"; - return tecINSUF_RESERVE_LINE; + STAmount const xrpBalance = xrpLiquid(ctx.view, accountID, 1, ctx.j); + // Insufficient reserve + if (xrpBalance <= beast::kZero) + { + JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves"; + return tecINSUF_RESERVE_LINE; + } } } @@ -510,6 +558,10 @@ AMMDeposit::deposit( std::optional const& lpTokensDepositMin, std::uint16_t tfee) { + auto const sponsorSle = getTxReserveSponsor(view, ctx_.tx); + if (!sponsorSle) + return {sponsorSle.error(), STAmount{}}; // LCOV_EXCL_LINE + // Check account has sufficient funds. // Return true if it does, false otherwise. auto checkBalance = [&](auto const& depositAmount) -> TER { @@ -519,8 +571,10 @@ AMMDeposit::deposit( { auto const& lpIssue = lpTokensDeposit.get(); // Adjust the reserve if LP doesn't have LPToken trustline - auto const sle = view.read(keylet::line(accountID_, lpIssue.account, lpIssue.currency)); - if (xrpLiquid(view, accountID_, !sle, j_) >= depositAmount) + auto const trustlineExists = + view.exists(keylet::line(accountID_, lpIssue.account, lpIssue.currency)); + auto const reserveAdj = (*sponsorSle || trustlineExists) ? 0 : 1; + if (xrpLiquid(view, accountID_, reserveAdj, j_) >= depositAmount) return tesSUCCESS; } else if ( @@ -574,7 +628,13 @@ AMMDeposit::deposit( } auto res = accountSend( - view, accountID_, ammAccount, amountDepositActual, ctx_.journal, WaiveTransferFee::Yes); + view, + accountID_, + ammAccount, + amountDepositActual, + ctx_.journal, + {}, // don't sponsor for AMM Trustline + WaiveTransferFee::Yes); if (!isTesSuccess(res)) { JLOG(ctx_.journal.debug()) << "AMM Deposit: failed to deposit " << amountDepositActual; @@ -598,6 +658,7 @@ AMMDeposit::deposit( ammAccount, *amount2DepositActual, ctx_.journal, + {}, // don't sponsor for AMM Trustline WaiveTransferFee::Yes); if (!isTesSuccess(res)) { @@ -608,7 +669,8 @@ AMMDeposit::deposit( } // Deposit LP tokens - res = accountSend(view, ammAccount, accountID_, lpTokensDepositActual, ctx_.journal); + res = + accountSend(view, ammAccount, accountID_, lpTokensDepositActual, ctx_.journal, *sponsorSle); if (!isTesSuccess(res)) { JLOG(ctx_.journal.debug()) << "AMM Deposit: failed to deposit LPTokens"; diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp index d3a6c9c74cf..cfbce8fc2b1 100644 --- a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp @@ -7,8 +7,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -352,6 +354,7 @@ AMMWithdraw::applyGuts(Sandbox& sb) { return equalWithdrawLimit( sb, + ctx_.tx, *ammSle, ammAccountID, amountBalance, @@ -365,6 +368,7 @@ AMMWithdraw::applyGuts(Sandbox& sb) { return singleWithdrawTokens( sb, + ctx_.tx, *ammSle, ammAccountID, amountBalance, @@ -376,17 +380,26 @@ AMMWithdraw::applyGuts(Sandbox& sb) if (subTxType & tfLimitLPToken) { return singleWithdrawEPrice( - sb, *ammSle, ammAccountID, amountBalance, lptAMMBalance, *amount, *ePrice, tfee); + sb, + ctx_.tx, + *ammSle, + ammAccountID, + amountBalance, + lptAMMBalance, + *amount, + *ePrice, + tfee); } if (subTxType & tfSingleAsset) { return singleWithdraw( - sb, *ammSle, ammAccountID, amountBalance, lptAMMBalance, *amount, tfee); + sb, ctx_.tx, *ammSle, ammAccountID, amountBalance, lptAMMBalance, *amount, tfee); } if (subTxType & tfLPToken || subTxType & tfWithdrawAll) { return equalWithdrawTokens( sb, + ctx_.tx, *ammSle, ammAccountID, amountBalance, @@ -437,6 +450,7 @@ AMMWithdraw::doApply() std::pair AMMWithdraw::withdraw( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -450,6 +464,7 @@ AMMWithdraw::withdraw( STAmount newLPTokenBalance; std::tie(ter, newLPTokenBalance, std::ignore, std::ignore) = withdraw( view, + tx, ammSle, ammAccount, accountID_, @@ -470,6 +485,7 @@ AMMWithdraw::withdraw( std::tuple> AMMWithdraw::withdraw( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, AccountID const& account, @@ -593,6 +609,17 @@ AMMWithdraw::withdraw( } } + // this is also called from AMMClawback, but only AMMWithdraw does sponsor + // the new trustline + SLE::pointer sponsorSle; + if (tx[sfAccount] == account) + { + auto sle = getTxReserveSponsor(view, tx); + if (!sle) + return {sle.error(), STAmount{}, STAmount{}, STAmount{}}; // LCOV_EXCL_LINE + sponsorSle = std::move(*sle); + } + // Check the reserve in case a trustline or MPT has to be created bool const enabledFixAmMv12 = view.rules().enabled(fixAMMv1_2); // If seated after a call to sufficientReserve() then MPToken must be @@ -618,17 +645,25 @@ AMMWithdraw::withdraw( auto sleAccount = view.peek(keylet::account(account)); if (!sleAccount) return tecINTERNAL; // LCOV_EXCL_LINE - STAmount const balance = (*sleAccount)[sfBalance]; - std::uint32_t const ownerCount = sleAccount->at(sfOwnerCount); + auto const balance = (*sleAccount)[sfBalance]->xrp(); + std::uint32_t const count = + ownerCount(view, sponsorSle ? sponsorSle : sleAccount, journal); // See also TrustSet::doApply() and MPTokenAuthorize::authorize() - XRPAmount const reserve( - (ownerCount < 2) ? XRPAmount(beast::kZero) - : view.fees().accountReserve(ownerCount + 1)); - - auto const balanceAdj = isIssue ? std::max(priorBalance, balance.xrp()) : priorBalance; - if (balanceAdj < reserve) - return tecINSUFFICIENT_RESERVE; + if (count >= 2) + { + if (auto const ret = checkInsufficientReserve( + view, + tx, + sleAccount, + std::max(priorBalance, balance), + sponsorSle, + 1, + 0, + journal); + !isTesSuccess(ret)) + return ret; + } } return tesSUCCESS; }; @@ -643,7 +678,7 @@ AMMWithdraw::withdraw( !isTesSuccess(err)) return err; - if (auto const err = checkCreateMPT(view, mptIssue, account, journal); + if (auto const err = checkCreateMPT(view, mptIssue, account, sponsorSle, journal); !isTesSuccess(err)) { return err; @@ -660,7 +695,13 @@ AMMWithdraw::withdraw( // Withdraw amountWithdraw auto res = accountSend( - view, ammAccount, account, amountWithdrawActual, journal, WaiveTransferFee::Yes); + view, + ammAccount, + account, + amountWithdrawActual, + journal, + sponsorSle, + WaiveTransferFee::Yes); if (!isTesSuccess(res)) { // LCOV_EXCL_START @@ -679,7 +720,13 @@ AMMWithdraw::withdraw( return {res, STAmount{}, STAmount{}, STAmount{}}; res = accountSend( - view, ammAccount, account, *amount2WithdrawActual, journal, WaiveTransferFee::Yes); + view, + ammAccount, + account, + *amount2WithdrawActual, + journal, + sponsorSle, + WaiveTransferFee::Yes); if (!isTesSuccess(res)) { // LCOV_EXCL_START @@ -724,6 +771,7 @@ adjustLPTokensIn( std::pair AMMWithdraw::equalWithdrawTokens( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -737,6 +785,7 @@ AMMWithdraw::equalWithdrawTokens( STAmount newLPTokenBalance; std::tie(ter, newLPTokenBalance, std::ignore, std::ignore) = equalWithdrawTokens( view, + tx, ammSle, accountID_, ammAccount, @@ -788,6 +837,7 @@ AMMWithdraw::deleteAMMAccountIfEmpty( std::tuple> AMMWithdraw::equalWithdrawTokens( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const account, AccountID const& ammAccount, @@ -810,6 +860,7 @@ AMMWithdraw::equalWithdrawTokens( { return withdraw( view, + tx, ammSle, ammAccount, account, @@ -845,6 +896,7 @@ AMMWithdraw::equalWithdrawTokens( return withdraw( view, + tx, ammSle, ammAccount, account, @@ -897,6 +949,7 @@ AMMWithdraw::equalWithdrawTokens( std::pair AMMWithdraw::equalWithdrawLimit( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -917,6 +970,7 @@ AMMWithdraw::equalWithdrawLimit( { return withdraw( view, + tx, ammSle, ammAccount, amountBalance, @@ -949,6 +1003,7 @@ AMMWithdraw::equalWithdrawLimit( } return withdraw( view, + tx, ammSle, ammAccount, amountBalance, @@ -967,6 +1022,7 @@ AMMWithdraw::equalWithdrawLimit( std::pair AMMWithdraw::singleWithdraw( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -995,6 +1051,7 @@ AMMWithdraw::singleWithdraw( return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE return withdraw( view, + tx, ammSle, ammAccount, amountBalance, @@ -1018,6 +1075,7 @@ AMMWithdraw::singleWithdraw( std::pair AMMWithdraw::singleWithdrawTokens( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -1036,6 +1094,7 @@ AMMWithdraw::singleWithdrawTokens( { return withdraw( view, + tx, ammSle, ammAccount, amountBalance, @@ -1071,6 +1130,7 @@ AMMWithdraw::singleWithdrawTokens( std::pair AMMWithdraw::singleWithdrawEPrice( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -1118,6 +1178,7 @@ AMMWithdraw::singleWithdrawEPrice( { return withdraw( view, + tx, ammSle, ammAccount, amountBalance, diff --git a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp index 547d40e7b80..b3cc744332e 100644 --- a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -831,25 +832,26 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel) if (!sleCreator) return {tefINTERNAL, false}; + auto const sponsorSle = getTxReserveSponsor(sb, ctx_.tx); + if (!sponsorSle) + return {sponsorSle.error(), false}; // LCOV_EXCL_LINE + + if (auto const ret = checkInsufficientReserve( + sb, ctx_.tx, sleCreator, preFeeBalance_, *sponsorSle, 1, 0, j_); + !isTesSuccess(ret)) { - XRPAmount const reserve = - sb.fees().accountReserve(sleCreator->getFieldU32(sfOwnerCount) + 1); + // If we are here, the signing account had an insufficient reserve + // *prior* to our processing. If something actually crossed, then + // we allow this; otherwise, we just claim a fee. + if (!crossed) + result = tecINSUF_RESERVE_OFFER; - if (preFeeBalance_ < reserve) + if (!isTesSuccess(result)) { - // If we are here, the signing account had an insufficient reserve - // *prior* to our processing. If something actually crossed, then - // we allow this; otherwise, we just claim a fee. - if (!crossed) - result = tecINSUF_RESERVE_OFFER; - - if (!isTesSuccess(result)) - { - JLOG(j_.debug()) << "final result: " << transToken(result); - } - - return {result, true}; + JLOG(j_.debug()) << "final result: " << transToken(result); } + + return {result, true}; } // We need to place the remainder of the offer into its order book. @@ -868,7 +870,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel) } // Update owner count. - adjustOwnerCount(sb, sleCreator, 1, viewJ); + adjustOwnerCount(sb, sleCreator, *sponsorSle, 1, viewJ); JLOG(j_.trace()) << "adding to book: " << to_string(saTakerPays.asset()) << " : " << to_string(saTakerGets.asset()) @@ -937,6 +939,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel) sleOffer->setFlag(lsfSell); if (domainID) sleOffer->setFieldH256(sfDomainID, *domainID); + addSponsorToLedgerEntry(sleOffer, *sponsorSle); // if it's a hybrid offer, set hybrid flag, and create an open dir if (bHybrid) diff --git a/src/libxrpl/tx/transactors/did/DIDDelete.cpp b/src/libxrpl/tx/transactors/did/DIDDelete.cpp index 90aa21d8a1f..a2f328dec45 100644 --- a/src/libxrpl/tx/transactors/did/DIDDelete.cpp +++ b/src/libxrpl/tx/transactors/did/DIDDelete.cpp @@ -50,8 +50,7 @@ DIDDelete::deleteSLE(ApplyView& view, SLE::pointer sle, AccountID const owner, b if (!sleOwner) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleOwner, -1, j); - view.update(sleOwner); + adjustOwnerCountObj(view, sleOwner, sle, -1, j); // Remove object from ledger view.erase(sle); diff --git a/src/libxrpl/tx/transactors/did/DIDSet.cpp b/src/libxrpl/tx/transactors/did/DIDSet.cpp index 1392581bb04..094d0548f3b 100644 --- a/src/libxrpl/tx/transactors/did/DIDSet.cpp +++ b/src/libxrpl/tx/transactors/did/DIDSet.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -70,13 +71,14 @@ addSLE(ApplyContext& ctx, SLE::ref sle, AccountID const& owner) return tefINTERNAL; // LCOV_EXCL_LINE // Check reserve availability for new object creation - { - auto const balance = STAmount((*sleAccount)[sfBalance]).xrp(); - auto const reserve = ctx.view().fees().accountReserve((*sleAccount)[sfOwnerCount] + 1); - - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; - } + auto const sponsorSle = getTxReserveSponsor(ctx.view(), ctx.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + auto const balance = STAmount((*sleAccount)[sfBalance]).xrp(); + if (auto const ret = checkInsufficientReserve( + ctx.view(), ctx.tx, sleAccount, balance, *sponsorSle, 1, 0, ctx.journal); + !isTesSuccess(ret)) + return ret; // Add ledger object to ledger ctx.view().insert(sle); @@ -89,7 +91,8 @@ addSLE(ApplyContext& ctx, SLE::ref sle, AccountID const& owner) return tecDIR_FULL; // LCOV_EXCL_LINE (*sle)[sfOwnerNode] = *page; } - adjustOwnerCount(ctx.view(), sleAccount, 1, ctx.journal); + adjustOwnerCount(ctx.view(), sleAccount, *sponsorSle, 1, ctx.journal); + addSponsorToLedgerEntry(sle, *sponsorSle); ctx.view().update(sleAccount); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp index b8f4604d73c..2d515b95810 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp @@ -182,6 +182,7 @@ EscrowCancel::doApply() [&](T const&) { return escrowUnlockApplyHelper( ctx_.view(), + ctx_.tx, kParityRate, ctx_.view().rules().enabled(fixCleanup3_2_0) ? sle : slep, preFeeBalance_, @@ -209,8 +210,7 @@ EscrowCancel::doApply() } } - adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); - ctx_.view().update(sle); + adjustOwnerCountObj(ctx_.view(), sle, slep, -1, ctx_.journal); // Remove escrow from ledger ctx_.view().erase(slep); diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp index 0a12e2d1bc0..bdb4bcddaee 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -434,16 +435,21 @@ EscrowCreate::doApply() // Check reserve and funds availability STAmount const amount{ctx_.tx[sfAmount]}; - auto const reserve = ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1); - auto const balance = sle->getFieldAmount(sfBalance).xrp(); - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + if (auto const ret = + checkInsufficientReserve(ctx_.view(), ctx_.tx, sle, balance, *sponsorSle, 1, 0, j_); + !isTesSuccess(ret)) + return ret; // Check reserve and funds availability if (isXRP(amount)) { - if (balance < reserve + STAmount(amount).xrp()) + if (auto const ret = checkInsufficientReserve( + ctx_.view(), ctx_.tx, sle, balance - STAmount(amount).xrp(), {}, 1, 0, j_); + !isTesSuccess(ret)) return tecUNFUNDED; } @@ -535,7 +541,8 @@ EscrowCreate::doApply() } // increment owner count - adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); + adjustOwnerCount(ctx_.view(), sle, *sponsorSle, 1, ctx_.journal); + addSponsorToLedgerEntry(slep, *sponsorSle); ctx_.view().update(sle); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp index 4cda867b482..4bc12fbe56d 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -222,6 +223,11 @@ EscrowFinish::preclaim(PreclaimContext const& ctx) return ret; } } + + auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); + if (!sponsorSle) + return sponsorSle.error(); + return tesSUCCESS; } @@ -360,6 +366,7 @@ EscrowFinish::doApply() [&](T const&) { return escrowUnlockApplyHelper( ctx_.view(), + ctx_.tx, lockedRate, sled, preFeeBalance_, @@ -390,9 +397,7 @@ EscrowFinish::doApply() ctx_.view().update(sled); // Adjust source owner count - auto const sle = ctx_.view().peek(keylet::account(account)); - adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); - ctx_.view().update(sle); + adjustOwnerCountObj(ctx_.view(), account, slep, -1, ctx_.journal); // Remove escrow from ledger ctx_.view().erase(slep); diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp index 041bf73abf7..7b520ae111b 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp @@ -372,7 +372,7 @@ LoanBrokerCoverClawback::doApply() associateAsset(*sleBroker, vaultAsset); // Transfer assets from pseudo-account to depositor. - return accountSend(view(), brokerPseudoID, account, clawAmount, j_, WaiveTransferFee::Yes); + return accountSend(view(), brokerPseudoID, account, clawAmount, j_, {}, WaiveTransferFee::Yes); } void diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp index 537996ba57f..6597dae7da0 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp @@ -166,7 +166,7 @@ LoanBrokerCoverDeposit::doApply() // Transfer assets from depositor to pseudo-account. if (auto ter = - accountSend(view(), accountID_, brokerPseudoID, amount, j_, WaiveTransferFee::Yes)) + accountSend(view(), accountID_, brokerPseudoID, amount, j_, {}, WaiveTransferFee::Yes)) return ter; // Increase the LoanBroker's CoverAvailable by Amount diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp index f3c000bf0bd..9de281f4a6d 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp @@ -155,11 +155,11 @@ LoanBrokerDelete::doApply() { auto const coverAvailable = STAmount{vaultAsset, broker->at(sfCoverAvailable)}; if (auto const ter = accountSend( - view(), brokerPseudoID, accountID_, coverAvailable, j_, WaiveTransferFee::Yes)) + view(), brokerPseudoID, accountID_, coverAvailable, j_, {}, WaiveTransferFee::Yes)) return ter; } - if (auto ter = removeEmptyHolding(view(), brokerPseudoID, vaultAsset, j_)) + if (auto ter = removeEmptyHolding(view(), tx, brokerPseudoID, vaultAsset, j_)) return ter; auto brokerPseudoSLE = view().peek(keylet::account(brokerPseudoID)); @@ -184,10 +184,6 @@ LoanBrokerDelete::doApply() return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE } - view().erase(brokerPseudoSLE); - - view().erase(broker); - { auto owner = view().peek(keylet::account(accountID_)); if (!owner) @@ -195,9 +191,15 @@ LoanBrokerDelete::doApply() // Decreases the owner count by two: one for the LoanBroker object, and // one for the pseudo-account. - adjustOwnerCount(view(), owner, -2, j_); + // LoanBroker object can be sponsored + adjustOwnerCountObj(view(), owner, broker, -1, j_); + + // pseudo-account cannot be sponsored + adjustOwnerCount(view(), owner, {}, -1, j_); } + view().erase(brokerPseudoSLE); + view().erase(broker); associateAsset(*broker, vaultAsset); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp index 2dc003eb7fe..826ff7f6fac 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -236,12 +237,29 @@ LoanBrokerSet::doApply() if (auto const ter = dirLink(view, vaultPseudoID, broker, sfVaultNode)) return ter; // LCOV_EXCL_LINE + auto const sponsorSle = getTxReserveSponsor(view, tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + + if (auto const ret = checkInsufficientReserve( + view, tx, owner, preFeeBalance_, {}, *sponsorSle ? 1 : 2, 0, j_); + !isTesSuccess(ret)) + return ret; + + if (*sponsorSle) + { + if (auto const ret = checkInsufficientReserve( + view, tx, owner, preFeeBalance_, *sponsorSle, 1, 0, j_); + !isTesSuccess(ret)) + return ret; + } + // Increases the owner count by two: one for the LoanBroker object, and // one for the pseudo-account. - adjustOwnerCount(view, owner, 2, j_); - auto const ownerCount = owner->at(sfOwnerCount); - if (preFeeBalance_ < view.fees().accountReserve(ownerCount)) - return tecINSUFFICIENT_RESERVE; + // Pseudo-account cannot be sponsored + adjustOwnerCount(view, owner, {}, 1, j_); + // LoanBroker object can be sponsored + adjustOwnerCount(view, owner, *sponsorSle, 1, j_); auto maybePseudo = createPseudoAccount(view, broker->key(), sfLoanBrokerID); if (!maybePseudo) @@ -249,7 +267,8 @@ LoanBrokerSet::doApply() auto& pseudo = *maybePseudo; auto pseudoId = pseudo->at(sfAccount); - if (auto ter = addEmptyHolding(view, pseudoId, preFeeBalance_, sleVault->at(sfAsset), j_)) + if (auto ter = + addEmptyHolding(view, tx, pseudoId, preFeeBalance_, sleVault->at(sfAsset), j_)) return ter; // Initialize data fields: @@ -270,6 +289,8 @@ LoanBrokerSet::doApply() if (auto const coverLiq = tx[~sfCoverRateLiquidation]) broker->at(sfCoverRateLiquidation) = *coverLiq; + addSponsorToLedgerEntry(broker, *sponsorSle); + view.insert(broker); associateAsset(*broker, vaultAsset); diff --git a/src/libxrpl/tx/transactors/lending/LoanDelete.cpp b/src/libxrpl/tx/transactors/lending/LoanDelete.cpp index d4ec92a9fb6..bc2842fbb36 100644 --- a/src/libxrpl/tx/transactors/lending/LoanDelete.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanDelete.cpp @@ -103,13 +103,11 @@ LoanDelete::doApply() if (!view.dirRemove(keylet::ownerDir(borrower), loanSle->at(sfOwnerNode), loanID, false)) return tefBAD_LEDGER; // LCOV_EXCL_LINE - // Delete the Loan object - view.erase(loanSle); - // Decrement the LoanBroker's owner count. // The broker's owner count is solely for the number of outstanding loans, // and is distinct from the broker's pseudo-account's owner count - adjustOwnerCount(view, brokerSle, -1, j_); + adjustOwnerCount(view, brokerSle, {}, -1, j_); + // If there are no loans left, then any remaining debt must be forgiven, // because there is no other way to pay it back. if (brokerSle->at(sfOwnerCount) == 0) @@ -129,7 +127,10 @@ LoanDelete::doApply() } } // Decrement the borrower's owner count - adjustOwnerCount(view, borrowerSle, -1, j_); + adjustOwnerCountObj(view, borrowerSle, loanSle, -1, j_); + + // Delete the Loan object + view.erase(loanSle); // These associations shouldn't do anything, but do them just to be safe associateAsset(*loanSle, vaultAsset); diff --git a/src/libxrpl/tx/transactors/lending/LoanManage.cpp b/src/libxrpl/tx/transactors/lending/LoanManage.cpp index 2b5c9d25f6e..b059617b73d 100644 --- a/src/libxrpl/tx/transactors/lending/LoanManage.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanManage.cpp @@ -292,6 +292,7 @@ LoanManage::defaultLoan( vaultSle->at(sfAccount), STAmount{vaultAsset, defaultCovered}, j, + {}, WaiveTransferFee::Yes); } diff --git a/src/libxrpl/tx/transactors/lending/LoanPay.cpp b/src/libxrpl/tx/transactors/lending/LoanPay.cpp index 5c0b46de42f..2ad8b32bcc5 100644 --- a/src/libxrpl/tx/transactors/lending/LoanPay.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanPay.cpp @@ -625,7 +625,7 @@ LoanPay::doApply() { // The broker may have deleted their holding. Recreate it if needed if (auto const ter = addEmptyHolding( - view, brokerPayee, brokerPayeeSle->at(sfBalance).value().xrp(), asset, j_); + view, tx, brokerPayee, brokerPayeeSle->at(sfBalance).value().xrp(), asset, j_); ter && ter != tecDUPLICATE) { // ignore tecDUPLICATE. That means the holding already exists, @@ -643,6 +643,7 @@ LoanPay::doApply() asset, {{vaultPseudoAccount, totalPaidToVaultRounded}, {brokerPayee, totalPaidToBroker}}, j_, + {}, // Vault and Broker cannot be sponsored WaiveTransferFee::Yes)) return ter; diff --git a/src/libxrpl/tx/transactors/lending/LoanSet.cpp b/src/libxrpl/tx/transactors/lending/LoanSet.cpp index 573f700f519..59a762206b1 100644 --- a/src/libxrpl/tx/transactors/lending/LoanSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -512,14 +513,18 @@ LoanSet::doApply() } } - adjustOwnerCount(view, borrowerSle, 1, j_); + auto const sponsorSle = getTxReserveSponsor(view, tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE { - auto const ownerCount = borrowerSle->at(sfOwnerCount); auto const balance = accountID_ == borrower ? preFeeBalance_ : borrowerSle->at(sfBalance).value().xrp(); - if (balance < view.fees().accountReserve(ownerCount)) - return tecINSUFFICIENT_RESERVE; + if (auto const ret = + checkInsufficientReserve(view, tx, borrowerSle, balance, *sponsorSle, 1, 0, j_); + !isTesSuccess(ret)) + return ret; } + adjustOwnerCount(view, borrowerSle, *sponsorSle, 1, j_); // Account for the origination fee using two payments // @@ -532,7 +537,7 @@ LoanSet::doApply() "xrpl::LoanSet::doApply", "borrower signed transaction"); if (auto const ter = addEmptyHolding( - view, borrower, borrowerSle->at(sfBalance).value().xrp(), vaultAsset, j_); + view, tx, borrower, borrowerSle->at(sfBalance).value().xrp(), vaultAsset, j_); ter && ter != tecDUPLICATE) { // ignore tecDUPLICATE. That means the holding already exists, and @@ -555,7 +560,7 @@ LoanSet::doApply() "broker owner signed transaction"); if (auto const ter = addEmptyHolding( - view, brokerOwner, brokerOwnerSle->at(sfBalance).value().xrp(), vaultAsset, j_); + view, tx, brokerOwner, brokerOwnerSle->at(sfBalance).value().xrp(), vaultAsset, j_); ter && ter != tecDUPLICATE) { // ignore tecDUPLICATE. That means the holding already exists, @@ -573,6 +578,7 @@ LoanSet::doApply() vaultAsset, {{borrower, loanAssetsToBorrower}, {brokerOwner, originationFee}}, j_, + {}, // Vault and Broker cannot be sponsored WaiveTransferFee::Yes)) return ter; @@ -618,6 +624,7 @@ LoanSet::doApply() loan->at(sfPreviousPaymentDueDate) = 0; loan->at(sfNextPaymentDueDate) = startDate + paymentInterval; loan->at(sfPaymentRemaining) = paymentTotal; + addSponsorToLedgerEntry(loan, *sponsorSle); view.insert(loan); // Update the balances in the vault @@ -633,7 +640,7 @@ LoanSet::doApply() adjustImpreciseNumber(brokerSle->at(sfDebtTotal), newDebtDelta, vaultAsset, vaultScale); // The broker's owner count is solely for the number of outstanding loans, // and is distinct from the broker's pseudo-account's owner count - adjustOwnerCount(view, brokerSle, 1, j_); + adjustOwnerCount(view, brokerSle, {}, 1, j_); loanSequenceProxy += 1; // The sequence should be extremely unlikely to roll over, but fail if it // does diff --git a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp index 6a82a150449..2298e2a8a68 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp @@ -5,7 +5,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -372,7 +374,12 @@ NFTokenAcceptOffer::transferNFToken( std::uint32_t const buyerOwnerCountBefore = sleBuyer->getFieldU32(sfOwnerCount); - auto const insertRet = nft::insertToken(view(), buyer, std::move(tokenAndPage->token)); + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + + auto const insertRet = + nft::insertToken(view(), ctx_.tx, buyer, *sponsorSle, std::move(tokenAndPage->token)); // if fixNFTokenReserve is enabled, check if the buyer has sufficient // reserve to own a new object, if their OwnerCount changed. @@ -392,9 +399,13 @@ NFTokenAcceptOffer::transferNFToken( auto const buyerOwnerCountAfter = sleBuyer->getFieldU32(sfOwnerCount); if (buyerOwnerCountAfter > buyerOwnerCountBefore) { - if (auto const reserve = view().fees().accountReserve(buyerOwnerCountAfter); - buyerBalance < reserve) - return tecINSUFFICIENT_RESERVE; + SLE::const_pointer buyerSponsorSle; + if (accountID_ == buyer) + buyerSponsorSle = *sponsorSle; + if (auto const ret = checkInsufficientReserve( + ctx_.view(), ctx_.tx, sleBuyer, buyerBalance, buyerSponsorSle, 0, 0, j_); + !isTesSuccess(ret)) + return ret; } } diff --git a/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp index 1948f3803d3..11a2f388725 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp @@ -77,6 +77,7 @@ NFTokenCreateOffer::doApply() // Use implementation shared with NFTokenMint return nft::tokenOfferCreateApply( view(), + ctx_.tx, ctx_.tx[sfAccount], ctx_.tx[sfAmount], ctx_.tx[~sfDestination], diff --git a/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp b/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp index d8e5a7b235f..4270f50c599 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp @@ -3,7 +3,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -304,7 +306,12 @@ NFTokenMint::doApply() object.setFieldVL(sfURI, *uri); }); - if (TER const ret = nft::insertToken(ctx_.view(), accountID_, std::move(newToken)); + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + + if (TER const ret = + nft::insertToken(ctx_.view(), ctx_.tx, accountID_, *sponsorSle, std::move(newToken)); !isTesSuccess(ret)) return ret; @@ -315,6 +322,7 @@ NFTokenMint::doApply() // because a Mint is only allowed to create a sell offer. if (TER const ter = nft::tokenOfferCreateApply( view(), + ctx_.tx, ctx_.tx[sfAccount], ctx_.tx[sfAmount], ctx_.tx[~sfDestination], @@ -335,9 +343,17 @@ NFTokenMint::doApply() view().read(keylet::account(accountID_))->getFieldU32(sfOwnerCount); ownerCountAfter > ownerCountBefore) { - if (auto const reserve = view().fees().accountReserve(ownerCountAfter); - preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; + if (auto const ret = checkInsufficientReserve( + ctx_.view(), + ctx_.tx, + view().read(keylet::account(accountID_)), + preFeeBalance_, + *sponsorSle, + 0, + 0, + j_); + !isTesSuccess(ret)) + return ret; } return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp b/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp index 9a268161555..6862616eb94 100644 --- a/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp +++ b/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp @@ -69,9 +69,7 @@ OracleDelete::deleteOracle( return tecINTERNAL; // LCOV_EXCL_LINE auto const count = sle->getFieldArray(sfPriceDataSeries).size() > 5 ? -2 : -1; - - adjustOwnerCount(view, sleOwner, count, j); - + adjustOwnerCountObj(view, sleOwner, sle, count, j); view.erase(sle); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/oracle/OracleSet.cpp b/src/libxrpl/tx/transactors/oracle/OracleSet.cpp index 12d826e54df..779d312e3b3 100644 --- a/src/libxrpl/tx/transactors/oracle/OracleSet.cpp +++ b/src/libxrpl/tx/transactors/oracle/OracleSet.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -150,9 +151,21 @@ OracleSet::preclaim(PreclaimContext const& ctx) if (!pairsDel.empty()) return tecTOKEN_PAIR_NOT_FOUND; - auto const oldCount = sle->getFieldArray(sfPriceDataSeries).size() > 5 ? 2 : 1; - auto const newCount = pairs.size() > 5 ? 2 : 1; - adjustReserve = newCount - oldCount; + auto const oldCount = calculateOracleReserve(sle->getFieldArray(sfPriceDataSeries).size()); + auto const newCount = calculateOracleReserve(pairs.size()); + + // if different sponsors, check with newCount + auto const currentSponsor = getLedgerEntryReserveSponsorAccountID(sle); + auto const newSponsor = getTxReserveSponsorAccountID(ctx.tx); + if ((!currentSponsor && !newSponsor) || + (currentSponsor && newSponsor && *currentSponsor == *newSponsor)) + { + adjustReserve = newCount - oldCount; + } + else + { + adjustReserve = newCount; + } } else { @@ -160,7 +173,7 @@ OracleSet::preclaim(PreclaimContext const& ctx) if (!ctx.tx.isFieldPresent(sfProvider) || !ctx.tx.isFieldPresent(sfAssetClass)) return temMALFORMED; - adjustReserve = pairs.size() > 5 ? 2 : 1; + adjustReserve = calculateOracleReserve(pairs.size()); } if (pairs.empty()) @@ -168,28 +181,18 @@ OracleSet::preclaim(PreclaimContext const& ctx) if (pairs.size() > kMaxOracleDataSeries) return tecARRAY_TOO_LARGE; - auto const reserve = - ctx.view.fees().accountReserve(sleSetter->getFieldU32(sfOwnerCount) + adjustReserve); auto const& balance = sleSetter->getFieldAmount(sfBalance); - - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; + auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + if (auto const ret = checkInsufficientReserve( + ctx.view, ctx.tx, sleSetter, balance, *sponsorSle, adjustReserve, 0, ctx.j); + !isTesSuccess(ret)) + return ret; return tesSUCCESS; } -static bool -adjustOwnerCount(ApplyContext& ctx, int count) -{ - if (auto const sleAccount = ctx.view().peek(keylet::account(ctx.tx[sfAccount]))) - { - adjustOwnerCount(ctx.view(), sleAccount, count, ctx.journal); - return true; - } - - return false; // LCOV_EXCL_LINE -} - static void setPriceDataInnerObjTemplate(STObject& obj) { @@ -228,7 +231,7 @@ OracleSet::doApply() priceData.setFieldCurrency(sfQuoteAsset, entry.getFieldCurrency(sfQuoteAsset)); pairs.emplace(tokenPairKey(entry), std::move(priceData)); } - auto const oldCount = pairs.size() > 5 ? 2 : 1; + auto const oldCount = calculateOracleReserve(pairs.size()); // update/add/delete pairs for (auto const& entry : ctx_.tx.getFieldArray(sfPriceDataSeries)) { @@ -266,11 +269,37 @@ OracleSet::doApply() (*sle)[sfOracleDocumentID] = ctx_.tx[sfOracleDocumentID]; } - auto const newCount = pairs.size() > 5 ? 2 : 1; - auto const adjust = newCount - oldCount; - if (adjust != 0 && !adjustOwnerCount(ctx_, adjust)) + auto const newCount = calculateOracleReserve(pairs.size()); + int32_t const adjust = newCount - oldCount; + + auto const accountSle = ctx_.view().peek(keylet::account(ctx_.tx[sfAccount])); + if (!accountSle) return tefINTERNAL; // LCOV_EXCL_LINE + if (adjust > 0) + { + // To continue receiving sponsorship from the same account after the + // OwnerCount increases from 1 to 2, it is necessary to sign with + // the sponsor decrease current sponsored owner count. + // Otherwise, the sponsorship will be deleted. + + auto const newSponsorSle = getTxReserveSponsor(ctx_.view(), ctx_.tx); + if (!newSponsorSle) + return newSponsorSle.error(); // LCOV_EXCL_LINE + + // decrease current sponsored owner count + adjustOwnerCountObj(ctx_.view(), accountSle, sle, -oldCount, ctx_.journal); + removeSponsorFromLedgerEntry(sle); + // increase new owner count + adjustOwnerCount(ctx_.view(), accountSle, *newSponsorSle, newCount, ctx_.journal); + addSponsorToLedgerEntry(sle, *newSponsorSle); + } + else if (adjust < 0) + { + // decrease owner count + adjustOwnerCountObj(ctx_.view(), accountSle, sle, adjust, ctx_.journal); + } + ctx_.view().update(sle); } else @@ -317,10 +346,17 @@ OracleSet::doApply() (*sle)[sfOwnerNode] = *page; - auto const count = series.size() > 5 ? 2 : 1; - if (!adjustOwnerCount(ctx_, count)) + auto const count = calculateOracleReserve(series.size()); + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + auto const accountSle = ctx_.view().peek(keylet::account(ctx_.tx[sfAccount])); + if (!accountSle) return tefINTERNAL; // LCOV_EXCL_LINE + adjustOwnerCount(ctx_.view(), accountSle, *sponsorSle, count, ctx_.journal); + addSponsorToLedgerEntry(sle, *sponsorSle); + ctx_.view().insert(sle); } diff --git a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp index 7e950dc7433..61f33661743 100644 --- a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp +++ b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -160,13 +161,13 @@ DepositPreauth::doApply() // A preauth counts against the reserve of the issuing account, but we // check the starting balance because we want to allow dipping into the // reserve to pay fees. - { - STAmount const reserve{ - view().fees().accountReserve(sleOwner->getFieldU32(sfOwnerCount) + 1)}; - - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; - } + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + if (auto const ret = checkInsufficientReserve( + view(), ctx_.tx, sleOwner, preFeeBalance_, *sponsorSle, 1, 0, j_); + !isTesSuccess(ret)) + return ret; // Preclaim already verified that the Preauth entry does not yet exist. // Create and populate the Preauth entry. @@ -190,7 +191,8 @@ DepositPreauth::doApply() slePreauth->setFieldU64(sfOwnerNode, *page); // If we succeeded, the new entry counts against the creator's reserve. - adjustOwnerCount(view(), sleOwner, 1, j_); + adjustOwnerCount(view(), sleOwner, *sponsorSle, 1, j_); + addSponsorToLedgerEntry(slePreauth, *sponsorSle); } else if (ctx_.tx.isFieldPresent(sfUnauthorize)) { @@ -207,13 +209,13 @@ DepositPreauth::doApply() // A preauth counts against the reserve of the issuing account, but we // check the starting balance because we want to allow dipping into the // reserve to pay fees. - { - STAmount const reserve{ - view().fees().accountReserve(sleOwner->getFieldU32(sfOwnerCount) + 1)}; - - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; - } + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + if (auto const ret = checkInsufficientReserve( + view(), ctx_.tx, sleOwner, preFeeBalance_, *sponsorSle, 1, 0, j_); + !isTesSuccess(ret)) + return ret; // Preclaim already verified that the Preauth entry does not yet exist. // Create and populate the Preauth entry. @@ -251,7 +253,8 @@ DepositPreauth::doApply() slePreauth->setFieldU64(sfOwnerNode, *page); // If we succeeded, the new entry counts against the creator's reserve. - adjustOwnerCount(view(), sleOwner, 1, j_); + adjustOwnerCount(view(), sleOwner, *sponsorSle, 1, j_); + addSponsorToLedgerEntry(slePreauth, *sponsorSle); } else if (ctx_.tx.isFieldPresent(sfUnauthorizeCredentials)) { @@ -289,8 +292,7 @@ DepositPreauth::removeFromLedger(ApplyView& view, uint256 const& preauthIndex, b if (!sleOwner) return tefINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleOwner, -1, j); - + adjustOwnerCountObj(view, sleOwner, slePreauth, -1, j); // Remove DepositPreauth from ledger. view.erase(slePreauth); diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp b/src/libxrpl/tx/transactors/payment/Payment.cpp index 805ebe3684e..08632158ac2 100644 --- a/src/libxrpl/tx/transactors/payment/Payment.cpp +++ b/src/libxrpl/tx/transactors/payment/Payment.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,7 @@ #include #include +#include #include #include #include @@ -122,6 +124,18 @@ Payment::preflight(PreflightContext const& ctx) if (!ctx.rules.enabled(featureMPTokensV1) && isDstMPT) return temDISABLED; + if (tx.isFlag(tfSponsorCreatedAccount)) + { + if (!ctx.rules.enabled(featureSponsor)) + return temDISABLED; + + if (tx.isFlag(tfNoRippleDirect) || tx.isFlag(tfPartialPayment) || tx.isFlag(tfLimitQuality)) + return temINVALID_FLAG; + + if (!dstAmount.native()) + return temBAD_AMOUNT; + } + if (!mpTokensV2 && isDstMPT && ctx.tx.isFieldPresent(sfPaths)) return temMALFORMED; @@ -347,7 +361,13 @@ Payment::preclaim(PreclaimContext const& ctx) // transaction would succeed. return telNO_DST_PARTIAL; } - if (dstAmount < STAmount(ctx.view.fees().reserve)) + if (ctx.tx.isFlag(tfSponsorCreatedAccount)) + { + // The minimum amount when creating a Sponsored Account is 1 drop. + // Since the reserve is covered by the sponsor, you don't need to hold the 1-increment + // reserve yourself. + } + else if (dstAmount < STAmount(ctx.view.fees().reserve)) { // accountReserve is the minimum amount that an account can have. // Reserve is not scaled by load. @@ -360,16 +380,25 @@ Payment::preclaim(PreclaimContext const& ctx) return tecNO_DST_INSUF_XRP; } } - else if (sleDst->isFlag(lsfRequireDestTag) && !ctx.tx.isFieldPresent(sfDestinationTag)) + else { - // The tag is basically account-specific information we don't - // understand, but we can require someone to fill it in. + // The tfSponsorCreatedAccount flag is specific to account creation via + // sponsorship. If the destination account already exists, applying this + // flag is invalid. + if (ctx.tx.isFlag(tfSponsorCreatedAccount)) + return tecNO_SPONSOR_PERMISSION; - // We didn't make this test for a newly-formed account because there's - // no way for this field to be set. - JLOG(ctx.j.trace()) << "Malformed transaction: DestinationTag required."; + if (sleDst->isFlag(lsfRequireDestTag) && !ctx.tx.isFieldPresent(sfDestinationTag)) + { + // The tag is basically account-specific information we don't + // understand, but we can require someone to fill it in. + + // We didn't make this test for a newly-formed account because + // there's no way for this field to be set. + JLOG(ctx.j.trace()) << "Malformed transaction: DestinationTag required."; - return tecDST_TAG_NEEDED; + return tecDST_TAG_NEEDED; + } } // Payment with at least one intermediate step and uses transitive balances. @@ -433,6 +462,25 @@ Payment::doApply() sleDst->setFieldU32(sfSequence, view().seq()); sleDst->setFieldAmount(sfBalance, XRPAmount(beast::kZero)); + if (ctx_.tx.isFlag(tfSponsorCreatedAccount)) + { + auto const sponsor = view().peek(keylet::account(accountID_)); + if (!sponsor) + return tefINTERNAL; // LCOV_EXCL_LINE + auto const currentSponsoringAccountCount = + sponsor->getFieldU32(sfSponsoringAccountCount); + if (currentSponsoringAccountCount == std::numeric_limits::max()) + { + JLOG(j_.fatal()) << "Sponsoring account count overflow for account " + << to_string(accountID_); + return tecINTERNAL; // LCOV_EXCL_LINE + } + sponsor->setFieldU32(sfSponsoringAccountCount, currentSponsoringAccountCount + 1); + + addSponsorToLedgerEntry(sleDst, sponsor); + view().update(sponsor); + } + view().insert(sleDst); } else @@ -599,12 +647,9 @@ Payment::doApply() if (!sleSrc) return tefINTERNAL; // LCOV_EXCL_LINE - // ownerCount is the number of entries in this ledger for this - // account that require a reserve. - auto const ownerCount = sleSrc->getFieldU32(sfOwnerCount); - - // This is the total reserve in drops. - auto const reserve = view().fees().accountReserve(ownerCount); + // the number of reserves in this ledger for this account that require a + // reserve. + auto const reserve = accountReserve(view(), sleSrc, j_); // In a delegated payment, the fee payer is the delegated account, // not the source account (accountID_). diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp index 63dbe01944f..04d8e6280b9 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -79,12 +80,17 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx) // Check reserve and funds availability { auto const balance = (*sle)[sfBalance]; - auto const reserve = ctx.view.fees().accountReserve((*sle)[sfOwnerCount] + 1); - - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; - - if (balance < reserve + ctx.tx[sfAmount]) + auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + if (auto const ret = + checkInsufficientReserve(ctx.view, ctx.tx, sle, balance, *sponsorSle, 1, 0, ctx.j); + !isTesSuccess(ret)) + return ret; + + if (auto const ret = checkInsufficientReserve( + ctx.view, ctx.tx, sle, balance - ctx.tx[sfAmount], *sponsorSle, 1, 0, ctx.j); + !isTesSuccess(ret)) return tecUNFUNDED; } @@ -178,7 +184,11 @@ PaymentChannelCreate::doApply() // Deduct owner's balance, increment owner count (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; - adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + adjustOwnerCount(ctx_.view(), sle, *sponsorSle, 1, ctx_.journal); + addSponsorToLedgerEntry(slep, *sponsorSle); ctx_.view().update(sle); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp index 4e3c5dd6384..db14a9b06ca 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp @@ -4,7 +4,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -88,12 +90,17 @@ PaymentChannelFund::doApply() { // Check reserve and funds availability auto const balance = (*sle)[sfBalance]; - auto const reserve = ctx_.view().fees().accountReserve((*sle)[sfOwnerCount]); - - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; - - if (balance < reserve + ctx_.tx[sfAmount]) + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + if (auto const ret = + checkInsufficientReserve(ctx_.view(), ctx_.tx, sle, balance, *sponsorSle, 0, 0, j_); + !isTesSuccess(ret)) + return ret; + + if (auto const ret = checkInsufficientReserve( + ctx_.view(), ctx_.tx, sle, balance - ctx_.tx[sfAmount], {}, 0, 0, j_); + !isTesSuccess(ret)) return tecUNFUNDED; } diff --git a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp index 7eb3f282b98..fc81d914d85 100644 --- a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp +++ b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp @@ -65,7 +65,7 @@ PermissionedDomainDelete::doApply() XRPL_ASSERT( ownerSle && ownerSle->getFieldU32(sfOwnerCount) > 0, "xrpl::PermissionedDomainDelete::doApply : nonzero owner count"); - adjustOwnerCount(view(), ownerSle, -1, ctx_.journal); + adjustOwnerCountObj(view(), ownerSle, slePd, -1, ctx_.journal); view().erase(slePd); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp index 0e71ceada15..d485ec22e99 100644 --- a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp +++ b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -106,9 +107,13 @@ PermissionedDomainSet::doApply() // Create new permissioned domain. // Check reserve availability for new object creation auto const balance = STAmount((*ownerSle)[sfBalance]).xrp(); - auto const reserve = ctx_.view().fees().accountReserve((*ownerSle)[sfOwnerCount] + 1); - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + if (auto const ret = checkInsufficientReserve( + ctx_.view(), ctx_.tx, ownerSle, balance, *sponsorSle, 1, 0, j_); + !isTesSuccess(ret)) + return ret; bool const fixEnabled = view().rules().enabled(fixCleanup3_1_3); auto const seq = fixEnabled ? ctx_.tx.getSeqValue() : ctx_.tx.getFieldU32(sfSequence); @@ -125,7 +130,8 @@ PermissionedDomainSet::doApply() slePd->setFieldU64(sfOwnerNode, *page); // If we succeeded, the new entry counts against the creator's reserve. - adjustOwnerCount(view(), ownerSle, 1, ctx_.journal); + adjustOwnerCount(view(), ownerSle, *sponsorSle, 1, ctx_.journal); + addSponsorToLedgerEntry(slePd, *sponsorSle); view().insert(slePd); } diff --git a/src/libxrpl/tx/transactors/system/Batch.cpp b/src/libxrpl/tx/transactors/system/Batch.cpp index 64a62ac2736..adddd9de7c1 100644 --- a/src/libxrpl/tx/transactors/system/Batch.cpp +++ b/src/libxrpl/tx/transactors/system/Batch.cpp @@ -212,6 +212,17 @@ Batch::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } + if (ctx.tx.isFieldPresent(sfSponsorFlags)) + { + auto const sponsorFlags = ctx.tx.getFieldU32(sfSponsorFlags); + if ((sponsorFlags & spfSponsorReserve) != 0u) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:" + << "spfSponsorReserve is not allowed on outer Batch."; + return temINVALID_FLAG; + } + } + auto const& rawTxns = ctx.tx.getFieldArray(sfRawTransactions); if (rawTxns.size() <= 1) { @@ -308,6 +319,14 @@ Batch::preflight(PreflightContext const& ctx) return ret; } } + if (stx.isFieldPresent(sfSponsorSignature)) + { + auto const sponsorSignature = stx.getFieldObject(sfSponsorSignature); + if (auto const ret = checkSignatureFields(sponsorSignature, hash, "sponsor signature ")) + { + return ret; + } + } // Check that the Fee is native asset (XRP) and zero if (auto const fee = stx.getFieldAmount(sfFee); !fee.native() || fee.xrp() != beast::kZero) @@ -403,6 +422,10 @@ Batch::preflightSigValidated(PreflightContext const& ctx) if (auto const counterparty = rb.at(~sfCounterparty); counterparty && counterparty != outerAccount) requiredSigners.insert(*counterparty); + + if (auto const sponsor = rb.at(~sfSponsor); + sponsor && rb.isFieldPresent(sfSponsorSignature) && sponsor != outerAccount) + requiredSigners.insert(*sponsor); } // Validation Batch Signers diff --git a/src/libxrpl/tx/transactors/system/TicketCreate.cpp b/src/libxrpl/tx/transactors/system/TicketCreate.cpp index 5be00fe76c0..3d301ebd77b 100644 --- a/src/libxrpl/tx/transactors/system/TicketCreate.cpp +++ b/src/libxrpl/tx/transactors/system/TicketCreate.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -75,13 +76,13 @@ TicketCreate::doApply() // check the starting balance because we want to allow dipping into the // reserve to pay fees. std::uint32_t const ticketCount = ctx_.tx[sfTicketCount]; - { - XRPAmount const reserve = - view().fees().accountReserve(sleAccountRoot->getFieldU32(sfOwnerCount) + ticketCount); - - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; - } + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + if (auto const ret = checkInsufficientReserve( + view(), ctx_.tx, sleAccountRoot, preFeeBalance_, *sponsorSle, ticketCount, 0, j_); + !isTesSuccess(ret)) + return ret; beast::Journal const viewJ{ctx_.registry.get().getJournal("View")}; @@ -105,6 +106,7 @@ TicketCreate::doApply() sleTicket->setAccountID(sfAccount, accountID_); sleTicket->setFieldU32(sfTicketSequence, curTicketSeq); + view().insert(sleTicket); auto const page = view().dirInsert( @@ -117,6 +119,7 @@ TicketCreate::doApply() return tecDIR_FULL; // LCOV_EXCL_LINE sleTicket->setFieldU64(sfOwnerNode, *page); + addSponsorToLedgerEntry(sleTicket, *sponsorSle); } // Update the record of the number of Tickets this account owns. @@ -125,7 +128,7 @@ TicketCreate::doApply() sleAccountRoot->setFieldU32(sfTicketCount, oldTicketCount + ticketCount); // Every added Ticket counts against the creator's reserve. - adjustOwnerCount(view(), sleAccountRoot, ticketCount, viewJ); + adjustOwnerCount(view(), sleAccountRoot, *sponsorSle, ticketCount, viewJ); // TicketCreate is the only transaction that can cause an account root's // Sequence field to increase by more than one. October 2018. diff --git a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp index 332d160cac1..645d499f1d9 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp @@ -145,6 +145,7 @@ MPTokenAuthorize::doApply() auto const& tx = ctx_.tx; return authorizeMPToken( ctx_.view(), + tx, preFeeBalance_, tx[sfMPTokenIssuanceID], accountID_, diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp index 90e33d3a704..6f041980f13 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,7 @@ #include #include #include +#include namespace xrpl { @@ -101,15 +103,32 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) } std::expected -MPTokenIssuanceCreate::create(ApplyView& view, beast::Journal journal, MPTCreateArgs const& args) +MPTokenIssuanceCreate::create( + ApplyView& view, + STTx const& tx, + beast::Journal journal, + MPTCreateArgs const& args) { auto const acct = view.peek(keylet::account(args.account)); if (!acct) return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE - if (args.priorBalance && - *(args.priorBalance) < view.fees().accountReserve((*acct)[sfOwnerCount] + 1)) - return std::unexpected(tecINSUFFICIENT_RESERVE); + SLE::pointer sponsorSle; + if (!isPseudoAccount(acct)) + { + auto sle = getTxReserveSponsor(view, tx); + if (!sle) + return std::unexpected(sle.error()); + sponsorSle = std::move(*sle); + } + + if (args.priorBalance) + { + if (auto const ret = checkInsufficientReserve( + view, tx, acct, *(args.priorBalance), sponsorSle, 1, 0, journal); + !isTesSuccess(ret)) + return std::unexpected(ret); // tecINSUFFICIENT_RESERVE + } auto const mptId = makeMptID(args.sequence, args.account); auto const mptIssuanceKeylet = keylet::mptIssuance(mptId); @@ -163,11 +182,13 @@ MPTokenIssuanceCreate::create(ApplyView& view, beast::Journal journal, MPTCreate (*mptIssuance)[sfReferenceHolding] = *args.referenceHolding; } + addSponsorToLedgerEntry(mptIssuance, sponsorSle); + view.insert(mptIssuance); } // Update owner count. - adjustOwnerCount(view, acct, 1, journal); + adjustOwnerCount(view, acct, sponsorSle, 1, journal); return mptId; } @@ -178,6 +199,7 @@ MPTokenIssuanceCreate::doApply() auto const& tx = ctx_.tx; auto const result = create( view(), + tx, j_, { .priorBalance = preFeeBalance_, diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp index 1029c25813a..62b4da4c19b 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp @@ -49,10 +49,9 @@ MPTokenIssuanceDestroy::doApply() if (!view().dirRemove(keylet::ownerDir(accountID_), (*mpt)[sfOwnerNode], mpt->key(), false)) return tefBAD_LEDGER; // LCOV_EXCL_LINE + adjustOwnerCountObj(view(), accountID_, mpt, -1, j_); view().erase(mpt); - adjustOwnerCount(view(), view().peek(keylet::account(accountID_)), -1, j_); - return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/token/TrustSet.cpp b/src/libxrpl/tx/transactors/token/TrustSet.cpp index 1d2bc966937..5f00bfb720c 100644 --- a/src/libxrpl/tx/transactors/token/TrustSet.cpp +++ b/src/libxrpl/tx/transactors/token/TrustSet.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -341,8 +342,6 @@ TrustSet::doApply() if (!sle) return tefINTERNAL; // LCOV_EXCL_LINE - std::uint32_t const uOwnerCount = sle->getFieldU32(sfOwnerCount); - // The reserve that is required to create the line. Note // that although the reserve increases with every item // an account owns, in the case of trust lines we only @@ -361,9 +360,16 @@ TrustSet::doApply() // well. A person with no intention of using the gateway // could use the extra XRP for their own purposes. - XRPAmount const reserveCreate( - (uOwnerCount < 2) ? XRPAmount(beast::kZero) - : view().fees().accountReserve(uOwnerCount + 1)); + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + + std::uint32_t const uOwnerCount = ownerCount(view(), *sponsorSle ? *sponsorSle : sle, j_); + + bool const isSponsoredAndPreFunded = *sponsorSle && !isSponsorReserveCoSigning(ctx_.tx); + // If PreFunded Sponsor, it must be checked whether sufficient + // ReserveCount exists. + bool const freeTrustLine = uOwnerCount < 2 && !*sponsorSle; std::uint32_t const uQualityIn(bQualityIn ? ctx_.tx.getFieldU32(sfQualityIn) : 0); std::uint32_t uQualityOut(bQualityOut ? ctx_.tx.getFieldU32(sfQualityOut) : 0); @@ -549,6 +555,11 @@ TrustSet::doApply() bool bReserveIncrease = false; + auto const currentHighSponsor = + getLedgerEntryReserveSponsor(view(), sleRippleState, sfHighSponsor); + auto const currentLowSponsor = + getLedgerEntryReserveSponsor(view(), sleRippleState, sfLowSponsor); + if (bSetAuth) { uFlagsOut |= (bHigh ? lsfHighAuth : lsfLowAuth); @@ -556,10 +567,20 @@ TrustSet::doApply() if (bLowReserveSet && !bLowReserved) { + // should be checked PreFunded Sponsor before adjustOwnerCount() + // For PreFunded sponsors, we need to check if there are sufficient reserves before + // calling adjustOwnerCount(). + if (auto const ret = checkInsufficientReserve( + view(), ctx_.tx, sleLowAccount, preFeeBalance_, *sponsorSle, 1, 0, j_); + isSponsoredAndPreFunded && !isTesSuccess(ret)) + return tecINSUF_RESERVE_LINE; + // Set reserve for low account. - adjustOwnerCount(view(), sleLowAccount, 1, viewJ); + adjustOwnerCount(view(), sleLowAccount, *sponsorSle, 1, viewJ); uFlagsOut |= lsfLowReserve; + addSponsorToLedgerEntry(sleRippleState, *sponsorSle, sfLowSponsor); + if (!bHigh) bReserveIncrease = true; } @@ -567,16 +588,28 @@ TrustSet::doApply() if (bLowReserveClear && bLowReserved) { // Clear reserve for low account. - adjustOwnerCount(view(), sleLowAccount, -1, viewJ); + adjustOwnerCount(view(), sleLowAccount, currentLowSponsor, -1, viewJ); uFlagsOut &= ~lsfLowReserve; + + removeSponsorFromLedgerEntry(sleRippleState, sfLowSponsor); } if (bHighReserveSet && !bHighReserved) { + // should be checked PreFunded Sponsor before adjustOwnerCount() + // For PreFunded sponsors, we need to check if there are sufficient reserves before + // calling adjustOwnerCount(). + if (auto const ret = checkInsufficientReserve( + view(), ctx_.tx, sleHighAccount, preFeeBalance_, *sponsorSle, 1, 0, j_); + isSponsoredAndPreFunded && !isTesSuccess(ret)) + return tecINSUF_RESERVE_LINE; + // Set reserve for high account. - adjustOwnerCount(view(), sleHighAccount, 1, viewJ); + adjustOwnerCount(view(), sleHighAccount, *sponsorSle, 1, viewJ); uFlagsOut |= lsfHighReserve; + addSponsorToLedgerEntry(sleRippleState, *sponsorSle, sfHighSponsor); + if (bHigh) bReserveIncrease = true; } @@ -584,8 +617,10 @@ TrustSet::doApply() if (bHighReserveClear && bHighReserved) { // Clear reserve for high account. - adjustOwnerCount(view(), sleHighAccount, -1, viewJ); + adjustOwnerCount(view(), sleHighAccount, currentHighSponsor, -1, viewJ); uFlagsOut &= ~lsfHighReserve; + + removeSponsorFromLedgerEntry(sleRippleState, sfHighSponsor); } if (uFlagsIn != uFlagsOut) @@ -598,7 +633,10 @@ TrustSet::doApply() terResult = trustDelete(view(), sleRippleState, uLowAccountID, uHighAccountID, viewJ); } // Reserve is not scaled by load. - else if (bReserveIncrease && preFeeBalance_ < reserveCreate) + else if ( + auto const ret = checkInsufficientReserve( + view(), ctx_.tx, sle, preFeeBalance_, *sponsorSle, 0, 0, j_); + !freeTrustLine && bReserveIncrease && !isTesSuccess(ret)) { JLOG(j_.trace()) << "Delay transaction: Insufficent reserve to " "add trust line."; @@ -626,8 +664,10 @@ TrustSet::doApply() JLOG(j_.trace()) << "Redundant: Setting non-existent ripple line to defaults."; return tecNO_LINE_REDUNDANT; } - else if (preFeeBalance_ < reserveCreate) // Reserve is not scaled by - // load. + else if ( + auto const ret = checkInsufficientReserve( + ctx_.view(), ctx_.tx, sle, preFeeBalance_, *sponsorSle, 1, 0, j_); + !freeTrustLine && !isTesSuccess(ret)) // Reserve is not scaled by load. { JLOG(j_.trace()) << "Delay transaction: Line does not exist. " "Insufficent reserve to create line."; @@ -661,6 +701,7 @@ TrustSet::doApply() saLimitAllow, // Limit for who is being charged. uQualityIn, uQualityOut, + *sponsorSle, viewJ); } diff --git a/src/libxrpl/tx/transactors/vault/VaultClawback.cpp b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp index a8587feaebf..acaaa9b8207 100644 --- a/src/libxrpl/tx/transactors/vault/VaultClawback.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp @@ -389,8 +389,8 @@ VaultClawback::doApply() auto const& vaultAccount = vault->at(sfAccount); // Transfer shares from holder to vault. - if (auto const ter = - accountSend(view(), holder, vaultAccount, sharesDestroyed, j_, WaiveTransferFee::Yes); + if (auto const ter = accountSend( + view(), holder, vaultAccount, sharesDestroyed, j_, {}, WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; @@ -399,7 +399,7 @@ VaultClawback::doApply() // Keep MPToken if holder is the vault owner. if (holder != vault->at(sfOwner)) { - if (auto const ter = removeEmptyHolding(view(), holder, sharesDestroyed.asset(), j_); + if (auto const ter = removeEmptyHolding(view(), tx, holder, sharesDestroyed.asset(), j_); isTesSuccess(ter)) { JLOG(j_.debug()) // @@ -425,7 +425,7 @@ VaultClawback::doApply() { // Transfer assets from vault to issuer. if (auto const ter = accountSend( - view(), vaultAccount, accountID_, assetsRecovered, j_, WaiveTransferFee::Yes); + view(), vaultAccount, accountID_, assetsRecovered, j_, {}, WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp index 41637530146..7e4a8a8156a 100644 --- a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -156,10 +157,28 @@ VaultCreate::doApply() if (auto ter = dirLink(view(), accountID_, vault)) return ter; // We will create Vault and PseudoAccount, hence increase OwnerCount by 2 - adjustOwnerCount(view(), owner, 2, j_); - auto const ownerCount = owner->at(sfOwnerCount); - if (preFeeBalance_ < view().fees().accountReserve(ownerCount)) - return tecINSUFFICIENT_RESERVE; + auto const sponsorSle = getTxReserveSponsor(view(), tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + if (!ctx_.view().rules().enabled(featureSponsor)) + { + adjustOwnerCount(view(), owner, *sponsorSle, 2, j_); + addSponsorToLedgerEntry(vault, *sponsorSle); + if (auto const ret = + checkInsufficientReserve(view(), tx, owner, preFeeBalance_, *sponsorSle, 0, 0, j_); + !isTesSuccess(ret)) + return ret; + } + else + { + // after Sponsor Amendment, check insufficient reserve first + if (auto const ret = + checkInsufficientReserve(view(), tx, owner, preFeeBalance_, *sponsorSle, 2, 0, j_); + !isTesSuccess(ret)) + return ret; + adjustOwnerCount(view(), owner, *sponsorSle, 2, j_); + addSponsorToLedgerEntry(vault, *sponsorSle); + } auto maybePseudo = createPseudoAccount(view(), vault->key(), sfVaultID); if (!maybePseudo) @@ -168,7 +187,8 @@ VaultCreate::doApply() AccountID const pseudoId = pseudo->at(sfAccount); auto const asset = tx[sfAsset]; - if (auto ter = addEmptyHolding(view(), pseudoId, preFeeBalance_, asset, j_); !isTesSuccess(ter)) + if (auto ter = addEmptyHolding(view(), tx, pseudoId, preFeeBalance_, asset, j_); + !isTesSuccess(ter)) return ter; std::uint8_t const scale = (asset.holds() || asset.native()) @@ -198,6 +218,7 @@ VaultCreate::doApply() }(); auto const maybeShare = MPTokenIssuanceCreate::create( view(), + tx, j_, { .priorBalance = std::nullopt, @@ -244,7 +265,7 @@ VaultCreate::doApply() // Explicitly create MPToken for the vault owner if (auto const err = - authorizeMPToken(view(), preFeeBalance_, mptIssuanceID, accountID_, ctx_.journal); + authorizeMPToken(view(), tx, preFeeBalance_, mptIssuanceID, accountID_, ctx_.journal); !isTesSuccess(err)) return err; @@ -252,7 +273,7 @@ VaultCreate::doApply() if (tx.isFlag(tfVaultPrivate)) { if (auto const err = authorizeMPToken( - view(), preFeeBalance_, mptIssuanceID, pseudoId, ctx_.journal, {}, accountID_); + view(), tx, preFeeBalance_, mptIssuanceID, pseudoId, ctx_.journal, {}, accountID_); !isTesSuccess(err)) return err; } diff --git a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp index 030a7e971c5..3ccb9498c78 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp @@ -94,7 +94,8 @@ VaultDelete::doApply() // Destroy the asset holding. auto asset = vault->at(sfAsset); - if (auto ter = removeEmptyHolding(view(), vault->at(sfAccount), asset, j_); !isTesSuccess(ter)) + if (auto ter = removeEmptyHolding(view(), ctx_.tx, vault->at(sfAccount), asset, j_); + !isTesSuccess(ter)) return ter; auto const& pseudoID = vault->at(sfAccount); @@ -122,7 +123,8 @@ VaultDelete::doApply() // Try to remove MPToken for vault shares for the vault owner if it exists. if (auto const mptoken = view().peek(keylet::mptoken(shareMPTID, accountID_))) { - if (auto const ter = removeEmptyHolding(view(), accountID_, MPTIssue(shareMPTID), j_); + if (auto const ter = + removeEmptyHolding(view(), ctx_.tx, accountID_, MPTIssue(shareMPTID), j_); !isTesSuccess(ter)) { // LCOV_EXCL_START @@ -143,7 +145,7 @@ VaultDelete::doApply() return tefBAD_LEDGER; // LCOV_EXCL_STOP } - adjustOwnerCount(view(), pseudoAcct, -1, j_); + adjustOwnerCount(view(), pseudoAcct, {}, -1, j_); view().erase(mpt); @@ -202,7 +204,7 @@ VaultDelete::doApply() } // We are destroying Vault and PseudoAccount, hence decrease by 2 - adjustOwnerCount(view(), owner, -2, j_); + adjustOwnerCountObj(view(), owner, vault, -2, j_); // Destroy the vault. view().erase(vault); diff --git a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp index c08d1e957ca..9707a923fc0 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,7 @@ #include #include +#include #include namespace xrpl { @@ -220,7 +222,7 @@ VaultDeposit::doApply() if (vault->isFlag(lsfVaultPrivate) && accountID_ != vault->at(sfOwner)) { if (auto const err = enforceMPTokenAuthorization( - ctx_.view(), mptIssuanceID, accountID_, preFeeBalance_, j_); + ctx_.view(), ctx_.tx, mptIssuanceID, accountID_, preFeeBalance_, j_); !isTesSuccess(err)) return err; } @@ -230,7 +232,12 @@ VaultDeposit::doApply() if (!view().exists(keylet::mptoken(mptIssuanceID, accountID_))) { if (auto const err = authorizeMPToken( - view(), preFeeBalance_, mptIssuanceID->value(), accountID_, ctx_.journal); + view(), + ctx_.tx, + preFeeBalance_, + mptIssuanceID->value(), + accountID_, + ctx_.journal); !isTesSuccess(err)) return err; } @@ -243,6 +250,7 @@ VaultDeposit::doApply() accountID_ == vault->at(sfOwner), "xrpl::VaultDeposit::doApply : account is owner"); if (auto const err = authorizeMPToken( view(), + ctx_.tx, preFeeBalance_, // priorBalance mptIssuanceID->value(), // mptIssuanceID sleIssuance->at(sfIssuer), // account @@ -309,7 +317,7 @@ VaultDeposit::doApply() // Transfer assets from depositor to vault. if (auto const ter = accountSend( - view(), accountID_, vaultAccount, assetsDeposited, j_, WaiveTransferFee::Yes); + view(), accountID_, vaultAccount, assetsDeposited, j_, {}, WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; @@ -335,9 +343,19 @@ VaultDeposit::doApply() } } + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + // Transfer shares from vault to depositor. - if (auto const ter = - accountSend(view(), vaultAccount, accountID_, sharesCreated, j_, WaiveTransferFee::Yes); + if (auto const ter = accountSend( + view(), + vaultAccount, + accountID_, + sharesCreated, + j_, + *sponsorSle, + WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; diff --git a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp index 05dcfea506b..59f0ce0a2ce 100644 --- a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -325,9 +326,19 @@ VaultWithdraw::doApply() view().update(vault); auto const& vaultAccount = vault->at(sfAccount); + auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + // Transfer shares from depositor to vault. if (auto const ter = accountSend( - view(), accountID_, vaultAccount, sharesRedeemed, j_, WaiveTransferFee::Yes); + view(), + accountID_, + vaultAccount, + sharesRedeemed, + j_, + *sponsorSle, + WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; @@ -336,7 +347,8 @@ VaultWithdraw::doApply() // Keep MPToken if holder is the vault owner. if (accountID_ != vault->at(sfOwner)) { - if (auto const ter = removeEmptyHolding(view(), accountID_, sharesRedeemed.asset(), j_); + if (auto const ter = + removeEmptyHolding(view(), ctx_.tx, accountID_, sharesRedeemed.asset(), j_); isTesSuccess(ter)) { JLOG(j_.debug()) // diff --git a/src/test/app/AMMExtendedMPT_test.cpp b/src/test/app/AMMExtendedMPT_test.cpp index e6dcc957330..a409cbee883 100644 --- a/src/test/app/AMMExtendedMPT_test.cpp +++ b/src/test/app/AMMExtendedMPT_test.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -463,7 +464,7 @@ struct AMMExtendedMPT_test : public jtx::AMMTest // Provide micro amounts to compensate for fees to make results round // nice. auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(2) + env.current()->fees().base * 3; + XRP(100) + baseAccountReserve(*env.current(), 2) + env.current()->fees().base * 3; env.fund(startingXrp, gw_, alice_); env.fund(XRP(2'000), bob_); diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index a0a7d0fb151..c7fe147511b 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -546,7 +547,7 @@ class AMMExtended_test : public jtx::AMMTest // 1 for each trust limit == 3 (alice_ < mtgox/amazon/bitstamp) + // 1 for payment == 4 auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(3) + env.current()->fees().base * 4; + XRP(100) + baseAccountReserve(*env.current(), 3) + env.current()->fees().base * 4; env.fund(startingXrp, gw1, gw2, gw3, localAlice); env.fund(XRP(2'000), localBob); diff --git a/src/test/app/AMMMPT_test.cpp b/src/test/app/AMMMPT_test.cpp index 31b54ceee04..97a0f8e9c6e 100644 --- a/src/test/app/AMMMPT_test.cpp +++ b/src/test/app/AMMMPT_test.cpp @@ -976,7 +976,7 @@ struct AMMMPT_test : public jtx::AMMTest // Insufficient reserve, XRP/MPT { - Env env(*this); + Env env(*this, features); auto const startingXrp = reserve(env, 4) + env.current()->fees().base * 4; env.fund(XRP(10'000), gw_); env.fund(XRP(10'000), alice_); @@ -1006,7 +1006,13 @@ struct AMMMPT_test : public jtx::AMMTest std::nullopt, std::nullopt, std::nullopt, - Ter(tecINSUF_RESERVE_LINE)); + // After the Sponsor Amendment, it will result in tesSUCCESS + // if the current XRP == balance the required XRP balance calculated from the + // reserve. + // Before the Amendment, it will result in tecINSUF_RESERVE_LINE + // if the current XRP == balance the required XRP balance calculated from the + // reserve. + features[featureSponsor] ? Ter(tesSUCCESS) : Ter(tecINSUF_RESERVE_LINE)); } // Invalid min @@ -7083,6 +7089,7 @@ struct AMMMPT_test : public jtx::AMMTest testInvalidInstance(); testInvalidDeposit(all); testInvalidDeposit(all - featureAMMClawback); + testInvalidDeposit(all - featureSponsor); testDeposit(); testInvalidWithdraw(); testWithdraw(); diff --git a/src/test/app/AccountSet_test.cpp b/src/test/app/AccountSet_test.cpp index 6688e3693f2..3f98e3aaac5 100644 --- a/src/test/app/AccountSet_test.cpp +++ b/src/test/app/AccountSet_test.cpp @@ -1,8 +1,8 @@ - #include #include #include #include +#include #include #include #include @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -430,8 +431,10 @@ class AccountSet_test : public beast::unit_test::Suite env.close(); // Because we're hacking the ledger we need the account to have - // non-zero sfMintedNFTokens and sfBurnedNFTokens fields. This - // prevents an exception when the AccountRoot template is applied. + // non-zero sfMintedNFTokens, sfBurnedNFTokens, + // sfSponsoredOwnerCount, sfSponsoringOwnerCount, + // sfSponsoringAccountCount fields. This prevents an exception when + // the AccountRoot template is applied. { uint256 const nftId0{token::getNextID(env, gw, 0u)}; env(token::mint(gw, 0u)); @@ -439,6 +442,23 @@ class AccountSet_test : public beast::unit_test::Suite env(token::burn(gw, nftId0)); env.close(); + + env(did::set(gw), + did::Uri("uri"), + sponsor::As(alice, spfSponsorReserve), + Sig(sfSponsorSignature, alice)); + env.close(); + + env(did::set(alice), + did::Uri("uri"), + sponsor::As(gw, spfSponsorReserve), + Sig(sfSponsorSignature, gw)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(gw, spfSponsorReserve), + Sig(sfSponsorSignature, gw)); + env.close(); } // Note that we're bypassing almost all of the ledger's safety diff --git a/src/test/app/CheckMPT_test.cpp b/src/test/app/CheckMPT_test.cpp index 861a115fc9c..b7cded444ef 100644 --- a/src/test/app/CheckMPT_test.cpp +++ b/src/test/app/CheckMPT_test.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -409,7 +410,7 @@ class CheckMPT_test : public beast::unit_test::Suite // Insufficient reserve. Account const cheri{"cheri"}; - env.fund(env.current()->fees().accountReserve(1) - drops(1), cheri); + env.fund(baseAccountReserve(*env.current(), 1) - drops(1), cheri); env(check::create(cheri, bob, usd(50)), Fee(drops(env.current()->fees().base)), diff --git a/src/test/app/Check_test.cpp b/src/test/app/Check_test.cpp index 9b814a24b1b..f97ca9435cc 100644 --- a/src/test/app/Check_test.cpp +++ b/src/test/app/Check_test.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -470,7 +471,7 @@ class Check_test : public beast::unit_test::Suite // Insufficient reserve. Account const cheri{"cheri"}; - env.fund(env.current()->fees().accountReserve(1) - drops(1), cheri); + env.fund(baseAccountReserve(*env.current(), 1) - drops(1), cheri); env.close(); env(check::create(cheri, bob, usd(50)), diff --git a/src/test/app/Credentials_test.cpp b/src/test/app/Credentials_test.cpp index 456a53bc01a..9e7575cd504 100644 --- a/src/test/app/Credentials_test.cpp +++ b/src/test/app/Credentials_test.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -638,8 +639,8 @@ struct Credentials_test : public beast::unit_test::Suite { Env env{*this, features}; - env.fund(drops(env.current()->fees().accountReserve(1)), issuer); - env.fund(drops(env.current()->fees().accountReserve(0)), subject); + env.fund(drops(baseAccountReserve(*env.current(), 1)), issuer); + env.fund(drops(baseAccountReserve(*env.current(), 0)), subject); env.close(); { diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index 70b091290c7..8c16cc9b048 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -254,7 +255,7 @@ class Delegate_test : public beast::unit_test::Suite Account const bob{"bob"}; auto const txFee = env.current()->fees().base; - env.fund(env.current()->fees().accountReserve(0) + txFee, alice); + env.fund(baseAccountReserve(*env.current(), 0) + txFee, alice); env.fund(XRP(100000), bob); env.close(); @@ -271,7 +272,7 @@ class Delegate_test : public beast::unit_test::Suite auto const txFee = env.current()->fees().base; - env.fund(env.current()->fees().accountReserve(1) + (txFee * 4), alice); + env.fund(baseAccountReserve(*env.current(), 1) + (txFee * 4), alice); env.fund(XRP(100000), bob, carol); env.close(); @@ -297,8 +298,8 @@ class Delegate_test : public beast::unit_test::Suite Account const alice{"alice"}; Account const bob{"bob"}; - env.fund(drops(env.current()->fees().accountReserve(1)), alice); - env.fund(drops(env.current()->fees().accountReserve(2)), bob); + env.fund(drops(baseAccountReserve(*env.current(), 1)), alice); + env.fund(drops(baseAccountReserve(*env.current(), 2)), bob); env.close(); // alice gives bob permission @@ -401,7 +402,7 @@ class Delegate_test : public beast::unit_test::Suite Account const carol{"carol"}; auto const baseFee = env.current()->fees().base; - auto const reserve = env.current()->fees().accountReserve(1); + auto const reserve = baseAccountReserve(*env.current(), 1); auto const paymentAmount = XRP(1); auto const highFee = reserve + baseFee; BEAST_EXPECT(highFee > reserve); @@ -467,9 +468,9 @@ class Delegate_test : public beast::unit_test::Suite Account const carol{"carol"}; auto const baseFee = env.current()->fees().base; - auto const baseReserve = env.current()->fees().accountReserve(0); + auto const baseReserve = baseAccountReserve(*env.current(), 0); - env.fund(env.current()->fees().accountReserve(1) + baseFee + XRP(1), alice); + env.fund(baseAccountReserve(*env.current(), 1) + baseFee + XRP(1), alice); env.fund(baseReserve, bob); env.fund(XRP(1000), carol); env.close(); @@ -502,7 +503,7 @@ class Delegate_test : public beast::unit_test::Suite Account const carol{"carol"}; auto const baseFee = env.current()->fees().base; - auto const reserve = env.current()->fees().accountReserve(1); + auto const reserve = baseAccountReserve(*env.current(), 1); // Alice is funded with (reserve + baseFee): after DelegateSet she has // exactly 'reserve', which is insufficient to send XRP(10) while keeping @@ -2103,7 +2104,10 @@ class Delegate_test : public beast::unit_test::Suite {"CredentialDelete", featureCredentials}, {"NFTokenModify", featureDynamicNFT}, {"PermissionedDomainSet", featurePermissionedDomains}, - {"PermissionedDomainDelete", featurePermissionedDomains}}; + {"PermissionedDomainDelete", featurePermissionedDomains}, + {"SponsorshipTransfer", featureSponsor}, + {"SponsorshipSet", featureSponsor}, + }; // Can not delegate tx if any required feature disabled. { @@ -2182,7 +2186,7 @@ class Delegate_test : public beast::unit_test::Suite // DO NOT modify expectedDelegableCount unless all scenarios, including // edge cases, have been fully tested and verified. // ==================================================================== - std::size_t const expectedDelegableCount = 51; + std::size_t const expectedDelegableCount = 53; BEAST_EXPECTS( delegableCount == expectedDelegableCount, diff --git a/src/test/app/DepositAuth_test.cpp b/src/test/app/DepositAuth_test.cpp index 3496e67b543..c615bded164 100644 --- a/src/test/app/DepositAuth_test.cpp +++ b/src/test/app/DepositAuth_test.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -51,7 +52,7 @@ namespace xrpl::test { static XRPAmount reserve(jtx::Env& env, std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return baseAccountReserve(*env.current(), count); } // Helper function that returns true if acct has the lsfDepositAuth flag set. @@ -1025,7 +1026,7 @@ struct DepositPreauth_test : public beast::unit_test::Suite { // not enough reserve Account const john{"john"}; - env.fund(env.current()->fees().accountReserve(0), john); + env.fund(baseAccountReserve(*env.current(), 0), john); env.close(); auto jv = deposit::authCredentials(john, {{.issuer = issuer, .credType = credType}}); diff --git a/src/test/app/FlowMPT_test.cpp b/src/test/app/FlowMPT_test.cpp index 2e224a7eb1d..01dea709393 100644 --- a/src/test/app/FlowMPT_test.cpp +++ b/src/test/app/FlowMPT_test.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -725,7 +726,7 @@ struct FlowMPT_test : public beast::unit_test::Suite static XRPAmount reserve(jtx::Env& env, std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return baseAccountReserve(*env.current(), count); } // Helper function that returns the Offers on an account. diff --git a/src/test/app/Flow_test.cpp b/src/test/app/Flow_test.cpp index 5f56a0ceb1a..83b050e07cf 100644 --- a/src/test/app/Flow_test.cpp +++ b/src/test/app/Flow_test.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -706,7 +707,7 @@ struct Flow_test : public beast::unit_test::Suite static XRPAmount reserve(jtx::Env& env, std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return baseAccountReserve(*env.current(), count); } // Helper function that returns the Offers on an account. diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 6d53d256615..eef6af1fea2 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -335,7 +335,72 @@ class Invariants_test : public beast::unit_test::Suite // check. sleA1->at(sfBalance) = beast::kZero; BEAST_EXPECT(sleA1->at(sfOwnerCount) == 0); - adjustOwnerCount(ac.view(), sleA1, 1, ac.journal); + adjustOwnerCount(ac.view(), sleA1, {}, 1, ac.journal); + + ac.view().erase(sleA1); + + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); + + doInvariantCheck( + {{"account deletion left behind a sponsorship field"}}, + [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sleA1 = ac.view().peek(keylet::account(a1.id())); + if (!sleA1) + return false; + sleA1->at(sfBalance) = beast::kZero; + sleA1->setFieldU32(sfSponsoredOwnerCount, 1); + + ac.view().erase(sleA1); + + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); + + doInvariantCheck( + {{"account deletion left behind a sponsorship field"}}, + [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sleA1 = ac.view().peek(keylet::account(a1.id())); + if (!sleA1) + return false; + sleA1->at(sfBalance) = beast::kZero; + sleA1->setFieldU32(sfSponsoringOwnerCount, 1); + + ac.view().erase(sleA1); + + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); + + doInvariantCheck( + {{"account deletion left behind a sponsorship field"}}, + [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const a1Id = a1.id(); + auto const sleA1 = ac.view().peek(keylet::account(a1Id)); + if (!sleA1) + return false; + sleA1->at(sfBalance) = beast::kZero; + sleA1->setFieldU32(sfSponsoringAccountCount, 1); + + ac.view().erase(sleA1); + + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); + + doInvariantCheck( + {{"account deletion left behind a sponsorship field"}}, + [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sleA1 = ac.view().peek(keylet::account(a1.id())); + if (!sleA1) + return false; + sleA1->at(sfBalance) = beast::kZero; + sleA1->setAccountID(sfSponsor, a2.id()); ac.view().erase(sleA1); @@ -1683,6 +1748,7 @@ class Invariants_test : public beast::unit_test::Suite "pseudo-account sequence changed" "pseudo-account flags are not set" "pseudo-account has a regular key" + "pseudo-account has a sponsorship field" */ struct Mod { @@ -1710,6 +1776,22 @@ class Invariants_test : public beast::unit_test::Suite .expectedFailure = "pseudo-account has a regular key", .func = [](SLE::pointer& sle) { sle->at(sfRegularKey) = Account("regular").id(); }, }, + { + .expectedFailure = "pseudo-account has a sponsorship field", + .func = [](SLE::pointer& sle) { sle->at(sfSponsoredOwnerCount) = 1; }, + }, + { + .expectedFailure = "pseudo-account has a sponsorship field", + .func = [](SLE::pointer& sle) { sle->at(sfSponsoringOwnerCount) = 1; }, + }, + { + .expectedFailure = "pseudo-account has a sponsorship field", + .func = [](SLE::pointer& sle) { sle->at(sfSponsoringAccountCount) = 1; }, + }, + { + .expectedFailure = "pseudo-account has a sponsorship field", + .func = [](SLE::pointer& sle) { sle->at(sfSponsor) = Account("sponsor").id(); }, + }, }); for (auto const& mod : mods) @@ -4883,6 +4965,87 @@ class Invariants_test : public beast::unit_test::Suite } } + void + testSponsorship() + { + using namespace test::jtx; + using namespace std::string_literals; + testcase << "Sponsorship"; + { + auto const expectMessage = + "SponsoredOwnerCount does not equal " + "SponsoringOwnerCount delta."; + + doInvariantCheck( + {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sle = ac.view().peek(keylet::account(a1.id())); + if (!sle) + return false; + sle->setFieldU32(sfSponsoredOwnerCount, 1); + ac.view().update(sle); + return true; + }); + + doInvariantCheck( + {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sle = ac.view().peek(keylet::account(a1.id())); + if (!sle) + return false; + sle->setFieldU32(sfSponsoringOwnerCount, 1); + ac.view().update(sle); + return true; + }); + } + + { + auto const expectMessage = + "OwnerCount must be greater than or equal to SponsoredOwnerCount."; + + doInvariantCheck( + {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sle = ac.view().peek(keylet::account(a1.id())); + if (!sle) + return false; + sle->setFieldU32(sfOwnerCount, 0); + sle->setFieldU32(sfSponsoredOwnerCount, 1); + ac.view().update(sle); + + auto const sle2 = ac.view().peek(keylet::account(a2.id())); + if (!sle2) + return false; + sle2->setFieldU32(sfSponsoringOwnerCount, 1); + ac.view().update(sle2); + return true; + }); + } + + { + auto const expectMessage = + "Invariant failed: Net delta of SponsoringAccountCount does " + "not match net delta of sfSponsor presence."; + + doInvariantCheck( + {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sle = ac.view().peek(keylet::account(a1.id())); + if (!sle) + return false; + sle->setFieldU32(sfSponsoringAccountCount, 1); + ac.view().update(sle); + return true; + }); + + doInvariantCheck( + {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const sle = ac.view().peek(keylet::account(a1.id())); + if (!sle) + return false; + sle->setAccountID(sfSponsor, a2.id()); + ac.view().update(sle); + return true; + }); + } + } + public: void run() override diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 0edb955b902..fe966d02581 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -1094,7 +1095,7 @@ class LoanBroker_test : public beast::unit_test::Suite } auto const amt = - env.balance(alice) - env.current()->fees().accountReserve(env.ownerCount(alice)); + env.balance(alice) - accountReserve(*env.current(), alice.id(), env.journal); env(pay(alice, issuer, amt)); // preclaim:: tecINSUFFICIENT_RESERVE diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index c3b58502311..7cc3a8ac484 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -689,11 +690,11 @@ class Loan_test : public beast::unit_test::Suite case AssetType::MPT: { // Enough to cover initial fees if (!env.le(keylet::account(issuer))) - env.fund(env.current()->fees().accountReserve(10) * 10, issuer); + env.fund(baseAccountReserve(*env.current(), 10) * 10, issuer); if (!env.le(keylet::account(lender))) - env.fund(env.current()->fees().accountReserve(10) * 10, noripple(lender)); + env.fund(baseAccountReserve(*env.current(), 10) * 10, noripple(lender)); if (!env.le(keylet::account(borrower))) - env.fund(env.current()->fees().accountReserve(10) * 10, noripple(borrower)); + env.fund(baseAccountReserve(*env.current(), 10) * 10, noripple(borrower)); MPTTester mptt{env, issuer, kMptInitNoFund}; mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); @@ -778,11 +779,11 @@ class Loan_test : public beast::unit_test::Suite using namespace jtx; // Enough to cover initial fees - env.fund(env.current()->fees().accountReserve(10) * 10, issuer); + env.fund(baseAccountReserve(*env.current(), 10) * 10, issuer); if (lender != issuer) - env.fund(env.current()->fees().accountReserve(10) * 10, noripple(lender)); + env.fund(baseAccountReserve(*env.current(), 10) * 10, noripple(lender)); if (borrower != issuer && borrower != lender) - env.fund(env.current()->fees().accountReserve(10) * 10, noripple(borrower)); + env.fund(baseAccountReserve(*env.current(), 10) * 10, noripple(borrower)); describeLoan(env, brokerParams, loanParams, assetType, issuer, lender, borrower); @@ -859,11 +860,10 @@ class Loan_test : public beast::unit_test::Suite // Add extra for transaction fees and reserves, if appropriate, or a // tiny amount for the extra paid in each transaction auto const totalNeeded = state.totalValue + (serviceFee * state.paymentRemaining) + - (broker.asset.native() - ? Number( - baseFee * state.paymentRemaining + - env.current()->fees().accountReserve(env.ownerCount(borrower))) - : broker.asset(15).number()); + (broker.asset.native() ? Number( + baseFee * state.paymentRemaining + + accountReserve(*env.current(), borrower.id(), env.journal)) + : broker.asset(15).number()); auto const shortage = totalNeeded - borrowerBalance.number(); @@ -3089,7 +3089,7 @@ class Loan_test : public beast::unit_test::Suite auto const [acctReserve, incReserve] = [this]() -> std::pair { Env const env{*this, testableAmendments()}; return { - env.current()->fees().accountReserve(0).drops() / kDropsPerXrp.drops(), + baseAccountReserve(*env.current(), 0).drops() / kDropsPerXrp.drops(), env.current()->fees().increment.drops() / kDropsPerXrp.drops()}; }(); @@ -4546,8 +4546,8 @@ class Loan_test : public beast::unit_test::Suite BrokerInfo const& brokerInfo, jtx::Fee const& loanSetFee, Number const& debtMaximumRequest) { - auto const amt = env.balance(borrower) - - env.current()->fees().accountReserve(env.ownerCount(borrower)); + auto const amt = + env.balance(borrower) - accountReserve(*env.current(), borrower.id(), env.journal); env(pay(borrower, issuer, amt)); // tecINSUFFICIENT_RESERVE diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 3d6cff08857..dedc7011560 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -2118,6 +2118,15 @@ class MPToken_test : public beast::unit_test::Suite reward = STAmount{sfSignatureReward, usd(10)}; minAmount = STAmount{sfMinAccountCreateAmount, mpt}; } + // SponsorshipSet + { + json::Value jv; + 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.c_str()); + } } BEAST_EXPECT(txWithAmounts.empty()); } @@ -4221,8 +4230,8 @@ class MPToken_test : public beast::unit_test::Suite std::optional expectedOutstanding, std::string const& label) { ApplyViewImpl av(&*env.current(), TapNone); - auto const ter = - accountSendMulti(av, issuer.id(), asset, receivers, env.app().getJournal("View")); + auto const ter = accountSendMulti( + av, issuer.id(), asset, receivers, env.app().getJournal("View"), {}); BEAST_EXPECTS(ter == expectedTer, label); // Only verify OutstandingAmount on success — on error the diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index ba8f09c449d..ad9a5f59968 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -6,6 +6,7 @@ #include #include // IWYU pragma: keep #include +#include #include #include #include @@ -13,6 +14,8 @@ #include // IWYU pragma: keep #include #include +#include +#include #include #include #include @@ -423,15 +426,17 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite using namespace test::jtx; Account const alice{"alice"}; + Account const bob{"bob"}; Env env{*this, features}; - env.fund(XRP(1000), alice); + env.fund(XRP(1000), alice, bob); env.close(); // We're going to hack the ledger in order to avoid generating // 4 billion or so NFTs. Because we're hacking the ledger we - // need alice's account to have non-zero sfMintedNFTokens and - // sfBurnedNFTokens fields. This prevents an exception when the - // AccountRoot template is applied. + // need alice's account to have non-zero sfMintedNFTokens, + // sfBurnedNFTokens, sfSponsoredOwnerCount, sfSponsoringOwnerCount, + // sfSponsoringAccountCount fields. This prevents an exception when + // the AccountRoot template is applied. { uint256 const nftId0{token::getNextID(env, alice, 0u)}; env(token::mint(alice, 0u)); @@ -439,6 +444,23 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite env(token::burn(alice, nftId0)); env.close(); + + env(did::set(alice), + did::Uri("uri"), + sponsor::As(bob, spfSponsorReserve), + Sig(sfSponsorSignature, bob)); + env.close(); + + env(did::set(bob), + did::Uri("uri"), + sponsor::As(alice, spfSponsorReserve), + Sig(sfSponsorSignature, alice)); + env.close(); + + env(sponsor::transfer(bob, tfSponsorshipCreate), + sponsor::As(alice, spfSponsorReserve), + Sig(sfSponsorSignature, alice)); + env.close(); } // Note that we're bypassing almost all of the ledger's safety diff --git a/src/test/app/OfferMPT_test.cpp b/src/test/app/OfferMPT_test.cpp index ed0b2ffbe3f..d7597dcd546 100644 --- a/src/test/app/OfferMPT_test.cpp +++ b/src/test/app/OfferMPT_test.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -57,7 +58,7 @@ class OfferMPT_test : public beast::unit_test::Suite static XRPAmount reserve(jtx::Env& env, std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return baseAccountReserve(*env.current(), count); } static std::uint32_t @@ -1792,7 +1793,7 @@ class OfferMPT_test : public beast::unit_test::Suite // 1 for each trust limit == 3 (alice < mtgox/amazon/bitstamp) + // 1 for payment == 4 auto const base = env.current()->fees().base; - auto const startingXrp = XRP(100) + env.current()->fees().accountReserve(3) + base * 4; + auto const startingXrp = XRP(100) + baseAccountReserve(*env.current(), 3) + base * 4; env.fund(startingXrp, gw1, gw2, gw3, alice, bob); env.close(); @@ -1812,7 +1813,7 @@ class OfferMPT_test : public beast::unit_test::Suite env(offer(alice, usD1(200), XRP(200))); BEAST_EXPECT(env.balance(alice, usD1) == usD1(100)); - BEAST_EXPECT(env.balance(alice) == STAmount(env.current()->fees().accountReserve(3))); + BEAST_EXPECT(env.balance(alice) == STAmount(baseAccountReserve(*env.current(), 3))); BEAST_EXPECT(env.balance(bob, usD1) == usD1(400)); }; @@ -1863,7 +1864,7 @@ class OfferMPT_test : public beast::unit_test::Suite auto const bob = Account{"bob"}; auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2; + XRP(100) + baseAccountReserve(*env.current(), 1) + env.current()->fees().base * 2; env.fund(startingXrp, gw, alice, bob); @@ -1882,7 +1883,7 @@ class OfferMPT_test : public beast::unit_test::Suite jrr = ledgerEntryRoot(env, alice); BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName] == - STAmount(env.current()->fees().accountReserve(1)).getText()); + STAmount(baseAccountReserve(*env.current(), 1)).getText()); jrr = ledgerEntryMPT(env, bob, usd); BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "400"); @@ -1902,7 +1903,7 @@ class OfferMPT_test : public beast::unit_test::Suite auto const bob = Account{"bob"}; auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2; + XRP(100) + baseAccountReserve(*env.current(), 1) + env.current()->fees().base * 2; env.fund(startingXrp, gw, alice, bob); @@ -1923,7 +1924,7 @@ class OfferMPT_test : public beast::unit_test::Suite jrr = ledgerEntryRoot(env, alice); BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName] == - STAmount(env.current()->fees().accountReserve(1)).getText()); + STAmount(baseAccountReserve(*env.current(), 1)).getText()); jrr = ledgerEntryMPT(env, bob, usd); BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "300"); @@ -1943,8 +1944,7 @@ class OfferMPT_test : public beast::unit_test::Suite Env env{*this, features}; auto const base = env.current()->fees().base; - auto const startingXrp = - XRP(100.1) + env.current()->fees().accountReserve(1) + base * 2; + auto const startingXrp = XRP(100.1) + baseAccountReserve(*env.current(), 1) + base * 2; env.fund(startingXrp, gw, alice, bob); env.close(); diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 7382f4f0908..5fc049cd1c5 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -60,7 +61,7 @@ class OfferBaseUtil_test : public beast::unit_test::Suite static XRPAmount reserve(jtx::Env& env, std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return baseAccountReserve(*env.current(), count); } static std::uint32_t @@ -1937,7 +1938,7 @@ class OfferBaseUtil_test : public beast::unit_test::Suite // 1 for each trust limit == 3 (alice < mtgox/amazon/bitstamp) + // 1 for payment == 4 auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(3) + env.current()->fees().base * 4; + XRP(100) + baseAccountReserve(*env.current(), 3) + env.current()->fees().base * 4; env.fund(startingXrp, gw1, gw2, gw3, alice, bob); env.close(); @@ -1960,7 +1961,7 @@ class OfferBaseUtil_test : public beast::unit_test::Suite jrr = ledgerEntryRoot(env, alice); BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName] == - STAmount(env.current()->fees().accountReserve(3)).getText()); + STAmount(baseAccountReserve(*env.current(), 3)).getText()); jrr = ledgerEntryState(env, bob, gw1, "USD"); BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-400"); @@ -2020,7 +2021,7 @@ class OfferBaseUtil_test : public beast::unit_test::Suite auto const usd = gw["USD"]; auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2; + XRP(100) + baseAccountReserve(*env.current(), 1) + env.current()->fees().base * 2; env.fund(startingXrp, gw, alice, bob); env.close(); @@ -2041,7 +2042,7 @@ class OfferBaseUtil_test : public beast::unit_test::Suite jrr = ledgerEntryRoot(env, alice); BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName] == - STAmount(env.current()->fees().accountReserve(1)).getText()); + STAmount(baseAccountReserve(*env.current(), 1)).getText()); jrr = ledgerEntryState(env, bob, gw, "USD"); BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-400"); @@ -2062,7 +2063,7 @@ class OfferBaseUtil_test : public beast::unit_test::Suite auto const usd = gw["USD"]; auto const startingXrp = - XRP(100) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2; + XRP(100) + baseAccountReserve(*env.current(), 1) + env.current()->fees().base * 2; env.fund(startingXrp, gw, alice, bob); env.close(); @@ -2085,7 +2086,7 @@ class OfferBaseUtil_test : public beast::unit_test::Suite jrr = ledgerEntryRoot(env, alice); BEAST_EXPECT( jrr[jss::node][sfBalance.fieldName] == - STAmount(env.current()->fees().accountReserve(1)).getText()); + STAmount(baseAccountReserve(*env.current(), 1)).getText()); jrr = ledgerEntryState(env, bob, gw, "USD"); BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-300"); @@ -2107,7 +2108,7 @@ class OfferBaseUtil_test : public beast::unit_test::Suite auto const xxx = gw["XXX"]; auto const startingXrp = - XRP(100.1) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2; + XRP(100.1) + baseAccountReserve(*env.current(), 1) + env.current()->fees().base * 2; env.fund(startingXrp, gw, alice, bob); env.close(); diff --git a/src/test/app/Oracle_test.cpp b/src/test/app/Oracle_test.cpp index 7e7f5e9bd0f..97c3d29ac62 100644 --- a/src/test/app/Oracle_test.cpp +++ b/src/test/app/Oracle_test.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -60,7 +61,7 @@ struct Oracle_test : public beast::unit_test::Suite // Insufficient reserve { Env env(*this); - env.fund(env.current()->fees().accountReserve(0), owner); + env.fund(baseAccountReserve(*env.current(), 0), owner); Oracle const oracle( env, {.owner = owner, @@ -70,8 +71,7 @@ struct Oracle_test : public beast::unit_test::Suite // Insufficient reserve if the data series extends to greater than 5 { Env env(*this); - env.fund( - env.current()->fees().accountReserve(1) + env.current()->fees().base * 2, owner); + env.fund(baseAccountReserve(*env.current(), 1) + env.current()->fees().base * 2, owner); Oracle oracle( env, {.owner = owner, .fee = static_cast(env.current()->fees().base.drops())}); BEAST_EXPECT(oracle.exists()); @@ -639,8 +639,7 @@ struct Oracle_test : public beast::unit_test::Suite { Env env(*this); auto const baseFee = static_cast(env.current()->fees().base.drops()); - env.fund( - env.current()->fees().accountReserve(1) + env.current()->fees().base * 2, owner); + env.fund(baseAccountReserve(*env.current(), 1) + env.current()->fees().base * 2, owner); Oracle oracle(env, {.owner = owner, .fee = baseFee}); oracle.set(UpdateArg{.series = {{"XRP", "USD", 742, 2}}, .fee = baseFee}); } diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp new file mode 100644 index 00000000000..d3f94c2ccb6 --- /dev/null +++ b/src/test/app/Sponsor_test.cpp @@ -0,0 +1,6124 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::test { + +static STAmount +accountReserve(jtx::Env& env, std::uint32_t count = 1) +{ + return env.current()->fees().reserve * count; +} + +static STAmount +reserve(jtx::Env& env, std::uint32_t count) +{ + return baseAccountReserve(*env.current(), count); +} + +static void +adjustAccountXRPBalance(jtx::Env& env, jtx::Account const& account, STAmount const& balanceTo) +{ + using namespace test::jtx; + XRPL_ASSERT(isXRP(balanceTo), "adjustAccountXRPBalance: balanceTo must be XRP"); + auto const currentBalance = env.balance(account); + if (currentBalance == balanceTo) + return; + + auto const baseFee = env.current()->fees().base; + if (currentBalance > balanceTo) + { + env(pay(account, env.master, currentBalance - (balanceTo)), + Fee(XRP(1)), + sponsor::As(env.master, spfSponsorFee), + Sig(sfSponsorSignature, env.master)); + } + else + { + env(pay(env.master, account, balanceTo - currentBalance), Fee(baseFee)); + } + + env.close(); +} + +class Sponsor_test : public beast::unit_test::Suite +{ +public: + void + testDisabled() + { + testcase("Disabled"); + using namespace test::jtx; + Env env{*this, testableAmendments() - featureSponsor}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, sponsor); + + // check Sponsor fields + auto const jt = noop(alice); + auto jt1 = jt; + jt1[sfSponsor.jsonName] = sponsor.human(); + env(jt1, Ter(temDISABLED)); + env(jt, Sig(sfSponsorSignature, sponsor), Ter(temDISABLED)); + + auto jt2 = jt; + jt2[sfSponsorFlags.jsonName] = spfSponsorFee | spfSponsorReserve; + env(jt2, Ter(temDISABLED)); + + // check Sponsor transactions + env(sponsor::transfer(alice, 0), Ter(temDISABLED)); + env(sponsor::set(sponsor, 0), Ter(temDISABLED)); + } + + void + testInvalidSponsorshipSet() + { + testcase("Invalid SponsorshipSet"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const noFunded("noFunded"); + Account const gw("gw"); + + auto const usd = gw["usd"]; + env.fund(XRP(10000), alice, sponsor, gw); + env.close(); + + // + // preflight + // + + // Invalid flags + { + env(sponsor::set(sponsor, ~tfSponsorshipSetMask - tfInnerBatchTxn), + sponsor::SponseeAcc(alice), + Ter(temINVALID_FLAG)); + + env(sponsor::set( + sponsor, + tfSponsorshipSetRequireSignForFee | tfSponsorshipClearRequireSignForFee), + sponsor::SponseeAcc(alice), + Ter(temINVALID_FLAG)); + + env(sponsor::set( + sponsor, + tfSponsorshipSetRequireSignForReserve | + tfSponsorshipClearRequireSignForReserve), + sponsor::SponseeAcc(alice), + Ter(temINVALID_FLAG)); + + for (auto flag : + {tfSponsorshipSetRequireSignForFee, + tfSponsorshipClearRequireSignForFee, + tfSponsorshipSetRequireSignForReserve, + tfSponsorshipClearRequireSignForReserve}) + { + env(sponsor::set(sponsor, tfDeleteObject | flag), + sponsor::SponseeAcc(alice), + Ter(temINVALID_FLAG)); + } + } + + // invalid SponsorAccount / Sponsee + // Account = Sponsor + env(sponsor::set(alice, tfDeleteObject), + sponsor::CounterpartySponsor(alice), + Ter(temMALFORMED)); + // Account = Sponsee + env(sponsor::set(alice, tfDeleteObject), sponsor::SponseeAcc(alice), Ter(temMALFORMED)); + // Both Sponsor and Sponsee are specified + env(sponsor::set(alice, 0), + sponsor::CounterpartySponsor(sponsor), + sponsor::SponseeAcc(alice), + Ter(temMALFORMED)); + + // Invalid feeAmount + for (auto const& amt : {XRP(-1), usd(1)}) + { + env(sponsor::set_fee(sponsor, 0, amt), sponsor::SponseeAcc(alice), Ter(temBAD_AMOUNT)); + } + // Invalid MaxFee + for (auto const& amt : {XRP(-1), usd(1)}) + { + env(sponsor::set_fee(sponsor, 0, XRP(1), amt), + sponsor::SponseeAcc(alice), + Ter(temBAD_AMOUNT)); + } + + // Invalid Delete operation + env(sponsor::set_reserve(sponsor, tfDeleteObject, 1), + sponsor::SponseeAcc(alice), + Ter(temMALFORMED)); + env(sponsor::set_fee(sponsor, tfDeleteObject, XRP(1)), + sponsor::SponseeAcc(alice), + Ter(temMALFORMED)); + env(sponsor::set_max_fee(sponsor, tfDeleteObject, XRP(1)), + sponsor::SponseeAcc(alice), + Ter(temMALFORMED)); + + // Invalid SponsorAccount with non-Delete operation + env(sponsor::set_reserve(sponsor, 0, 100), + sponsor::CounterpartySponsor(alice), + Ter(temMALFORMED)); + env(sponsor::set_fee(sponsor, 0, XRP(1), XRP(1)), + sponsor::CounterpartySponsor(alice), + Ter(temMALFORMED)); + + // + // preclaim + // + + // Invalid Sponsee + env(sponsor::set(sponsor, 0), sponsor::SponseeAcc(noFunded), Ter(tecNO_DST)); + env.close(); + + // Invalid Sponsor + env(sponsor::set(sponsor, tfDeleteObject), + sponsor::CounterpartySponsor(noFunded), + Ter(tecNO_DST)); + env.close(); + + // Invalid Delete operation (sponsorship not found) + env(sponsor::set(sponsor, tfDeleteObject), sponsor::SponseeAcc(alice), Ter(tecNO_ENTRY)); + env.close(); + + // insufficient balance to sponsor Fee + adjustAccountXRPBalance(env, sponsor, env.current()->fees().reserve); + env(sponsor::set_fee(sponsor, 0, XRP(4)), sponsor::SponseeAcc(alice), Ter(tecUNFUNDED)); + env.close(); + + // insufficent reserve to create sponsorship + adjustAccountXRPBalance(env, sponsor, XRP(100) + XRP(1) + reserve(env, 1) - drops(1)); + env(sponsor::set(sponsor, 0, 100, XRP(100)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tecUNFUNDED)); + env.close(); + + // FeeAmount + Fee > Balance + /// Balance = 1000XRP, FeeAmount = 1001XRP + adjustAccountXRPBalance(env, sponsor, XRP(1000)); + env(sponsor::set_fee(sponsor, 0, XRP(1001)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tecUNFUNDED)); + env.close(); + /// Balance = 1000XRP, FeeAmount = 999XRP, Fee=2XRP + adjustAccountXRPBalance(env, sponsor, XRP(1000)); + env(sponsor::set_fee(sponsor, 0, XRP(999)), + sponsor::SponseeAcc(alice), + Fee(XRP(2)), + Ter(tecUNFUNDED)); + env.close(); + + // create sponsor to use above tests + // need feeAmount(1000) + Fee(1) + reserve(~250) = ~1251 + adjustAccountXRPBalance(env, sponsor, XRP(1000) + XRP(1) + reserve(env, 1)); + env(sponsor::set(sponsor, 0, 100, XRP(1000)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + // delta-based balance check + // After create: sponsor balance ~ 0, feeAmount = XRP(1000) + + // Decreasing feeAmount should succeed (refund, negative delta) + adjustAccountXRPBalance(env, sponsor, XRP(500)); + env(sponsor::set_fee(sponsor, 0, XRP(800)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + // balance was 500, delta = 800-1000 = -200 (refund), balance = 500+200-1 = 699 + + // Increasing feeAmount within delta budget should succeed + adjustAccountXRPBalance(env, sponsor, XRP(500)); + env(sponsor::set_fee(sponsor, 0, XRP(850)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + // balance was 500, delta = 850-800 = 50, balance = 500-50-1 = 449 + + // Increasing feeAmount where delta exceeds balance should fail + adjustAccountXRPBalance(env, sponsor, XRP(310)); + env(sponsor::set_fee(sponsor, 0, XRP(1200)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tecUNFUNDED)); + env.close(); + + // Increasing feeAmount to reach insufficient reserve + auto const currentFeeAmount = + env.le(keylet::sponsor(sponsor.id(), alice.id()))->getFieldAmount(sfFeeAmount).xrp(); + adjustAccountXRPBalance(env, sponsor, XRP(310)); + env(sponsor::set_fee(sponsor, 0, currentFeeAmount + XRP(309)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tecUNFUNDED)); + env.close(); + } + + void + testPseudoAccountSponsorship() + { + testcase("Pseudo account sponsorship"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const gw("gw"); + Account const sp("sponsor"); + + Asset const asset = gw["IOU"].asset(); + + env.fund(XRP(1000000), alice, bob, gw, sp); + env.close(); + + // Create a vault to get a pseudo account + Vault const vault{env}; + auto [tx, keylet] = vault.create({.owner = alice, .asset = asset}); + env(tx); + env.close(); + + auto const vaultSle = env.le(keylet); + BEAST_EXPECT(vaultSle); + Account const pseudoAcc("vault", vaultSle->getAccountID(sfAccount)); + env.memoize(pseudoAcc); + + // Sponsee is a pseudo account -> tecNO_PERMISSION + env(sponsor::set(sp, 0, 100, XRP(100)), + sponsor::SponseeAcc(pseudoAcc), + Ter(tecNO_PERMISSION)); + env.close(); + + // Sponsor is a pseudo account -> tecNO_PERMISSION + // (submitted by bob with counterpartySponsor pointing to pseudo account) + env(sponsor::set(bob, tfDeleteObject), + sponsor::CounterpartySponsor(pseudoAcc), + Ter(tecNO_PERMISSION)); + env.close(); + } + + void + testSingleSigning() + { + testcase("Single signing"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const invalid("invalid"); + + env.fund(XRP(10000), alice, sponsor); + env.close(); + + // Signature doesn't exist + auto tx = noop(alice); + tx[sfSponsor.jsonName] = sponsor.human(); + tx[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = strHex(sponsor.pk().slice()); + + env(tx, Fee(XRP(1)), sponsor::As(sponsor, spfSponsorReserve), Ter(telENV_RPC_FAILED)); + + // Invalid signature + tx[sfSponsorSignature.jsonName][sfTxnSignature.jsonName] = "DEADBEEF"; + env(tx, Fee(XRP(1)), sponsor::As(sponsor, spfSponsorReserve), Ter(telENV_RPC_FAILED)); + + // Signer account doesn't exist + env(noop(alice), + Fee(XRP(1)), + sponsor::As(invalid, spfSponsorReserve), + Sig(sfSponsorSignature, invalid), + Ter(terNO_ACCOUNT)); + + // Success + env(noop(alice), + Fee(XRP(1)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + } + + void + testMultiSigning() + { + testcase("Multi signing"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const invalid("invalid"); + + Account const signer1("signer1"); + Account const signer2("signer2"); + + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + env(signers(sponsor, 1, {{signer1, 1}, {signer2, 1}})); + env.close(); + + // Invalid signature + auto tx = noop(alice); + auto& signers1 = tx[sfSponsorSignature.jsonName][sfSigners.jsonName][0U][sfSigner.jsonName]; + signers1[sfAccount.jsonName] = signer1.human(); + signers1[sfSigningPubKey.jsonName] = strHex(signer1.pk().slice()); + signers1[sfTxnSignature.jsonName] = "DEADBEEF"; + env(tx, Fee(XRP(1)), sponsor::As(sponsor, spfSponsorReserve), Ter(telENV_RPC_FAILED)); + + // bob is not a multi-signing account. + env(noop(alice), + Fee(XRP(1)), + sponsor::As(bob, spfSponsorReserve), + Msig(sfSponsorSignature, {signer1}), + Ter(tefNOT_MULTI_SIGNING)); + + env(noop(alice), + Fee(XRP(1)), + sponsor::As(sponsor, spfSponsorReserve), + Msig(sfSponsorSignature, {signer1}), + Ter(tesSUCCESS)); + env.close(); + + env(signers(sponsor, 2, {{signer1, 1}, {signer2, 1}})); + env.close(); + + // test calculateBaseFee for multisigned sponsor + auto const baseFee = env.current()->fees().base; + env(noop(alice), + Fee(baseFee + 2 * baseFee - 1), + sponsor::As(sponsor, spfSponsorReserve), + Msig(sfSponsorSignature, {signer1, signer2}), + Ter(telINSUF_FEE_P)); + + env(noop(alice), + Fee(baseFee + 2 * baseFee), + sponsor::As(sponsor, spfSponsorReserve), + Msig(sfSponsorSignature, {signer1, signer2}), + Ter(tesSUCCESS)); + } + + void + testInvalidSponsorField() + { + testcase("Invalid Sponsor Field"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const noFunded("noFunded"); + env.fund(XRP(10000), alice, sponsor); + env.close(); + + // Invalid Sponsor Account (Account = Sponsor.Account) + env(noop(alice), sponsor::As(alice, spfSponsorFee), Ter(temMALFORMED)); + + // Invalid Sponsor Account + // (SponsorSignature is specified but Sponsor.Account is not specified) + env(noop(alice), Sig(sfSponsorSignature, sponsor), Ter(temMALFORMED)); + + // Invalid Sponsor Account (Sponsor.Account doesn't exist) + env(noop(alice), sponsor::As(noFunded, spfSponsorReserve), Ter(terNO_ACCOUNT)); + env(noop(alice), + sponsor::As(noFunded, spfSponsorReserve), + Sig(sfSponsorSignature, noFunded), + Ter(terNO_ACCOUNT)); + + // Invalid Flags + env(noop(alice), + sponsor::As(sponsor, (spfSponsorFee | spfSponsorReserve) + 1), + Ter(temINVALID_FLAG)); + + // SponsorFlags=0 with valid sponsor (no sponsorship purpose) + env(noop(alice), sponsor::As(sponsor, 0), Ter(temINVALID_FLAG)); + + // no SponsorFlag with valid sponsor + auto tx = noop(alice); + tx[sfSponsor.jsonName] = sponsor.human(); + env(tx, Ter(temINVALID_FLAG)); + + // Invalid Flags without sponsor + tx = noop(alice); + tx[sfSponsorFlags.jsonName] = spfSponsorFee | spfSponsorReserve; + env(tx, Ter(temINVALID_FLAG)); + } + + void + testSimpleSponsorshipSet() + { + testcase("Simple SponsorshipSet"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, sponsor); + env.close(); + + { + // create sponsorship + env(sponsor::set( + sponsor, + tfSponsorshipSetRequireSignForFee | tfSponsorshipSetRequireSignForReserve, + 100, + XRP(100), + XRP(1)), + Fee(XRP(1)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + auto sle = env.le(keylet::sponsor(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfReserveCount) == 100); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100)); + BEAST_EXPECT(sle->at(sfMaxFee) == XRP(1)); + BEAST_EXPECT(sle->isFlag(lsfSponsorshipRequireSignForFee)); + BEAST_EXPECT(sle->isFlag(lsfSponsorshipRequireSignForReserve)); + 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)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + sle = env.le(keylet::sponsor(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfReserveCount) == 50); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(50)); + BEAST_EXPECT(sle->at(sfMaxFee) == XRP(0.5)); + 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)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + sle = env.le(keylet::sponsor(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfReserveCount) == 200); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(200)); + BEAST_EXPECT(sle->at(sfMaxFee) == XRP(2)); + BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - sle->at(sfFeeAmount) - XRP(3)); + + // delete from sponsor + env(sponsor::del(sponsor), sponsor::SponseeAcc(alice), Fee(XRP(1)), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - XRP(4)); + + env(sponsor::set( + sponsor, + tfSponsorshipSetRequireSignForFee | tfSponsorshipSetRequireSignForReserve, + 100, + XRP(100), + XRP(1)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + // delete from sponsee + env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(!env.le(keylet::sponsor(sponsor, alice))); + + // create sponsorship with zero value + env(sponsor::set(sponsor, 0, 0, XRP(0), XRP(0)), + sponsor::SponseeAcc(alice), + Fee(XRP(1))); + env.close(); + + sle = env.le(keylet::sponsor(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(!sle->isFieldPresent(sfReserveCount)); + BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount)); + BEAST_EXPECT(!sle->isFieldPresent(sfMaxFee)); + // verify flags from previous sponsorship are not carried over + BEAST_EXPECT(!sle->isFlag(lsfSponsorshipRequireSignForFee)); + BEAST_EXPECT(!sle->isFlag(lsfSponsorshipRequireSignForReserve)); + + // update sponsorship with non-zero value + env(sponsor::set(sponsor, 0, 100, XRP(100), XRP(1)), + sponsor::SponseeAcc(alice), + Fee(XRP(1))); + env.close(); + + sle = env.le(keylet::sponsor(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfReserveCount) == 100); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100)); + BEAST_EXPECT(sle->at(sfMaxFee) == XRP(1)); + + // update sponsorship with zero value + env(sponsor::set(sponsor, 0, 0, XRP(0), XRP(0)), + sponsor::SponseeAcc(alice), + Fee(XRP(1))); + env.close(); + + sle = env.le(keylet::sponsor(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(!sle->isFieldPresent(sfReserveCount)); + BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount)); + BEAST_EXPECT(!sle->isFieldPresent(sfMaxFee)); + } + + { + // Update Sponsorship (FeeAmount) + // set empty FeeAmount + env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + + // add FeeAmount + env(sponsor::set_fee(sponsor, 0, XRP(100)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS)); + env.close(); + } + { + // Update Sponsorship (ReserveCount) + // set empty ReserveCount + env(sponsor::set_fee(sponsor, 0, XRP(100)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + // add ReserveCount + env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + + env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS)); + env.close(); + } + { + // delete Sponsorship (only with FeeAmount) + env(sponsor::set_fee(sponsor, 0, XRP(100)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS)); + env.close(); + } + { + // delete Sponsorship (only with ReserveCount) + env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + + env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS)); + env.close(); + } + } + + void + testPreFundAndCosign() + { + testcase("PreFund and Cosign"); + using namespace test::jtx; + Account const alice("alice"); + Account const sponsor("sponsor"); + + { + // both pre-funded and co-signed,pre-funded value is used + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, sponsor); + env.close(); + + env(sponsor::set(sponsor, 0, 100, XRP(100), XRP(1)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + env(did::set(alice), + did::Uri("uri"), + sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + auto sle = env.le(keylet::sponsor(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfReserveCount) == 99); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(99)); + + env(did::del(alice), Ter(tesSUCCESS)); + env.close(); + + sle = env.le(keylet::sponsor(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->at(sfReserveCount) == 99); // not paybacked + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(99)); + } + + { + // if pre-funded value is not enough, error + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, sponsor); + env.close(); + + env(sponsor::set(sponsor, 0, 10, XRP(10), XRP(100)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + // Fee insufficient + env(ticket::create(alice, 1), + sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Fee(XRP(11)), + Ter(terINSUF_FEE_B)); + env.close(); + + // reserve insufficient + env(ticket::create(alice, 11), + sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Fee(XRP(1)), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + } + + void + testTransferSponsor() + { + testcase("Transfer Sponsor"); + using namespace test::jtx; + + { + // invalid fields + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor1("sponsor1"); + Account const sponsor2("sponsor2"); + env.fund(XRP(10000), alice, bob, sponsor1, sponsor2); + env.close(); + + env(sponsor::transfer( + alice, (tfSponsorshipCreate | tfSponsorshipReassign | tfSponsorshipEnd) + 1), + Ter(temINVALID_FLAG)); + + // invalid combination of flags + for (auto flag : { + tfSponsorshipCreate | tfSponsorshipReassign, + tfSponsorshipCreate | tfSponsorshipEnd, + tfSponsorshipReassign | tfSponsorshipEnd, + tfSponsorshipCreate | tfSponsorshipReassign | tfSponsorshipEnd, + }) + env(sponsor::transfer(alice, flag), Ter(temINVALID_FLAG)); + + // invalid tfSponsorshipCreate + // no sponsor field present + env(sponsor::transfer(alice, tfSponsorshipCreate), Ter(temINVALID_FLAG)); + // sponsee field present + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::SponseeAcc(bob), + sponsor::As(sponsor1, spfSponsorReserve), + Ter(temMALFORMED)); + + // invalid tfSponsorshipReassign + // no sponsor field present + env(sponsor::transfer(alice, tfSponsorshipReassign), Ter(temINVALID_FLAG)); + // sponsee field present + env(sponsor::transfer(alice, tfSponsorshipReassign), + sponsor::SponseeAcc(bob), + sponsor::As(sponsor1, spfSponsorReserve), + Ter(temMALFORMED)); + + // invalid tfSponsorshipEnd + // sponsor field present + env(sponsor::transfer(alice, tfSponsorshipEnd), + sponsor::As(sponsor1, spfSponsorReserve), + Ter(temINVALID_FLAG)); + // account = sponsee + env(sponsor::transfer(alice, tfSponsorshipEnd), + sponsor::SponseeAcc(alice), + Ter(temMALFORMED)); + } + + { + // Invalid SponsorshipEnd permission (sponsor object/sponsor account) + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + { + // sponsor object + env(did::set(alice), + did::Uri("uri"), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + auto const keylet = keylet::did(alice); + env(sponsor::transfer(bob, tfSponsorshipEnd, keylet.key), + sponsor::SponseeAcc(alice), + Ter(tecNO_PERMISSION)); + } + { + // sponsor object + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + env(sponsor::transfer(bob, tfSponsorshipEnd), + sponsor::SponseeAcc(alice), + Ter(tecNO_PERMISSION)); + } + } + + { + // sponsor account + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor1("sponsor1"); + Account const sponsor2("sponsor2"); + env.fund(XRP(10000), alice, bob, sponsor1, sponsor2); + + // sfSponsor provided but sfSponsorSignature not provided + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor1, spfSponsorReserve), + Ter(temMALFORMED)); + env.close(); + + adjustAccountXRPBalance(env, sponsor1, accountReserve(env, 2) - drops(1)); + + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + adjustAccountXRPBalance(env, sponsor1, accountReserve(env, 2)); + + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 1); + auto const sle1 = env.le(keylet::account(alice)); + BEAST_EXPECT(sle1->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sle1->getAccountID(sfSponsor) == sponsor1.id()); + + // transfer sponsor + adjustAccountXRPBalance(env, sponsor2, accountReserve(env, 2) - drops(1)); + + env(sponsor::transfer(alice, tfSponsorshipReassign), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + adjustAccountXRPBalance(env, sponsor2, accountReserve(env, 2)); + + env(sponsor::transfer(alice, tfSponsorshipReassign), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 1); + BEAST_EXPECT( + !env.le(keylet::account(sponsor1))->isFieldPresent(sfSponsoringAccountCount)); + auto const sle2 = env.le(keylet::account(alice)); + BEAST_EXPECT(sle2->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sle2->getAccountID(sfSponsor) == sponsor2.id()); + + // sponsor 2 accounts + adjustAccountXRPBalance(env, sponsor2, accountReserve(env, 3)); + env(sponsor::transfer(bob, tfSponsorshipCreate), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + + // dissolve sponsors + adjustAccountXRPBalance(env, alice, accountReserve(env, 1) - drops(1)); + + env(sponsor::transfer(alice, tfSponsorshipEnd), Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + adjustAccountXRPBalance(env, alice, accountReserve(env, 1)); + + env(sponsor::transfer(alice, tfSponsorshipEnd)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 1); + auto const sle3 = env.le(keylet::account(alice)); + BEAST_EXPECT(!sle3->isFieldPresent(sfSponsor)); + + env(sponsor::transfer(bob, tfSponsorshipEnd)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, bob) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0); + BEAST_EXPECT( + !env.le(keylet::account(sponsor2))->isFieldPresent(sfSponsoringAccountCount)); + auto const sle4 = env.le(keylet::account(bob)); + BEAST_EXPECT(!sle4->isFieldPresent(sfSponsor)); + + // not sponsored + env(sponsor::transfer(bob, tfSponsorshipEnd), Ter(tecNO_PERMISSION)); + env.close(); + } + { + // dissolve account sponsorship from sponsor + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + BEAST_EXPECT(env.le(alice)->getAccountID(sfSponsor) == sponsor.id()); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor) == 1); + + env(sponsor::transfer(sponsor, tfSponsorshipEnd), sponsor::SponseeAcc(alice)); + env.close(); + + BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor) == 0); + } + + { + // sponsor object (co-signing) + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor1("sponsor1"); + Account const sponsor2("sponsor2"); + env.fund(XRP(10000), alice, bob, sponsor1, sponsor2); + env.close(); + + adjustAccountXRPBalance(env, sponsor1, reserve(env, 1) - drops(1)); + adjustAccountXRPBalance(env, sponsor2, reserve(env, 1) - drops(1)); + + auto const seq = env.seq(alice); + env(check::create(alice, bob, XRP(1))); + env.close(); + + auto const checkId = keylet::check(alice, seq).key; + BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); + + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + env(pay(alice, sponsor1, drops(1))); + env.close(); + + // Invalid ObjectID (not found) + env(sponsor::transfer(alice, tfSponsorshipCreate, keylet::check(alice, 0).key), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1), + Ter(tecNO_ENTRY)); + env.close(); + + // Invalid Owner + env(sponsor::transfer(bob, tfSponsorshipCreate, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1), + Ter(tecNO_PERMISSION)); + env.close(); + + // Valid Owner + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 1); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + auto const sle1 = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(sle1->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sle1->getAccountID(sfSponsor) == sponsor1.id()); + + // transfer sponsor + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecINSUFFICIENT_RESERVE)); + + env(pay(alice, sponsor2, drops(1))); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0); + auto const sle2 = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(sle2->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sle2->getAccountID(sfSponsor) == sponsor2.id()); + + // dissolve sponsor + adjustAccountXRPBalance(env, alice, reserve(env, 1) - drops(1)); + + env(sponsor::transfer(alice, tfSponsorshipEnd, checkId), Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + adjustAccountXRPBalance(env, alice, reserve(env, 1)); + + // object doesn't sponsored + auto const ticketSeq = env.seq(alice); + env(ticket::create(alice, 1)); + env.close(); + auto ticketId = keylet::TicketT()(alice, ticketSeq + 1).key; + BEAST_EXPECT(env.le(keylet::unchecked(ticketId))); + env(sponsor::transfer(alice, tfSponsorshipEnd, ticketId), Ter(tecNO_PERMISSION)); + env.close(); + env(noop(alice), ticket::Use(ticketSeq + 1)); + env.close(); + + adjustAccountXRPBalance(env, alice, reserve(env, 1)); + + env(sponsor::transfer(alice, tfSponsorshipEnd, checkId)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0); + BEAST_EXPECT( + !env.le(keylet::account(sponsor2))->isFieldPresent(sfSponsoringOwnerCount)); + auto const sle3 = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(!sle3->isFieldPresent(sfSponsor)); + } + { + // sponsor object (pre-funded + no ltSponsorship entry) + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor1("sponsor1"); + Account const sponsor2("sponsor2"); + env.fund(XRP(10000), alice, bob, sponsor1, sponsor2); + env.close(); + + auto const seq = env.seq(alice); + env(check::create(alice, bob, XRP(1))); + env.close(); + + auto const checkId = keylet::check(alice, seq).key; + BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); + + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Ter(terNO_SPONSORSHIP)); + env.close(); + + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Ter(terNO_SPONSORSHIP)); + env.close(); + } + { + // sponsor object (pre-funded) + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor1("sponsor1"); + Account const sponsor2("sponsor2"); + env.fund(XRP(10000), alice, bob, sponsor1, sponsor2); + env.close(); + + auto const seq = env.seq(alice); + env(check::create(alice, bob, XRP(1))); + env.close(); + + auto const checkId = keylet::check(alice, seq).key; + BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); + + // insufficient reserve count + env(sponsor::set_fee(sponsor1, 0, XRP(100)), sponsor::SponseeAcc(alice)); + env.close(); + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + env(sponsor::set_reserve(sponsor1, 0, 100), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor1, spfSponsorReserve)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 1); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + auto checkSle = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(checkSle->isFieldPresent(sfSponsor)); + BEAST_EXPECT(checkSle->getAccountID(sfSponsor) == sponsor1.id()); + auto sponsor1Sle = env.le(keylet::sponsor(sponsor1, alice)); + BEAST_EXPECT(sponsor1Sle->getFieldU32(sfReserveCount) == 99); + + // transfer sponsor + env(sponsor::set_reserve(sponsor2, 0, 100), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0); + checkSle = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(checkSle->isFieldPresent(sfSponsor)); + BEAST_EXPECT(checkSle->getAccountID(sfSponsor) == sponsor2.id()); + sponsor1Sle = env.le(keylet::sponsor(sponsor1, alice)); + BEAST_EXPECT(sponsor1Sle->getFieldU32(sfReserveCount) == 99); + auto sponsor2Sle = env.le(keylet::sponsor(sponsor2, alice)); + BEAST_EXPECT(sponsor2Sle->getFieldU32(sfReserveCount) == 99); + + // dissolve sponsor + adjustAccountXRPBalance(env, alice, reserve(env, 1)); + env(sponsor::transfer(alice, tfSponsorshipEnd, checkId)); + env.close(); + + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0); + BEAST_EXPECT( + !env.le(keylet::account(sponsor2))->isFieldPresent(sfSponsoringOwnerCount)); + checkSle = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(!checkSle->isFieldPresent(sfSponsor)); + sponsor2Sle = env.le(keylet::sponsor(sponsor2, alice)); + BEAST_EXPECT(sponsor2Sle->getFieldU32(sfReserveCount) == 99); + } + + { + // Dissolve object sponsorship from sponsor(no-ltSponsorship) + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + auto const seq = env.seq(alice); + env(check::create(alice, bob, XRP(1))); + env.close(); + + auto const checkId = keylet::check(alice, seq).key; + BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); + + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + BEAST_EXPECT( + env.le(keylet::unchecked(checkId))->getAccountID(sfSponsor) == sponsor.id()); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + // not the owner of the object + env(sponsor::transfer(sponsor, tfSponsorshipEnd, checkId), Ter(tecNO_PERMISSION)); + env.close(); + + env(sponsor::transfer(sponsor, tfSponsorshipEnd, checkId), sponsor::SponseeAcc(alice)); + env.close(); + + BEAST_EXPECT(!env.le(keylet::unchecked(checkId))->isFieldPresent(sfSponsor)); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + + { + // Dissolve object sponsorship from sponsor (with ltSponsorship) + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + auto const seq = env.seq(alice); + env(check::create(alice, bob, XRP(1))); + env.close(); + + auto const checkId = keylet::check(alice, seq).key; + BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); + + env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(alice)); + env.close(); + + BEAST_EXPECT( + env.le(keylet::unchecked(checkId))->getAccountID(sfSponsor) == sponsor.id()); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT( + env.le(keylet::sponsor(sponsor, alice))->getFieldU32(sfReserveCount) == 100); + + // not the owner of the object + env(sponsor::transfer(sponsor, tfSponsorshipEnd, checkId), Ter(tecNO_PERMISSION)); + env.close(); + + env(sponsor::transfer(sponsor, tfSponsorshipEnd, checkId), sponsor::SponseeAcc(alice)); + env.close(); + + BEAST_EXPECT(!env.le(keylet::unchecked(checkId))->isFieldPresent(sfSponsor)); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT( + env.le(keylet::sponsor(sponsor, alice))->getFieldU32(sfReserveCount) == 100); + } + + { + // sponsor trustline + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + + auto const& highAcc = alice > bob ? alice : bob; + auto const& lowAcc = alice > bob ? bob : alice; + + for (bool const isIssuerHigh : {false, true}) + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + auto const& issuer = isIssuerHigh ? highAcc : lowAcc; + auto const& user = isIssuerHigh ? lowAcc : highAcc; + + auto const usd = issuer["usd"]; + auto const currency = usd.currency; + + env(trust(user, issuer["usd"](100))); + env.close(); + + auto const trustId = keylet::line(user, issuer, currency); + BEAST_EXPECT(env.le(trustId)); + + // transfer sponsor + env(sponsor::transfer(user, tfSponsorshipCreate, trustId.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + BEAST_EXPECT(env.le(trustId)); + + BEAST_EXPECT( + env.le(trustId)->getAccountID(isIssuerHigh ? sfLowSponsor : sfHighSponsor) == + sponsor.id()); + BEAST_EXPECT( + !env.le(trustId)->isFieldPresent(isIssuerHigh ? sfHighSponsor : sfLowSponsor)); + + // dissolve sponsor + env(sponsor::transfer(user, tfSponsorshipEnd, trustId.key)); + env.close(); + + BEAST_EXPECT(env.le(trustId)); + BEAST_EXPECT( + !env.le(trustId)->isFieldPresent(isIssuerHigh ? sfLowSponsor : sfHighSponsor)); + BEAST_EXPECT( + !env.le(trustId)->isFieldPresent(isIssuerHigh ? sfHighSponsor : sfLowSponsor)); + } + } + + { + // invalid transfer + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // create owner dir + env(ticket::create(alice, 1)); + env.close(); + + // AccountRoot + // Amendments + // LedgerHashes + // FeeSettings + // NegativeUNL + // DirNode + auto const keylets = { + keylet::account(alice), + // keylet::amendments(), + keylet::skip(), + keylet::fees(), + // keylet::negativeUNL(), + keylet::ownerDir(alice), + }; + for (auto const& keylet : keylets) + { + env(sponsor::transfer(alice, tfSponsorshipCreate, keylet.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecNO_PERMISSION)); + } + } + } + + void + testSponsorFee() + { + using namespace test::jtx; + + testcase("Sponsor Fee"); + + { + // co-signing + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob); + env.close(); + + { + // Fee should be checked before permission check, + // otherwise tecNO_SPONSOR_PERMISSION returned when permission + // check fails could cause context reset to pay Fee because it + // is tec error + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(2000)), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(terNO_ACCOUNT)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + } + + env.fund(XRP(1000), sponsor); + env.close(); + + { + // Sponsor pays the Fee + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + + auto const sendAmt = XRP(100); + auto const feeAmt = XRP(10); + env(pay(alice, bob, sendAmt), + Fee(feeAmt), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance - sendAmt); + BEAST_EXPECT(env.balance(bob) == bobBalance + sendAmt); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - feeAmt); + } + + { + // insufficient balance to pay Fee + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(2000)), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(terINSUF_FEE_B)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + } + + { + // Fee is paid by Sponsor + // on context reset (tec error) + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + auto const feeAmt = XRP(10); + + env(pay(alice, bob, XRP(20000)), + Fee(feeAmt), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(tecUNFUNDED_PAYMENT)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - feeAmt); + } + + { + // below reserve + adjustAccountXRPBalance(env, sponsor, env.current()->fees().reserve); + env.close(); + auto const feeAmt = XRP(4); + + env(noop(alice), + Fee(env.current()->fees().base), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(terINSUF_FEE_B)); + env.close(); + + env(noop(alice), + Fee(XRP(10)), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(terINSUF_FEE_B)); + env.close(); + } + } + + { + // pre funded + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + auto const sponsorFeeBalance = [&](Account const& sponsor, Account const& sponsee) { + return env.le(keylet::sponsor(sponsor, sponsee))->getFieldAmount(sfFeeAmount).xrp(); + }; + + { + // Fee should be checked before permission check, + // otherwise tecNO_SPONSOR_PERMISSION returned when permission + // check fails could cause context reset to pay Fee because it + // is tec error + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(2000)), + sponsor::As(sponsor, spfSponsorFee), + Ter(terNO_SPONSORSHIP)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + } + + env(sponsor::set_fee(sponsor, 0, XRP(100)), sponsor::SponseeAcc(alice)); + env.close(); + + { + // Sponsor pays the Fee + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + auto sponsorFee = sponsorFeeBalance(sponsor, alice); + + auto const sendAmt = XRP(100); + auto const feeAmt = XRP(10); + env(pay(alice, bob, sendAmt), Fee(feeAmt), sponsor::As(sponsor, spfSponsorFee)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance - sendAmt); + BEAST_EXPECT(env.balance(bob) == bobBalance + sendAmt); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + BEAST_EXPECT(sponsorFeeBalance(sponsor, alice) == sponsorFee - feeAmt); + } + + { + // insufficient balance to pay Fee + { + // > FeeAmount + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + auto sponsorFee = sponsorFeeBalance(sponsor, alice); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(90) + drops(1)), + sponsor::As(sponsor, spfSponsorFee), + Ter(terINSUF_FEE_B)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + BEAST_EXPECT(sponsorFeeBalance(sponsor, alice) == sponsorFee); + } + // use all FeeAmount + { + // = FeeAmount + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(90)), + sponsor::As(sponsor, spfSponsorFee), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance - XRP(100)); + BEAST_EXPECT(env.balance(bob) == bobBalance + XRP(100)); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + BEAST_EXPECT( + !env.le(keylet::sponsor(sponsor, alice))->isFieldPresent(sfFeeAmount)); + } + + // reset FeeAmount and MaxFee + env(sponsor::del(sponsor), sponsor::SponseeAcc(alice)); + env.close(); + env(sponsor::set_fee(sponsor, 0, XRP(10), XRP(1)), sponsor::SponseeAcc(alice)); + env.close(); + + { + // > MaxFee + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + auto sponsorFee = sponsorFeeBalance(sponsor, alice); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(1) + drops(1)), + sponsor::As(sponsor, spfSponsorFee), + Ter(terINSUF_FEE_B)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + BEAST_EXPECT(sponsorFeeBalance(sponsor, alice) == sponsorFee); + } + } + + { + // Fee is paid by Sponsor + // on context reset (tec error) + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto sponsorBalance = env.balance(sponsor); + auto sponsorFee = sponsorFeeBalance(sponsor, alice); + auto const feeAmt = XRP(1); + + env(pay(alice, bob, XRP(20000)), + Fee(feeAmt), + sponsor::As(sponsor, spfSponsorFee), + Ter(tecUNFUNDED_PAYMENT)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); + BEAST_EXPECT(sponsorFeeBalance(sponsor, alice) == sponsorFee - feeAmt); + } + + // make sfFeeAmount absent if tec error and all Fee is paid + { + // reset FeeAmount and MaxFee + env(sponsor::del(sponsor), sponsor::SponseeAcc(alice)); + env(sponsor::set_fee(sponsor, 0, XRP(10)), sponsor::SponseeAcc(alice)); + env.close(); + + BEAST_EXPECT(env.le(keylet::sponsor(sponsor, alice))->isFieldPresent(sfFeeAmount)); + auto sponsorAvailableFee = sponsorFeeBalance(sponsor, alice); + env(check::cancel(alice, uint256(1)), + Fee(sponsorAvailableFee), + sponsor::As(sponsor, spfSponsorFee), + Ter(tecNO_ENTRY)); + env.close(); + BEAST_EXPECT(!env.le(keylet::sponsor(sponsor, alice))->isFieldPresent(sfFeeAmount)); + } + } + + // MaxFee cap is enforced in reset() for tec-failing transactions. + // On a closed ledger view (!view.open()), checkFee returns tecINSUFF_FEE when + // Fee > MaxFee (not terINSUF_FEE_B), triggering reset() + { + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const carol("sponsor"); + + env.fund(XRP(10000), alice, carol); + env.close(); + + // FeeAmount=1000 drops, MaxFee=10 drops + env(sponsor::set_fee(carol, 0, drops(1000), drops(10)), sponsor::SponseeAcc(alice)); + env.close(); + + // Apply directly against the closed ledger view (open_ = false) so that + // checkFee returns tecINSUFF_FEE and reset() is invoked. + OpenView overlay(&*env.closed()); + + auto jt = env.jt( + noop(alice), + Fee(drops(1000)), + Seq(env.seq(alice)), + sponsor::As(carol, spfSponsorFee)); + + auto const result = xrpl::apply(env.app(), overlay, *jt.stx, TapNone, env.journal); + BEAST_EXPECT(result.ter == tecINSUFF_FEE); + BEAST_EXPECT(result.applied); + + // Only MaxFee (10 drops) must be deducted, not the full 1000 drops. + auto const sle = overlay.read(keylet::sponsor(carol.id(), alice.id())); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->isFieldPresent(sfFeeAmount)); + BEAST_EXPECT(sle->getFieldAmount(sfFeeAmount) == drops(990)); // 1000 - MaxFee(10) + } + + // test lsfSponsorshipRequireSignForFee + { + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // set flag + env(sponsor::set_fee(sponsor, tfSponsorshipSetRequireSignForFee, XRP(10)), + sponsor::SponseeAcc(alice)); + env.close(); + + env(pay(alice, bob, XRP(100)), + Fee(XRP(10)), + sponsor::As(sponsor, spfSponsorFee), + Ter(terNO_SPONSORSHIP)); + env.close(); + + BEAST_EXPECT( + env.le(keylet::sponsor(sponsor, alice))->getFieldAmount(sfFeeAmount) == XRP(10)); + + // clear flag + env(sponsor::set_fee(sponsor, tfSponsorshipClearRequireSignForFee, XRP(10)), + sponsor::SponseeAcc(alice)); + env.close(); + + // Payment is re-applied + BEAST_EXPECT(!env.le(keylet::sponsor(sponsor, alice))->isFieldPresent(sfFeeAmount)); + } + + // RequireSignForFee: co-signing should succeed + { + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // set flag + env(sponsor::set_fee(sponsor, tfSponsorshipSetRequireSignForFee, XRP(10)), + sponsor::SponseeAcc(alice)); + env.close(); + + // pre-funded (no sig) should fail + env(pay(alice, bob, XRP(100)), + Fee(XRP(1)), + sponsor::As(sponsor, spfSponsorFee), + Ter(terNO_SPONSORSHIP)); + env.close(); + + // co-signing (with sig) should succeed + env(pay(alice, bob, XRP(100)), + Fee(XRP(1)), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT( + env.le(keylet::sponsor(sponsor, alice))->getFieldAmount(sfFeeAmount) == XRP(9)); + } + } + + void + testSponsorAccount() + { + testcase("Sponsor Account"); + using namespace test::jtx; + + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + Account const sponsor3("sponsor3"); + Account const bob("bob"); + Account const charlie("charlie"); + Account const dave("dave"); + Account const gw("gw"); + auto const usd = gw["usd"]; + + { + // Disabled + Env env{*this, testableAmendments() - featureSponsor}; + env.fund(XRP(10000), alice, sponsor); + env.close(); + env(pay(alice, bob, XRP(100)), Txflags(tfSponsorCreatedAccount), Ter(temDISABLED)); + env.close(); + } + + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, sponsor, sponsor2, sponsor3); + env.close(); + + // Invalid flags + for (auto flag : { + tfNoRippleDirect, + tfPartialPayment, + tfLimitQuality, + }) + { + env(pay(alice, bob, XRP(100)), + Txflags(tfSponsorCreatedAccount | flag), + Ter(temINVALID_FLAG)); + env.close(); + } + + // Invalid amount(iou) + env(pay(alice, bob, usd(100)), Txflags(tfSponsorCreatedAccount), Ter(temBAD_AMOUNT)); + env.close(); + + // Account is not sponsored by normal Sponsor specification + { + env(pay(alice, bob, drops(baseAccountReserve(*env.current(), 0))), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + auto const bobSle = env.le(keylet::account(bob)); + BEAST_EXPECT(!bobSle->isFieldPresent(sfSponsor)); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor) == 0); + } + + // Use tfSponsorCreatedAccount to sponsor an account + { + // to funded account + env(pay(sponsor2, bob, drops(1)), + Txflags(tfSponsorCreatedAccount), + Fee(XRP(1)), + Ter(tecNO_SPONSOR_PERMISSION)); + env.close(); + + BEAST_EXPECT(env.balance(sponsor2) == XRP(9999)); + + // to non-funded account / insufficient balance for reserve + env(pay(sponsor2, charlie, XRP(9999) - env.current()->fees().reserve + drops(1)), + Txflags(tfSponsorCreatedAccount), + Ter(tecUNFUNDED_PAYMENT)); + env.close(); + + // to non-funded account + auto const sponsor2BalanceBefore = env.balance(sponsor2); + env(pay(sponsor2, charlie, drops(1)), Txflags(tfSponsorCreatedAccount), Fee(XRP(1))); + env.close(); + + auto const charlieSle = env.le(keylet::account(charlie)); + BEAST_EXPECT(charlieSle->isFieldPresent(sfSponsor)); + BEAST_EXPECT(charlieSle->getAccountID(sfSponsor) == sponsor2.id()); + BEAST_EXPECT(sponsoredOwnerCount(env, charlie) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 1); + // verify sponsor balance decreased by payment + Fee + BEAST_EXPECT(env.balance(sponsor2) == sponsor2BalanceBefore - drops(1) - XRP(1)); + } + { + // insufficient reserve to sponsor acount + + auto const sendAmount = drops(1); + // 2 account reserve + send amount + auto const requireBalance = accountReserve(env, 2) + sendAmount; + adjustAccountXRPBalance(env, sponsor3, requireBalance - drops(1)); + env(pay(sponsor3, dave, sendAmount), + Txflags(tfSponsorCreatedAccount), + Fee(XRP(1)), + Ter(tecUNFUNDED_PAYMENT)); + env.close(); + + adjustAccountXRPBalance(env, sponsor3, requireBalance); + env(pay(sponsor3, dave, sendAmount), + Txflags(tfSponsorCreatedAccount), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + } + } + + void + testRequireFlag() + { + using namespace test::jtx; + { + testcase("SponsorshipRequireSignForReserve"); + + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // set flag + env(sponsor::set_reserve(sponsor, tfSponsorshipSetRequireSignForReserve, 10), + sponsor::SponseeAcc(alice)); + env.close(); + + env(check::create(alice, bob, XRP(100)), + Fee(XRP(10)), + sponsor::As(sponsor, spfSponsorReserve), + Ter(terNO_SPONSORSHIP)); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // clear flag + env(sponsor::set_reserve(sponsor, tfSponsorshipClearRequireSignForReserve, 1), + sponsor::SponseeAcc(alice)); + env.close(); + + // CheckCreate is re-applied + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + } + + { + testcase("SponsorshipRequireSignForFee"); + + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // set flag + env(sponsor::set_fee(sponsor, tfSponsorshipSetRequireSignForFee, XRP(10)), + sponsor::SponseeAcc(alice)); + env.close(); + + env(check::create(alice, bob, XRP(100)), + Fee(XRP(10)), + sponsor::As(sponsor, spfSponsorFee), + Ter(terNO_SPONSORSHIP)); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT( + env.le(keylet::sponsor(sponsor, alice))->getFieldAmount(sfFeeAmount) == XRP(10)); + + // clear flag + env(sponsor::set_fee(sponsor, tfSponsorshipClearRequireSignForFee, XRP(10)), + sponsor::SponseeAcc(alice)); + env.close(); + + // CheckCreate is re-applied + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(!env.le(keylet::sponsor(sponsor, alice))->isFieldPresent(sfFeeAmount)); + } + } + + void + testSponsorReserveSimple(bool cosigning) + { + testcase("SponsorReserveSimple"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + + env.fund(XRP(10000), alice, sponsor); + env.close(); + + // test Sufficient sponsor balance + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 99)); + + env(ticket::create(alice, 100), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + adjustAccountXRPBalance(env, sponsor, reserve(env, 100)); + + env(ticket::create(alice, 100), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor, 0, 250), sponsor::SponseeAcc(alice)); + env.close(); + + adjustAccountXRPBalance(env, sponsor, reserve(env, 99 + 1 /* sponsor object*/)); + + env(ticket::create(alice, 100), + sponsor::As(sponsor, spfSponsorReserve), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + adjustAccountXRPBalance(env, sponsor, reserve(env, 100 + 1 /* sponsor object*/)); + + env(ticket::create(alice, 100), + sponsor::As(sponsor, spfSponsorReserve), + Ter(tesSUCCESS)); + env.close(); + } + } + + // test helper for both cosigning and pre-funded sponsorship + template + void + testEachSponsorship( + test::jtx::Env& env, + bool cosigning, + jtx::Account const& sponsor, + jtx::Account const& sponsee, + uint32_t reserveCount, + uint32_t sponsorReserveCount, + TER insufficientReserveResult, + SubmitCallback callback, + std::optional> expected = std::nullopt) + { + using namespace test::jtx; + // auto const sponsorOwnerCountBefore = ownerCount(env, sponsor); + auto const sponseeOwnerCountBefore = ownerCount(env, sponsee); + auto const sponseeSponsoredOwnerCountBefore = sponsoredOwnerCount(env, sponsee); + auto const sponseeSponsoringOwnerCountBefore = sponsoringOwnerCount(env, sponsee); + auto const sponsorSponsoringOwnerCountBefore = sponsoringOwnerCount(env, sponsor); + + std::optional sponsorSig = + cosigning ? std::optional(Sig(sfSponsorSignature, sponsor)) : std::nullopt; + + auto const sponsorCurrentOwnerCount = ownerCount(env, sponsor) - + sponsoredOwnerCount(env, sponsor) + sponsoringOwnerCount(env, sponsor); + + auto submit = [&](TER ter) { + return [&, ter](json::Value const& jv, auto const&... fN) { + if (sponsorSig) + { + env(jv, fN..., sponsor::As(sponsor, spfSponsorReserve), *sponsorSig, Ter(ter)); + } + else + { + env(jv, fN..., sponsor::As(sponsor, spfSponsorReserve), Ter(ter)); + } + }; + }; + + // Insufficient Reserve + { + if (cosigning) + { + adjustAccountXRPBalance( + env, + sponsor, + reserve(env, sponsorCurrentOwnerCount + sponsorReserveCount) - drops(1)); + } + else + { + // cleanup previous sponsorship + if (env.le(keylet::sponsor(sponsor, sponsee))) + { + env(sponsor::del(sponsor), sponsor::SponseeAcc(sponsee)); + env.close(); + } + + if (sponsorReserveCount - 1 > 0) + { + env(sponsor::set(sponsor, 0, sponsorReserveCount - 1, XRP(1)), + sponsor::SponseeAcc(sponsee)); + } + else + { + // just create sponsor object + env(sponsor::set(sponsor, 0, std::nullopt, XRP(1)), + sponsor::SponseeAcc(sponsee)); + } + env.close(); + } + callback(env, submit(insufficientReserveResult)); + env.close(); + } + + // Success + { + if (cosigning) + { + adjustAccountXRPBalance( + env, sponsor, reserve(env, sponsorCurrentOwnerCount + sponsorReserveCount)); + } + else + { + // reset sponsorship + env(sponsor::del(sponsor), sponsor::SponseeAcc(sponsee)); + env(sponsor::set(sponsor, 0, sponsorReserveCount, XRP(1)), + sponsor::SponseeAcc(sponsee)); + env.close(); + } + callback(env, submit(tesSUCCESS)); + env.close(); + + if (!cosigning) + { + // cleanup sponsorship + env(sponsor::del(sponsor), sponsor::SponseeAcc(sponsee)); + env.close(); + } + } + + if (expected) + { + (*expected)(); + } + else + { + BEAST_EXPECT(ownerCount(env, sponsee) - sponseeOwnerCountBefore == reserveCount); + BEAST_EXPECT( + sponsoredOwnerCount(env, sponsee) - sponseeSponsoredOwnerCountBefore == + sponsorReserveCount); + BEAST_EXPECT( + sponsoringOwnerCount(env, sponsee) - sponseeSponsoringOwnerCountBefore == 0); + BEAST_EXPECT( + sponsoringOwnerCount(env, sponsor) - sponsorSponsoringOwnerCountBefore == + sponsorReserveCount); + } + }; + + void + testAMM(bool cosigning) + { + testcase("AMM"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const gw("gw"); + Account const sponsor("sponsor"); + + auto const usd = gw["usd"]; + auto const eur = gw["eur"]; + + auto const ammCreate = [&](Env& env, + Account const& account, + STAmount const& amount1, + STAmount const& amount2) { + json::Value jv; + jv[jss::TransactionType] = jss::AMMCreate; + jv[jss::Account] = account.human(); + jv[jss::Amount] = amount1.getJson(JsonOptions::Values::None); + jv[jss::Amount2] = amount2.getJson(JsonOptions::Values::None); + jv[jss::TradingFee] = 0; + jv[jss::Fee] = std::to_string(env.current()->fees().increment.drops()); + return jv; + }; + + auto const ammDeposit = [&](Env& env, + Account const& account, + STAmount const& amount1, + STAmount const& amount2) { + json::Value jv; + jv[jss::TransactionType] = jss::AMMDeposit; + jv[jss::Account] = account.human(); + jv[jss::Asset] = STIssue(sfAsset, amount1.asset()).getJson(JsonOptions::Values::None); + jv[jss::Asset2] = STIssue(sfAsset, amount2.asset()).getJson(JsonOptions::Values::None); + jv[jss::Amount] = amount1.value().getJson(JsonOptions::Values::None); + jv[jss::Amount2] = amount2.value().getJson(JsonOptions::Values::None); + jv[jss::Flags] = tfTwoAsset; + return jv; + }; + + { + // AMMCreate + // - sponsor LPToken + // - doesn't sponsor AMM object + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, gw, sponsor); + env.close(); + + env(trust(alice, usd(10000))); + env(trust(alice, eur(10000))); + env.close(); + + env(pay(gw, alice, usd(1000))); + env(pay(gw, alice, eur(1000))); + env.close(); + + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUF_RESERVE_LINE, + [&](Env& env, auto const& submit) { + submit(ammCreate(env, alice, usd(100), eur(100))); + }, + [&]() { + auto const amm = env.current()->read(keylet::amm(usd.issue(), eur.issue())); + auto const ammAccount = Account("amm", amm->getAccountID(sfAccount)); + BEAST_EXPECT(ownerCount(env, alice) == 3); // RippleState (usd,eur/LP Token) + BEAST_EXPECT(ownerCount(env, ammAccount) == 2); // usd, eur + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); // LPToken + BEAST_EXPECT(sponsoredOwnerCount(env, ammAccount) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); // LPToken + BEAST_EXPECT( + !env.le(keylet::amm(usd.issue(), eur.issue()))->isFieldPresent(sfSponsor)); + }); + + auto const ammKeylet = keylet::amm(usd.issue(), eur.issue()); + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipCreate, ammKeylet.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecNO_PERMISSION)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(alice)); + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(alice)); + env(sponsor::transfer(alice, tfSponsorshipCreate, ammKeylet.key), + sponsor::As(sponsor, spfSponsorReserve), + Ter(tecNO_PERMISSION)); + env.close(); + } + } + { + // AMMDeposit + // - sponsor new LPToken + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, gw, sponsor); + env.close(); + + env(trust(alice, usd(10000))); + env(trust(alice, eur(10000))); + env(trust(bob, usd(10000))); + env(trust(bob, eur(10000))); + env.close(); + + env(pay(gw, alice, usd(1000))); + env(pay(gw, alice, eur(1000))); + env(pay(gw, bob, usd(1000))); + env(pay(gw, bob, eur(1000))); + env.close(); + + env(ammCreate(env, alice, usd(100), eur(100))); + env.close(); + + BEAST_EXPECT(ownerCount(env, bob) == 2); // RippleState (usd,eur) + + testEachSponsorship( + env, + cosigning, + sponsor, + bob, + 1, + 1, + tecINSUF_RESERVE_LINE, + [&](Env& env, auto const& submit) { + submit(ammDeposit(env, bob, usd(100), eur(100))); + }); + } + { + // AMMDeposit single-asset XRP: reserve sponsor covers LP trustline reserve + // but depositor's own liquid XRP is insufficient for the deposit → tecUNFUNDED_AMM + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, gw, sponsor); + env.close(); + + env(trust(bob, usd(10000))); + env(trust(alice, usd(10000))); + env.close(); + env(pay(gw, bob, usd(1000))); + env.close(); + + AMM const amm(env, bob, XRP(1000), usd(100)); + + // alice has 1 owner object (usd trust line); give her reserve + 5 XRP liquid + adjustAccountXRPBalance(env, alice, reserve(env, ownerCount(env, alice)) + XRP(5)); + + auto const jv = AMM::depositJv( + {.account = alice, + .asset1In = XRP(10), + .assets = std::make_pair(Asset{xrpIssue()}, Asset{usd.issue()})}); + + if (cosigning) + { + env(jv, + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecINSUF_RESERVE_LINE)); + } + else + { + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + env(jv, sponsor::As(sponsor, spfSponsorReserve), Ter(tecINSUF_RESERVE_LINE)); + env(sponsor::del(sponsor), sponsor::SponseeAcc(alice)); + } + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); // no LP token was created + } + { + // AMMWithdraw + { + // Single Asset Withdraw + // - sponsor new RippleState + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, gw, sponsor); + env.close(); + + env(trust(alice, usd(10000))); + env(trust(alice, eur(10000))); + env.close(); + + env(pay(gw, alice, usd(1000))); + env(pay(gw, alice, eur(1000))); + env.close(); + + env(ammCreate(env, alice, usd(1000), eur(1000)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + env(trust(alice, usd(0))); + env(trust(alice, eur(0))); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); // LPToken + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); // LPToken + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); // LPToken + + json::Value jv; + jv[jss::TransactionType] = jss::AMMWithdraw; + jv[jss::Account] = alice.human(); + jv[jss::Asset] = STIssue(sfAsset, usd.issue()).getJson(JsonOptions::Values::None); + jv[jss::Asset2] = STIssue(sfAsset, eur.issue()).getJson(JsonOptions::Values::None); + jv[jss::Amount] = usd(100).value().getJson(JsonOptions::Values::None); + jv[jss::Flags] = tfSingleAsset; + + env(ticket::create(sponsor, 1)); // adjust for free + env.close(); + + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(jv); }); + } + { + // Double Asset Withdraw + // - sponsor new RippleState * 2 + // - remove sponsored LPToken + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, gw, sponsor); + env.close(); + + env(trust(alice, usd(10000))); + env(trust(alice, eur(10000))); + env.close(); + + env(pay(gw, alice, usd(1000))); + env(pay(gw, alice, eur(1000))); + env.close(); + + env(ammCreate(env, alice, usd(1000), eur(1000)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + env(trust(alice, usd(0))); + env(trust(alice, eur(0))); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); // LPToken + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + json::Value jv; + jv[jss::TransactionType] = jss::AMMWithdraw; + jv[jss::Account] = alice.human(); + jv[jss::Asset] = STIssue(sfAsset, usd.issue()).getJson(JsonOptions::Values::None); + jv[jss::Asset2] = STIssue(sfAsset, eur.issue()).getJson(JsonOptions::Values::None); + jv[jss::Flags] = tfWithdrawAll; + + env(ticket::create(sponsor, 1)); // adjust for free trustline + env.close(); + + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 2, + 2, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(jv); }, + [&]() { + // LPToken deleted, usd, eur created + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 2); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2); + }); + } + } + { + // AMMClawback + // - doesn't sponsor holder's new RippleState + // - remove sponsored LPToken + Account const gw2("gw2"); + auto const eur2 = gw2["eur"]; + + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, gw, gw2, sponsor); + env.close(); + + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + + env(trust(alice, usd(10000))); + env(trust(alice, eur2(10000))); + env.close(); + + env(pay(gw, alice, usd(100))); + env(pay(gw2, alice, eur2(100))); + env.close(); + + env(ammCreate(env, alice, usd(100), eur2(100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + env(trust(alice, usd(0))); + env(trust(alice, eur2(0))); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); // LPToken + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + { + // doesn't sponsor holder's new RippleState + env(amm::ammClawback(gw, alice, usd, eur2, usd(10)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 2); // LPToken, eur2 + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + } + { + // remove sponsored LPToken + env(amm::ammClawback(gw, alice, usd, eur2, std::nullopt)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); // eur2 + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + } + { + // AMMDelete + // - remove sponsored LPToken trustlines + Env env( + *this, + envconfig([](std::unique_ptr cfg) { + cfg->fees.referenceFee = XRPAmount(1); + return cfg; + }), + testableAmendments()); + env.fund(XRP(20'000), alice, gw, sponsor); + env.close(); + env(trust(alice, usd(10'000))); + env.close(); + env(pay(gw, alice, usd(10'000))); + env.close(); + + AMM amm(env, gw, XRP(10'000), usd(10'000)); + for (auto i = 0; i < (kMaxDeletableAmmTrustLines * 2) + 10; ++i) + { + Account const a{std::to_string(i)}; + env.fund(XRP(1'000), a); + if (cosigning) + { + env(trust(a, STAmount{amm.lptIssue(), 10'000}), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(a)); + env.close(); + env(trust(a, STAmount{amm.lptIssue(), 10'000}), + sponsor::As(sponsor, spfSponsorReserve)); + env.close(); + } + } + + BEAST_EXPECT( + sponsoringOwnerCount(env, sponsor) == ((kMaxDeletableAmmTrustLines * 2) + 10)); + + // The trustlines are partially deleted. + amm.withdrawAll(gw); + BEAST_EXPECT(amm.ammExists()); + + // AMMDelete has to be called twice to delete AMM. + amm.ammDelete(alice, Ter(tecINCOMPLETE)); + BEAST_EXPECT(amm.ammExists()); + + // Deletes remaining trustlines and deletes AMM. + amm.ammDelete(alice); + BEAST_EXPECT(!amm.ammExists()); + BEAST_EXPECT(!env.le(keylet::ownerDir(amm.ammAccount()))); + + BEAST_EXPECT( + !env.le(keylet::account(sponsor))->isFieldPresent(sfSponsoringAccountCount)); + } + } + + void + testCheck(bool cosigning) + { + testcase("Check"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const gw("gw"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + auto const usd = gw["usd"]; + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, sponsor, sponsor2); + env.close(); + + // CheckCreate -> Check = 0Cancel + + uint32_t seq = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq = env.seq(alice); + submit(check::create(alice, bob, XRP(1))); + }); + + BEAST_EXPECT(ownerCount(env, alice) == 1); // Check + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + auto const keylet = keylet::check(alice, seq); + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id()); + + if (cosigning) + { + // transfer sponsor + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + // transfer sponsor + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); // Check + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor2.id()); + + // CheckCancel + env(check::cancel(alice, keylet.key)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + // CheckCreate -> = 0 CheckCash + uint32_t seq2 = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq2 = env.seq(alice); + submit(check::create(alice, bob, XRP(1))); + }); + + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + + // CheckCash + auto const checkId2 = keylet::check(alice, seq2).key; + env(check::cash(bob, checkId2, XRP(1))); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + + // RippleState sponsor (CheckCashMakesTrustLine) + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, gw, sponsor, sponsor2); + env.close(); + + env.trust(usd(100), alice); + env.close(); + env(pay(gw, alice, usd(100))); + env.close(); + + // CheckCreat = 0e -> CheckCash + uint32_t seq2 = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq2 = env.seq(alice); + submit(check::create(alice, bob, usd(1))); + }); + + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + + auto const keylet = keylet::check(alice, seq2); + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id()); + + // CheckCash + testEachSponsorship( + env, + cosigning, + sponsor, + bob, + 1, + 1, + tecNO_LINE_INSUF_RESERVE, + [&](Env& env, auto const& submit) { submit(check::cash(bob, keylet.key, usd(1))); }, + [&]() { + BEAST_EXPECT(ownerCount(env, alice) == 1); // RippleState + BEAST_EXPECT(ownerCount(env, bob) == 1); // RippleState + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + }); + } + } + + void + testOffer(bool cosigning) + { + testcase("Offer"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const gw("gw"); + Account const sponsor1("sponsor1"); + Account const sponsor2("sponsor2"); + + auto usd = gw["usd"]; + auto eur = gw["eur"]; + + { + Env env{*this, testableAmendments()}; + + env.fund(XRP(10000), alice, gw, sponsor1, sponsor2); + env.close(); + + // OfferCreate + uint32_t seq = 0; + testEachSponsorship( + env, + cosigning, + sponsor1, + alice, + 1, + 1, + tecINSUF_RESERVE_OFFER, + [&](Env& env, auto const& submit) { + seq = env.seq(alice); + submit(offer(alice, usd(1), XRP(1))); + }); + + // transfer sponsor + auto const keylet = keylet::offer(alice, seq); + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor2.id()); + + // OfferCancel + env(offerCancel(alice, seq)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + { + Env env{*this, testableAmendments()}; + + env.fund(XRP(10000), alice, gw, sponsor1, sponsor2); + env.close(); + + // OfferCreate + uint32_t seq = 0; + testEachSponsorship( + env, + cosigning, + sponsor1, + alice, + 1, + 1, + tecINSUF_RESERVE_OFFER, + [&](Env& env, auto const& submit) { + seq = env.seq(alice); + submit(offer(alice, usd(1), XRP(1))); + }); + + // OfferCreate with Cancel (new sponsor) + auto const seq2 = env.seq(alice); + if (cosigning) + { + env(offer(alice, usd(1), XRP(1)), + Json(jss::OfferSequence, seq), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(offer(alice, usd(1), XRP(1)), + Json(jss::OfferSequence, seq), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + // OfferCreate with Cancel (no sponsor) + env(offer(alice, usd(1), XRP(1)), Json(jss::OfferSequence, seq2)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + // test Offer Execution doesn't sponsor new trustline + { + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, bob, gw, sponsor1, sponsor2); + env.close(); + + env(trust(alice, usd(100))); + env(trust(bob, eur(100))); + env.close(); + + env(pay(gw, alice, usd(100))); + env(pay(gw, bob, eur(100))); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 1); + + // OfferCreate + if (cosigning) + { + env(offer(alice, eur(1), usd(1)), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor1, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(offer(alice, eur(1), usd(1)), sponsor::As(sponsor1, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 1); + + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + + // OfferCreate (cross offer) + if (cosigning) + { + env(offer(bob, usd(1), eur(1)), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(bob)); + env.close(); + + env(offer(bob, usd(1), eur(1)), sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); + + // does not sponsor new trustline by cross offer + BEAST_EXPECT(ownerCount(env, bob) == 2); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + } + + void + testTicket(bool cosigning) + { + testcase("Ticket"); + using namespace test::jtx; + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, sponsor, sponsor2); + env.close(); + + // TicketCreate + uint32_t ticketSeq = 0; + + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 250, + 250, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + ticketSeq = env.seq(alice) + 1; + submit(ticket::create(alice, 250)); + }); + + auto const keylet = keylet::TicketT()(alice, ticketSeq); + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id()); + + // transfer sponsor + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 250); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 250); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 249); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor2.id()); + + // use a Ticket + env(noop(alice), ticket::Use(ticketSeq)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 249); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 249); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 249); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + } + + void + testCredentials(bool cosigning) + { + testcase("Credentials"); + using namespace test::jtx; + Account const issuer("issuer"); + Account const subject("subject"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + auto const credType = std::string("credType"); + auto const credTypeSlice = Slice(credType.data(), credType.size()); + + // CredentialsCreate + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), issuer, subject, sponsor, sponsor2); + env.close(); + + testEachSponsorship( + env, + cosigning, + sponsor, + issuer, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(credentials::create(subject, issuer, credType), credentials::Uri("uri")); + }); + + BEAST_EXPECT(ownerCount(env, subject) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, subject) == 0); + + // transfer sponsor + auto const keylet = keylet::credential(subject, issuer, credTypeSlice); + if (cosigning) + { + env(sponsor::transfer(issuer, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(issuer)); + env.close(); + + env(sponsor::transfer(issuer, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, subject) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, issuer) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, subject) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + // CredentialsAccept + testEachSponsorship( + env, + cosigning, + sponsor, + subject, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(credentials::accept(subject, issuer, credType)); + }); + + BEAST_EXPECT(ownerCount(env, issuer) == 0); + BEAST_EXPECT(ownerCount(env, subject) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, issuer) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, subject) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + + // transfer accepted credential + if (cosigning) + { + env(sponsor::transfer(subject, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(subject)); + env.close(); + + env(sponsor::transfer(subject, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + // CredentialsDelete + env(credentials::deleteCred(subject, subject, issuer, credType)); + env.close(); + + BEAST_EXPECT(ownerCount(env, issuer) == 0); + BEAST_EXPECT(ownerCount(env, subject) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, issuer) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, subject) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), issuer, subject, sponsor); + env.close(); + + // Accept Sponsored Credentials without sponsoring + testEachSponsorship( + env, + cosigning, + sponsor, + issuer, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(credentials::create(subject, issuer, credType)); + }); + + env(credentials::accept(subject, issuer, credType)); + env.close(); + + // sponsorship is removed + BEAST_EXPECT(ownerCount(env, issuer) == 0); + BEAST_EXPECT(ownerCount(env, subject) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, issuer) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, subject) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(!env.le(keylet::credential(subject, issuer, credTypeSlice)) + ->isFieldPresent(sfSponsor)); + + env(credentials::deleteCred(subject, subject, issuer, credType)); + env.close(); + } + } + + void + testDelegate(bool cosigning) + { + testcase("Delegate"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + // DelegateSet + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(delegate::set(alice, bob, {"Payment"})); + }); + + // transfer sponsor + auto const keylet = keylet::delegate(alice, bob); + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + // delete + env(delegate::set(alice, bob, {})); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + } + + void + testDepositPreauth(bool cosigning) + { + testcase("DepositPreauth"); + using namespace test::jtx; + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, sponsor, sponsor2); + env.close(); + + // DepositPreauthSet + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(deposit::auth(alice, sponsor)); }); + + // transfer sponsor + auto const keylet = keylet::depositPreauth(alice, sponsor); + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + // DepositPreauthDelete + env(deposit::unauth(alice, sponsor)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + } + + void + testDID(bool cosigning) + { + testcase("DID"); + using namespace test::jtx; + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, sponsor, sponsor2); + env.close(); + + // DIDSet + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(did::set(alice), did::Uri("uri")); }); + + // transfer sponsor + auto const keylet = keylet::did(alice); + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + // DIDDelete + env(did::del(alice)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + } + + void + testEscrow(bool cosigning) + { + testcase("Escrow"); + using namespace test::jtx; + using namespace std::chrono_literals; + + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + { + // Native Escrow + Env env{*this, testableAmendments()}; + auto const baseFee = env.current()->fees().base; + + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + // EscrowCreate + uint32_t seq = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq = env.seq(alice); + submit( + escrow::create(alice, bob, XRP(100)), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s)); + }); + BEAST_EXPECT( + env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id()); + + // transfer sponsor + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::escrow(alice, seq).key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::escrow(alice, seq).key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + BEAST_EXPECT( + env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor2.id()); + + // EscrowFinish + env(escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + Fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + Account const gw("gw"); + auto const usd = gw["usd"]; + { + // IOU Escrow + Env env{*this, testableAmendments()}; + auto const baseFee = env.current()->fees().base; + + env.fund(XRP(1000000), alice, bob, gw, sponsor, sponsor2); + env.close(); + + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + + env.trust(usd(1000000), alice); + env.close(); + env(pay(gw, alice, usd(10000))); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + + // EscrowCreate + uint32_t seq = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq = env.seq(alice); + submit( + escrow::create(alice, bob, usd(100)), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s)); + }); + + BEAST_EXPECT( + env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id()); + + // EscrowFinish + testEachSponsorship( + env, + cosigning, + sponsor2, + bob, + 1, + 1, + tecNO_LINE_INSUF_RESERVE, + [&](Env& env, auto const& submit) { + submit( + escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + Fee(baseFee * 150)); + }); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + BEAST_EXPECT( + env.le(keylet::line(bob, gw, usd.currency))->getAccountID(sfHighSponsor) == + sponsor2.id()); + } + { + // MPT Escrow + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), bob, sponsor); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const mpt = mptGw["MPT"]; + env(pay(gw, alice, mpt(10'000))); + env.close(); + + // create Escrow from alice to bob + auto const seq = env.seq(alice); + env(escrow::create(alice, bob, mpt(100)), + escrow::kCondition(escrow::kCb1), + escrow::kCancelTime(env.now() + 100s)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // finish Escrow + env(escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Fee(XRP(1))); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + } + } + + void + testMPToken(bool cosigning) + { + testcase("MPToken"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + // MPTokenIssuanceCreate + json::Value jv = {}; + jv[sfAccount] = alice.human(); + jv[sfTransactionType] = jss::MPTokenIssuanceCreate; + MPTID mptid; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + mptid = makeMptID(env.seq(alice), alice.id()); + submit(jv); + }); + + // transfer sponsor + auto const mptIssuanceKeylet = keylet::mptIssuance(mptid); + + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, mptIssuanceKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, mptIssuanceKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + // MPTokenAuthorize + jv = {}; + jv[sfTransactionType] = jss::MPTokenAuthorize; + jv[sfAccount] = bob.human(); + jv[sfMPTokenIssuanceID] = to_string(mptid); + + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); + env(ticket::create(sponsor, 2)); // adjust for free mptoken + env.close(); + } + + testEachSponsorship( + env, + cosigning, + sponsor, + bob, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(jv); }); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + + // transfer sponsor + auto const mptTokenKeylet = keylet::mptoken(mptid, bob); + if (cosigning) + { + env(sponsor::transfer(bob, tfSponsorshipReassign, mptTokenKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(bob)); + env.close(); + + env(sponsor::transfer(bob, tfSponsorshipReassign, mptTokenKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 2); + + // MPTokenAuthorize Unauthorize + jv = {}; + jv[sfTransactionType] = jss::MPTokenAuthorize; + jv[sfAccount] = bob.human(); + jv[sfMPTokenIssuanceID] = to_string(mptid); + jv[sfFlags] = tfMPTUnauthorize; + env(jv); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + // MPTokenIssuanceDestroy + jv = {}; + jv[sfTransactionType] = jss::MPTokenIssuanceDestroy; + jv[sfAccount] = alice.human(); + jv[sfMPTokenIssuanceID] = to_string(mptid); + env(jv); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + { + // check INSUFFICIENT_RESERVE for MPToken + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + // MPTokenAuthorize + json::Value jv = {}; + jv[sfAccount] = alice.human(); + jv[sfTransactionType] = jss::MPTokenIssuanceCreate; + auto const mptid = makeMptID(env.seq(alice), alice.id()); + env(jv); + env.close(); + + // for free mptoken checks + // adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); + std::uint32_t const ticketSeq{env.seq(sponsor) + 1}; + env(ticket::create(sponsor, 2)); + env.close(); + + // adjustAccountXRPBalance(env, sponsor, reserve(env, 3) - + // drops(1)); + jv = {}; + jv[sfTransactionType] = jss::MPTokenAuthorize; + jv[sfAccount] = bob.human(); + jv[sfMPTokenIssuanceID] = to_string(mptid); + // error (non-free mptoken) + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 3) - drops(1)); + env(jv, + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + else + { + env(sponsor::set(sponsor, 0, std::nullopt, XRP(1)), sponsor::SponseeAcc(bob)); + env.close(); + + env(jv, sponsor::As(sponsor, spfSponsorReserve), Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + + env(noop(sponsor), ticket::Use(ticketSeq)); + env.close(); + + // pass (free mptoken) + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 2) - drops(1)); + env(jv, + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(bob)); + env.close(); + env(jv, sponsor::As(sponsor, spfSponsorReserve), Ter(tesSUCCESS)); + env.close(); + } + } + } + + void + testNFToken(bool cosigning) + { + testcase("NFToken"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + { + Env env{*this, testableAmendments()}; + + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + // NFTokenMint + uint256 nftId; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + nftId = token::getNextID(env, alice, 0); + submit(token::mint(alice)); + }); + + // transfer sponsor + auto const keylet = keylet::nftpageMax(alice); + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + } + // NFTokenBurn + env(token::burn(alice, nftId)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + + // NFTokenMintOffer + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 2, + 2, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(token::mint(alice), token::Amount(XRP(100))); + }); + } + + { + // multiple nft page process + Env env{*this, testableAmendments()}; + + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + auto const nftCount = 200; + + // NFTokenMint + if (cosigning) + { + for (auto i = 0; i < nftCount; i++) + { + env(token::mint(alice), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + } + } + else + { + env(sponsor::set_reserve(sponsor, 0, 8), sponsor::SponseeAcc(alice)); + env.close(); + for (auto i = 0; i < nftCount; i++) + { + env(token::mint(alice), sponsor::As(sponsor, spfSponsorReserve)); + } + } + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == sponsoredOwnerCount(env, alice)); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == sponsoringOwnerCount(env, sponsor)); + + // NFTokenBurn + for (auto i = 0; i < nftCount; i++) + { + auto const nftId = token::getID(env, alice, 0, i, 0, 0); + env(token::burn(alice, nftId)); + } + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + } + + void + testNFTokenOffer(bool cosigning) + { + testcase("NFTokenOffer"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const broker("broker"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + auto const taxon = 0u; + + { + // Mint + CreateOffer + CancelOffer + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + // Mint + uint256 const nftId{token::getNextID(env, alice, taxon, tfTransferable)}; + env(token::mint(alice, taxon), Txflags(tfTransferable)); + env.close(); + + // NFTokenOfferCreate + uint256 offerIndex1; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + offerIndex1 = keylet::nftoffer(alice, env.seq(alice)).key; + submit( + token::createOffer(alice, nftId, XRP(1)), + token::Destination(bob), + Txflags(tfSellNFToken)); + }); + + uint256 offerIndex2; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + offerIndex2 = keylet::nftoffer(alice, env.seq(alice)).key; + submit( + token::createOffer(alice, nftId, XRP(1)), + token::Destination(bob), + Txflags(tfSellNFToken)); + }); + + // transfer sponsor + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, offerIndex1), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, offerIndex1), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 3); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 2); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + // NFTokenOfferCancel + env(token::cancelOffer(alice, {offerIndex1, offerIndex2})); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + { + // Mint + CreateSellOffer + AcceptSellOffer + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + // Mint + uint256 const nftId{token::getNextID(env, alice, taxon, tfTransferable)}; + env(token::mint(alice, taxon), Txflags(tfTransferable)); + env.close(); + + // NFTokenOfferCreate + uint256 offerIndex; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + offerIndex = keylet::nftoffer(alice, env.seq(alice)).key; + submit( + token::createOffer(alice, nftId, XRP(1)), + token::Destination(bob), + Txflags(tfSellNFToken)); + }); + + // NFTokenOfferAccept + env(token::acceptSellOffer(bob, offerIndex)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + + { + // Mint + CreateBuyOffer + AcceptBuyOffer + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + // Mint + uint256 const nftId{token::getNextID(env, alice, taxon, tfTransferable)}; + env(token::mint(alice, taxon), Txflags(tfTransferable)); + env.close(); + + // NFTokenOfferCreate + uint256 offerIndex; + testEachSponsorship( + env, + cosigning, + sponsor, + bob, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + offerIndex = keylet::nftoffer(bob, env.seq(bob)).key; + submit( + token::createOffer(bob, nftId, XRP(1)), + token::Owner(alice), + token::Destination(alice)); + }); + + // NFTokenOfferAccept + env(token::acceptBuyOffer(alice, offerIndex)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + { + // Broker + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, broker, sponsor, sponsor2); + env.close(); + + // Mint + uint256 const nftId{token::getNextID(env, alice, taxon, tfTransferable)}; + env(token::mint(alice, taxon), Txflags(tfTransferable)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + + // NFTokenOfferCreate (BuyOffer) + uint256 buyOfferIndex; + testEachSponsorship( + env, + cosigning, + sponsor, + bob, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + buyOfferIndex = keylet::nftoffer(bob, env.seq(bob)).key; + submit( + token::createOffer(bob, nftId, XRP(1)), + token::Owner(alice), + token::Destination(broker)); + }); + + // NFTokenOfferCreate (SellOffer) + uint256 sellOfferIndex; + testEachSponsorship( + env, + cosigning, + sponsor2, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + sellOfferIndex = keylet::nftoffer(alice, env.seq(alice)).key; + submit( + token::createOffer(alice, nftId, XRP(1)), + Txflags(tfSellNFToken), + token::Destination(broker)); + }); + + // NFTokenOfferAccept + env(token::brokerOffers(broker, buyOfferIndex, sellOfferIndex)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + } + + void + testPayChan(bool cosigning) + { + testcase("PayChan"); + using namespace test::jtx; + using namespace std::literals::chrono_literals; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + // PayChanCreate + auto const pk = alice.pk(); + auto const settleDelay = 10s; + uint256 chan; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + chan = paychan::channel(alice, bob, env.seq(alice)); + submit(paychan::create(alice, bob, XRP(100), settleDelay, pk)); + }); + + // transfer sponsor + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, chan), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipReassign, chan), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + env.close(env.now() + settleDelay); + // PayChanClaim (delete PayChan) + env(paychan::claim(bob, chan), Txflags(tfClose)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + } + + void + testPermissionedDomain(bool cosigning) + { + testcase("PermissionedDomain"); + using namespace test::jtx; + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, sponsor, sponsor2); + env.close(); + + // PermissionedDomainSet + pdomain::Credentials credentials{{.issuer = alice, .credType = "first credential"}}; + uint32_t seq = 0; + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + seq = env.seq(alice); + submit(pdomain::setTx(alice, credentials)); + }); + + // transfer sponsor + auto const keylet = keylet::permissionedDomain(alice, seq); + + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + // PermissionedDomainDelete + auto objects = pdomain::getObjects(alice, env); + auto const domain = objects.begin()->first; + env(pdomain::deleteTx(alice, domain)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + } + + void + testOracle(bool cosigning) + { + testcase("Oracle"); + using namespace test::jtx; + using namespace std::chrono; + using DataSeries = + std::vector>; + + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + auto const oracleSet = [](Env& env, Account const& account, uint8_t dataSeriesSize) { + auto const now = env.timeKeeper().now(); + env.close(now + oracle::kTestStartTime - kEpochOffset); + json::Value jv; + jv[jss::TransactionType] = jss::OracleSet; + jv[jss::Account] = to_string(account); + jv[jss::OracleDocumentID] = 1; + jv[jss::LastUpdateTime] = to_string( + duration_cast(env.current()->header().closeTime.time_since_epoch()) + .count() + + kEpochOffset.count() + 100); + jv[jss::PriceDataSeries] = json::ValueType::Array; + jv[jss::Provider] = strHex(std::string{"provider"}); + jv[jss::AssetClass] = strHex(std::string{"currency"}); + + DataSeries const series = { + {"XRP", "US1", 740, 1}, + {"XRP", "US2", 750, 1}, + {"XRP", "US3", 740, 1}, + {"XRP", "US4", 750, 1}, + {"XRP", "US5", 740, 1}, + {"XRP", "US6", 750, 1}, + {"XRP", "US7", 740, 1}, + {"XRP", "US8", 750, 1}, + {"XRP", "US9", 740, 1}, + {"XRP", "U10", 750, 1}, + }; + + DataSeries const actualSeries(series.begin(), series.begin() + dataSeriesSize); + + json::Value dataSeries(json::ValueType::Array); + for (auto const& data : actualSeries) + { + json::Value priceData; + json::Value price; + price[jss::BaseAsset] = std::get<0>(data); + price[jss::QuoteAsset] = std::get<1>(data); + price[jss::AssetPrice] = std::get<2>(data); + price[jss::Scale] = std::get<3>(data); + priceData[jss::PriceData] = price; + dataSeries.append(priceData); + } + jv[jss::PriceDataSeries] = dataSeries; + return jv; + }; + + auto const oracleDelete = [&](Account const& account) { + json::Value jv; + jv[jss::TransactionType] = jss::OracleDelete; + jv[jss::Account] = to_string(account); + jv[jss::OracleDocumentID] = 1; + return jv; + }; + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, sponsor, sponsor2); + env.close(); + + { + // OracleSet (reserve 1) + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(oracleSet(env, alice, 5)); }); + + // transfer sponsor + auto const keylet = keylet::oracle(alice, 1); + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + // OracleDelete + env(oracleDelete(alice)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + { + // OracleSet (reserve 2) + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 2, + 2, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(oracleSet(env, alice, 6)); }); + + // transfer sponsor + auto const keylet = keylet::oracle(alice, 1); + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 2), sponsor::SponseeAcc(alice)); + env.close(); + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 2); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 2); + + // OracleDelete + env(oracleDelete(alice)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + { + // OracleSet (reserve 1->2, sponsor1 -> no-sponsor) + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(oracleSet(env, alice, 5)); }); + + // reserve 1->2 + env(oracleSet(env, alice, 6)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // OracleDelete + env(oracleDelete(alice)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + { + // OracleSet (reserve 1->2, sponsor1 -> sponsor2) + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(oracleSet(env, alice, 5)); }); + // return; + + // reserve 1->2 + testEachSponsorship( + env, + cosigning, + sponsor2, + alice, + 1, + 2, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(oracleSet(env, alice, 6)); }, + [&]() { + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 2); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 2); + }); + + // OracleDelete + env(oracleDelete(alice)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + { + // OracleSet (reserve 1->2, non-sponsor -> sponsor1) + env(oracleSet(env, alice, 5)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + + // reserve 1->2 + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 2, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(oracleSet(env, alice, 6)); }); + + // OracleDelete + env(oracleDelete(alice)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + for (bool const isTwoOwnerCount : {false, true}) + { + // test sponsor transfer + auto const dataSeriesSize = isTwoOwnerCount ? 6 : 5; + auto const ocount = isTwoOwnerCount ? 2 : 1; + + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + ocount, + ocount, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(oracleSet(env, alice, dataSeriesSize)); + }); + + // transfer sponsor + if (cosigning) + { + env(sponsor::transfer( + alice, tfSponsorshipReassign, keylet::oracle(alice, 1).key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, ocount), sponsor::SponseeAcc(alice)); + env.close(); + env(sponsor::transfer( + alice, tfSponsorshipReassign, keylet::oracle(alice, 1).key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == ocount); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == ocount); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == ocount); + + // dissolve sponsor + env(sponsor::transfer(alice, tfSponsorshipEnd, keylet::oracle(alice, 1).key)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == ocount); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + + // remove sponsor + env(oracleDelete(alice)); + env.close(); + } + } + } + + void + testSignerList(bool cosigning) + { + testcase("SignerList"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + // SignerListSet + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(signers(alice, 1, {{bob, 1}})); }); + + // transfer sponsor + if (cosigning) + { + // invalid signer list owner 1 + // account doesn't have signer list but specified signer list exists + env(sponsor::transfer(bob, tfSponsorshipReassign, keylet::signers(alice).key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecNO_PERMISSION)); + // invalid signer list owner 2 + // account has signer list and specified signer list exists + env(signers(bob, 1, {{alice, 1}})); + env.close(); + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::signers(bob).key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecNO_PERMISSION)); + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::signers(alice).key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::signers(alice).key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + // Delete + env(signers(alice, NoneT())); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + void + testTrustSet(bool cosigning) + { + testcase("TrustSet"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const charlie("charlie"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + auto const validateSponsoredTrustline = + [&](std::shared_ptr const& sle, bool isIssuerHigh, Account const& sponsor) { + BEAST_EXPECT( + sle->getAccountID(isIssuerHigh ? sfLowSponsor : sfHighSponsor) == sponsor.id()); + BEAST_EXPECT(!sle->isFieldPresent(isIssuerHigh ? sfHighSponsor : sfLowSponsor)); + }; + + auto const& highAcc = alice > bob ? alice : bob; + auto const& lowAcc = alice > bob ? bob : alice; + + // create and delete + for (bool const isIssuerHigh : {false, true}) + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, charlie, sponsor, sponsor2); + env.close(); + + auto const& issuer = isIssuerHigh ? highAcc : lowAcc; + auto const& user = isIssuerHigh ? lowAcc : highAcc; + + auto const usd = issuer["usd"]; + auto const currency = usd.currency; + + // create TrustLine + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); + env(ticket::create(sponsor, 2)); // adjust for free trustline + env.close(); + } + + testEachSponsorship( + env, + cosigning, + sponsor, + user, + 1, + 1, + tecNO_LINE_INSUF_RESERVE, + [&](Env& env, auto const& submit) { submit(trust(user, usd(100))); }); + + auto const keylet = keylet::line(user, issuer, currency); + + if (cosigning) + { + // invalid owner + env(sponsor::transfer(charlie, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecNO_PERMISSION)); + // invalid reserve owner + env(sponsor::transfer(issuer, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecNO_PERMISSION)); + env(sponsor::transfer(user, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(user)); + env.close(); + env(sponsor::transfer(user, tfSponsorshipReassign, keylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + // delete TrustLine + env(trust(user, usd(0))); + env.close(); + + BEAST_EXPECT(ownerCount(env, user) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, user) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + BEAST_EXPECT(!env.le(keylet)); + } + + // update + for (bool const isIssuerHigh : {false, true}) + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + auto const& issuer = isIssuerHigh ? highAcc : lowAcc; + auto const& user = isIssuerHigh ? lowAcc : highAcc; + + auto const usd = issuer["usd"]; + auto const currency = usd.currency; + + // create TrustLine from issuer + env(trust(issuer, user["usd"](100))); + env.close(); + + BEAST_EXPECT(env.le(keylet::line(user, issuer, currency))); + + if (cosigning) + { + adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); + env(ticket::create(sponsor, 2)); // adjust for free trustline + env.close(); + } + + testEachSponsorship( + env, + cosigning, + sponsor, + user, + 1, + 1, + tecINSUF_RESERVE_LINE, + [&](Env& env, auto const& submit) { submit(trust(user, usd(100))); }); + + auto const line = env.le(keylet::line(user, issuer, currency)); + validateSponsoredTrustline(line, isIssuerHigh, sponsor); + + // update TrustLine from user to clear reserve + env(trust(user, usd(0))); + env.close(); + + BEAST_EXPECT(ownerCount(env, user) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, user) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(env.le(keylet::line(user, issuer, currency))); + + // remove TrustLine from issuer + env(trust(issuer, user["usd"](0))); + env.close(); + BEAST_EXPECT(!env.le(keylet::line(user, issuer, currency))); + } + + // both High and Low sponsored + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + // create TrustLines + env(trust(alice, bob["usd"](100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + env(trust(bob, alice["usd"](100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + auto sle = env.le(keylet::line(alice, bob, alice["usd"].currency)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->isFlag(lsfHighReserve)); + BEAST_EXPECT(sle->isFlag(lsfLowReserve)); + BEAST_EXPECT(sle->getAccountID(sfHighSponsor) == sponsor.id()); + BEAST_EXPECT(sle->getAccountID(sfLowSponsor) == sponsor.id()); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2); + + // clear TrustLines + env(trust(alice, bob["usd"](0))); + env.close(); + env(trust(bob, alice["usd"](0))); + env.close(); + + sle = env.le(keylet::line(alice, bob, alice["usd"].currency)); + BEAST_EXPECT(!sle); + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + } + + void + testVault(bool cosigning) + { + testcase("Vault"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const gw("gw"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + Asset asset = gw["IOU"].asset(); + + // VaultCreate + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, gw, sponsor); + env.close(); + + Vault const vault{env}; + auto [tx, keylet] = vault.create({.owner = alice, .asset = asset}); + + env(ticket::create(sponsor, 2)); + env.close(); + + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 3, // Vault, PseudoAccount, MPToken(Share Token) + 3, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + auto result = vault.create({.owner = alice, .asset = asset}); + submit(std::get<0>(result)); + keylet = std::get<1>(result); + }); + BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id()); + } + // VaultDeposit + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, gw, sponsor); + env.close(); + + Vault const vault{env}; + auto [tx, keylet] = vault.create({.owner = alice, .asset = asset}); + env(tx); + env.close(); + + env(trust(bob, asset(1000))); + env.close(); + env(pay(gw, bob, asset(1000))); + env.close(); + + BEAST_EXPECT(ownerCount(env, bob) == 1); // RippleState + + auto const depositTx = + vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(100)}); + + env(ticket::create(sponsor, 2)); // for free MPToken + env.close(); + + testEachSponsorship( + env, + cosigning, + sponsor, + bob, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(depositTx); }); + } + // VaultWithdraw + { + // RippleState Vault + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, gw, sponsor); + env.close(); + + Vault const vault{env}; + auto [tx, keylet] = vault.create({.owner = alice, .asset = asset}); + env(tx); + env.close(); + + env(trust(bob, asset(100))); + env.close(); + env(pay(gw, bob, asset(100))); + env.close(); + + auto const depositTx = + vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(100)}); + + env(ticket::create(sponsor, 2)); // for free MPToken + env.close(); + + testEachSponsorship( + env, + cosigning, + sponsor, + bob, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(depositTx); }); + + env(trust(bob, asset(0))); // remove trustline + env.close(); + + BEAST_EXPECT(ownerCount(env, bob) == 1); // MPToken(share) + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); // MPToken(share) + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); // MPToken(share) + + // create Trustline with vault withdraw + testEachSponsorship( + env, + cosigning, + sponsor, + bob, + 1, + 1, + tecNO_LINE_INSUF_RESERVE, + [&](Env& env, auto const& submit) { + submit(vault.withdraw( + {.depositor = bob, .id = keylet.key, .amount = asset(50)})); + }); + + BEAST_EXPECT(ownerCount(env, bob) == 2); // RippleState, MPToken(share) + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 2); // RippleState, MPToken(share) + BEAST_EXPECT( + sponsoringOwnerCount(env, sponsor) == 2); // RippleState, MPToken(share) + + // remove sponsored MPToken(share) + env(vault.withdraw({.depositor = bob, .id = keylet.key, .amount = asset(50)})); + env.close(); + + BEAST_EXPECT(ownerCount(env, bob) == 1); // RippleState + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); // RippleState + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); // RippleState + } + // MPToken Vault + { + // VaultWithdraw doesn't create MPToken for depositor + } + } + // VaultClawback + { + // remove sponsored shares MPToken + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, gw, sponsor); + env.close(); + + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + + Vault const vault{env}; + auto [tx, keylet] = vault.create({.owner = alice, .asset = asset}); + env(tx); + env.close(); + + env(trust(bob, asset(100))); + env.close(); + env(pay(gw, bob, asset(100))); + env.close(); + + auto const depositTx = + vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(100)}); + + env(ticket::create(sponsor, 2)); // for free MPToken + env.close(); + + testEachSponsorship( + env, + cosigning, + sponsor, + bob, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { submit(depositTx); }); + + BEAST_EXPECT(ownerCount(env, bob) == 2); // RippleState, MPToken(share) + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); // MPToken(share) + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); // MPToken(share) + + env(vault.clawback({.issuer = gw, .id = keylet.key, .holder = bob, .amount = asset(0)}), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + BEAST_EXPECT(ownerCount(env, bob) == 1); // RippleState + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + // VaultDelete + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, gw, sponsor); + env.close(); + + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + + Vault const vault{env}; + auto [tx, keylet] = vault.create({.owner = alice, .asset = asset}); + env(tx, sponsor::As(sponsor, spfSponsorReserve), Sig(sfSponsorSignature, sponsor)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 3); // Vault, PseudoAccount, MPToken(share) + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 3); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 3); + + env(vault.del({.owner = alice, .id = keylet.key})); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + } + + void + testXChain(bool cosigning) + { + testcase("XChain"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const doorA("doorA"); + Account const signer("signer"); + Account const sponsor("sponsor"); + + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, doorA); + env.close(); + + auto jvb = bridge(doorA, XRP, env.master, XRP); + + env(signers(doorA, 1, {signer})); + env.close(); + + // XChainCreateBridge + { + testEachSponsorship( + env, + cosigning, + sponsor, + doorA, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(bridgeCreate(doorA, jvb, XRP(1), XRP(1))); + }); + } + // XChainCreateClaimID + { + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(xchainCreateClaimId(alice, jvb, XRP(1), bob)); + }); + } + // XChainCommit + { + BEAST_EXPECT(ownerCount(env, alice) == 1); // XChainOwnedClaimID + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2); + + if (cosigning) + { + env(xchainCommit(alice, jvb, 1, XRP(100), bob), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(xchainCommit(alice, jvb, 1, XRP(100), bob), + sponsor::As(sponsor, spfSponsorReserve)); + env.close(); + + env(sponsor::del(sponsor), sponsor::SponseeAcc(alice)); + env.close(); + } + + // doesn't sponsor anything + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2); + } + // XChainAddClaimAttestation + { + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2); + + if (cosigning) + { + env(claimAttestation(alice, jvb, bob, XRP(1), bob, false, 1, bob, signer), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + env(claimAttestation(alice, jvb, bob, XRP(1), bob, false, 1, bob, signer), + sponsor::As(sponsor, spfSponsorReserve)); + env.close(); + + env(sponsor::del(sponsor), sponsor::SponseeAcc(alice)); + env.close(); + } + + // XChainOwnedClaimID deleted + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + } + // XChainClaim + { + // prepare for claim + { + env(xchainCreateClaimId(alice, jvb, XRP(1), bob), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env(xchainCommit(alice, jvb, 2, XRP(100))); // omit destination + env(claimAttestation( + alice, jvb, bob, XRP(100), bob, false, 2, std::nullopt, signer)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2); + + env(xchainClaim(alice, jvb, 2, XRP(100), bob)); + env.close(); + + // XChainOwnedClaimID deleted + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + } + // XChainCreateAccountClaimID + { + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, doorA) == 2); + BEAST_EXPECT(sponsoredOwnerCount(env, doorA) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + env(createAccountAttestation( + alice, jvb, alice, XRP(20), XRP(0), bob, false, 2, bob, signer), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + + // XChainCreateAccountClaimID not sponsored + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, doorA) == 3); + BEAST_EXPECT(sponsoredOwnerCount(env, doorA) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + } + } + + void + testLending(bool cosigning) + { + testcase("Lending"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const issuer("issuer"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + + // LoanBrokerSet / LoanBrokerDelete + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + PrettyAsset const asset{xrpIssue(), 1'000'000}; + + Vault const vault{env}; + auto const [tx, keylet] = vault.create({.owner = alice, .asset = asset}); + env(tx); + env.close(); + + BEAST_EXPECT( + ownerCount(env, alice) == 3); // Vault, PseudoAccount(Vault), MPToken(Vault) + + // LoanBrokerSet + testEachSponsorship( + // Both the Pseudo-account and LoanBroker objects are created, but only the + // LoanBroker is sponsored. + env, + cosigning, + sponsor, + alice, + 2, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(loanBroker::set(alice, keylet.key, 0)); + }); + + BEAST_EXPECT( + ownerCount(env, alice) == + 5); // LoanBroker, PseudoAccount(LB), (Vault, PseudoAccount(Vault), MPToken(Vault)) + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice) - 1); + + if (cosigning) + { + // transfer sponsor + env(sponsor::transfer(alice, tfSponsorshipReassign, brokerKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + // transfer sponsor + env(sponsor::transfer(alice, tfSponsorshipReassign, brokerKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT( + ownerCount(env, alice) == + 5); // LoanBroker, PseudoAccount(LB), (Vault, PseudoAccount(Vault), MPToken(Vault)) + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + // LoanBrokerDelete + env(loanBroker::del(alice, brokerKeylet.key, 0)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 3); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + // LoanBrokerConverDeposit/Withdraw/Clawback + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, issuer, sponsor); + env.close(); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + env.close(); + PrettyAsset const asset = mptt["MPT"]; + mptt.authorize({.account = alice}); + env.close(); + + env(pay(issuer, alice, asset(100))); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 1); + + Vault const vault{env}; + auto const [tx, keylet] = vault.create({.owner = alice, .asset = asset}); + env(tx); + env.close(); + + env(loanBroker::set(alice, keylet.key, 0)); + env.close(); + BEAST_EXPECT( + ownerCount(env, alice) == + 6); // LoanBroker, PseudoAccount(LB), (Vault, PseudoAccount(Vault), + // MPToken(Vault), MPToken(issuer)) + + auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice) - 1); + // LoanBrokerCoverDeposit + // doesn't sponsor anything + env(loanBroker::coverDeposit(alice, brokerKeylet.key, asset(100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 6); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // remove MPToken(issuer) + mptt.authorize({.account = alice, .flags = tfMPTUnauthorize}); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 5); + + env(ticket::create(sponsor, 2)); // for avoid free MPToken + env.close(); + + // LoanBrokerCoverWithdraw + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(loanBroker::coverWithdraw(alice, brokerKeylet.key, asset(10))); + }); + + BEAST_EXPECT(ownerCount(env, alice) == 6); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + // LoanBrokerCoverClawback + // doesn't sponsor anything + env(loanBroker::coverClawback(issuer), + loanBroker::kLoanBrokerId(brokerKeylet.key), + kAmount(asset(1)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 6); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + } + // LoanSet + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, issuer, sponsor, sponsor2); + env.close(); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + env.close(); + PrettyAsset const asset = mptt["MPT"]; + mptt.authorize({.account = alice}); + mptt.authorize({.account = bob}); + env.close(); + + env(pay(issuer, alice, asset(1000))); + env(pay(issuer, bob, asset(1000))); + env.close(); + + Vault const vault{env}; + auto const [tx, keylet] = vault.create({.owner = bob, .asset = asset}); + env(tx); + env.close(); + env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(100)})); + env.close(); + + auto const brokerKeylet = keylet::loanbroker(bob.id(), env.seq(bob)); + env(loanBroker::set(bob, keylet.key, 0)); + env.close(); + env(loanBroker::coverDeposit(bob, brokerKeylet.key, asset(100))); + env.close(); + + auto broker = env.le(brokerKeylet); + BEAST_EXPECT(broker->getFieldU32(sfOwnerCount) == 0); + BEAST_EXPECT(!broker->isFieldPresent(sfSponsoredOwnerCount)); + BEAST_EXPECT(!broker->isFieldPresent(sfSponsoringOwnerCount)); + + auto const loanSeq = broker->getFieldU32(sfLoanSequence); + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit( + loan::set(alice, brokerKeylet.key, 10), + Sig(sfCounterpartySignature, bob), + Fee(XRP(1))); + }); + broker = env.le(brokerKeylet); + // broker'object doesn't sponsored + BEAST_EXPECT(broker->getFieldU32(sfOwnerCount) == 1); + BEAST_EXPECT(!broker->isFieldPresent(sfSponsoredOwnerCount)); + BEAST_EXPECT(!broker->isFieldPresent(sfSponsoringOwnerCount)); + + auto const loanKeylet = keylet::loan(brokerKeylet.key, loanSeq); + + auto sponsorSle = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSle->getFieldU32(sfOwnerCount) == 0); + BEAST_EXPECT(!sponsorSle->isFieldPresent(sfSponsoredOwnerCount)); + BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringOwnerCount) == 1); + + // LoanManage + env(loan::manage(bob, loanKeylet.key, lsfLoanImpaired), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + // doesn't sponsor anything + sponsorSle = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSle->getFieldU32(sfOwnerCount) == 0); + BEAST_EXPECT(!sponsorSle->isFieldPresent(sfSponsoredOwnerCount)); + BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringOwnerCount) == 1); + + // LoanPay + env(loan::pay(alice, loanKeylet.key, asset(10)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + // doesn't sponsor anything + sponsorSle = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSle->getFieldU32(sfOwnerCount) == 0); + BEAST_EXPECT(!sponsorSle->isFieldPresent(sfSponsoredOwnerCount)); + BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringOwnerCount) == 1); + + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + + // before transfer + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + if (cosigning) + { + // transfer sponsor + env(sponsor::transfer(alice, tfSponsorshipReassign, loanKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + + // transfer sponsor + env(sponsor::transfer(alice, tfSponsorshipReassign, loanKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + // after transfer + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + + // LoanDelete + env(loan::del(alice, loanKeylet.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + // Sponsored ltLoan is deleted + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + // Sponsor for ltLoan object is deleted + sponsorSle = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSle->getFieldU32(sfOwnerCount) == 0); + BEAST_EXPECT(!sponsorSle->isFieldPresent(sfSponsoredOwnerCount)); + } + } + + void + testAccountDelete() + { + testcase("AccountDelete"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + + { + // A Sponsorship object blocks deletion of the sponsor, + // but NOT of the sponsee. + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + // set sponsor + env(sponsor::set(sponsor, 0, 100, XRP(100)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + auto const keylet = keylet::sponsor(sponsor, alice); + BEAST_EXPECT(env.le(keylet)); + // sponsor pays its own reserve here, so there is no SponsoredOwnerCount. + BEAST_EXPECT(ownerCount(env, sponsor) == 1); + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // sponsor's sequence is the higher one, so a single call readies + // both accounts for deletion + incLgrSeqForAccDel(env, sponsor); + auto const requiredFee = drops(env.current()->fees().increment); + + // sponsor cannot be deleted while the Sponsorship exists + env(acctdelete(sponsor, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS)); + env.close(); + + // sponsee can be deleted + auto const sponsorBalBefore = env.balance(sponsor); + env(acctdelete(alice, bob), Fee(requiredFee), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(!env.le(keylet)); + BEAST_EXPECT(!env.le(keylet::account(alice))); + // deleting sponsee makes those counts zero. + BEAST_EXPECT(ownerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // FeeAmount is returned to the sponsor + BEAST_EXPECT(env.balance(sponsor) == sponsorBalBefore + XRP(100)); + } + + { + // The SponsorshipSet transaction itself + // is sponsored by a 3rd counterparty. Test deleting sponsee. + Env env{*this, testableAmendments()}; + Account const counterparty("counterparty"); + env.fund(XRP(1000000), alice, bob, sponsor, counterparty); + env.close(); + + // sponsor creates a Sponsorship for alice; the SponsorshipSet tx is + // itself reserve-sponsored by counterparty, so the object's reserve is counterparty's. + env(sponsor::set(sponsor, 0, 100, XRP(100)), + sponsor::SponseeAcc(alice), + sponsor::As(counterparty, spfSponsorReserve), + Sig(sfSponsorSignature, counterparty), + Ter(tesSUCCESS)); + env.close(); + + auto const keylet = keylet::sponsor(sponsor, alice); + auto const obj = env.le(keylet); + BEAST_EXPECT(obj); + BEAST_EXPECT( + obj->isFieldPresent(sfSponsor) && + obj->getAccountID(sfSponsor) == counterparty.id()); + // sponsor owns the object; its reserve is sponsored by counterparty. + BEAST_EXPECT(ownerCount(env, sponsor) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, counterparty) == 1); + + auto const sponsorBalBefore = env.balance(sponsor); + incLgrSeqForAccDel(env, alice); + auto const requiredFee = drops(env.current()->fees().increment); + env(acctdelete(alice, bob), Fee(requiredFee), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(!env.le(keylet)); + // deleting sponsee makes those counts zero. + BEAST_EXPECT(ownerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, counterparty) == 0); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalBefore + XRP(100)); + } + + { + // Deleting SponsoredAccount, whose account reserve is paid by a sponsor. + Env env{*this, testableAmendments()}; + env.memoize(alice); + env.fund(XRP(1000000), bob, sponsor); + env.close(); + + // create SponsoredAccount + env(pay(sponsor, alice, XRP(10000)), Txflags(tfSponsorCreatedAccount)); + env.close(); + + incLgrSeqForAccDel(env, alice); + + // AccountDelete: destination = non-sponsor + auto const requiredFee = drops(env.current()->fees().increment); + env(acctdelete(alice, bob), Fee(requiredFee), Ter(tecNO_SPONSOR_PERMISSION)); + + auto const sponsorSle = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringAccountCount) == 1); + + incLgrSeqForAccDel(env, alice); + + // AccountDelete: destination = sponsor + env(acctdelete(alice, sponsor), Fee(requiredFee), Ter(tesSUCCESS)); + + auto const sponsorSle2 = env.le(keylet::account(sponsor)); + BEAST_EXPECT(!sponsorSle2->isFieldPresent(sfSponsoringAccountCount)); + } + + { + // Sponsor with sfSponsoringOwnerCount cannot delete (tecHAS_OBLIGATIONS) + Env env{*this, testableAmendments()}; + Account const gw("gw"); + env.fund(XRP(1000000), alice, bob, sponsor, gw); + env.close(); + + auto const usd = gw["usd"]; + + // Create sponsorship allowing reserve sponsoring + env(sponsor::set(sponsor, 0, 100, XRP(100)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + + // Create a trust line for alice + env(trust(alice, usd(1000))); + env.close(); + + // Transfer reserve sponsorship of trust line to sponsor + auto const trustId = keylet::line(alice, gw, usd.currency); + BEAST_EXPECT(env.le(trustId)); + + env(sponsor::transfer(alice, tfSponsorshipCreate, trustId.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + // Verify sfSponsoringOwnerCount is set on sponsor + auto const sponsorSle = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSle->isFieldPresent(sfSponsoringOwnerCount)); + BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringOwnerCount) >= 1); + + incLgrSeqForAccDel(env, sponsor); + + // AccountDelete should fail + auto const requiredFee = drops(env.current()->fees().increment); + env(acctdelete(sponsor, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS)); + } + + { + // Sponsor with sfSponsoringAccountCount cannot delete (tecHAS_OBLIGATIONS) + Env env{*this, testableAmendments()}; + env.memoize(alice); + env.fund(XRP(1000000), bob, sponsor); + env.close(); + + // Create SponsoredAccount (sets sfSponsoringAccountCount on sponsor) + env(pay(sponsor, alice, XRP(10000)), Txflags(tfSponsorCreatedAccount)); + env.close(); + + // Verify sfSponsoringAccountCount is set on sponsor + auto const sponsorSle = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSle->isFieldPresent(sfSponsoringAccountCount)); + BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringAccountCount) == 1); + + incLgrSeqForAccDel(env, sponsor); + + // AccountDelete should fail + auto const requiredFee = drops(env.current()->fees().increment); + env(acctdelete(sponsor, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS)); + } + } + + void + testDelegatePermission() + { + testcase("DelegatePermission"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + + // + // SponsorshipTransfer + // + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, carol); + env.close(); + + auto const seq = env.seq(alice); + env(check::create(alice, bob, XRP(1))); + env.close(); + + auto const keylet = keylet::check(alice, seq); + + env(sponsor::transfer(alice, tfSponsorshipCreate, keylet.key), + sponsor::As(bob, spfSponsorReserve), + Sig(sfSponsorSignature, bob), + delegate::As(carol), + Ter(terNO_DELEGATE_PERMISSION)); + + env(delegate::set(alice, carol, {"SponsorshipTransfer"})); + env.close(); + + env(sponsor::transfer(alice, tfSponsorshipCreate, keylet.key), + sponsor::As(bob, spfSponsorReserve), + Sig(sfSponsorSignature, bob), + delegate::As(carol), + Ter(tesSUCCESS)); + env.close(); + } + // + // SponsorshipSet + // + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, carol); + env.close(); + + env(sponsor::set(alice, 0, 100, XRP(100)), + sponsor::SponseeAcc(bob), + delegate::As(carol), + Ter(terNO_DELEGATE_PERMISSION)); + + env(delegate::set(alice, carol, {"SponsorshipSet"})); + env.close(); + + env(sponsor::set(alice, 0, 100, XRP(100)), + sponsor::SponseeAcc(bob), + delegate::As(carol), + Ter(tesSUCCESS)); + env.close(); + } + + // + // Permission SponsorFee + // + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, carol); + env.close(); + auto const testFeePermission = [&](TER result) { + // FeeAmount + env(sponsor::set(alice, 0, std::nullopt, XRP(100)), + sponsor::SponseeAcc(bob), + delegate::As(carol), + Ter(result)); + // MaxFee + env(sponsor::set(alice, 0, std::nullopt, std::nullopt, XRP(100)), + sponsor::SponseeAcc(bob), + delegate::As(carol), + Ter(result)); + // SetRequireSignForFee flag + env(sponsor::set(alice, tfSponsorshipSetRequireSignForFee), + sponsor::SponseeAcc(bob), + delegate::As(carol), + Ter(result)); + // ClearRequireSignForFee flag + env(sponsor::set(alice, tfSponsorshipClearRequireSignForFee), + sponsor::SponseeAcc(bob), + delegate::As(carol), + Ter(result)); + env.close(); + }; + + // no delegated + testFeePermission(terNO_DELEGATE_PERMISSION); + + // set non-SponsorFee Permission + env(delegate::set(alice, carol, {"SponsorReserve"})); + env.close(); + + testFeePermission(terNO_DELEGATE_PERMISSION); + + // set SponsorFee Permission + env(delegate::set(alice, carol, {"SponsorFee"})); + env.close(); + + testFeePermission(tesSUCCESS); + + // test with SponsorReserve (should failed) + env(sponsor::set(alice, 0, 100, XRP(100)), + sponsor::SponseeAcc(bob), + delegate::As(carol), + Ter(terNO_DELEGATE_PERMISSION)); + } + + // + // Permission SponsorReserve + // + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, carol); + env.close(); + + auto const testReservePermission = [&](TER result) { + // ReserveCount + env(sponsor::set(alice, 0, 100), + sponsor::SponseeAcc(bob), + delegate::As(carol), + Ter(result)); + // SetRequireSignForReserve flag + env(sponsor::set(alice, tfSponsorshipSetRequireSignForReserve), + sponsor::SponseeAcc(bob), + delegate::As(carol), + Ter(result)); + // ClearRequireSignForReserve flag + env(sponsor::set(alice, tfSponsorshipClearRequireSignForReserve), + sponsor::SponseeAcc(bob), + delegate::As(carol), + Ter(result)); + env.close(); + }; + + // no delegated + testReservePermission(terNO_DELEGATE_PERMISSION); + + // set non-SponsorReserve Permission + env(delegate::set(alice, carol, {"SponsorFee"})); + env.close(); + + testReservePermission(terNO_DELEGATE_PERMISSION); + + // set SponsorReserve Permission + env(delegate::set(alice, carol, {"SponsorReserve"})); + env.close(); + + testReservePermission(tesSUCCESS); + + // test with SponsorFee (should failed) + env(sponsor::set(alice, 0, 100, XRP(100)), + sponsor::SponseeAcc(bob), + delegate::As(carol), + Ter(terNO_DELEGATE_PERMISSION)); + } + } + + void + testBatch() + { + testcase("Batch"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + + // + // outer transaction + // + { + // test outer transaction with co-signing sponsor + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor); + env.close(); + + auto const seq = env.seq(alice); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(noop(alice), seq + 1), + batch::Inner(ticket::create(alice, 1), seq + 2), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + + // does not affect reserve + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // Fee is paid by sponsor + BEAST_EXPECT(env.balance(alice) == XRP(1000)); + BEAST_EXPECT(env.balance(sponsor) == XRP(1000 - 1)); + } + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor); + env.close(); + + // spfSponsorReserve on outer Batch is rejected + for (auto const flags : {spfSponsorReserve | spfSponsorFee, spfSponsorReserve}) + { + auto const seq = env.seq(alice); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(noop(alice), seq + 1), + batch::Inner(noop(alice), seq + 2), + sponsor::As(sponsor, flags), + Sig(sfSponsorSignature, sponsor), + Ter(temINVALID_FLAG)); + env.close(); + } + } + { + // test outer transaction with prefunded sponsor + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob); + env.fund(XRP(1001), sponsor); + env.close(); + + env(sponsor::set(sponsor, 0, 100, XRP(100)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + auto const seq = env.seq(alice); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(noop(alice), seq + 1), + batch::Inner(ticket::create(alice, 1), seq + 2), + sponsor::As(sponsor, spfSponsorFee), + Ter(tesSUCCESS)); + env.close(); + + // does not affect reserve + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // Fee is paid by sponsor object + BEAST_EXPECT(env.balance(alice) == XRP(1000)); + BEAST_EXPECT(env.balance(sponsor) == XRP(900)); + + auto const sponsorshipSle = env.le(keylet::sponsor(sponsor, alice)); + BEAST_EXPECT(sponsorshipSle); + BEAST_EXPECT(sponsorshipSle->at(sfFeeAmount) == XRP(100 - 1)); + BEAST_EXPECT(sponsorshipSle->at(sfReserveCount) == 100); + } + // + // Inner transaction + // + { + // test invalid Inner transaction with co-signing sponsor + Account const signerAccount("signer"); + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor, signerAccount); + env.close(); + + env(signers(sponsor, 1, {Signer(signerAccount, 1)})); + env.close(); + + { + auto jt = env.jtnofill( + noop(alice), + sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), + Sig(sfSponsorSignature, sponsor)); + jt.jv.removeMember(sfTxnSignature.jsonName); + + auto const seq = env.seq(alice); + // should fail because Inner transaction cannot include SponsorSignature with + // TxnSignature + BEAST_EXPECT(jt.jv[sfSponsorSignature.jsonName].isMember(sfTxnSignature.jsonName)); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(jt.jv, seq + 1), + batch::Inner(ticket::create(alice, 1), seq + 2), + Ter(temBAD_SIGNATURE)); + } + + { + auto jt = env.jtnofill( + noop(alice), + sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), + Msig(sfSponsorSignature, sponsor, signerAccount)); + jt.jv.removeMember(sfTxnSignature.jsonName); + + auto const seq = env.seq(alice); + // should fail because Inner transaction cannot include SponsorSignature with + // Signers + BEAST_EXPECT(jt.jv[sfSponsorSignature.jsonName].isMember(sfSigners.jsonName)); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(jt.jv, seq + 1), + batch::Inner(ticket::create(alice, 1), seq + 2), + Ter(temBAD_SIGNER)); + } + + { + auto jt = env.jtnofill( + noop(alice), + sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), + Sig(sfSponsorSignature, sponsor)); + jt.jv.removeMember(sfTxnSignature.jsonName); + jt.jv[sfSponsorSignature.jsonName].removeMember(sfTxnSignature.jsonName); + jt.jv[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = ""; + + auto const seq = env.seq(alice); + // should fail BatchSigners does have signer for SponsorSignature + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(jt.jv, seq + 1), + batch::Inner(ticket::create(alice, 1), seq + 2), + Ter(temBAD_SIGNER)); + } + } + + { + // test Inner transaction with prefunded sponsor + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob); + env.fund(XRP(1001), sponsor); + env.close(); + + env(sponsor::set(sponsor, 0, 100, XRP(100)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(env.balance(sponsor) == XRP(900)); + + auto jt = env.jtnofill( + ticket::create(alice, 1), sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee)); + // remove txn signature since it is filled by env.jtnofill() + jt.jv.removeMember(jss::TxnSignature); + + auto const seq = env.seq(alice); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(noop(alice), seq + 1), + batch::Inner(jt.jv, seq + 2), + Ter(tesSUCCESS)); + env.close(); + + // affect sponsor reserve + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + // Fee is paid by outer transaction originator (alice) + BEAST_EXPECT(env.balance(alice) == XRP(999)); + BEAST_EXPECT(env.balance(sponsor) == XRP(900)); + + // reserve count is decreased + auto const sponsorshipSle = env.le(keylet::sponsor(sponsor, alice)); + BEAST_EXPECT(sponsorshipSle); + BEAST_EXPECT(sponsorshipSle->at(sfFeeAmount) == XRP(100)); + BEAST_EXPECT(sponsorshipSle->at(sfReserveCount) == 99); + } + + { + // test Inner transaction with co-signing sponsor + Env env{*this, testableAmendments()}; + env.fund(XRP(1000), alice, bob, sponsor); + env.close(); + + auto jt = env.jtnofill( + ticket::create(alice, 1), + sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), + Sig(sfSponsorSignature, sponsor)); + // remove txn signature since it is filled by env.jtnofill() + jt.jv.removeMember(sfTxnSignature.jsonName); + jt.jv[sfSponsorSignature.jsonName].removeMember(sfTxnSignature.jsonName); + jt.jv[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = ""; + + auto const seq = env.seq(alice); + env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), + batch::Inner(noop(alice), seq + 1), + batch::Inner(jt.jv, seq + 2), + batch::Sig(sponsor), + Ter(tesSUCCESS)); + env.close(); + + // affect sponsor reserve + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + // Fee is paid by outer transaction originator (alice) + BEAST_EXPECT(env.balance(alice) == XRP(999)); + BEAST_EXPECT(env.balance(sponsor) == XRP(1000)); + } + } + + void + testSponsorReserve(bool cosigning) + { + testRequireFlag(); + testSponsorReserveSimple(cosigning); + testAMM(cosigning); + testCheck(cosigning); + testOffer(cosigning); + testTicket(cosigning); + testCredentials(cosigning); + testDelegate(cosigning); + testDepositPreauth(cosigning); + testDID(cosigning); + testEscrow(cosigning); + testMPToken(cosigning); + testNFToken(cosigning); + testNFTokenOffer(cosigning); + testPayChan(cosigning); + testPermissionedDomain(cosigning); + testOracle(cosigning); + testSignerList(cosigning); + testTrustSet(cosigning); + testVault(cosigning); + testXChain(cosigning); + testLending(cosigning); + } + +protected: + void + testSponsor() + { + testDisabled(); + testInvalidSponsorshipSet(); + testPseudoAccountSponsorship(); + + testSingleSigning(); + testMultiSigning(); + + testInvalidSponsorField(); + + testSimpleSponsorshipSet(); + + testPreFundAndCosign(); + + testTransferSponsor(); + testSponsorFee(); + testSponsorAccount(); + + testAccountDelete(); + + testDelegatePermission(); + testBatch(); + } + + void + testTxSponsor(bool cosigning) + { + testSponsorReserve(cosigning); + } + +public: + void + run() override + { + testSponsor(); + } +}; + +class SponsorTxCosigning_test : public Sponsor_test +{ + void + run() override + { + testTxSponsor(true); + } +}; + +class SponsorTxPrefunded_test : public Sponsor_test +{ + void + run() override + { + testTxSponsor(false); + } +}; + +BEAST_DEFINE_TESTSUITE(Sponsor, app, xrpl); +BEAST_DEFINE_TESTSUITE(SponsorTxCosigning, app, xrpl); +BEAST_DEFINE_TESTSUITE(SponsorTxPrefunded, app, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/Ticket_test.cpp b/src/test/app/Ticket_test.cpp index 626cdbe44d1..c236971a9a9 100644 --- a/src/test/app/Ticket_test.cpp +++ b/src/test/app/Ticket_test.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -503,7 +504,7 @@ class Ticket_test : public beast::unit_test::Suite Account const alice{"alice"}; // Fund alice not quite enough to make the reserve for a Ticket. - env.fund(env.current()->fees().accountReserve(1) - drops(1), alice); + env.fund(baseAccountReserve(*env.current(), 1) - drops(1), alice); env.close(); env(ticket::create(alice, 1), Ter(tecINSUFFICIENT_RESERVE)); @@ -511,7 +512,7 @@ class Ticket_test : public beast::unit_test::Suite env.require(Owners(alice, 0), tickets(alice, 0)); // Give alice enough to exactly meet the reserve for one Ticket. - env(pay(env.master, alice, env.current()->fees().accountReserve(1) - env.balance(alice))); + env(pay(env.master, alice, baseAccountReserve(*env.current(), 1) - env.balance(alice))); env.close(); env(ticket::create(alice, 1)); @@ -524,7 +525,7 @@ class Ticket_test : public beast::unit_test::Suite env( pay(env.master, alice, - env.current()->fees().accountReserve(250) - drops(1) - env.balance(alice))); + baseAccountReserve(*env.current(), 250) - drops(1) - env.balance(alice))); env.close(); // alice doesn't quite have the reserve for a total of 250 @@ -535,7 +536,7 @@ class Ticket_test : public beast::unit_test::Suite // Give alice enough so she can make the reserve for all 250 // Tickets. - env(pay(env.master, alice, env.current()->fees().accountReserve(250) - env.balance(alice))); + env(pay(env.master, alice, baseAccountReserve(*env.current(), 250) - env.balance(alice))); env.close(); std::uint32_t const ticketSeq{env.seq(alice) + 1}; diff --git a/src/test/app/TrustSet_test.cpp b/src/test/app/TrustSet_test.cpp index e9f05538400..e4bf6cb0786 100644 --- a/src/test/app/TrustSet_test.cpp +++ b/src/test/app/TrustSet_test.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -191,7 +192,7 @@ class TrustSet_test : public beast::unit_test::Suite auto const txFee = env.current()->fees().base; auto const baseReserve = env.current()->fees().reserve; - auto const threelineReserve = env.current()->fees().accountReserve(3); + auto const threelineReserve = baseAccountReserve(*env.current(), 3); env.fund(XRP(10000), gwA, gwB, assistor); diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 2c83ad91ecc..2f43add2c80 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -1877,7 +1877,7 @@ class Vault_test : public beast::unit_test::Suite auto const [acctReserve, incReserve] = [this]() -> std::pair { Env const env{*this, testableAmendments()}; return { - env.current()->fees().accountReserve(0).drops() / kDropsPerXrp.drops(), + baseAccountReserve(*env.current(), 0).drops() / kDropsPerXrp.drops(), env.current()->fees().increment.drops() / kDropsPerXrp.drops()}; }(); @@ -3153,7 +3153,7 @@ class Vault_test : public beast::unit_test::Suite auto const [acctReserve, incReserve] = [this]() -> std::pair { Env const env{*this, testableAmendments()}; return { - env.current()->fees().accountReserve(0).drops() / kDropsPerXrp.drops(), + baseAccountReserve(*env.current(), 0).drops() / kDropsPerXrp.drops(), env.current()->fees().increment.drops() / kDropsPerXrp.drops()}; }(); diff --git a/src/test/app/XChain_test.cpp b/src/test/app/XChain_test.cpp index de80444f2ef..75e0f0e3eff 100644 --- a/src/test/app/XChain_test.cpp +++ b/src/test/app/XChain_test.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -141,7 +142,7 @@ struct SEnv XRPAmount reserve(std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return baseAccountReserve(*env.current(), count); } XRPAmount @@ -371,7 +372,7 @@ struct XChain_test : public beast::unit_test::Suite, public jtx::XChainBridgeObj XRPAmount reserve(std::uint32_t count) { - return XEnv(*this).env.current()->fees().accountReserve(count); + return baseAccountReserve(*XEnv(*this).env.current(), count); } XRPAmount diff --git a/src/test/jtx.h b/src/test/jtx.h index d4b88b0b9ef..c416842fb0e 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -48,6 +49,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 3d813d993ca..ec4614fbf76 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -528,6 +528,24 @@ class Env [[nodiscard]] std::uint32_t ownerCount(Account const& account) const; + /** Return the number of sponsored objects owned by an account. + * Returns 0 if the account does not exist. + */ + [[nodiscard]] std::uint32_t + sponsoredOwnerCount(Account const& account) const; + + /** Return the number of sponsoring objects owned by an account. + * Returns 0 if the account does not exist. + */ + [[nodiscard]] std::uint32_t + sponsoringOwnerCount(Account const& account) const; + + /** Return the number of sponsoring accounts owned by an account. + * Returns 0 if the account does not exist. + */ + [[nodiscard]] std::uint32_t + sponsoringAccountCount(Account const& account) const; + /** Return an account root. @return empty if the account does not exist. */ diff --git a/src/test/jtx/JTx.h b/src/test/jtx/JTx.h index 121e6cc8251..7a2cda4b005 100644 --- a/src/test/jtx/JTx.h +++ b/src/test/jtx/JTx.h @@ -34,7 +34,7 @@ struct JTx // Functions that sign the transaction from the Account std::vector> mainSigners; // Functions that sign something else after the mainSigners, such as - // sfCounterpartySignature + // sfCounterpartySignature and sfSponsorSignature std::vector> postSigners; JTx() = default; diff --git a/src/test/jtx/Oracle.h b/src/test/jtx/Oracle.h index cf091c6d13e..56630ecf3b1 100644 --- a/src/test/jtx/Oracle.h +++ b/src/test/jtx/Oracle.h @@ -1,6 +1,9 @@ #pragma once -#include +#include +#include +#include +#include #include diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 27c54d830be..a23bf80a523 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -345,6 +345,18 @@ checkArraySize(json::Value const& val, unsigned int size); std::uint32_t ownerCount(test::jtx::Env const& env, test::jtx::Account const& account); +// Helper function that returns the sponsored owner count on an account. +std::uint32_t +sponsoredOwnerCount(test::jtx::Env const& env, test::jtx::Account const& account); + +// Helper function that returns the sponsoring owner count on an account. +std::uint32_t +sponsoringOwnerCount(test::jtx::Env const& env, test::jtx::Account const& account); + +// Helper function that returns the sponsoring account count on an account. +std::uint32_t +sponsoringAccountCount(test::jtx::Env const& env, test::jtx::Account const& account); + [[nodiscard]] inline bool checkVL(Slice const& result, std::string const& expected) diff --git a/src/test/jtx/impl/AMMTest.cpp b/src/test/jtx/impl/AMMTest.cpp index 46c5e44bd30..6b9a075735f 100644 --- a/src/test/jtx/impl/AMMTest.cpp +++ b/src/test/jtx/impl/AMMTest.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -197,7 +198,7 @@ AMMTestBase::testAMM(std::function const& cb, TestAM XRPAmount AMMTest::reserve(jtx::Env& env, std::uint32_t count) { - return env.current()->fees().accountReserve(count); + return baseAccountReserve(*env.current(), count); } XRPAmount diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index 707e1338a74..000fe9bfb09 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -271,6 +271,33 @@ Env::ownerCount(Account const& account) const return sle->getFieldU32(sfOwnerCount); } +std::uint32_t +Env::sponsoredOwnerCount(Account const& account) const +{ + auto const sle = le(account); + if (!sle) + Throw("missing account root"); + return sle->getFieldU32(sfSponsoredOwnerCount); +} + +std::uint32_t +Env::sponsoringOwnerCount(Account const& account) const +{ + auto const sle = le(account); + if (!sle) + Throw("missing account root"); + return sle->getFieldU32(sfSponsoringOwnerCount); +} + +std::uint32_t +Env::sponsoringAccountCount(Account const& account) const +{ + auto const sle = le(account); + if (!sle) + Throw("missing account root"); + return sle->getFieldU32(sfSponsoringAccountCount); +} + std::uint32_t Env::seq(Account const& account) const { diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index a8ec899c8a2..981fc5c0d51 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -94,6 +94,24 @@ ownerCount(Env const& env, Account const& account) return env.ownerCount(account); } +std::uint32_t +sponsoredOwnerCount(Env const& env, Account const& account) +{ + return env.sponsoredOwnerCount(account); +} + +std::uint32_t +sponsoringOwnerCount(Env const& env, Account const& account) +{ + return env.sponsoringOwnerCount(account); +} + +std::uint32_t +sponsoringAccountCount(Env const& env, Account const& account) +{ + return env.sponsoringAccountCount(account); +} + /* Path finding */ /******************************************************************************/ void diff --git a/src/test/jtx/impl/multisign.cpp b/src/test/jtx/impl/multisign.cpp index 66c5a4f27d5..d948042bda4 100644 --- a/src/test/jtx/impl/multisign.cpp +++ b/src/test/jtx/impl/multisign.cpp @@ -64,7 +64,8 @@ Msig::operator()(Env& env, JTx& jt) const { auto const mySigners = signers; auto callback = [subField = subField, mySigners, &env](Env&, JTx& jtx) { - // Where to put the signature. Supports sfCounterPartySignature. + // Where to put the signature. Supports sfCounterPartySignature and + // sfSponsorSignature. auto& sigObject = subField ? jtx[*subField] : jtx.jv; // The signing pub key is only required at the top level. diff --git a/src/test/jtx/impl/owners.cpp b/src/test/jtx/impl/owners.cpp index 2ff93757f09..ed10ca69309 100644 --- a/src/test/jtx/impl/owners.cpp +++ b/src/test/jtx/impl/owners.cpp @@ -44,6 +44,24 @@ Owners::operator()(Env& env) const env.test.expect(env.le(account_)->getFieldU32(sfOwnerCount) == value_); } +void +SponsoredOwners::operator()(Env& env) const +{ + env.test.expect(env.le(account_)->getFieldU32(sfSponsoredOwnerCount) == value_); +} + +void +SponsoringOwners::operator()(Env& env) const +{ + env.test.expect(env.le(account_)->getFieldU32(sfSponsoringOwnerCount) == value_); +} + +void +SponsoringAccountCount::operator()(Env& env) const +{ + env.test.expect(env.le(account_)->getFieldU32(sfSponsoringAccountCount) == value_); +} + } // namespace test::jtx } // namespace xrpl diff --git a/src/test/jtx/impl/sig.cpp b/src/test/jtx/impl/sig.cpp index 7140fad3de1..e0123073b1c 100644 --- a/src/test/jtx/impl/sig.cpp +++ b/src/test/jtx/impl/sig.cpp @@ -18,7 +18,7 @@ Sig::operator()(Env&, JTx& jt) const // VFALCO Inefficient pre-C++14 auto const account = *account_; auto callback = [subField = subField_, account](Env&, JTx& jtx) { - // Where to put the signature. Supports sfCounterPartySignature. + // Where to put the signature. Supports sfCounterPartySignature and sfSponsorSignature. auto& sigObject = subField ? jtx[*subField] : jtx.jv; jtx::sign(jtx.jv, account, sigObject); diff --git a/src/test/jtx/impl/sponsor.cpp b/src/test/jtx/impl/sponsor.cpp new file mode 100644 index 00000000000..5593e1b78cb --- /dev/null +++ b/src/test/jtx/impl/sponsor.cpp @@ -0,0 +1,130 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::test::jtx::sponsor { + +json::Value +set(jtx::Account const& account, + uint32_t flags, + std::optional reserveCount, + std::optional feeAmount, + std::optional maxFee) +{ + json::Value jv; + jv[jss::TransactionType] = jss::SponsorshipSet; + jv[jss::Account] = account.human(); + jv[sfFlags.jsonName] = flags; + if (reserveCount) + jv[sfReserveCount.jsonName] = *reserveCount; + if (feeAmount) + jv[sfFeeAmount.jsonName] = feeAmount->getJson(JsonOptions::Values::None); + if (maxFee) + jv[sfMaxFee.jsonName] = maxFee->getJson(JsonOptions::Values::None); + return jv; +} + +json::Value +set_fee( + jtx::Account const& account, + uint32_t flags, + STAmount feeAmount, + std::optional maxFee) +{ + json::Value jv; + jv[jss::TransactionType] = jss::SponsorshipSet; + jv[jss::Account] = account.human(); + jv[sfFlags.jsonName] = flags; + jv[sfFeeAmount.jsonName] = feeAmount.getJson(JsonOptions::Values::None); + if (maxFee) + jv[sfMaxFee.jsonName] = maxFee->getJson(JsonOptions::Values::None); + return jv; +} + +json::Value +set_reserve(jtx::Account const& account, uint32_t flags, uint32_t reserveCount) +{ + json::Value jv; + jv[jss::TransactionType] = jss::SponsorshipSet; + jv[jss::Account] = account.human(); + jv[sfFlags.jsonName] = flags; + jv[sfReserveCount.jsonName] = reserveCount; + return jv; +} + +json::Value +set_max_fee(jtx::Account const& account, uint32_t flags, STAmount maxFee) +{ + json::Value jv; + jv[jss::TransactionType] = jss::SponsorshipSet; + jv[jss::Account] = account.human(); + jv[sfFlags.jsonName] = flags; + jv[sfMaxFee.jsonName] = maxFee.getJson(JsonOptions::Values::None); + return jv; +} + +json::Value +del(jtx::Account const& account) +{ + json::Value jv; + jv[jss::TransactionType] = jss::SponsorshipSet; + jv[jss::Account] = account.human(); + jv[sfFlags.jsonName] = tfDeleteObject; + return jv; +} + +json::Value +transfer(jtx::Account const& account, uint32_t flags, std::optional const& index) +{ + json::Value jv; + jv[jss::TransactionType] = jss::SponsorshipTransfer; + jv[jss::Account] = account.human(); + jv[sfFlags.jsonName] = flags; + if (index) + jv[sfObjectID.jsonName] = to_string(*index); + return jv; +} + +void +CounterpartySponsor::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfCounterpartySponsor.jsonName] = sponsor_.human(); +} + +void +SponseeAcc::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfSponsee.jsonName] = sponsee_.human(); +} + +void +As::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfSponsor.jsonName] = sponsor_.human(); + jt.jv[sfSponsorFlags.jsonName] = flags_; +} + +json::Value +ledgerEntry(jtx::Env& env, jtx::Account const& sponsor, jtx::Account const& sponsee) +{ + json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::sponsorship][jss::sponsor] = sponsor.human(); + jvParams[jss::sponsorship][jss::sponsee] = sponsee.human(); + return env.rpc("json", "ledger_entry", to_string(jvParams)); +} + +} // namespace xrpl::test::jtx::sponsor diff --git a/src/test/jtx/owners.h b/src/test/jtx/owners.h index 44fd3762c20..2aedf2d741b 100644 --- a/src/test/jtx/owners.h +++ b/src/test/jtx/owners.h @@ -63,6 +63,57 @@ class Owners operator()(Env& env) const; }; +/** Match the number of items in the account's owner directory */ +class SponsoredOwners +{ +private: + Account account_; + std::uint32_t value_; + +public: + SponsoredOwners(Account account, std::uint32_t value) + : account_(std::move(account)), value_(value) + { + } + + void + operator()(Env& env) const; +}; + +/** Match the number of items in the account's owner directory */ +class SponsoringOwners +{ +private: + Account account_; + std::uint32_t value_; + +public: + SponsoringOwners(Account account, std::uint32_t value) + : account_(std::move(account)), value_(value) + { + } + + void + operator()(Env& env) const; +}; + +/** Match the number of items in the account's owner directory */ +class SponsoringAccountCount +{ +private: + Account account_; + std::uint32_t value_; + +public: + SponsoringAccountCount(Account account, std::uint32_t value) + : account_(std::move(account)), value_(value) + { + } + + void + operator()(Env& env) const; +}; + /** Match the number of trust lines in the account's owner directory */ using lines = OwnerCount; diff --git a/src/test/jtx/sponsor.h b/src/test/jtx/sponsor.h new file mode 100644 index 00000000000..570fe431e68 --- /dev/null +++ b/src/test/jtx/sponsor.h @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include + +#include + +namespace xrpl::test::jtx::sponsor { + +json::Value +set(jtx::Account const& account, + std::uint32_t flags, + std::optional reserveCount = std::nullopt, + std::optional feeAmount = std::nullopt, + std::optional maxFee = std::nullopt); + +json::Value +set_fee( + jtx::Account const& account, + std::uint32_t flags, + STAmount feeAmount, + std::optional maxFee = std::nullopt); + +json::Value +set_reserve(jtx::Account const& account, std::uint32_t flags, std::uint32_t reserveCount); + +json::Value +set_max_fee(jtx::Account const& account, std::uint32_t flags, STAmount maxFee); + +json::Value +del(jtx::Account const& account); + +json::Value +transfer( + jtx::Account const& account, + uint32_t flags, + std::optional const& index = std::nullopt); + +struct CounterpartySponsor +{ +private: + jtx::Account sponsor_; + +public: + CounterpartySponsor(jtx::Account account) : sponsor_(std::move(account)) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const; +}; + +struct SponseeAcc +{ +private: + jtx::Account sponsee_; + +public: + SponseeAcc(jtx::Account account) : sponsee_(std::move(account)) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const; +}; + +struct As +{ +private: + jtx::Account sponsor_; + std::uint32_t flags_; + +public: + As(jtx::Account account, std::uint32_t flags = 0) : sponsor_(std::move(account)), flags_(flags) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const; +}; + +json::Value +ledgerEntry(jtx::Env& env, jtx::Account const& sponsor, jtx::Account const& sponsee); + +} // namespace xrpl::test::jtx::sponsor diff --git a/src/test/ledger/PaymentSandbox_test.cpp b/src/test/ledger/PaymentSandbox_test.cpp index f59b75091a9..a89d07849b0 100644 --- a/src/test/ledger/PaymentSandbox_test.cpp +++ b/src/test/ledger/PaymentSandbox_test.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -330,7 +331,7 @@ class PaymentSandbox_test : public beast::unit_test::Suite }; auto reserve = [](jtx::Env& env, std::uint32_t count) -> XRPAmount { - return env.current()->fees().accountReserve(count); + return baseAccountReserve(*env.current(), count); }; Env env(*this, features); diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index 31b20b37d4c..83a7122b545 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -4,13 +4,17 @@ #include #include #include +#include #include #include #include // IWYU pragma: keep #include #include +#include +#include #include #include +#include #include #include @@ -22,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -605,6 +610,7 @@ class AccountObjects_test : public beast::unit_test::Suite BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::amm), 0)); BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::did), 0)); BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::permissioned_domain), 0)); + BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::sponsorship), 0)); // we expect invalid field type reported for the following types BEAST_EXPECT(acctObjsTypeIsInvalid(acctObjs(gw, jss::amendments))); @@ -926,6 +932,30 @@ class AccountObjects_test : public beast::unit_test::Suite BEAST_EXPECT(ticket[sfTicketSequence.jsonName].asUInt() == seq + 1); } + { + // Create a sponsorship + env(sponsor::set(alice, tfSponsorshipSetRequireSignForFee, 200, XRP(100), drops(10)), + sponsor::SponseeAcc(gw)); + env.close(); + + // Find the sponsorship. + for (auto const& acct : {alice, gw}) + { + json::Value const resp = acctObjs(acct, jss::sponsorship); + BEAST_EXPECT(acctObjsIsSize(resp, 1)); + + auto const& sponsorship = resp[jss::result][jss::account_objects][0u]; + + BEAST_EXPECT(sponsorship[sfOwner.jsonName] == alice.human()); + BEAST_EXPECT(sponsorship[sfSponsee.jsonName] == gw.human()); + BEAST_EXPECT( + sponsorship[sfFlags.jsonName].asUInt() == tfSponsorshipSetRequireSignForFee); + BEAST_EXPECT(sponsorship[sfReserveCount.jsonName].asUInt() == 200); + BEAST_EXPECT(sponsorship[sfFeeAmount.jsonName].asUInt() == 100000000); + BEAST_EXPECT(sponsorship[sfMaxFee.jsonName].asUInt() == 10); + } + } + { // See how "deletion_blockers_only" handles gw's directory. json::Value params; @@ -940,7 +970,8 @@ class AccountObjects_test : public beast::unit_test::Suite jss::NFTokenPage.cStr(), jss::RippleState.cStr(), jss::PayChannel.cStr(), - jss::PermissionedDomain.cStr()}; + jss::PermissionedDomain.cStr(), + jss::Sponsorship.cStr()}; std::ranges::sort(v); return v; }(); @@ -1350,6 +1381,137 @@ class AccountObjects_test : public beast::unit_test::Suite } } + void + testSponsoredFilter() + { + testcase("SponsoredFilter"); + using namespace jtx; + + Env env(*this, testableAmendments()); + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor1("sponsor1"); + Account const gw("gw"); + auto const usd = gw["USD"]; + + env.fund(XRP(10000), alice, bob, sponsor1, gw); + env.close(); + + // Helper to call account_objects with sponsored filter + auto acctObjsSponsored = [&env]( + AccountID const& acct, + bool sponsored, + std::optional const& type = std::nullopt) { + json::Value params; + params[jss::account] = to_string(acct); + params[jss::sponsored] = sponsored; + if (type) + params[jss::type] = *type; + params[jss::ledger_index] = "validated"; + return env.rpc("json", "account_objects", to_string(params)); + }; + + // Create a sponsorship (alice sponsors bob) + env(sponsor::set(alice, 0, 100, XRP(100)), sponsor::SponseeAcc(bob), Fee(XRP(1))); + env.close(); + + // Create a trust line for bob (not sponsored) + env(trust(bob, usd(1000))); + env.close(); + + // sponsored=true should not find any objects for bob (doesn't have any sponsored objects) + { + auto const resp = acctObjsSponsored(bob.id(), true); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + + // Now sponsor bob's trust line + auto const trustId = keylet::line(bob, gw, usd.currency); + BEAST_EXPECT(env.le(trustId)); + + env(sponsor::transfer(bob, tfSponsorshipCreate, trustId.key), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1)); + env.close(); + + // Verify trust line has sponsor field + { + auto const sle = env.le(trustId); + BEAST_EXPECT(sle->isFieldPresent(sfHighSponsor) || sle->isFieldPresent(sfLowSponsor)); + } + + // sponsored=true on bob should include the sponsored trust line + { + auto const resp = acctObjsSponsored(bob.id(), true); + auto const& objs = resp[jss::result][jss::account_objects]; + bool foundTrustLine = false; + BEAST_EXPECT(objs.size() == 1); + for (auto const& obj : objs) + { + if (obj[sfLedgerEntryType.jsonName] == jss::RippleState) + { + BEAST_EXPECT( + obj.isMember(sfHighSponsor.jsonName) || + obj.isMember(sfLowSponsor.jsonName)); + foundTrustLine = true; + } + } + BEAST_EXPECT(foundTrustLine); + } + + // sponsored=false on bob should NOT include the sponsored trust line + { + auto const resp = acctObjsSponsored(bob.id(), false); + auto const& objs = resp[jss::result][jss::account_objects]; + bool foundSponsoredTrustLine = false; + for (auto const& obj : objs) + { + if (obj[sfLedgerEntryType.jsonName] == jss::RippleState) + { + if (obj.isMember(sfHighSponsor.jsonName) || obj.isMember(sfLowSponsor.jsonName)) + foundSponsoredTrustLine = true; + } + } + BEAST_EXPECT(!foundSponsoredTrustLine); + } + + // NFT page sponsored filter + { + // Mint an NFT for bob (creates NFT page) + env(token::mint(bob, 0)); + env.close(); + + auto const nftPageKeylet = keylet::nftpageMax(bob); + BEAST_EXPECT(env.le(nftPageKeylet)); + + // Sponsor the NFT page + env(sponsor::transfer(bob, tfSponsorshipCreate, nftPageKeylet.key), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1)); + env.close(); + + // Verify NFT page has sponsor field + BEAST_EXPECT(env.le(nftPageKeylet)->isFieldPresent(sfSponsor)); + + // sponsored=true should include the sponsored NFT page + // sponsored=false should NOT include the sponsored NFT page + for (auto const sponsored : {true, false}) + { + auto const resp = acctObjsSponsored(bob.id(), sponsored); + auto const& objs = resp[jss::result][jss::account_objects]; + bool foundNFTPage = false; + for (auto const& obj : objs) + { + if (obj[sfLedgerEntryType.jsonName] == jss::NFTokenPage && + obj.isMember(sfSponsor.jsonName)) + foundNFTPage = true; + } + BEAST_EXPECT(foundNFTPage == sponsored); + } + } + } + void run() override { @@ -1360,6 +1522,7 @@ class AccountObjects_test : public beast::unit_test::Suite testNFTsMarker(); testAccountNFTs(); testAccountObjectMarker(); + testSponsoredFilter(); } }; diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp index 421f6bd1fad..8f41bb230e5 100644 --- a/src/test/rpc/AccountTx_test.cpp +++ b/src/test/rpc/AccountTx_test.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -774,7 +776,7 @@ class AccountTx_test : public beast::unit_test::Suite // All it takes is a large enough XRP payment to resurrect // becky's account. Try too small a payment. - env(pay(alice, becky, drops(env.current()->fees().accountReserve(0)) - drops(1)), + env(pay(alice, becky, drops(baseAccountReserve(*env.current(), 0)) - drops(1)), Ter(tecNO_DST_INSUF_XRP)); env.close(); @@ -889,6 +891,83 @@ class AccountTx_test : public beast::unit_test::Suite checkAliceAcctTx(9, jss::Payment); } + void + testSponsorship() + { + // test all sponsored transactions are in sponsor and sponsee's account + // tx list + testcase("Sponsorship"); + + using namespace test::jtx; + Env env(*this); + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + env.fund(XRP(10000), alice, sponsor, sponsor2); + env.close(); + + // check the latest sponsorship-related txn is in account tx list + auto const checkTx = [&](Account const& account, json::StaticString txType) { + json::Value params; + params[jss::account] = account.human(); + params[jss::limit] = 100; + auto const jv = env.rpc("json", "account_tx", to_string(params))[jss::result]; + + auto const& tx0(jv[jss::transactions][0u][jss::tx]); + BEAST_EXPECT(tx0[jss::TransactionType] == txType); + + std::string const txHash{ + env.tx()->getJson(JsonOptions::Values::None)[jss::hash].asString()}; + BEAST_EXPECT(tx0[jss::hash] == txHash); + }; + + // fee sponsorship + env(noop(alice), sponsor::As(sponsor, spfSponsorFee), Sig(sfSponsorSignature, sponsor)); + env.close(); + checkTx(alice, jss::AccountSet); + checkTx(sponsor, jss::AccountSet); + + // set sponsor + env(sponsor::set(sponsor, 0, 100, XRP(100)), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + checkTx(alice, jss::SponsorshipSet); + checkTx(sponsor, jss::SponsorshipSet); + + // create a ticket with sponsor + auto const seq = env.seq(alice); + env(ticket::create(alice, 1), sponsor::As(sponsor, spfSponsorReserve)); + env.close(); + checkTx(alice, jss::TicketCreate); + checkTx(sponsor, jss::TicketCreate); + + // transfer object sponsorship + env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::TicketT()(alice, seq + 1).key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + checkTx(alice, jss::SponsorshipTransfer); + checkTx(sponsor, jss::SponsorshipTransfer); + checkTx(sponsor2, jss::SponsorshipTransfer); + + // use a ticket + env(noop(alice), + ticket::Use(seq + 1), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor)); + env.close(); + checkTx(alice, jss::AccountSet); + checkTx(sponsor, jss::AccountSet); + checkTx(sponsor2, jss::AccountSet); + + // account sponsorship + env(sponsor::transfer(alice, tfSponsorshipCreate), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + checkTx(alice, jss::SponsorshipTransfer); + checkTx(sponsor, jss::SponsorshipTransfer); + } + public: void run() override @@ -897,6 +976,7 @@ class AccountTx_test : public beast::unit_test::Suite testContents(); testAccountDelete(); testMPT(); + testSponsorship(); } }; BEAST_DEFINE_TESTSUITE(AccountTx, rpc, xrpl); diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index 5ea79c39961..ed090245a39 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -547,6 +547,74 @@ class Simulate_test : public beast::unit_test::Suite // test without autofill testTx(env, tx, validateOutput); } + + { + // autofill sponsor signature + + auto validateOutput = [&](json::Value const& resp, json::Value const& tx) { + auto result = resp[jss::result]; + checkBasicReturnValidity(result, tx, 2, env.current()->fees().base); + + BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS"); + BEAST_EXPECT(result[jss::engine_result_code] == 0); + BEAST_EXPECT( + result[jss::engine_result_message] == + "The simulated transaction would have been applied."); + + if (BEAST_EXPECT(result.isMember(jss::meta) || result.isMember(jss::meta_blob))) + { + json::Value const metadata = getJsonMetadata(result); + + if (BEAST_EXPECT(metadata.isMember(sfAffectedNodes.jsonName))) + { + BEAST_EXPECT(metadata[sfAffectedNodes.jsonName].size() == 2); + + auto node = metadata[sfAffectedNodes.jsonName][0u]; + if (BEAST_EXPECT(node.isMember(sfModifiedNode.jsonName))) + { + auto modifiedNode = node[sfModifiedNode]; + BEAST_EXPECT(modifiedNode[sfLedgerEntryType] == "AccountRoot"); + auto previousFields = modifiedNode[sfPreviousFields]; + BEAST_EXPECT(!previousFields.isMember(sfBalance.jsonName)); + } + + auto node2 = metadata[sfAffectedNodes.jsonName][1u]; + if (BEAST_EXPECT(node2.isMember(sfModifiedNode.jsonName))) + { + auto modifiedNode = node2[sfModifiedNode]; + BEAST_EXPECT(modifiedNode[sfLedgerEntryType] == "AccountRoot"); + + auto previousFields = modifiedNode[sfPreviousFields]; + BEAST_EXPECT(previousFields.isMember(sfBalance.jsonName)); + } + } + BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0); + BEAST_EXPECT(metadata[sfTransactionResult.jsonName] == "tesSUCCESS"); + } + }; + + Account const sponsor("sponsor"); + env.fund(XRP(10000), sponsor); + env.close(); + + json::Value tx; + + tx[jss::Account] = env.master.human(); + tx[jss::TransactionType] = jss::AccountSet; + tx[sfDomain.jsonName] = kNewDomain; + tx[sfSponsor.jsonName] = sponsor.human(); + tx[sfSponsorFlags.jsonName] = spfSponsorFee; + tx[sfSponsorSignature.jsonName] = json::ValueType::Object; + + // test with autofill + testTx(env, tx, validateOutput); + + tx[sfSponsorSignature.jsonName][sfTxnSignature.jsonName] = ""; + tx[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = ""; + + // test without autofill + testTx(env, tx, validateOutput); + } } void diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/AccountRootTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/AccountRootTests.cpp index e967b614b70..4da3c83b07c 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/AccountRootTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/AccountRootTests.cpp @@ -43,6 +43,9 @@ TEST(AccountRootTests, BuilderSettersRoundTrip) auto const aMMIDValue = canonical_UINT256(); auto const vaultIDValue = canonical_UINT256(); auto const loanBrokerIDValue = canonical_UINT256(); + auto const sponsoredOwnerCountValue = canonical_UINT32(); + auto const sponsoringOwnerCountValue = canonical_UINT32(); + auto const sponsoringAccountCountValue = canonical_UINT32(); AccountRootBuilder builder{ accountValue, @@ -70,6 +73,9 @@ TEST(AccountRootTests, BuilderSettersRoundTrip) builder.setAMMID(aMMIDValue); builder.setVaultID(vaultIDValue); builder.setLoanBrokerID(loanBrokerIDValue); + builder.setSponsoredOwnerCount(sponsoredOwnerCountValue); + builder.setSponsoringOwnerCount(sponsoringOwnerCountValue); + builder.setSponsoringAccountCount(sponsoringAccountCountValue); builder.setLedgerIndex(index); builder.setFlags(0x1u); @@ -252,6 +258,30 @@ TEST(AccountRootTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasLoanBrokerID()); } + { + auto const& expected = sponsoredOwnerCountValue; + auto const actualOpt = entry.getSponsoredOwnerCount(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfSponsoredOwnerCount"); + EXPECT_TRUE(entry.hasSponsoredOwnerCount()); + } + + { + auto const& expected = sponsoringOwnerCountValue; + auto const actualOpt = entry.getSponsoringOwnerCount(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfSponsoringOwnerCount"); + EXPECT_TRUE(entry.hasSponsoringOwnerCount()); + } + + { + auto const& expected = sponsoringAccountCountValue; + auto const actualOpt = entry.getSponsoringAccountCount(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfSponsoringAccountCount"); + EXPECT_TRUE(entry.hasSponsoringAccountCount()); + } + EXPECT_TRUE(entry.hasLedgerIndex()); auto const ledgerIndex = entry.getLedgerIndex(); ASSERT_TRUE(ledgerIndex.has_value()); @@ -288,6 +318,9 @@ TEST(AccountRootTests, BuilderFromSleRoundTrip) auto const aMMIDValue = canonical_UINT256(); auto const vaultIDValue = canonical_UINT256(); auto const loanBrokerIDValue = canonical_UINT256(); + auto const sponsoredOwnerCountValue = canonical_UINT32(); + auto const sponsoringOwnerCountValue = canonical_UINT32(); + auto const sponsoringAccountCountValue = canonical_UINT32(); auto sle = std::make_shared(AccountRoot::entryType, index); @@ -314,6 +347,9 @@ TEST(AccountRootTests, BuilderFromSleRoundTrip) sle->at(sfAMMID) = aMMIDValue; sle->at(sfVaultID) = vaultIDValue; sle->at(sfLoanBrokerID) = loanBrokerIDValue; + sle->at(sfSponsoredOwnerCount) = sponsoredOwnerCountValue; + sle->at(sfSponsoringOwnerCount) = sponsoringOwnerCountValue; + sle->at(sfSponsoringAccountCount) = sponsoringAccountCountValue; AccountRootBuilder builderFromSle{sle}; EXPECT_TRUE(builderFromSle.validate()); @@ -605,6 +641,45 @@ TEST(AccountRootTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfLoanBrokerID"); } + { + auto const& expected = sponsoredOwnerCountValue; + + auto const fromSleOpt = entryFromSle.getSponsoredOwnerCount(); + auto const fromBuilderOpt = entryFromBuilder.getSponsoredOwnerCount(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfSponsoredOwnerCount"); + expectEqualField(expected, *fromBuilderOpt, "sfSponsoredOwnerCount"); + } + + { + auto const& expected = sponsoringOwnerCountValue; + + auto const fromSleOpt = entryFromSle.getSponsoringOwnerCount(); + auto const fromBuilderOpt = entryFromBuilder.getSponsoringOwnerCount(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfSponsoringOwnerCount"); + expectEqualField(expected, *fromBuilderOpt, "sfSponsoringOwnerCount"); + } + + { + auto const& expected = sponsoringAccountCountValue; + + auto const fromSleOpt = entryFromSle.getSponsoringAccountCount(); + auto const fromBuilderOpt = entryFromBuilder.getSponsoringAccountCount(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfSponsoringAccountCount"); + expectEqualField(expected, *fromBuilderOpt, "sfSponsoringAccountCount"); + } + EXPECT_EQ(entryFromSle.getKey(), index); EXPECT_EQ(entryFromBuilder.getKey(), index); } @@ -703,5 +778,11 @@ TEST(AccountRootTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getVaultID().has_value()); EXPECT_FALSE(entry.hasLoanBrokerID()); EXPECT_FALSE(entry.getLoanBrokerID().has_value()); + EXPECT_FALSE(entry.hasSponsoredOwnerCount()); + EXPECT_FALSE(entry.getSponsoredOwnerCount().has_value()); + EXPECT_FALSE(entry.hasSponsoringOwnerCount()); + EXPECT_FALSE(entry.getSponsoringOwnerCount().has_value()); + EXPECT_FALSE(entry.hasSponsoringAccountCount()); + EXPECT_FALSE(entry.getSponsoringAccountCount().has_value()); } } diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/RippleStateTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/RippleStateTests.cpp index a51ce55f6f8..de0769793b3 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/RippleStateTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/RippleStateTests.cpp @@ -31,6 +31,8 @@ TEST(RippleStateTests, BuilderSettersRoundTrip) auto const highNodeValue = canonical_UINT64(); auto const highQualityInValue = canonical_UINT32(); auto const highQualityOutValue = canonical_UINT32(); + auto const highSponsorValue = canonical_ACCOUNT(); + auto const lowSponsorValue = canonical_ACCOUNT(); RippleStateBuilder builder{ balanceValue, @@ -46,6 +48,8 @@ TEST(RippleStateTests, BuilderSettersRoundTrip) builder.setHighNode(highNodeValue); builder.setHighQualityIn(highQualityInValue); builder.setHighQualityOut(highQualityOutValue); + builder.setHighSponsor(highSponsorValue); + builder.setLowSponsor(lowSponsorValue); builder.setLedgerIndex(index); builder.setFlags(0x1u); @@ -134,6 +138,22 @@ TEST(RippleStateTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasHighQualityOut()); } + { + auto const& expected = highSponsorValue; + auto const actualOpt = entry.getHighSponsor(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfHighSponsor"); + EXPECT_TRUE(entry.hasHighSponsor()); + } + + { + auto const& expected = lowSponsorValue; + auto const actualOpt = entry.getLowSponsor(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfLowSponsor"); + EXPECT_TRUE(entry.hasLowSponsor()); + } + EXPECT_TRUE(entry.hasLedgerIndex()); auto const ledgerIndex = entry.getLedgerIndex(); ASSERT_TRUE(ledgerIndex.has_value()); @@ -158,6 +178,8 @@ TEST(RippleStateTests, BuilderFromSleRoundTrip) auto const highNodeValue = canonical_UINT64(); auto const highQualityInValue = canonical_UINT32(); auto const highQualityOutValue = canonical_UINT32(); + auto const highSponsorValue = canonical_ACCOUNT(); + auto const lowSponsorValue = canonical_ACCOUNT(); auto sle = std::make_shared(RippleState::entryType, index); @@ -172,6 +194,8 @@ TEST(RippleStateTests, BuilderFromSleRoundTrip) sle->at(sfHighNode) = highNodeValue; sle->at(sfHighQualityIn) = highQualityInValue; sle->at(sfHighQualityOut) = highQualityOutValue; + sle->at(sfHighSponsor) = highSponsorValue; + sle->at(sfLowSponsor) = lowSponsorValue; RippleStateBuilder builderFromSle{sle}; EXPECT_TRUE(builderFromSle.validate()); @@ -310,6 +334,32 @@ TEST(RippleStateTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfHighQualityOut"); } + { + auto const& expected = highSponsorValue; + + auto const fromSleOpt = entryFromSle.getHighSponsor(); + auto const fromBuilderOpt = entryFromBuilder.getHighSponsor(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfHighSponsor"); + expectEqualField(expected, *fromBuilderOpt, "sfHighSponsor"); + } + + { + auto const& expected = lowSponsorValue; + + auto const fromSleOpt = entryFromSle.getLowSponsor(); + auto const fromBuilderOpt = entryFromBuilder.getLowSponsor(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfLowSponsor"); + expectEqualField(expected, *fromBuilderOpt, "sfLowSponsor"); + } + EXPECT_EQ(entryFromSle.getKey(), index); EXPECT_EQ(entryFromBuilder.getKey(), index); } @@ -384,5 +434,9 @@ TEST(RippleStateTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getHighQualityIn().has_value()); EXPECT_FALSE(entry.hasHighQualityOut()); EXPECT_FALSE(entry.getHighQualityOut().has_value()); + EXPECT_FALSE(entry.hasHighSponsor()); + EXPECT_FALSE(entry.getHighSponsor().has_value()); + EXPECT_FALSE(entry.hasLowSponsor()); + EXPECT_FALSE(entry.getLowSponsor().has_value()); } } diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/SponsorshipTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/SponsorshipTests.cpp new file mode 100644 index 00000000000..5e805164f40 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/SponsorshipTests.cpp @@ -0,0 +1,329 @@ +// Auto-generated unit tests for ledger entry Sponsorship + + +#include + +#include + +#include +#include +#include + +#include + +namespace xrpl::ledger_entries { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed for both the +// builder's STObject and the wrapper's SLE. +TEST(SponsorshipTests, BuilderSettersRoundTrip) +{ + uint256 const index{1u}; + + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + auto const ownerValue = canonical_ACCOUNT(); + auto const sponseeValue = canonical_ACCOUNT(); + auto const feeAmountValue = canonical_AMOUNT(); + auto const maxFeeValue = canonical_AMOUNT(); + auto const reserveCountValue = canonical_UINT32(); + auto const ownerNodeValue = canonical_UINT64(); + auto const sponseeNodeValue = canonical_UINT64(); + + SponsorshipBuilder builder{ + previousTxnIDValue, + previousTxnLgrSeqValue, + ownerValue, + sponseeValue, + ownerNodeValue, + sponseeNodeValue + }; + + builder.setFeeAmount(feeAmountValue); + builder.setMaxFee(maxFeeValue); + builder.setReserveCount(reserveCountValue); + + builder.setLedgerIndex(index); + builder.setFlags(0x1u); + + EXPECT_TRUE(builder.validate()); + + auto const entry = builder.build(index); + + EXPECT_TRUE(entry.validate()); + + { + auto const& expected = previousTxnIDValue; + auto const actual = entry.getPreviousTxnID(); + expectEqualField(expected, actual, "sfPreviousTxnID"); + } + + { + auto const& expected = previousTxnLgrSeqValue; + auto const actual = entry.getPreviousTxnLgrSeq(); + expectEqualField(expected, actual, "sfPreviousTxnLgrSeq"); + } + + { + auto const& expected = ownerValue; + auto const actual = entry.getOwner(); + expectEqualField(expected, actual, "sfOwner"); + } + + { + auto const& expected = sponseeValue; + auto const actual = entry.getSponsee(); + expectEqualField(expected, actual, "sfSponsee"); + } + + { + auto const& expected = ownerNodeValue; + auto const actual = entry.getOwnerNode(); + expectEqualField(expected, actual, "sfOwnerNode"); + } + + { + auto const& expected = sponseeNodeValue; + auto const actual = entry.getSponseeNode(); + expectEqualField(expected, actual, "sfSponseeNode"); + } + + { + auto const& expected = feeAmountValue; + auto const actualOpt = entry.getFeeAmount(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfFeeAmount"); + EXPECT_TRUE(entry.hasFeeAmount()); + } + + { + auto const& expected = maxFeeValue; + auto const actualOpt = entry.getMaxFee(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfMaxFee"); + EXPECT_TRUE(entry.hasMaxFee()); + } + + { + auto const& expected = reserveCountValue; + auto const actualOpt = entry.getReserveCount(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfReserveCount"); + EXPECT_TRUE(entry.hasReserveCount()); + } + + EXPECT_TRUE(entry.hasLedgerIndex()); + auto const ledgerIndex = entry.getLedgerIndex(); + ASSERT_TRUE(ledgerIndex.has_value()); + EXPECT_EQ(*ledgerIndex, index); + EXPECT_EQ(entry.getKey(), index); +} + +// 2 & 4) Start from an SLE, set fields directly on it, construct a builder +// from that SLE, build a new wrapper, and verify all fields (and validate()). +TEST(SponsorshipTests, BuilderFromSleRoundTrip) +{ + uint256 const index{2u}; + + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + auto const ownerValue = canonical_ACCOUNT(); + auto const sponseeValue = canonical_ACCOUNT(); + auto const feeAmountValue = canonical_AMOUNT(); + auto const maxFeeValue = canonical_AMOUNT(); + auto const reserveCountValue = canonical_UINT32(); + auto const ownerNodeValue = canonical_UINT64(); + auto const sponseeNodeValue = canonical_UINT64(); + + auto sle = std::make_shared(Sponsorship::entryType, index); + + sle->at(sfPreviousTxnID) = previousTxnIDValue; + sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue; + sle->at(sfOwner) = ownerValue; + sle->at(sfSponsee) = sponseeValue; + sle->at(sfFeeAmount) = feeAmountValue; + sle->at(sfMaxFee) = maxFeeValue; + sle->at(sfReserveCount) = reserveCountValue; + sle->at(sfOwnerNode) = ownerNodeValue; + sle->at(sfSponseeNode) = sponseeNodeValue; + + SponsorshipBuilder builderFromSle{sle}; + EXPECT_TRUE(builderFromSle.validate()); + + auto const entryFromBuilder = builderFromSle.build(index); + + Sponsorship entryFromSle{sle}; + EXPECT_TRUE(entryFromBuilder.validate()); + EXPECT_TRUE(entryFromSle.validate()); + + { + auto const& expected = previousTxnIDValue; + + auto const fromSle = entryFromSle.getPreviousTxnID(); + auto const fromBuilder = entryFromBuilder.getPreviousTxnID(); + + expectEqualField(expected, fromSle, "sfPreviousTxnID"); + expectEqualField(expected, fromBuilder, "sfPreviousTxnID"); + } + + { + auto const& expected = previousTxnLgrSeqValue; + + auto const fromSle = entryFromSle.getPreviousTxnLgrSeq(); + auto const fromBuilder = entryFromBuilder.getPreviousTxnLgrSeq(); + + expectEqualField(expected, fromSle, "sfPreviousTxnLgrSeq"); + expectEqualField(expected, fromBuilder, "sfPreviousTxnLgrSeq"); + } + + { + auto const& expected = ownerValue; + + auto const fromSle = entryFromSle.getOwner(); + auto const fromBuilder = entryFromBuilder.getOwner(); + + expectEqualField(expected, fromSle, "sfOwner"); + expectEqualField(expected, fromBuilder, "sfOwner"); + } + + { + auto const& expected = sponseeValue; + + auto const fromSle = entryFromSle.getSponsee(); + auto const fromBuilder = entryFromBuilder.getSponsee(); + + expectEqualField(expected, fromSle, "sfSponsee"); + expectEqualField(expected, fromBuilder, "sfSponsee"); + } + + { + auto const& expected = ownerNodeValue; + + auto const fromSle = entryFromSle.getOwnerNode(); + auto const fromBuilder = entryFromBuilder.getOwnerNode(); + + expectEqualField(expected, fromSle, "sfOwnerNode"); + expectEqualField(expected, fromBuilder, "sfOwnerNode"); + } + + { + auto const& expected = sponseeNodeValue; + + auto const fromSle = entryFromSle.getSponseeNode(); + auto const fromBuilder = entryFromBuilder.getSponseeNode(); + + expectEqualField(expected, fromSle, "sfSponseeNode"); + expectEqualField(expected, fromBuilder, "sfSponseeNode"); + } + + { + auto const& expected = feeAmountValue; + + auto const fromSleOpt = entryFromSle.getFeeAmount(); + auto const fromBuilderOpt = entryFromBuilder.getFeeAmount(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfFeeAmount"); + expectEqualField(expected, *fromBuilderOpt, "sfFeeAmount"); + } + + { + auto const& expected = maxFeeValue; + + auto const fromSleOpt = entryFromSle.getMaxFee(); + auto const fromBuilderOpt = entryFromBuilder.getMaxFee(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfMaxFee"); + expectEqualField(expected, *fromBuilderOpt, "sfMaxFee"); + } + + { + auto const& expected = reserveCountValue; + + auto const fromSleOpt = entryFromSle.getReserveCount(); + auto const fromBuilderOpt = entryFromBuilder.getReserveCount(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfReserveCount"); + expectEqualField(expected, *fromBuilderOpt, "sfReserveCount"); + } + + EXPECT_EQ(entryFromSle.getKey(), index); + EXPECT_EQ(entryFromBuilder.getKey(), index); +} + +// 3) Verify wrapper throws when constructed from wrong ledger entry type. +TEST(SponsorshipTests, WrapperThrowsOnWrongEntryType) +{ + uint256 const index{3u}; + + // Build a valid ledger entry of a different type + // Ticket requires: Account, OwnerNode, TicketSequence, PreviousTxnID, PreviousTxnLgrSeq + // Check requires: Account, Destination, SendMax, Sequence, OwnerNode, DestinationNode, PreviousTxnID, PreviousTxnLgrSeq + TicketBuilder wrongBuilder{ + canonical_ACCOUNT(), + canonical_UINT64(), + canonical_UINT32(), + canonical_UINT256(), + canonical_UINT32()}; + auto wrongEntry = wrongBuilder.build(index); + + EXPECT_THROW(Sponsorship{wrongEntry.getSle()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong ledger entry type. +TEST(SponsorshipTests, BuilderThrowsOnWrongEntryType) +{ + uint256 const index{4u}; + + // Build a valid ledger entry of a different type + TicketBuilder wrongBuilder{ + canonical_ACCOUNT(), + canonical_UINT64(), + canonical_UINT32(), + canonical_UINT256(), + canonical_UINT32()}; + auto wrongEntry = wrongBuilder.build(index); + + EXPECT_THROW(SponsorshipBuilder{wrongEntry.getSle()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(SponsorshipTests, OptionalFieldsReturnNullopt) +{ + uint256 const index{3u}; + + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + auto const ownerValue = canonical_ACCOUNT(); + auto const sponseeValue = canonical_ACCOUNT(); + auto const ownerNodeValue = canonical_UINT64(); + auto const sponseeNodeValue = canonical_UINT64(); + + SponsorshipBuilder builder{ + previousTxnIDValue, + previousTxnLgrSeqValue, + ownerValue, + sponseeValue, + ownerNodeValue, + sponseeNodeValue + }; + + auto const entry = builder.build(index); + + // Verify optional fields are not present + EXPECT_FALSE(entry.hasFeeAmount()); + EXPECT_FALSE(entry.getFeeAmount().has_value()); + EXPECT_FALSE(entry.hasMaxFee()); + EXPECT_FALSE(entry.getMaxFee().has_value()); + EXPECT_FALSE(entry.hasReserveCount()); + EXPECT_FALSE(entry.getReserveCount().has_value()); +} +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipSetTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipSetTests.cpp new file mode 100644 index 00000000000..3a19038714b --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipSetTests.cpp @@ -0,0 +1,261 @@ +// Auto-generated unit tests for transaction SponsorshipSet + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsSponsorshipSetTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testSponsorshipSet")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const counterpartySponsorValue = canonical_ACCOUNT(); + auto const sponseeValue = canonical_ACCOUNT(); + auto const feeAmountValue = canonical_AMOUNT(); + auto const maxFeeValue = canonical_AMOUNT(); + auto const reserveCountValue = canonical_UINT32(); + + SponsorshipSetBuilder builder{ + accountValue, + sequenceValue, + feeValue + }; + + // Set optional fields + builder.setCounterpartySponsor(counterpartySponsorValue); + builder.setSponsee(sponseeValue); + builder.setFeeAmount(feeAmountValue); + builder.setMaxFee(maxFeeValue); + builder.setReserveCount(reserveCountValue); + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + // Verify optional fields + { + auto const& expected = counterpartySponsorValue; + auto const actualOpt = tx.getCounterpartySponsor(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCounterpartySponsor should be present"; + expectEqualField(expected, *actualOpt, "sfCounterpartySponsor"); + EXPECT_TRUE(tx.hasCounterpartySponsor()); + } + + { + auto const& expected = sponseeValue; + auto const actualOpt = tx.getSponsee(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSponsee should be present"; + expectEqualField(expected, *actualOpt, "sfSponsee"); + EXPECT_TRUE(tx.hasSponsee()); + } + + { + 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 = maxFeeValue; + auto const actualOpt = tx.getMaxFee(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMaxFee should be present"; + expectEqualField(expected, *actualOpt, "sfMaxFee"); + EXPECT_TRUE(tx.hasMaxFee()); + } + + { + auto const& expected = reserveCountValue; + auto const actualOpt = tx.getReserveCount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfReserveCount should be present"; + expectEqualField(expected, *actualOpt, "sfReserveCount"); + EXPECT_TRUE(tx.hasReserveCount()); + } + +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsSponsorshipSetTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testSponsorshipSetFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const counterpartySponsorValue = canonical_ACCOUNT(); + auto const sponseeValue = canonical_ACCOUNT(); + auto const feeAmountValue = canonical_AMOUNT(); + auto const maxFeeValue = canonical_AMOUNT(); + auto const reserveCountValue = canonical_UINT32(); + + // Build an initial transaction + SponsorshipSetBuilder initialBuilder{ + accountValue, + sequenceValue, + feeValue + }; + + initialBuilder.setCounterpartySponsor(counterpartySponsorValue); + initialBuilder.setSponsee(sponseeValue); + initialBuilder.setFeeAmount(feeAmountValue); + initialBuilder.setMaxFee(maxFeeValue); + initialBuilder.setReserveCount(reserveCountValue); + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + SponsorshipSetBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + // Verify optional fields + { + auto const& expected = counterpartySponsorValue; + auto const actualOpt = rebuiltTx.getCounterpartySponsor(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCounterpartySponsor should be present"; + expectEqualField(expected, *actualOpt, "sfCounterpartySponsor"); + } + + { + auto const& expected = sponseeValue; + auto const actualOpt = rebuiltTx.getSponsee(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSponsee should be present"; + expectEqualField(expected, *actualOpt, "sfSponsee"); + } + + { + 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 = maxFeeValue; + auto const actualOpt = rebuiltTx.getMaxFee(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMaxFee should be present"; + expectEqualField(expected, *actualOpt, "sfMaxFee"); + } + + { + auto const& expected = reserveCountValue; + auto const actualOpt = rebuiltTx.getReserveCount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfReserveCount should be present"; + expectEqualField(expected, *actualOpt, "sfReserveCount"); + } + +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsSponsorshipSetTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(SponsorshipSet{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsSponsorshipSetTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(SponsorshipSetBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsSponsorshipSetTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testSponsorshipSetNullopt")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 3; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific required field values + + SponsorshipSetBuilder builder{ + accountValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasCounterpartySponsor()); + 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.hasMaxFee()); + EXPECT_FALSE(tx.getMaxFee().has_value()); + EXPECT_FALSE(tx.hasReserveCount()); + EXPECT_FALSE(tx.getReserveCount().has_value()); +} + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipTransferTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipTransferTests.cpp new file mode 100644 index 00000000000..c8521b7b20c --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipTransferTests.cpp @@ -0,0 +1,198 @@ +// Auto-generated unit tests for transaction SponsorshipTransfer + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsSponsorshipTransferTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testSponsorshipTransfer")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const objectIDValue = canonical_UINT256(); + auto const sponseeValue = canonical_ACCOUNT(); + + SponsorshipTransferBuilder builder{ + accountValue, + sequenceValue, + feeValue + }; + + // Set optional fields + builder.setObjectID(objectIDValue); + builder.setSponsee(sponseeValue); + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + // Verify optional fields + { + auto const& expected = objectIDValue; + auto const actualOpt = tx.getObjectID(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfObjectID should be present"; + expectEqualField(expected, *actualOpt, "sfObjectID"); + EXPECT_TRUE(tx.hasObjectID()); + } + + { + auto const& expected = sponseeValue; + auto const actualOpt = tx.getSponsee(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSponsee should be present"; + expectEqualField(expected, *actualOpt, "sfSponsee"); + EXPECT_TRUE(tx.hasSponsee()); + } + +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsSponsorshipTransferTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testSponsorshipTransferFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const objectIDValue = canonical_UINT256(); + auto const sponseeValue = canonical_ACCOUNT(); + + // Build an initial transaction + SponsorshipTransferBuilder initialBuilder{ + accountValue, + sequenceValue, + feeValue + }; + + initialBuilder.setObjectID(objectIDValue); + initialBuilder.setSponsee(sponseeValue); + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + SponsorshipTransferBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + // Verify optional fields + { + auto const& expected = objectIDValue; + auto const actualOpt = rebuiltTx.getObjectID(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfObjectID should be present"; + expectEqualField(expected, *actualOpt, "sfObjectID"); + } + + { + auto const& expected = sponseeValue; + auto const actualOpt = rebuiltTx.getSponsee(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSponsee should be present"; + expectEqualField(expected, *actualOpt, "sfSponsee"); + } + +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsSponsorshipTransferTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(SponsorshipTransfer{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsSponsorshipTransferTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(SponsorshipTransferBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsSponsorshipTransferTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testSponsorshipTransferNullopt")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 3; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific required field values + + SponsorshipTransferBuilder builder{ + accountValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasObjectID()); + EXPECT_FALSE(tx.getObjectID().has_value()); + EXPECT_FALSE(tx.hasSponsee()); + EXPECT_FALSE(tx.getSponsee().has_value()); +} + +} diff --git a/src/xrpld/rpc/handlers/account/AccountObjects.cpp b/src/xrpld/rpc/handlers/account/AccountObjects.cpp index 08a7fbe44a8..a51737a3928 100644 --- a/src/xrpld/rpc/handlers/account/AccountObjects.cpp +++ b/src/xrpld/rpc/handlers/account/AccountObjects.cpp @@ -33,6 +33,7 @@ namespace xrpl { @param dirIndex Begin gathering account objects from this directory. @param entryIndex Begin gathering objects from this directory node. @param limit Maximum number of objects to find. + @param sponsored Whether to filter by sponsored objects. @param jvResult A JSON result that holds the request objects. */ bool @@ -43,6 +44,7 @@ getAccountObjects( uint256 dirIndex, uint256 entryIndex, std::uint32_t const limit, + std::optional const sponsored, json::Value& jvResult) { // check if dirIndex is valid @@ -55,6 +57,13 @@ getAccountObjects( return it != typeFilter.end(); }; + auto sponsoredMatchesFilter = [](bool const sponsored, + std::optional const& sponsor) { + if (sponsored) + return sponsor.has_value(); + return !sponsor.has_value(); + }; + // if dirIndex != 0, then all NFTs have already been returned. only // iterate NFT pages if the filter says so AND dirIndex == 0 bool iterateNFTPages = @@ -93,7 +102,17 @@ getAccountObjects( while (currentPage) { - jvObjects.append(currentPage->getJson(JsonOptions::Values::None)); + bool canAppendNFT = true; + if (sponsored.has_value()) + { + std::optional const nftSponsor = currentPage->isFieldPresent(sfSponsor) + ? currentPage->getAccountID(sfSponsor) + : std::optional(std::nullopt); + if (!sponsoredMatchesFilter(sponsored.value(), nftSponsor)) + canAppendNFT = false; + } + if (canAppendNFT) + jvObjects.append(currentPage->getJson(JsonOptions::Values::None)); auto const npm = (*currentPage)[~sfNextPageMin]; if (npm) { @@ -179,11 +198,31 @@ getAccountObjects( { auto const sleNode = ledger.read(keylet::child(*entryIter)); - if (!typeFilter.has_value() || - typeMatchesFilter(typeFilter.value(), sleNode->getType())) - { + bool canAppend = true; + + if (typeFilter.has_value() && + !typeMatchesFilter(typeFilter.value(), sleNode->getType())) + canAppend = false; + + auto const getSponsor = [&sleNode]() -> std::optional { + if (sleNode->isFieldPresent(sfSponsor)) + return sleNode->getAccountID(sfSponsor); + if (sleNode->getType() == ltRIPPLE_STATE) + { + if (sleNode->isFieldPresent(sfHighSponsor)) + return sleNode->getAccountID(sfHighSponsor); + if (sleNode->isFieldPresent(sfLowSponsor)) + return sleNode->getAccountID(sfLowSponsor); + } + return std::nullopt; + }; + std::optional const sponsor = getSponsor(); + + if (sponsored.has_value() && !sponsoredMatchesFilter(sponsored.value(), sponsor)) + canAppend = false; + + if (canAppend) jvObjects.append(sleNode->getJson(JsonOptions::Values::None)); - } if (++itemsAdded == limitLeft) { @@ -271,6 +310,7 @@ doAccountObjects(RPC::JsonContext& context) {.name = jss::mptoken, .type = ltMPTOKEN}, {.name = jss::permissioned_domain, .type = ltPERMISSIONED_DOMAIN}, {.name = jss::vault, .type = ltVAULT}, + {.name = jss::sponsorship, .type = ltSPONSORSHIP}, }; typeFilter.emplace(); @@ -329,7 +369,18 @@ doAccountObjects(RPC::JsonContext& context) return RPC::invalidFieldError(jss::marker); } - if (!getAccountObjects(*ledger, accountID, typeFilter, dirIndex, entryIndex, limit, result)) + std::optional sponsored; + if (params.isMember(jss::sponsored)) + { + auto const& sponsoredJv = params[jss::sponsored]; + if (!sponsoredJv.isBool()) + return RPC::expectedFieldError(jss::sponsored, "boolean"); + + sponsored = sponsoredJv.asBool(); + } + + if (!getAccountObjects( + *ledger, accountID, typeFilter, dirIndex, entryIndex, limit, sponsored, result)) return RPC::invalidFieldError(jss::marker); result[jss::account] = toBase58(accountID); diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp index 236712f0c20..7cccac8c3ed 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp @@ -764,6 +764,30 @@ parseVault( return keylet::vault(*id, *seq).key; } +static std::expected +parseSponsorship( + json::Value const& params, + json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) +{ + if (!params.isObject()) + { + return parseObjectID(params, fieldName); + } + + auto const sponsorAccountID = + LedgerEntryHelpers::requiredAccountID(params, jss::sponsor, "malformedSponsor"); + if (!sponsorAccountID) + return std::unexpected(sponsorAccountID.error()); + + auto const sponseeAccountID = + LedgerEntryHelpers::requiredAccountID(params, jss::sponsee, "malformedSponsee"); + if (!sponseeAccountID) + return std::unexpected(sponseeAccountID.error()); + + return keylet::sponsor(*sponsorAccountID, *sponseeAccountID).key; +} + static std::expected parseXChainOwnedClaimID( json::Value const& claimId, diff --git a/src/xrpld/rpc/handlers/transaction/Simulate.cpp b/src/xrpld/rpc/handlers/transaction/Simulate.cpp index 7ee28c48865..e0bb8d8a9fc 100644 --- a/src/xrpld/rpc/handlers/transaction/Simulate.cpp +++ b/src/xrpld/rpc/handlers/transaction/Simulate.cpp @@ -151,6 +151,12 @@ autofillTx(json::Value& txJson, RPC::JsonContext& context) if (auto error = autofillSignature(txJson)) return error; + if (txJson.isMember(sfSponsorSignature.jsonName)) + { + if (auto error = autofillSignature(txJson[sfSponsorSignature.jsonName])) + return error; + } + if (!txJson.isMember(jss::Sequence)) { auto const seq = getAutofillSequence(txJson, context);