diff --git a/cspell.config.yaml b/cspell.config.yaml index b1566ea505..3d5cd7c575 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -255,6 +255,8 @@ words: - sles - soci - socidb + - sponsee + - sponsees - sslws - statsd - STATSDCOLLECTOR diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index 4958a89d8c..aaa4bddbe4 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -125,6 +125,22 @@ areCompatible( beast::Journal::Stream& s, char const* reason); +uint32_t +ownerCount(SLE::const_ref sponsorSle); + +XRPAmount +calculateReserve(SLE::const_ref sle, Fees const& fees); + +TER +checkInsufficientReserve( + ReadView const& view, + STTx const& tx, + std::shared_ptr accSle, + STAmount const& accBalance, + std::shared_ptr const& sponsorSle, + std::int32_t ownerCountDelta, + std::int32_t accountCountDelta = 0); + //------------------------------------------------------------------------------ // // Modifiers diff --git a/include/xrpl/ledger/helpers/AccountRootHelpers.h b/include/xrpl/ledger/helpers/AccountRootHelpers.h index 353c27fe41..26f5e0d918 100644 --- a/include/xrpl/ledger/helpers/AccountRootHelpers.h +++ b/include/xrpl/ledger/helpers/AccountRootHelpers.h @@ -38,10 +38,27 @@ xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, void adjustOwnerCount( ApplyView& view, - std::shared_ptr const& sle, + std::shared_ptr const& accountSle, + std::shared_ptr const& sponsorSle, std::int32_t amount, beast::Journal j); +inline void +adjustOwnerCount( + ApplyView& view, + AccountID const& account, + std::optional const& sponsor, + std::int32_t amount, + beast::Journal j) +{ + return adjustOwnerCount( + view, + view.peek(keylet::account(account)), + sponsor ? view.peek(keylet::account(*sponsor)) : std::shared_ptr(), + amount, + j); +} + /** Returns IOU issuer transfer fee as Rate. Rate specifies * the fee as fractions of 1 billion. For example, 1% transfer rate * is represented as 1,010,000,000. diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h index b44601594d..42f682b1c5 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, std::shared_ptr const& sleDest, STAmount const& xrpBalance, @@ -31,6 +33,7 @@ template <> inline TER escrowUnlockApplyHelper( ApplyView& view, + STTx const& tx, Rate lockedRate, std::shared_ptr const& sleDest, STAmount const& xrpBalance, @@ -56,8 +59,12 @@ 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 sponsorAccountID = getTxReserveSponsorAccountID(tx); + std::shared_ptr sponsorSle = {}; + if (sponsorAccountID) + sponsorSle = view.peek(keylet::account(*sponsorAccountID)); + if (auto const ret = checkInsufficientReserve(view, tx, sleDest, xrpBalance, sponsorSle, 1); + !isTesSuccess(ret)) { JLOG(journal.trace()) << "Trust line does not exist. " "Insufficient reserve to create line."; @@ -84,6 +91,7 @@ escrowUnlockApplyHelper( Issue(currency, receiver), // limit of zero 0, // quality in 0, // quality out + sponsorAccountID, // sponsor journal); // journal !isTesSuccess(ter)) { @@ -161,6 +169,7 @@ template <> inline TER escrowUnlockApplyHelper( ApplyView& view, + STTx const& tx, Rate lockedRate, std::shared_ptr const& sleDest, STAmount const& xrpBalance, @@ -176,24 +185,30 @@ 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 sponsorAccountID = getTxReserveSponsorAccountID(tx); + std::shared_ptr sponsorSle = {}; + if (sponsorAccountID) + sponsorSle = view.peek(keylet::account(*sponsorAccountID)); + if (auto const ret = checkInsufficientReserve(view, tx, sleDest, xrpBalance, sponsorSle, 1); + !isTesSuccess(ret)) + return ret; - if (auto const ter = createMPToken(view, mptID, receiver, 0); !isTesSuccess(ter)) + if (auto const ter = createMPToken(view, mptID, receiver, sponsorAccountID, 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 5ba4766a62..3ae2f64d0f 100644 --- a/include/xrpl/ledger/helpers/MPTokenHelpers.h +++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h @@ -68,6 +68,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, @@ -99,6 +100,7 @@ requireAuth( [[nodiscard]] TER enforceMPTokenAuthorization( ApplyView& view, + STTx const& tx, MPTID const& mptIssuanceID, AccountID const& account, XRPAmount const& priorBalance, @@ -139,6 +141,7 @@ canMPTTradeAndTransfer( [[nodiscard]] TER addEmptyHolding( ApplyView& view, + STTx const& tx, AccountID const& accountID, XRPAmount priorBalance, MPTIssue const& mptIssue, @@ -147,6 +150,7 @@ addEmptyHolding( [[nodiscard]] TER removeEmptyHolding( ApplyView& view, + STTx const& tx, AccountID const& accountID, MPTIssue const& mptIssue, beast::Journal journal); @@ -178,6 +182,7 @@ createMPToken( ApplyView& view, MPTID const& mptIssuanceID, AccountID const& account, + std::optional const& sponsor, std::uint32_t const flags); TER @@ -185,6 +190,7 @@ checkCreateMPT( xrpl::ApplyView& view, xrpl::MPTIssue const& mptIssue, xrpl::AccountID const& holder, + std::optional const& sponsor, beast::Journal j); //------------------------------------------------------------------------------ diff --git a/include/xrpl/ledger/helpers/NFTokenHelpers.h b/include/xrpl/ledger/helpers/NFTokenHelpers.h index d8dac4caaf..c99ac9cd2e 100644 --- a/include/xrpl/ledger/helpers/NFTokenHelpers.h +++ b/include/xrpl/ledger/helpers/NFTokenHelpers.h @@ -44,7 +44,12 @@ 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, + std::optional const& sponsor, + STObject&& nft); /** Remove the token from the owner's token directory. */ TER @@ -116,6 +121,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 17b0f7673e..074cf750d9 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, + std::optional const& sponsorAccountID, 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 0000000000..3b9ae39a57 --- /dev/null +++ b/include/xrpl/ledger/helpers/SponsorHelpers.h @@ -0,0 +1,114 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +inline bool +isReserveSponsored(STTx const& tx) +{ + return tx.getFieldU32(sfSponsorFlags) & spfSponsorReserve; +} + +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::shared_ptr +getTxReserveSponsor(ApplyView& view, STTx const& tx) +{ + auto const sponsorID = getTxReserveSponsorAccountID(tx); + if (sponsorID) + return view.peek(keylet::account(*sponsorID)); + return {}; +} + +inline std::shared_ptr +getTxReserveSponsor(ReadView const& view, STTx const& tx) +{ + auto const sponsorID = getTxReserveSponsorAccountID(tx); + if (sponsorID) + return view.read(keylet::account(*sponsorID)); + return {}; +} + +inline std::optional +getLedgerEntryReserveSponsorAccountID( + std::shared_ptr const& sle, + SF_ACCOUNT const& field = sfSponsor) +{ + if (sle->isFieldPresent(field)) + return sle->getAccountID(field); + return {}; +} + +inline std::shared_ptr +getLedgerEntryReserveSponsor( + ApplyView& view, + std::shared_ptr const& sle, + SF_ACCOUNT const& field = sfSponsor) +{ + auto const sponsorID = getLedgerEntryReserveSponsorAccountID(sle, field); + if (sponsorID) + return view.peek(keylet::account(*sponsorID)); + return {}; +} + +inline std::shared_ptr +getLedgerEntryReserveSponsor( + ReadView const& view, + std::shared_ptr const& sle, + SF_ACCOUNT const& field = sfSponsor) +{ + auto const sponsorID = getLedgerEntryReserveSponsorAccountID(sle, field); + if (sponsorID) + return view.read(keylet::account(*sponsorID)); + return {}; +} + +inline void +addSponsorToLedgerEntry( + std::shared_ptr const& sle, + std::shared_ptr const& 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(std::shared_ptr const& 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 xrpl diff --git a/include/xrpl/ledger/helpers/TokenHelpers.h b/include/xrpl/ledger/helpers/TokenHelpers.h index b79113dad0..55b2ecacd9 100644 --- a/include/xrpl/ledger/helpers/TokenHelpers.h +++ b/include/xrpl/ledger/helpers/TokenHelpers.h @@ -214,6 +214,7 @@ canAddHolding(ReadView const& view, Asset const& asset); [[nodiscard]] TER addEmptyHolding( ApplyView& view, + STTx const& tx, AccountID const& accountID, XRPAmount priorBalance, Asset const& asset, @@ -222,6 +223,7 @@ addEmptyHolding( [[nodiscard]] TER removeEmptyHolding( ApplyView& view, + STTx const& tx, AccountID const& accountID, Asset const& asset, beast::Journal journal); @@ -276,6 +278,7 @@ accountSend( AccountID const& to, STAmount const& saAmount, beast::Journal j, + std::optional const& sponsorAccountID = std::nullopt, WaiveTransferFee waiveFee = WaiveTransferFee::No, AllowMPTOverflow allowOverflow = AllowMPTOverflow::No); @@ -293,6 +296,7 @@ accountSendMulti( Asset const& asset, MultiplePaymentDestinations const& receivers, beast::Journal j, + std::optional const& sponsorAccountID, WaiveTransferFee waiveFee = WaiveTransferFee::No); [[nodiscard]] TER diff --git a/include/xrpl/protocol/Fees.h b/include/xrpl/protocol/Fees.h index ddf4acbf67..9d89ac70a7 100644 --- a/include/xrpl/protocol/Fees.h +++ b/include/xrpl/protocol/Fees.h @@ -40,9 +40,23 @@ struct Fees the reserve increment times the number of increments. */ XRPAmount - accountReserve(std::size_t ownerCount) const + accountReserve( + std::size_t ownerCount, + std::size_t sponsoredOwnerCount = 0, + std::size_t sponsoringOwnerCount = 0, + bool isAccountSponsored = false, + std::size_t sponsoringAccountCount = 0) const { - return reserve + ownerCount * increment; + auto const accountReserveUnits = (isAccountSponsored ? 0 : 1) + sponsoringAccountCount; + + XRPL_ASSERT( + ownerCount >= sponsoredOwnerCount, + "xrpl::Fees::accountReserve : OwnerCount must be greater than or equal to " + "SponsoredOwnerCount"); + + auto const ownerReserveUnits = (ownerCount - sponsoredOwnerCount) + sponsoringOwnerCount; + + return (reserve * accountReserveUnits) + (increment * ownerReserveUnits); } }; diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 574bbfbde6..71c688bed8 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -151,6 +151,10 @@ static ticket_t const ticket{}; Keylet signers(AccountID const& account) noexcept; +/** A Sponsor */ +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 dd25aaeffe..e0b819608a 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -135,7 +135,7 @@ enum LedgerEntryType : std::uint16_t { LSF_FLAG(lsfDisallowIncomingPayChan, 0x10000000) /* True, reject new paychans */ \ LSF_FLAG(lsfDisallowIncomingTrustline, 0x20000000) /* True, reject new trustlines (only if no issued assets) */ \ LSF_FLAG(lsfAllowTrustLineLocking, 0x40000000) /* True, enable trustline locking */ \ - LSF_FLAG(lsfAllowTrustLineClawback, 0x80000000)) /* True, enable clawback */ \ + LSF_FLAG(lsfAllowTrustLineClawback, 0x80000000)) /* True, enable clawback */ \ \ LEDGER_OBJECT(Offer, \ LSF_FLAG(lsfPassive, 0x00010000) \ @@ -201,7 +201,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 5d15cf1ad7..a81d4cdfb4 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -211,6 +211,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 }; //------------------------------------------------------------------------------ @@ -350,6 +351,7 @@ enum TECcodes : TERUnderlyingType { // reclaimed after those networks reset. tecNO_DELEGATE_PERMISSION = 198, tecBAD_PROOF = 199, + tecNO_SPONSOR_PERMISSION = 200, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index b90e48a3b8..426a620f03 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -100,7 +100,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, \ @@ -213,6 +214,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 @@ -337,6 +352,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. @@ -449,4 +467,33 @@ 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 diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 6c8b2b0a38..9bc00aad1c 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_FEATURE(BatchV1_1, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(ConfidentialTransfer, Supported::no, 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 f72de04e3e..aa459cdfde 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. @@ -615,5 +620,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 d85d5fe55c..45e50227fc 100644 --- a/include/xrpl/protocol/detail/permissions.macro +++ b/include/xrpl/protocol/detail/permissions.macro @@ -78,3 +78,18 @@ GRANULAR_PERMISSION(MPTokenIssuanceLock, ttMPTOKEN_ISSUANCE_SET, 65547, tfUniver GRANULAR_PERMISSION(MPTokenIssuanceUnlock, ttMPTOKEN_ISSUANCE_SET, 65548, tfUniversal | tfMPTUnlock, ({{sfMPTokenIssuanceID, soeREQUIRED}, {sfHolder, soeOPTIONAL}})) + +/** Grants the ability to set SponsorFee. */ +GRANULAR_PERMISSION(SponsorFee, ttSPONSORSHIP_SET, 65549, + tfUniversal | tfSponsorshipSetRequireSignForFee | tfSponsorshipClearRequireSignForFee, + ({{sfFeeAmount, soeOPTIONAL}, + {sfMaxFee, soeOPTIONAL}, + {sfSponsee, soeOPTIONAL}, + {sfCounterpartySponsor, soeOPTIONAL}})) + +/** Grants the ability to set SponsorReserve. */ +GRANULAR_PERMISSION(SponsorReserve, ttSPONSORSHIP_SET, 65550, + tfUniversal | tfSponsorshipSetRequireSignForReserve | tfSponsorshipClearRequireSignForReserve, + ({{sfReserveCount, soeOPTIONAL}, + {sfSponsee, soeOPTIONAL}, + {sfCounterpartySponsor, soeOPTIONAL}})) diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 1bbfadc093..51c033e1a7 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -114,6 +114,11 @@ TYPED_SFIELD(sfLateInterestRate, UINT32, 66) // 1/10 basis points (bi TYPED_SFIELD(sfCloseInterestRate, UINT32, 67) // 1/10 basis points (bips) TYPED_SFIELD(sfOverpaymentInterestRate, UINT32, 68) // 1/10 basis points (bips) TYPED_SFIELD(sfConfidentialBalanceVersion, UINT32, 69) +TYPED_SFIELD(sfSponsoredOwnerCount, UINT32, 70) +TYPED_SFIELD(sfSponsoringOwnerCount, UINT32, 71) +TYPED_SFIELD(sfSponsoringAccountCount, UINT32, 72) +TYPED_SFIELD(sfReserveCount, UINT32, 73) +TYPED_SFIELD(sfSponsorFlags, UINT32, 74) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -148,6 +153,7 @@ TYPED_SFIELD(sfLockedAmount, UINT64, 29, SField::sMD_BaseTen|SFie TYPED_SFIELD(sfVaultNode, UINT64, 30) TYPED_SFIELD(sfLoanBrokerNode, UINT64, 31) TYPED_SFIELD(sfConfidentialOutstandingAmount, UINT64, 32, SField::sMD_BaseTen|SField::sMD_Default) +TYPED_SFIELD(sfSponseeNode, UINT64, 33) // 128-bit TYPED_SFIELD(sfEmailHash, UINT128, 1) @@ -208,6 +214,7 @@ TYPED_SFIELD(sfLoanBrokerID, UINT256, 37, SField::sMD_PseudoAccount | SField::sMD_Default) TYPED_SFIELD(sfLoanID, UINT256, 38) TYPED_SFIELD(sfBlindingFactor, UINT256, 39) +TYPED_SFIELD(sfObjectID, UINT256, 40) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) @@ -267,6 +274,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) @@ -342,6 +351,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::sMD_Never) @@ -406,6 +420,7 @@ UNTYPED_SFIELD(sfRawTransaction, OBJECT, 34) UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35) UNTYPED_SFIELD(sfBook, OBJECT, 36) UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 37, SField::sMD_Default, SField::notSigning) +UNTYPED_SFIELD(sfSponsorSignature, OBJECT, 38, SField::sMD_Default, SField::notSigning) // 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 177662dc6d..fae8038624 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1162,6 +1162,35 @@ TRANSACTION(ttCONFIDENTIAL_MPT_CLAWBACK, 89, ConfidentialMPTClawback, {sfZKProof, soeREQUIRED}, })) +/** This transaction transfer sponsorship */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttSPONSORSHIP_TRANSFER, 90, SponsorshipTransfer, + Delegation::delegable, + featureSponsor, + noPriv, + ({ + {sfObjectID, soeOPTIONAL}, + {sfSponsee, soeOPTIONAL}, +})) + +/** This transaction create sponsorship object */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttSPONSORSHIP_SET, 91, 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 366a75df76..e8c7253e36 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -558,6 +558,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 15cf14b21a..2b3fbd5313 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h +++ b/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h @@ -518,6 +518,78 @@ public: { 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 @@ public: 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 993496d38f..9939c24c2d 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/RippleState.h +++ b/include/xrpl/protocol_autogen/ledger_entries/RippleState.h @@ -243,6 +243,54 @@ public: { 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 @@ public: 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 0000000000..fdc13e89a1 --- /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(std::shared_ptr 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 Json::Value 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(std::shared_ptr 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 0000000000..f355e98923 --- /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 (91) + * 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 Json::Value 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 0000000000..49e09a6d30 --- /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 (90) + * 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 Json::Value 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 4e0a1fb91e..8f61fc55c8 100644 --- a/include/xrpl/tx/Transactor.h +++ b/include/xrpl/tx/Transactor.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -106,6 +107,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: @@ -244,6 +259,9 @@ public: return T::checkGranularSemantics(view, tx, heldGranularPermissions); } + + static NotTEC + checkSponsor(ReadView const& view, STTx const& tx); ///////////////////////////////////////////////////// // Interface used by AccountDelete @@ -355,6 +373,9 @@ private: 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 5bf4d426c9..d84d4e452f 100644 --- a/include/xrpl/tx/invariants/InvariantCheck.h +++ b/include/xrpl/tx/invariants/InvariantCheck.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -401,7 +402,9 @@ using InvariantChecks = std::tuple< ValidVault, ValidConfidentialMPToken, ValidMPTPayment, - 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 0000000000..fc6f87f3f0 --- /dev/null +++ b/include/xrpl/tx/invariants/SponsorshipInvariant.h @@ -0,0 +1,55 @@ +#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::uint64_t invalidOwnerCountLessThanSponsoredOwnerCount_ = 0; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal 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&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/paths/AMMOffer.h b/include/xrpl/tx/paths/AMMOffer.h index de583a60d6..f2cf117ef7 100644 --- a/include/xrpl/tx/paths/AMMOffer.h +++ b/include/xrpl/tx/paths/AMMOffer.h @@ -104,7 +104,10 @@ public: send(Args&&... args) { return accountSend( - std::forward(args)..., WaiveTransferFee::Yes, AllowMPTOverflow::Yes); + std::forward(args)..., + std::nullopt, + WaiveTransferFee::Yes, + AllowMPTOverflow::Yes); } bool diff --git a/include/xrpl/tx/paths/Offer.h b/include/xrpl/tx/paths/Offer.h index ae7a47061a..b9979df875 100644 --- a/include/xrpl/tx/paths/Offer.h +++ b/include/xrpl/tx/paths/Offer.h @@ -213,7 +213,8 @@ template TER TOffer::send(Args&&... args) { - return accountSend(std::forward(args)..., WaiveTransferFee::No, AllowMPTOverflow::Yes); + return accountSend( + std::forward(args)..., std::nullopt, WaiveTransferFee::No, AllowMPTOverflow::Yes); } template diff --git a/include/xrpl/tx/transactors/dex/AMMWithdraw.h b/include/xrpl/tx/transactors/dex/AMMWithdraw.h index 087fd3a14c..9500e96bf6 100644 --- a/include/xrpl/tx/transactors/dex/AMMWithdraw.h +++ b/include/xrpl/tx/transactors/dex/AMMWithdraw.h @@ -89,6 +89,7 @@ public: static std::tuple> equalWithdrawTokens( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const account, AccountID const& ammAccount, @@ -123,6 +124,7 @@ public: static std::tuple> withdraw( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, AccountID const& account, @@ -166,6 +168,7 @@ private: std::pair withdraw( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -191,6 +194,7 @@ private: std::pair equalWithdrawTokens( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -216,6 +220,7 @@ private: std::pair equalWithdrawLimit( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -238,6 +243,7 @@ private: std::pair singleWithdraw( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -259,6 +265,7 @@ private: std::pair singleWithdrawTokens( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -281,6 +288,7 @@ private: 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 d879d14a84..35766ce564 100644 --- a/include/xrpl/tx/transactors/oracle/OracleSet.h +++ b/include/xrpl/tx/transactors/oracle/OracleSet.h @@ -22,6 +22,9 @@ public: { } + static uint32_t + calculateOracleReserve(std::size_t count); + 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 0000000000..bced6ba48a --- /dev/null +++ b/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace xrpl { + +class SponsorshipSet : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit SponsorshipSet(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; +}; + +} // 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 0000000000..077a003ae5 --- /dev/null +++ b/include/xrpl/tx/transactors/sponsor/SponsorshipTransfer.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace xrpl { + +class SponsorshipTransfer : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{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; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h index 5ef12df282..f1f04dd3cc 100644 --- a/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h @@ -42,7 +42,7 @@ public: doApply() override; static 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 c96940ec23..f861aff41c 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -285,6 +286,93 @@ hashOfSeq(ReadView const& ledger, LedgerIndex seq, beast::Journal journal) return std::nullopt; } +uint32_t +ownerCount(std::shared_ptr const& sponsorSle) +{ + auto const ownerCount = sponsorSle->getFieldU32(sfOwnerCount); + auto const sponsoredOwnerCount = sponsorSle->getFieldU32(sfSponsoredOwnerCount); + auto const sponsoringOwnerCount = sponsorSle->getFieldU32(sfSponsoringOwnerCount); + + return ownerCount + sponsoringOwnerCount - sponsoredOwnerCount; +} + +XRPAmount +calculateReserve(std::shared_ptr const& sle, Fees const& fees) +{ + XRPL_ASSERT(sle->getType() == ltACCOUNT_ROOT, "xrpl::calculateReserve : valid sle type"); + + return fees.accountReserve( + sle->getFieldU32(sfOwnerCount), + sle->getFieldU32(sfSponsoredOwnerCount), + sle->getFieldU32(sfSponsoringOwnerCount), + sle->isFieldPresent(sfSponsor), + sle->getFieldU32(sfSponsoringAccountCount)); +} + +TER +checkInsufficientReserve( + ReadView const& view, + STTx const& tx, + std::shared_ptr accSle, + STAmount const& accBalance, + std::shared_ptr const& sponsorSle, + std::int32_t ownerCountDelta, + std::int32_t accountCountDelta) +{ + if (sponsorSle) + { + auto const isCoSigning = isSponsorReserveCoSigning(tx); + + auto const sle = view.read( + keylet::sponsor(sponsorSle->getAccountID(sfAccount), accSle->getAccountID(sfAccount))); + + if (isCoSigning) + { + if (sle) + { + auto const reserveCountAllowed = sle->getFieldU32(sfReserveCount); + if (reserveCountAllowed < ownerCountDelta) + return tecINSUFFICIENT_RESERVE; + + return tesSUCCESS; + } + auto const sponsorBalance = sponsorSle->getFieldAmount(sfBalance); + STAmount const sponsorReserve{view.fees().accountReserve( + sponsorSle->getFieldU32(sfOwnerCount), + sponsorSle->getFieldU32(sfSponsoredOwnerCount), + sponsorSle->getFieldU32(sfSponsoringOwnerCount) + ownerCountDelta, + sponsorSle->isFieldPresent(sfSponsor), + sponsorSle->getFieldU32(sfSponsoringAccountCount) + accountCountDelta)}; + + if (sponsorBalance < sponsorReserve) + return tecINSUFFICIENT_RESERVE; + } + else + { + // pre funded + if (!sle) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const reserveCountAllowed = sle->getFieldU32(sfReserveCount); + if (reserveCountAllowed < ownerCountDelta) + return tecINSUFFICIENT_RESERVE; + } + } + else + { + STAmount const reserve{view.fees().accountReserve( + accSle->getFieldU32(sfOwnerCount) + ownerCountDelta, + accSle->getFieldU32(sfSponsoredOwnerCount), + accSle->getFieldU32(sfSponsoringOwnerCount), + accSle->isFieldPresent(sfSponsor), + accSle->getFieldU32(sfSponsoringAccountCount) + accountCountDelta)}; + + if (accBalance < reserve) + return tecINSUFFICIENT_RESERVE; + } + return tesSUCCESS; +} + //------------------------------------------------------------------------------ // // Modifiers @@ -408,7 +496,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; } @@ -434,9 +522,12 @@ doWithdraw( // LCOV_EXCL_STOP } + auto const sponsorAccountID = getTxReserveSponsorAccountID(tx); + // 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, sponsorAccountID, WaiveTransferFee::Yes); } TER diff --git a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp index 19c2a9d7a7..fc244c0440 100644 --- a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp +++ b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp @@ -74,9 +74,19 @@ xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, std::uint32_t const ownerCount = confineOwnerCount(view.ownerCountHook(id, sle->getFieldU32(sfOwnerCount)), ownerCountAdj); + std::uint32_t const sponsoredOwnerCount = sle->getFieldU32(sfSponsoredOwnerCount); + std::uint32_t const sponsoringOwnerCount = sle->getFieldU32(sfSponsoringOwnerCount); + bool const isAccountSponsored = sle->isFieldPresent(sfSponsor); + std::uint32_t const sponsoringAccountCount = sle->getFieldU32(sfSponsoringAccountCount); + // Pseudo-accounts have no reserve requirement - auto const reserve = - isPseudoAccount(sle) ? XRPAmount{0} : view.fees().accountReserve(ownerCount); + auto const reserve = isPseudoAccount(sle) ? XRPAmount{0} + : view.fees().accountReserve( + ownerCount, + sponsoredOwnerCount, + sponsoringOwnerCount, + isAccountSponsored, + sponsoringAccountCount); auto const fullBalance = sle->getFieldAmount(sfBalance); @@ -105,21 +115,69 @@ transferRate(ReadView const& view, AccountID const& issuer) } void -adjustOwnerCount( +adjustSponsorOwnerCountHlp( ApplyView& view, std::shared_ptr const& sle, + SField const& sfield, std::int32_t amount, beast::Journal j) { - if (!sle) + auto const accID = sle->getAccountID(sfAccount); + std::uint32_t const current{(sle)->getFieldU32(sfield)}; + std::uint32_t const adjusted = confineOwnerCount(current, amount, accID, j); + view.adjustOwnerCountHook(accID, current, adjusted); + if (adjusted == 0) + sle->makeFieldAbsent(sfield); + else + sle->setFieldU32(sfield, adjusted); + view.update(sle); +} + +void +adjustOwnerCount( + ApplyView& view, + std::shared_ptr const& accountSle, + std::shared_ptr const& sponsorSle, + std::int32_t amount, + beast::Journal j) +{ + if (!accountSle) return; XRPL_ASSERT(amount, "xrpl::adjustOwnerCount : nonzero amount input"); - std::uint32_t const current{sle->getFieldU32(sfOwnerCount)}; - AccountID const id = (*sle)[sfAccount]; + + if (sponsorSle) + { + adjustSponsorOwnerCountHlp(view, sponsorSle, sfSponsoringOwnerCount, amount, j); + adjustSponsorOwnerCountHlp(view, accountSle, sfSponsoredOwnerCount, amount, j); + + auto const account = accountSle->getAccountID(sfAccount); + auto const sponsorAccountID = (sponsorSle)->getAccountID(sfAccount); + + auto sponsorObjSle = view.peek(keylet::sponsor(sponsorAccountID, account)); + + if (sponsorObjSle) + { + // pre funded + // update the pre-funded ReserveCount on Sponsorship ledger object + + std::uint32_t const currentReserveCount = sponsorObjSle->getFieldU32(sfReserveCount); + // Reserve count moves opposite to amount: +amount => consume reserve (-), -amount => + // payback (+) + std::uint32_t const adjusted = + confineOwnerCount(currentReserveCount, -amount, sponsorAccountID, j); + if (adjusted == 0) + sponsorObjSle->makeFieldAbsent(sfReserveCount); + else + sponsorObjSle->setFieldU32(sfReserveCount, adjusted); + view.update(sponsorObjSle); + } + } + std::uint32_t const current{accountSle->getFieldU32(sfOwnerCount)}; + AccountID const id = (*accountSle)[sfAccount]; std::uint32_t const adjusted = confineOwnerCount(current, amount, id, j); view.adjustOwnerCountHook(id, current, adjusted); - sle->at(sfOwnerCount) = adjusted; - view.update(sle); + accountSle->at(sfOwnerCount) = adjusted; + view.update(accountSle); } AccountID diff --git a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp index 32db285f1e..5672281bb8 100644 --- a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp +++ b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp @@ -2,6 +2,7 @@ // #include #include +#include #include #include @@ -71,7 +72,10 @@ deleteSLE(ApplyView& view, std::shared_ptr const& sleCredential, beast::Jou } if (isOwner) - adjustOwnerCount(view, sleAccount, -1, j); + { + auto const sponsorSle = getLedgerEntryReserveSponsor(view, sleCredential); + adjustOwnerCount(view, sleAccount, sponsorSle, -1, j); + } return tesSUCCESS; }; diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index ff086939e9..6ca211352a 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -94,6 +95,7 @@ canAddHolding(ReadView const& view, MPTIssue const& mptIssue) [[nodiscard]] TER addEmptyHolding( ApplyView& view, + STTx const& tx, AccountID const& accountID, XRPAmount priorBalance, MPTIssue const& mptIssue, @@ -110,12 +112,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, @@ -148,7 +151,8 @@ authorizeMPToken( keylet::ownerDir(account), (*sleMpt)[sfOwnerNode], sleMpt->key(), false)) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleAcct, -1, journal); + auto const sponsor = getLedgerEntryReserveSponsor(view, sleMpt); + adjustOwnerCount(view, sleAcct, sponsor, -1, journal); view.erase(sleMpt); return tesSUCCESS; @@ -158,18 +162,24 @@ authorizeMPToken( // - add the new mptokenKey to the owner directory // - create the MPToken object for the holder + auto const sponsor = getTxReserveSponsor(view, tx); + + auto const isSponsoredAndPreFunded = sponsor && !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::zero) - : view.fees().accountReserve(uOwnerCount + 1)); - - if (priorBalance < reserveCreate) - return tecINSUFFICIENT_RESERVE; + // If PreFunded Sponsor, it must be checked whether sufficient + // ReserveCount exists. + if (ownerCount(sponsor ? sponsor : sleAcct) >= 2 || isSponsoredAndPreFunded) + { + if (auto const ret = + checkInsufficientReserve(view, tx, sleAcct, priorBalance, sponsor, 1); + !isTesSuccess(ret)) + return ret; + } // Defensive check before we attempt to create MPToken for the issuer auto const mpt = view.read(keylet::mptIssuance(mptIssuanceID)); @@ -193,7 +203,8 @@ authorizeMPToken( view.insert(mptoken); // Update owner count. - adjustOwnerCount(view, sleAcct, 1, journal); + adjustOwnerCount(view, sleAcct, sponsor, 1, journal); + addSponsorToLedgerEntry(mptoken, sponsor); return tesSUCCESS; } @@ -238,6 +249,7 @@ authorizeMPToken( [[nodiscard]] TER removeEmptyHolding( ApplyView& view, + STTx const& tx, AccountID const& accountID, MPTIssue const& mptIssue, beast::Journal journal) @@ -267,6 +279,7 @@ removeEmptyHolding( return authorizeMPToken( view, + tx, {}, // priorBalance mptID, accountID, @@ -385,6 +398,7 @@ requireAuth( [[nodiscard]] TER enforceMPTokenAuthorization( ApplyView& view, + STTx const& tx, MPTID const& mptIssuanceID, AccountID const& account, XRPAmount const& priorBalance, // for MPToken authorization @@ -466,6 +480,7 @@ enforceMPTokenAuthorization( "xrpl::enforceMPTokenAuthorization : new MPToken for domain"); if (auto const err = authorizeMPToken( view, + tx, priorBalance, // priorBalance mptIssuanceID, // mptIssuanceID account, // account @@ -795,6 +810,7 @@ createMPToken( ApplyView& view, MPTID const& mptIssuanceID, AccountID const& account, + std::optional const& sponsor, std::uint32_t const flags) { auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); @@ -811,6 +827,14 @@ createMPToken( (*mptoken)[sfFlags] = flags; (*mptoken)[sfOwnerNode] = *ownerNode; + if (sponsor) + { + auto const sponsorSle = view.peek(keylet::account(*sponsor)); + if (!sponsorSle) + return tecINTERNAL; + addSponsorToLedgerEntry(mptoken, sponsorSle); + } + view.insert(mptoken); return tesSUCCESS; @@ -821,6 +845,7 @@ checkCreateMPT( xrpl::ApplyView& view, xrpl::MPTIssue const& mptIssue, xrpl::AccountID const& holder, + std::optional const& sponsor, beast::Journal j) { if (mptIssue.getIssuer() == holder) @@ -830,7 +855,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, sponsor, 0); !isTesSuccess(err)) { return err; @@ -840,7 +865,9 @@ checkCreateMPT( { return tecINTERNAL; } - adjustOwnerCount(view, sleAcct, 1, j); + auto const sleSponsor = + sponsor ? view.peek(keylet::account(*sponsor)) : std::shared_ptr(); + adjustOwnerCount(view, sleAcct, sleSponsor, 1, j); } return tesSUCCESS; } diff --git a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp index 4652bccca8..e46fa8e71f 100644 --- a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -45,12 +46,19 @@ locatePage(ApplyView& view, AccountID const& owner, uint256 const& id) Keylet(ltNFTOKEN_PAGE, view.succ(first.key, last.key.next()).value_or(last.key))); } -static std::shared_ptr +static Expected, TER> getPageForToken( ApplyView& view, + STTx const& tx, AccountID const& owner, + std::optional const& sponsor, uint256 const& id, - std::function const& createCallback) + std::function< + TER(ApplyView&, + STTx const&, + std::shared_ptr const&, + AccountID const&, + std::optional const&)> const& createCallback) { auto const base = keylet::nftpage_min(owner); auto const first = keylet::nftpage(base, id); @@ -69,7 +77,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, sponsor); !isTesSuccess(ret)) + return Unexpected(ret); return cp; } @@ -182,7 +192,8 @@ getPageForToken( cp->setFieldH256(sfPreviousPageMin, np->key()); view.update(cp); - createCallback(view, owner); + if (auto const ret = createCallback(view, tx, np, owner, sponsor); ret != tesSUCCESS) + return Unexpected(ret); return (first.key < np->key()) ? np : cp; } @@ -239,37 +250,69 @@ 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, + std::optional const& sponsor, + 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. - std::shared_ptr const page = - getPageForToken(view, owner, nft[sfNFTokenID], [](ApplyView& view, AccountID const& owner) { + Expected, TER> const page = getPageForToken( + view, + tx, + owner, + sponsor, + nft[sfNFTokenID], + [](ApplyView& view, + STTx const& tx, + std::shared_ptr const& newPage, + AccountID const& owner, + std::optional const& sponsor) -> TER { + std::shared_ptr const sponsorSle = + sponsor ? view.peek(keylet::account(*sponsor)) : std::shared_ptr(); + + 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; + } adjustOwnerCount( view, view.peek(keylet::account(owner)), + sponsorSle, 1, beast::Journal{beast::Journal::getNullSink()}); + addSponsorToLedgerEntry(newPage, sponsorSle); + return tesSUCCESS; }); - if (!page) + 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.push_back(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; } @@ -400,20 +443,25 @@ removeToken( curr->setFieldArray(sfNFTokens, arr); view.update(curr); - int cnt = 0; - if (prev && mergePages(view, prev, curr)) - cnt--; - - if (next && mergePages(view, curr, next)) - cnt--; - - if (cnt != 0) { + auto const sponsor = getLedgerEntryReserveSponsor(view, prev); adjustOwnerCount( view, view.peek(keylet::account(owner)), - cnt, + sponsor, + -1, + beast::Journal{beast::Journal::getNullSink()}); + } + + if (next && mergePages(view, curr, next)) + { + auto const sponsor = getLedgerEntryReserveSponsor(view, curr); + adjustOwnerCount( + view, + view.peek(keylet::account(owner)), + sponsor, + -1, beast::Journal{beast::Journal::getNullSink()}); } @@ -449,9 +497,11 @@ removeToken( curr->makeFieldAbsent(sfPreviousPageMin); } + auto const sponsor = getLedgerEntryReserveSponsor(view, prev); adjustOwnerCount( view, view.peek(keylet::account(owner)), + sponsor, -1, beast::Journal{beast::Journal::getNullSink()}); @@ -489,9 +539,15 @@ removeToken( view.update(next); } - view.erase(curr); + auto const sponsor = getLedgerEntryReserveSponsor(view, curr); + adjustOwnerCount( + view, + view.peek(keylet::account(owner)), + sponsor, + -1, + beast::Journal{beast::Journal::getNullSink()}); - 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_ @@ -506,13 +562,14 @@ removeToken( 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()}); + { + adjustOwnerCount( + view, + view.peek(keylet::account(owner)), + getLedgerEntryReserveSponsor(view, prev), + -1, + beast::Journal{beast::Journal::getNullSink()}); + } return tesSUCCESS; } @@ -655,8 +712,13 @@ deleteTokenOffer(ApplyView& view, std::shared_ptr const& offer) false)) return false; + auto const sponsor = getLedgerEntryReserveSponsor(view, offer); adjustOwnerCount( - view, view.peek(keylet::account(owner)), -1, beast::Journal{beast::Journal::getNullSink()}); + view, + view.peek(keylet::account(owner)), + sponsor, + -1, + beast::Journal{beast::Journal::getNullSink()}); view.erase(offer); return true; @@ -935,6 +997,7 @@ tokenOfferCreatePreclaim( TER tokenOfferCreateApply( ApplyView& view, + STTx const& tx, AccountID const& acctID, STAmount const& amount, std::optional const& dest, @@ -946,9 +1009,11 @@ 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 sponsor = getTxReserveSponsor(view, tx); + if (auto const ret = checkInsufficientReserve(view, tx, acct, priorBalance, sponsor, 1); + !isTesSuccess(ret)) + return ret; auto const offerID = keylet::nftoffer(acctID, seqProxy.value()); @@ -995,11 +1060,13 @@ tokenOfferCreateApply( if (dest) (*offer)[sfDestination] = *dest; + addSponsorToLedgerEntry(offer, sponsor); + view.insert(offer); } // Update owner count. - adjustOwnerCount(view, view.peek(acctKeylet), 1, j); + adjustOwnerCount(view, view.peek(acctKeylet), sponsor, 1, j); return tesSUCCESS; } diff --git a/src/libxrpl/ledger/helpers/OfferHelpers.cpp b/src/libxrpl/ledger/helpers/OfferHelpers.cpp index 3d63240fd0..f5815d1040 100644 --- a/src/libxrpl/ledger/helpers/OfferHelpers.cpp +++ b/src/libxrpl/ledger/helpers/OfferHelpers.cpp @@ -1,6 +1,7 @@ #include // #include +#include #include #include #include @@ -48,7 +49,8 @@ offerDelete(ApplyView& view, std::shared_ptr const& sle, beast::Journal j) } } - adjustOwnerCount(view, view.peek(keylet::account(owner)), -1, j); + auto const sponsor = getLedgerEntryReserveSponsor(view, sle); + adjustOwnerCount(view, view.peek(keylet::account(owner)), sponsor, -1, j); view.erase(sle); diff --git a/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp index a9fab07194..1e68022c00 100644 --- a/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp +++ b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include namespace xrpl { @@ -47,7 +48,8 @@ closeChannel( XRPL_ASSERT( (*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount"); (*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance]; - adjustOwnerCount(view, sle, -1, j); + auto const sponsor = getLedgerEntryReserveSponsor(view, slep); + adjustOwnerCount(view, sle, sponsor, -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 2c676f14af..c5730e46d0 100644 --- a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp +++ b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -176,6 +177,7 @@ trustCreate( // Issuer should be the account being set. std::uint32_t uQualityIn, std::uint32_t uQualityOut, + std::optional const& sponsorAccountID, beast::Journal j) { JLOG(j.trace()) << "trustCreate: " << to_string(uSrcAccountID) << ", " @@ -261,8 +263,14 @@ trustCreate( uFlags |= (bSetHigh ? lsfLowNoRipple : lsfHighNoRipple); } + std::shared_ptr sponsorSle = {}; + if (sponsorAccountID) + sponsorSle = view.peek(keylet::account(*sponsorAccountID)); + 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); @@ -298,6 +306,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); @@ -347,11 +358,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->setFieldU32(sfFlags, flags & (!bSenderHigh ? ~lsfLowReserve : ~lsfHighReserve)); + removeSponsorFromLedgerEntry(state, !bSenderHigh ? sfLowSponsor : sfHighSponsor); + // Balance is zero, receiver reserve is clear. if (!after // Balance is zero. && ((flags & (bSenderHigh ? lsfLowReserve : lsfHighReserve)) == 0u)) @@ -451,6 +466,7 @@ issueIOU( limit, 0, 0, + {}, j); } @@ -601,6 +617,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, @@ -628,10 +645,19 @@ addEmptyHolding( // If the line already exists, don't create it again. if (view.read(index)) return tecDUPLICATE; + auto const& sponsorAccountID = + !isPseudoAccount(sleDst) ? getTxReserveSponsorAccountID(tx) : std::nullopt; // 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, + sponsorAccountID ? view.read(keylet::account(*sponsorAccountID)) + : std::shared_ptr(), + 1); + !isTesSuccess(ret)) return tecNO_LINE_INSUF_RESERVE; return trustCreate( @@ -649,6 +675,7 @@ addEmptyHolding( /*saLimit=*/STAmount{Issue{currency, dstId}}, /*uQualityIn=*/0, /*uQualityOut=*/0, + sponsorAccountID, journal); } @@ -690,11 +717,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)) @@ -704,11 +734,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( @@ -748,6 +781,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."; @@ -758,7 +794,7 @@ deleteAMMTrustLine( if ((sleState->getFlags() & uFlags) == 0u) 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 79785e1c5d..4366a1f761 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -453,6 +454,7 @@ canAddHolding(ReadView const& view, Asset const& asset) TER addEmptyHolding( ApplyView& view, + STTx const& tx, AccountID const& accountID, XRPAmount priorBalance, Asset const& asset, @@ -460,7 +462,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()); } @@ -468,13 +470,17 @@ 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()); } @@ -522,6 +528,7 @@ directSendNoFeeIOU( AccountID const& uReceiverID, STAmount const& saAmount, bool bCheckIssuer, + std::optional const& sponsorAccountID, beast::Journal j) { AccountID const& issuer = saAmount.getIssuer(); @@ -589,7 +596,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->setFieldU32( @@ -653,6 +665,7 @@ directSendNoFeeIOU( saReceiverLimit, 0, 0, + sponsorAccountID, j); } @@ -667,6 +680,7 @@ directSendNoLimitIOU( STAmount const& saAmount, STAmount& saActual, beast::Journal j, + std::optional const& sponsorAccountID, WaiveTransferFee waiveFee) { auto const& issuer = saAmount.getIssuer(); @@ -679,7 +693,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, sponsorAccountID, j); if (!isTesSuccess(ter)) return ter; saActual = saAmount; @@ -697,10 +712,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, sponsorAccountID, j); if (tesSUCCESS == terResult) - terResult = directSendNoFeeIOU(view, uSenderID, issuer, saActual, true, j); + terResult = + directSendNoFeeIOU(view, uSenderID, issuer, saActual, true, sponsorAccountID, j); return terResult; } @@ -716,6 +733,7 @@ directSendNoLimitMultiIOU( MultiplePaymentDestinations const& receivers, STAmount& actual, beast::Journal j, + std::optional const& sponsorAccountID, WaiveTransferFee waiveFee) { auto const& issuer = issue.getIssuer(); @@ -743,7 +761,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, sponsorAccountID, j)) return ter; actual += amount; // Do not add amount to takeFromSender, because directSendNoFeeIOU took @@ -766,14 +785,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, sponsorAccountID, j)) return terResult; } if (senderID != issuer && takeFromSender) { - if (TER const terResult = - directSendNoFeeIOU(view, senderID, issuer, takeFromSender, true, j)) + if (TER const terResult = directSendNoFeeIOU( + view, senderID, issuer, takeFromSender, true, sponsorAccountID, j)) return terResult; } @@ -787,6 +807,7 @@ accountSendIOU( AccountID const& uReceiverID, STAmount const& saAmount, beast::Journal j, + std::optional const& sponsorAccountID, WaiveTransferFee waiveFee) { if (view.rules().enabled(fixAMMv1_1)) @@ -818,7 +839,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, sponsorAccountID, waiveFee); } /* XRP send which does not check reserve and can do pure adjustment. @@ -906,6 +928,7 @@ accountSendMultiIOU( Issue const& issue, MultiplePaymentDestinations const& receivers, beast::Journal j, + std::optional const& sponsorAccountID, WaiveTransferFee waiveFee) { XRPL_ASSERT_PARTS( @@ -917,7 +940,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, sponsorAccountID, waiveFee); } /* XRP send which does not check reserve and can do pure adjustment. @@ -1342,7 +1366,8 @@ directSendNoFee( { return saAmount.asset().visit( [&](Issue const&) { - return directSendNoFeeIOU(view, uSenderID, uReceiverID, saAmount, bCheckIssuer, j); + return directSendNoFeeIOU( + view, uSenderID, uReceiverID, saAmount, bCheckIssuer, std::nullopt, j); }, [&](MPTIssue const&) { XRPL_ASSERT(!bCheckIssuer, "xrpl::directSendNoFee : not checking issuer"); @@ -1357,12 +1382,14 @@ accountSend( AccountID const& uReceiverID, STAmount const& saAmount, beast::Journal j, + std::optional const& sponsorAccountID, 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, sponsorAccountID, waiveFee); }, [&](MPTIssue const&) { return accountSendMPT( @@ -1377,13 +1404,15 @@ accountSendMulti( Asset const& asset, MultiplePaymentDestinations const& receivers, beast::Journal j, + std::optional const& sponsorAccountID, 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, sponsorAccountID, 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 7c771423a8..0c6b896077 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -79,6 +79,7 @@ enum class LedgerNameSpace : std::uint16_t { VAULT = 'V', LOAN_BROKER = 'l', // lower-case L LOAN = 'L', + SPONSORSHIP = '>', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -314,6 +315,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 8429c51aea..0f259dbf67 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -159,6 +159,14 @@ InnerObjectFormats::InnerObjectFormats() {sfTxnSignature, soeOPTIONAL}, {sfSigners, soeOPTIONAL}, }); + + add(sfSponsorSignature.jsonName.c_str(), + 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 9f8bd6a2ba..0354e47f0b 100644 --- a/src/libxrpl/protocol/LedgerFormats.cpp +++ b/src/libxrpl/protocol/LedgerFormats.cpp @@ -14,6 +14,7 @@ LedgerFormats::getCommonFields() {sfLedgerIndex, soeOPTIONAL}, {sfLedgerEntryType, soeREQUIRED}, {sfFlags, soeREQUIRED}, + {sfSponsor, soeOPTIONAL}, }; return commonFields; } diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index dd150aaca6..6f9e9ea88f 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -283,6 +283,14 @@ STTx::checkSign(Rules const& rules) const if (auto const ret = checkSign(rules, counterSig); !ret) return Unexpected("Counterparty: " + ret.error()); } + + if (isFieldPresent(sfSponsorSignature)) + { + auto const sponsorSignatureObj = getFieldObject(sfSponsorSignature); + if (auto const ret = checkSign(rules, sponsorSignatureObj); !ret) + return Unexpected("Sponsor: " + ret.error()); + } + return {}; } diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index bd3acfee64..9c7f488f2c 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -107,6 +107,7 @@ transResults() 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(tecBAD_PROOF, "Proof cannot be verified"), + 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."), @@ -218,6 +219,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 4492ae271b..6cfe715c66 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -28,6 +28,9 @@ TxFormats::getCommonFields() {sfSigners, soeOPTIONAL}, // submit_multisigned {sfNetworkID, soeOPTIONAL}, {sfDelegate, soeOPTIONAL}, + {sfSponsor, soeOPTIONAL}, + {sfSponsorFlags, soeOPTIONAL}, + {sfSponsorSignature, soeOPTIONAL}, }; return commonFields; } diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index d939c07652..44e78ee7fa 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -147,6 +148,49 @@ Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask) return temBAD_SIGNER; } + 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)) + { + 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)) || 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 (auto const ret = preflight0(ctx, flagMask)) return ret; @@ -187,6 +231,14 @@ Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask) if (ctx.tx.isFlag(tfInnerBatchTxn) != ctx.parentBatchId.has_value()) return temINVALID_INNER_BATCH; + // Sponsor checks + 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; } @@ -290,6 +342,35 @@ Transactor::checkPermissionImpl( return tesSUCCESS; } +NotTEC +Transactor::checkSponsor(ReadView const& view, STTx const& tx) +{ + if (!tx.isFieldPresent(sfSponsor)) + return tesSUCCESS; + + auto const hasSponsorSignature = tx.isFieldPresent(sfSponsorSignature); + + if (hasSponsorSignature) + return tesSUCCESS; + + auto const sponsorSle = + view.read(keylet::sponsor(tx.getAccountID(sfSponsor), tx.getAccountID(sfAccount))); + + // sponsorship object missing for pre-funded tx + if (!sponsorSle) + return terNO_SPONSORSHIP; + + auto const sponsorFlags = tx.getFieldU32(sfSponsorFlags); + + if (sponsorFlags & spfSponsorFee && sponsorSle->isFlag(lsfSponsorshipRequireSignForFee)) + return terNO_SPONSORSHIP; + + if (sponsorFlags & spfSponsorReserve && sponsorSle->isFlag(lsfSponsorshipRequireSignForReserve)) + return terNO_SPONSORSHIP; + + return tesSUCCESS; +} + XRPAmount Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) { @@ -298,6 +379,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 @@ -305,7 +387,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. @@ -375,12 +465,41 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) if (feePaid == beast::zero) return tesSUCCESS; - auto const id = ctx.tx.getFeePayer(); - auto const sle = ctx.view.read(keylet::account(id)); - if (!sle) - return terNO_ACCOUNT; + auto const payer = getFeePayer(ctx.view, ctx.tx); + auto const payerSle = ctx.view.read(payer.entry); - auto const balance = (*sle)[sfBalance].xrp(); + if (!payerSle) + { + if (payer.type == FeePayerType::SponsorPreFunded) + // Sanity check: already checked in checkSponsor + return tefINTERNAL; // LCOV_EXCL_LINE + + return terNO_ACCOUNT; + } + + XRPAmount maxSpendable = beast::zero; + + if (payer.type == FeePayerType::SponsorPreFunded) + { + if (payerSle->getType() != ltSPONSORSHIP) + return tefINTERNAL; // LCOV_EXCL_LINE + + if (payerSle->isFieldPresent(payer.balanceField)) + maxSpendable = payerSle->getFieldAmount(payer.balanceField).xrp(); + + if (payerSle->isFieldPresent(sfMaxFee)) + { + auto const cap = payerSle->getFieldAmount(sfMaxFee).xrp(); + maxSpendable = std::min(maxSpendable, cap); + } + } + else + { + if (payerSle->getType() != ltACCOUNT_ROOT) + return tefINTERNAL; // LCOV_EXCL_LINE + + maxSpendable = payerSle->getFieldAmount(payer.balanceField).xrp(); + } // NOTE: Because preclaim evaluates against a static readview, it // does not reflect fee deductions from other transactions paid by @@ -389,12 +508,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::zero) && !ctx.view.open()) + if ((maxSpendable > beast::zero) && !ctx.view.open()) { // Closed ledger, non-zero balance, less than fee return tecINSUFF_FEE; @@ -411,16 +530,23 @@ 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 payer = getFeePayer(view(), ctx_.tx); + auto const sle = view().peek(payer.entry); + + JLOG(j_.trace()) << "Fee payer: " + to_string(payer.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 != account_) - view().update(sle); // done in `apply()` for the account + auto const feeAmountAfter = sle->getFieldAmount(payer.balanceField) - feePaid; + + if (feeAmountAfter == beast::zero && payer.balanceField == sfFeeAmount) + // Because ltSponsorship.sfFeeAmount is soeOptional + sle->makeFieldAbsent(payer.balanceField); + else + sle->setFieldAmount(payer.balanceField, feeAmountAfter); + + view().update(sle); // VFALCO Should we call view().rawDestroyXRP() here as well? return tesSUCCESS; @@ -594,7 +720,8 @@ Transactor::ticketDelete( } // Update the Ticket owner's reserve. - adjustOwnerCount(view, sleAccount, -1, j); + auto const sponsor = getLedgerEntryReserveSponsor(view, sleTicket); + adjustOwnerCount(view, sleAccount, sponsor, -1, j); // Remove Ticket from ledger. view.erase(sleTicket); @@ -685,6 +812,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)) @@ -1018,11 +1161,13 @@ Transactor::reset(XRPAmount fee) if (!txnAcct) return {tefINTERNAL, beast::zero}; - auto const payerSle = view().peek(keylet::account(ctx_.tx.getFeePayer())); + auto const payer = getFeePayer(view(), ctx_.tx); + auto const payerSle = view().peek(payer.entry); + if (!payerSle) return {tefINTERNAL, beast::zero}; // LCOV_EXCL_LINE - auto const balance = payerSle->getFieldAmount(sfBalance).xrp(); + auto const balance = payerSle->getFieldAmount(payer.balanceField).xrp(); // balance should have already been checked in checkFee / preFlight. XRPL_ASSERT( @@ -1041,7 +1186,13 @@ 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::zero && payer.balanceField == sfFeeAmount) + // Because ltSponsorship.sfFeeAmount is soeOptional + payerSle->makeFieldAbsent(payer.balanceField); + else + payerSle->setFieldAmount(payer.balanceField, feeAmountAfter); + TER const ter{consumeSeqProxy(txnAcct)}; XRPL_ASSERT(isTesSuccess(ter), "xrpl::Transactor::reset : result is tesSUCCESS"); @@ -1055,6 +1206,33 @@ Transactor::reset(XRPAmount fee) return {ter, fee}; } +FeePayer +Transactor::getFeePayer(ReadView const& view, STTx const& tx) +{ + if (tx.isFieldPresent(sfSponsor) && (tx.getFieldU32(sfSponsorFlags) & spfSponsorFee)) + { + 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{ + keylet::account(sponsorAccountID), sfBalance, FeePayerType::SponsorCoSigned}; + + // pre funded + return FeePayer{sponsorshipKeylet, sfFeeAmount, FeePayerType::SponsorPreFunded}; + } + + auto const payerAccountKeylet = keylet::account(tx.getFeePayer()); + auto const payerType = + tx.isFieldPresent(sfDelegate) ? FeePayerType::Delegate : FeePayerType::Account; + + return FeePayer{payerAccountKeylet, sfBalance, 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 e329c3fc39..1f2d602583 100644 --- a/src/libxrpl/tx/applySteps.cpp +++ b/src/libxrpl/tx/applySteps.cpp @@ -174,6 +174,9 @@ invoke_preclaim(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 = Transactor::checkPermission(ctx.view, ctx.tx)) return result; diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp index 1d6756eaca..894d01857e 100644 --- a/src/libxrpl/tx/invariants/InvariantCheck.cpp +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp @@ -117,6 +117,10 @@ XRPNotCreated::visitEntry( if (isXRP((*before)[sfAmount])) drops_ -= (*before)[sfAmount].xrp().drops(); break; + case ltSPONSORSHIP: + if (before->isFieldPresent(sfFeeAmount)) + drops_ -= (*before)[sfFeeAmount].xrp().drops(); + break; default: break; } @@ -137,6 +141,10 @@ XRPNotCreated::visitEntry( 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; } @@ -495,6 +503,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] : directAccountKeylets) { @@ -853,8 +875,10 @@ ValidPseudoAccounts::visitEntry( // 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(); @@ -881,6 +905,12 @@ ValidPseudoAccounts::visitEntry( { 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 7365fc7b1a..0f82766ea2 100644 --- a/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp +++ b/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp @@ -138,6 +138,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 true; + } 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 0000000000..72fdb5c842 --- /dev/null +++ b/src/libxrpl/tx/invariants/SponsorshipInvariant.cpp @@ -0,0 +1,116 @@ +#include +// +#include + +namespace xrpl { + +// Add new sponsorship-related invariants implementations +void +SponsorshipOwnerCountsMatch::visitEntry( + bool, + 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 getSponsoredOwnerCount = [](std::shared_ptr const& sle) -> std::uint32_t { + if (sle && sle->getType() == ltACCOUNT_ROOT) + return sle->getFieldU32(sfSponsoredOwnerCount); + 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); + + deltaSponsoredOwnerCount_ += (afterSponsored - beforeSponsored); + deltaSponsoringOwnerCount_ += (afterSponsoring - beforeSponsoring); + + if (getOwnerCount(after) < getSponsoredOwnerCount(after)) + invalidOwnerCountLessThanSponsoredOwnerCount_ += 1; +} + +bool +SponsorshipOwnerCountsMatch::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) +{ + if (deltaSponsoredOwnerCount_ != deltaSponsoringOwnerCount_) + { + JLOG(j.fatal()) << "Invariant failed: SponsoredOwnerCount does not " + "equal SponsoringOwnerCount 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) +{ + 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 ab2efcf39a..92e8e6dd70 100644 --- a/src/libxrpl/tx/paths/BookStep.cpp +++ b/src/libxrpl/tx/paths/BookStep.cpp @@ -698,7 +698,8 @@ 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, std::nullopt, j_); !isTesSuccess(err)) { return true; diff --git a/src/libxrpl/tx/paths/MPTEndpointStep.cpp b/src/libxrpl/tx/paths/MPTEndpointStep.cpp index 8944df953d..f03892ec40 100644 --- a/src/libxrpl/tx/paths/MPTEndpointStep.cpp +++ b/src/libxrpl/tx/paths/MPTEndpointStep.cpp @@ -390,7 +390,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_, std::nullopt, 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 0000000000..af231ed55f --- /dev/null +++ b/src/libxrpl/tx/transactors/Sponsor/SponsorshipSet.cpp @@ -0,0 +1,298 @@ +#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) +{ + auto const flags = ctx.tx.getFlags(); + + if ((flags & tfSponsorshipSetRequireSignForFee) && + (flags & tfSponsorshipClearRequireSignForFee)) + return temINVALID_FLAG; + if ((flags & tfSponsorshipSetRequireSignForReserve) && + (flags & 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 ((flags & tfDeleteObject) != 0u) + { + // can not combine with any modification flags when deleting + constexpr std::uint32_t modifyFlags = tfSponsorshipSetRequireSignForFee | + tfSponsorshipSetRequireSignForReserve | tfSponsorshipClearRequireSignForFee | + tfSponsorshipClearRequireSignForReserve; + + if (flags & modifyFlags) + 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; +} + +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::doApply() +{ + auto const sponsorAccountID = ctx_.tx[~sfCounterpartySponsor].value_or(account_); + auto const sponseeAccountID = ctx_.tx[~sfSponsee].value_or(account_); + + 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 + + auto const sponsor = getLedgerEntryReserveSponsor(ctx_.view(), sponsorObjSle); + adjustOwnerCount(ctx_.view(), sponsorAccSle, sponsor, -1, ctx_.journal); + + ctx_.view().dirRemove( + keylet::ownerDir(sponsorAccountID), + (*sponsorObjSle)[sfOwnerNode], + sponsorObjSle->key(), + false); + ctx_.view().dirRemove( + keylet::ownerDir(sponseeAccountID), + (*sponsorObjSle)[sfSponseeNode], + sponsorObjSle->key(), + false); + + // transfer feeAmount from ledger entry + if (sponsorObjSle->isFieldPresent(sfFeeAmount)) + { + auto const feeAmount = sponsorObjSle->getFieldAmount(sfFeeAmount); + (*sponsorAccSle)[sfBalance] += feeAmount; + } + + ctx_.view().erase(sponsorObjSle); + + return tesSUCCESS; + } + + 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 (!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); + !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; + + adjustOwnerCount(view(), sponsorAccSle, reserveSponsorAccSle, 1, ctx_.journal); + addSponsorToLedgerEntry(newSle, reserveSponsorAccSle); + + ctx_.view().insert(newSle); + return tesSUCCESS; + } + + // Update + if (feeAmount) + { + auto const currentFeeAmount = (*sponsorObjSle)[~sfFeeAmount].value_or(XRPAmount(0)); + auto feeAmountDelta = XRPAmount(*feeAmount - currentFeeAmount); + + if (feeAmountDelta > beast::zero && feeAmountDelta > (*sponsorAccSle)[sfBalance]) + return tecUNFUNDED; + + // transfer feeAmount to ledger entry + if (feeAmountDelta != beast::zero) + { + (*sponsorAccSle)[sfBalance] -= feeAmountDelta; + + if (*feeAmount == XRPAmount(0)) + (*sponsorObjSle).makeFieldAbsent(sfFeeAmount); + else + (*sponsorObjSle).setFieldAmount(sfFeeAmount, *feeAmount); + } + } + + if (maxFee) + { + if (*maxFee == XRPAmount(0)) + (*sponsorObjSle).makeFieldAbsent(sfMaxFee); + else + (*sponsorObjSle)[sfMaxFee] = *maxFee; + } + + if (reserveCount) + { + if (*reserveCount == 0) + (*sponsorObjSle).makeFieldAbsent(sfReserveCount); + else + (*sponsorObjSle)[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; +} + +} // 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 0000000000..c914de5085 --- /dev/null +++ b/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp @@ -0,0 +1,594 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +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 (flags & 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 (flags & 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 (flags & 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: + 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 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."); + // LCOV_EXCL_STOP + } + default: + return sfSponsor; + } +}; + +TER +SponsorshipTransfer::preclaim(PreclaimContext const& ctx) +{ + auto const index = ctx.tx[~sfObjectID]; + auto const flags = ctx.tx.getFlags(); + auto const newSponsor = getTxReserveSponsor(ctx.view, ctx.tx); + + 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 (flags & tfSponsorshipCreate) + { + if (!newSponsor) + return tecNO_PERMISSION; + + // check object is not sponsored yet + if (sle->isFieldPresent(sponsorField)) + return tecNO_PERMISSION; + } + else if (flags & tfSponsorshipReassign) + { + if (!newSponsor) + return tecNO_PERMISSION; + + // check object is already sponsored + if (!sle->isFieldPresent(sponsorField)) + return tecNO_PERMISSION; + } + else if (flags & tfSponsorshipEnd) + { + if (newSponsor) + 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 + if (auto const ter = checkInsufficientReserve( + ctx.view, + ctx.tx, + sponseeSle, + sponseeSle->getFieldAmount(sfBalance), + newSponsor, + ownerCountDelta); + !isTesSuccess(ter)) + return ter; + } + else + { + if (flags & tfSponsorshipCreate) + { + if (!newSponsor) + return tecNO_PERMISSION; + + // check account is not sponsored yet + if (sponseeSle->isFieldPresent(sfSponsor)) + return tecNO_PERMISSION; + } + else if (flags & tfSponsorshipReassign) + { + if (!newSponsor) + return tecNO_PERMISSION; + + // check account is already sponsored + if (!sponseeSle->isFieldPresent(sfSponsor)) + return tecNO_PERMISSION; + } + else if (flags & tfSponsorshipEnd) + { + if (newSponsor) + 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. + if (auto const ter = checkInsufficientReserve( + ctx.view, + ctx.tx, + sponseeSle, + sponseeSle->getFieldAmount(sfBalance), + newSponsor, + 0, + 1); + !isTesSuccess(ter)) + return ter; + } + + return tesSUCCESS; +} + +TER +adjustReserveCount( + ApplyView& view, + AccountID const& account, + AccountID const& sponsor, + int32_t delta) +{ + if (delta == 0) + return tesSUCCESS; + auto const sponsorKeylet = keylet::sponsor(sponsor, account); + auto const sponsorSle = view.peek(sponsorKeylet); + if (!sponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const reserveCount = sponsorSle->getFieldU32(sfReserveCount); + int32_t const afterReserveCount = reserveCount + delta; + + if (afterReserveCount < 0) + { + // already checked in preclaim() + return tefINTERNAL; // LCOV_EXCL_LINE + } + + if (afterReserveCount == 0) + { + sponsorSle->makeFieldAbsent(sfReserveCount); + } + else + { + sponsorSle->setFieldU32(sfReserveCount, afterReserveCount); + } + view.update(sponsorSle); + return tesSUCCESS; +} + +TER +SponsorshipTransfer::doApply() +{ + auto const& tx = ctx_.tx; + + auto const index = tx[~sfObjectID]; + auto const flags = tx.getFlags(); + bool const isObjectSponsor = index != std::nullopt; + + auto const sponseeAccountID = tx[~sfSponsee].value_or(account_); + auto const sponseeSle = view().peek(keylet::account(sponseeAccountID)); + if (!sponseeSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const setSponsorFieldU32 = [](auto const& sle, auto const& field, auto const& delta) { + int32_t const newValue = static_cast(sle->getFieldU32(field)) + delta; + if (newValue <= 0) + { + sle->makeFieldAbsent(field); + } + else + { + sle->setFieldU32(field, static_cast(newValue)); + } + }; + + 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 + + auto const ownerCountDelta = getLedgerEntryOwnerCount(objSle); + + auto const& sponsorField = getLedgerEntrySponsorField(objSle, *ownerAccountID); + + if (flags & tfSponsorshipCreate) + { + auto const newSponsorAccountID = tx.getAccountID(sfSponsor); + XRPL_ASSERT(!!newSponsorAccountID, "New sponsor is required when creating sponsorship"); + + // update owner's sponsored count + setSponsorFieldU32(ownerSle, sfSponsoredOwnerCount, ownerCountDelta); + view().update(ownerSle); + + // increment new sponsor's sponsoring count + auto const newSponsorSle = view().peek(keylet::account(newSponsorAccountID)); + if (!newSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + setSponsorFieldU32(newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); + 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 = adjustReserveCount( + view(), sponseeAccountID, newSponsorAccountID, -ownerCountDelta); + !isTesSuccess(ter)) + return ter; + } + } + else if (flags & 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 + setSponsorFieldU32(oldSponsorSle, sfSponsoringOwnerCount, -ownerCountDelta); + view().update(oldSponsorSle); + + // increment new sponsor's sponsoring count + auto const newSponsorSle = view().peek(keylet::account(newSponsorAccountID)); + if (!newSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + setSponsorFieldU32(newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); + 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 = adjustReserveCount( + view(), sponseeAccountID, newSponsorAccountID, -ownerCountDelta); + !isTesSuccess(ter)) + return ter; + } + + // payback the reserve count if ltSponsorship exists + if (auto const sponsorSle = + view().exists(keylet::sponsor(oldSponsorAccountID, sponseeAccountID)); + sponsorSle) + { + if (auto const ter = adjustReserveCount( + view(), sponseeAccountID, oldSponsorAccountID, ownerCountDelta); + !isTesSuccess(ter)) + return ter; + } + } + else if (flags & 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 + setSponsorFieldU32(sponseeSle, sfSponsoredOwnerCount, -ownerCountDelta); + view().update(sponseeSle); + + // decrement old sponsoring count + setSponsorFieldU32(oldSponsorSle, sfSponsoringOwnerCount, -ownerCountDelta); + view().update(oldSponsorSle); + + // payback the reserve count if ltSponsorship exists + if (auto const sponsorSle = + view().exists(keylet::sponsor(oldSponsorAccountID, sponseeAccountID)); + sponsorSle) + { + if (auto const ter = adjustReserveCount( + view(), sponseeAccountID, oldSponsorAccountID, ownerCountDelta); + !isTesSuccess(ter)) + return ter; + } + + // remove sponsor from object + objSle->makeFieldAbsent(sponsorField); + view().update(objSle); + } + } + else + { + if (flags & 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 + setSponsorFieldU32(newSponsorSle, sfSponsoringAccountCount, 1); + view().update(newSponsorSle); + + // set new sponsor to account + sponseeSle->setAccountID(sfSponsor, newSponsorAccountID); + view().update(sponseeSle); + } + else if (flags & 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 + setSponsorFieldU32(newSponsorSle, sfSponsoringAccountCount, 1); + 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 + setSponsorFieldU32(oldSponsorSle, sfSponsoringAccountCount, -1); + view().update(oldSponsorSle); + + // set new sponsor to account + sponseeSle->setAccountID(sfSponsor, newSponsorAccountID); + view().update(sponseeSle); + } + else if (flags & 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 + setSponsorFieldU32(oldSponsorSle, sfSponsoringAccountCount, -1); + view().update(oldSponsorSle); + } + } + + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/account/AccountDelete.cpp b/src/libxrpl/tx/transactors/account/AccountDelete.cpp index a5b0fb60f8..25b3507d52 100644 --- a/src/libxrpl/tx/transactors/account/AccountDelete.cpp +++ b/src/libxrpl/tx/transactors/account/AccountDelete.cpp @@ -16,9 +16,9 @@ #include #include #include +#include namespace xrpl { - bool AccountDelete::checkExtraFeatures(PreflightContext const& ctx) { @@ -251,6 +251,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. @@ -371,12 +380,43 @@ 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 + // populated with a value of 0. + return tefINTERNAL; // LCOV_EXCL_LINE + + if (sponsoringAccountCount == 1) + sponsorSle->makeFieldAbsent(sfSponsoringAccountCount); + else + sponsorSle->setFieldU32(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 90ab8daf6f..05b8b17c22 100644 --- a/src/libxrpl/tx/transactors/account/SignerListSet.cpp +++ b/src/libxrpl/tx/transactors/account/SignerListSet.cpp @@ -195,8 +195,9 @@ removeSignersFromLedger( // LCOV_EXCL_STOP } + auto const sponsor = getLedgerEntryReserveSponsor(view, signers); adjustOwnerCount( - view, view.peek(accountKeylet), removeFromOwnerCount, registry.getJournal("View")); + view, view.peek(accountKeylet), sponsor, removeFromOwnerCount, registry.getJournal("View")); view.erase(signers); @@ -295,19 +296,17 @@ 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]}; - constexpr int addedOwnerCount = 1; std::uint32_t const flags{lsfOneOwnerCount}; - XRPAmount const newReserve{view().fees().accountReserve(oldOwnerCount + addedOwnerCount)}; - // 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; + // with CreateTicket. + auto const sponsor = getTxReserveSponsor(ctx_.view(), ctx_.tx); + if (auto const ret = checkInsufficientReserve( + ctx_.view(), ctx_.tx, sle, preFeeBalance_, sponsor, addedOwnerCount); + !isTesSuccess(ret)) + return ret; // Everything's ducky. Add the ltSIGNER_LIST to the ledger. auto signerList = std::make_shared(signerListKeylet); @@ -329,7 +328,8 @@ SignerListSet::replaceSignerList() // If we succeeded, the new entry counts against the // creator's reserve. - adjustOwnerCount(view(), sle, addedOwnerCount, viewJ); + adjustOwnerCount(view(), sle, sponsor, addedOwnerCount, viewJ); + addSponsorToLedgerEntry(signerList, sponsor); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp index bec17b010a..160c785f23 100644 --- a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp +++ b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp @@ -415,8 +415,7 @@ transferHelper( return tecINTERNAL; // LCOV_EXCL_LINE { - auto const ownerCount = sleSrc->getFieldU32(sfOwnerCount); - auto const reserve = psb.fees().accountReserve(ownerCount); + auto const reserve = calculateReserve(sleSrc, psb.fees()); auto const availableBalance = [&]() -> STAmount { STAmount curBal = (*sleSrc)[sfBalance]; @@ -708,7 +707,8 @@ finalizeClaimHelper( // Remove the claim id from the ledger outerSb.erase(sleClaimID); - adjustOwnerCount(outerSb, sleOwner, -1, j); + auto const sponsor = getLedgerEntryReserveSponsor(outerSb, sleClaimID); + adjustOwnerCount(outerSb, sleOwner, sponsor, -1, j); } } @@ -927,6 +927,7 @@ TER applyCreateAccountAttestations( ApplyView& view, RawView& rawView, + STTx const& tx, TIter attBegin, TIter attEnd, AccountID const& doorAccount, @@ -1008,10 +1009,10 @@ applyCreateAccountAttestations( // Check reserve auto const balance = (*sleDoor)[sfBalance]; - auto const reserve = psb.fees().accountReserve((*sleDoor)[sfOwnerCount] + 1); - - if (balance < reserve) - return Unexpected(tecINSUFFICIENT_RESERVE); + // Door account should not have a sponsor + if (auto const ret = checkInsufficientReserve(psb, tx, sleDoor, balance, {}, 1); + !isTesSuccess(ret)) + return Unexpected(ret); // tecINSUFFICIENT_RESERVE } std::vector atts; @@ -1117,7 +1118,9 @@ applyCreateAccountAttestations( return tecINTERNAL; // LCOV_EXCL_LINE // Reserve was already checked - adjustOwnerCount(psb, sleDoor, 1, j); + auto const sponsor = getTxReserveSponsor(psb, tx); + adjustOwnerCount(psb, sleDoor, sponsor, 1, j); + addSponsorToLedgerEntry(createdSleClaimID, sponsor); psb.insert(createdSleClaimID); psb.update(sleDoor); } @@ -1290,6 +1293,7 @@ attestationDoApply(ApplyContext& ctx) return applyCreateAccountAttestations( ctx.view(), ctx.rawView(), + ctx.tx, &*att, &*att + 1, thisDoor, @@ -1415,10 +1419,11 @@ 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 sponsor = getTxReserveSponsor(ctx.view, ctx.tx); + if (auto const ret = + checkInsufficientReserve(ctx.view, ctx.tx, sleAcc, balance, sponsor, 1); + !isTesSuccess(ret)) + return ret; } return tesSUCCESS; @@ -1460,7 +1465,9 @@ XChainCreateBridge::doApply() (*sleBridge)[sfOwnerNode] = *page; } - adjustOwnerCount(ctx_.view(), sleAcct, 1, ctx_.journal); + auto const sponsor = getTxReserveSponsor(ctx_.view(), ctx_.tx); + adjustOwnerCount(ctx_.view(), sleAcct, sponsor, 1, ctx_.journal); + addSponsorToLedgerEntry(sleBridge, sponsor); ctx_.view().insert(sleBridge); ctx_.view().update(sleAcct); @@ -1961,10 +1968,11 @@ 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 sponsor = getTxReserveSponsor(ctx.view, ctx.tx); + if (auto const ret = + checkInsufficientReserve(ctx.view, ctx.tx, sleAcc, balance, sponsor, 1); + !isTesSuccess(ret)) + return ret; } return tesSUCCESS; @@ -2020,7 +2028,9 @@ XChainCreateClaimID::doApply() (*sleClaimID)[sfOwnerNode] = *page; } - adjustOwnerCount(ctx_.view(), sleAcct, 1, ctx_.journal); + auto const sponsor = getTxReserveSponsor(ctx_.view(), ctx_.tx); + adjustOwnerCount(ctx_.view(), sleAcct, sponsor, 1, ctx_.journal); + addSponsorToLedgerEntry(sleClaimID, sponsor); 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 be3b434fb6..8f2d709582 100644 --- a/src/libxrpl/tx/transactors/check/CheckCancel.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCancel.cpp @@ -85,7 +85,8 @@ CheckCancel::doApply() // If we succeeded, update the check owner's reserve. auto const sleSrc = view().peek(keylet::account(srcId)); - adjustOwnerCount(view(), sleSrc, -1, viewJ); + auto const sponsor = getLedgerEntryReserveSponsor(view(), sleCheck); + adjustOwnerCount(view(), sleSrc, sponsor, -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 bbc53a67bf..3a99deebf6 100644 --- a/src/libxrpl/tx/transactors/check/CheckCash.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCash.cpp @@ -282,6 +282,8 @@ CheckCash::doApply() // LCOV_EXCL_STOP } + auto const sponsorSle = getLedgerEntryReserveSponsor(psb, sleCheck); + // Preclaim already checked that source has at least the requested // funds. // @@ -310,7 +312,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, sponsorSle ? 0 : -1, viewJ)}; // Now, how much do they need in order to be successful? STAmount const xrpDeliver{ @@ -360,14 +362,20 @@ CheckCash::doApply() STAmount const flowDeliver{ optDeliverMin ? maxDeliverMin() : ctx_.tx.getFieldAmount(sfAmount)}; + auto const sponsorAccountID = getTxReserveSponsorAccountID(ctx_.tx); + std::shared_ptr sponsorSle = {}; + if (sponsorAccountID) + sponsorSle = psb.peek(keylet::account(*sponsorAccountID)); + // Check reserve. Return destination account SLE if enough reserve, // otherwise return nullptr. auto checkReserve = [&]() -> std::shared_ptr { auto sleDst = psb.peek(keylet::account(account_)); // 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); + !isTesSuccess(ret)) { JLOG(j_.trace()) << "Trust line does not exist. " "Insufficient reserve to create line."; @@ -424,6 +432,7 @@ CheckCash::doApply() Issue(currency, account_), // limit of zero 0, // quality in 0, // quality out + sponsorAccountID, // sponsor viewJ); // journal !isTesSuccess(ter)) { @@ -470,7 +479,8 @@ CheckCash::doApply() if (sleDst == nullptr) return tecINSUFFICIENT_RESERVE; - if (auto const err = checkCreateMPT(psb, mptID, account_, j_); + if (auto const err = + checkCreateMPT(psb, mptID, account_, sponsorAccountID, j_); !isTesSuccess(err)) { return err; @@ -556,7 +566,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)), sponsorSle, -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 da63da8c07..9671802a95 100644 --- a/src/libxrpl/tx/transactors/check/CheckCreate.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCreate.cpp @@ -177,13 +177,10 @@ 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 sponsor = getTxReserveSponsor(view(), ctx_.tx); + if (auto const ret = checkInsufficientReserve(view(), ctx_.tx, sle, preFeeBalance_, sponsor, 1); + !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(); @@ -236,7 +233,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, sponsor, 1, viewJ); + addSponsorToLedgerEntry(sleCheck, sponsor); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp index 9c0018a647..187c86cf96 100644 --- a/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp +++ b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp @@ -82,16 +82,16 @@ 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 newSponsor = getTxReserveSponsor(view(), ctx_.tx); + if (auto const ret = + checkInsufficientReserve(view(), ctx_.tx, sleSubject, preFeeBalance_, newSponsor, 1); + !isTesSuccess(ret)) + return ret; auto const credType(ctx_.tx[sfCredentialType]); Keylet const credentialKey = keylet::credential(account_, issuer, credType); auto const sleCred = view().peek(credentialKey); // Checked in preclaim() + auto const currentSponsor = getLedgerEntryReserveSponsor(view(), sleCred); if (checkExpired(sleCred, view().header().parentCloseTime)) { @@ -104,8 +104,10 @@ CredentialAccept::doApply() sleCred->setFieldU32(sfFlags, lsfAccepted); view().update(sleCred); - adjustOwnerCount(view(), sleIssuer, -1, j_); - adjustOwnerCount(view(), sleSubject, 1, j_); + adjustOwnerCount(view(), sleIssuer, currentSponsor, -1, j_); + removeSponsorFromLedgerEntry(sleCred); + adjustOwnerCount(view(), sleSubject, newSponsor, 1, j_); + addSponsorToLedgerEntry(sleCred, newSponsor); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp index f8f0f01b63..e8550cb73e 100644 --- a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp +++ b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp @@ -115,12 +115,11 @@ 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 sponsor = getTxReserveSponsor(view(), ctx_.tx); + if (auto const ret = + checkInsufficientReserve(view(), ctx_.tx, sleIssuer, preFeeBalance_, sponsor, 1); + !isTesSuccess(ret)) + return ret; sleCred->setAccountID(sfSubject, subject); sleCred->setAccountID(sfIssuer, account_); @@ -138,7 +137,8 @@ CredentialCreate::doApply() return tecDIR_FULL; sleCred->setFieldU64(sfIssuerNode, *page); - adjustOwnerCount(view(), sleIssuer, 1, j_); + adjustOwnerCount(view(), sleIssuer, sponsor, 1, j_); + addSponsorToLedgerEntry(sleCred, sponsor); } if (subject == account_) diff --git a/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp index f141832fdc..9fba16cdec 100644 --- a/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp +++ b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp @@ -82,11 +82,11 @@ 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 sponsor = getTxReserveSponsor(view(), ctx_.tx); + if (auto const ret = + checkInsufficientReserve(view(), ctx_.tx, sleOwner, preFeeBalance_, sponsor, 1); + !isTesSuccess(ret)) + return ret; sle = std::make_shared(delegateKey); sle->setAccountID(sfAccount, account_); @@ -114,7 +114,8 @@ DelegateSet::doApply() (*sle)[sfDestinationNode] = *destPage; ctx_.view().insert(sle); - adjustOwnerCount(ctx_.view(), sleOwner, 1, ctx_.journal); + adjustOwnerCount(ctx_.view(), sleOwner, sponsor, 1, ctx_.journal); + addSponsorToLedgerEntry(sle, sponsor); return tesSUCCESS; } @@ -154,7 +155,8 @@ DelegateSet::deleteDelegate(ApplyView& view, std::shared_ptr const& sle, be if (!sleOwner) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleOwner, -1, j); + auto const sponsor = getLedgerEntryReserveSponsor(view, sle); + adjustOwnerCount(view, sleOwner, sponsor, -1, j); view.erase(sle); diff --git a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp index e437da3e70..61e1906cc3 100644 --- a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp @@ -207,6 +207,7 @@ AMMClawback::applyGuts(Sandbox& sb) std::tie(result, newLPTokenBalance, amountWithdraw, amount2Withdraw) = AMMWithdraw::equalWithdrawTokens( sb, + ctx_.tx, *ammSle, holder, ammAccount, @@ -295,6 +296,7 @@ AMMClawback::equalWithdrawMatchingOneAmount( // tfee is actually not used, so pass tfee as 0. return AMMWithdraw::equalWithdrawTokens( sb, + ctx_.tx, ammSle, holder, ammAccount, @@ -328,6 +330,7 @@ AMMClawback::equalWithdrawMatchingOneAmount( return AMMWithdraw::withdraw( sb, + ctx_.tx, ammSle, ammAccount, holder, @@ -348,6 +351,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 7e3805adce..5ca7951526 100644 --- a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp @@ -122,15 +122,33 @@ 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::zero) + 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); + // 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); + !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::zero) + { + 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; @@ -269,7 +287,8 @@ applyCreate(ApplyContext& ctx_, Sandbox& sb, AccountID const& account_, beast::J sb.insert(ammSle); // Send LPT to LP. - auto res = accountSend(sb, accountId, account_, lpTokens, ctx_.journal); + auto const sponsor = getTxReserveSponsorAccountID(ctx_.tx); + auto res = accountSend(sb, accountId, account_, lpTokens, ctx_.journal, sponsor); if (!isTesSuccess(res)) { JLOG(j_.debug()) << "AMM Instance: failed to send LPT " << lpTokens; @@ -298,17 +317,30 @@ applyCreate(ApplyContext& ctx_, Sandbox& sb, AccountID const& account_, beast::J } } - if (auto const err = createMPToken(sb, mptID, accountId, flags); !isTesSuccess(err)) + if (auto const err = createMPToken(sb, mptID, accountId, std::nullopt, 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, + std::nullopt, // 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, + std::nullopt, // 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 b4da5aa1fc..72472bfa90 100644 --- a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp @@ -212,7 +212,18 @@ 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) + + auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); + auto const accountSle = ctx.view.read(keylet::account(accountID)); + if (auto const ret = checkInsufficientReserve( + ctx.view, + ctx.tx, + accountSle, + accountSle->getFieldAmount(sfBalance) - deposit, + sponsorSle, + 1, + !sle); + isTesSuccess(ret)) return TER(tesSUCCESS); if (sle) return tecUNFUNDED_AMM; @@ -337,12 +348,34 @@ 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::zero) { - STAmount const xrpBalance = xrpLiquid(ctx.view, accountID, 1, ctx.j); - // Insufficient reserve - if (xrpBalance <= beast::zero) + if (ctx.view.rules().enabled(featureSponsor)) { - JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves"; - return tecINSUF_RESERVE_LINE; + auto const accountSle = ctx.view.read(keylet::account(accountID)); + auto const sponsor = getTxReserveSponsor(ctx.view, ctx.tx); + // Insufficient reserve + if (auto const ret = checkInsufficientReserve( + ctx.view, + ctx.tx, + accountSle, + accountSle->getFieldAmount(sfBalance), + sponsor, + 1); + !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::zero) + { + JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves"; + return tecINSUF_RESERVE_LINE; + } + JLOG(ctx.j.fatal()) << "AMM Instance: insufficient reserves"; } } @@ -488,6 +521,8 @@ AMMDeposit::deposit( std::optional const& lpTokensDepositMin, std::uint16_t tfee) { + auto const sponsor = getTxReserveSponsorAccountID(ctx_.tx); + // Check account has sufficient funds. // Return true if it does, false otherwise. auto checkBalance = [&](auto const& depositAmount) -> TER { @@ -497,8 +532,11 @@ AMMDeposit::deposit( { auto const& lpIssue = lpTokensDeposit.get(); // Adjust the reserve if LP doesn't have LPToken trustline - auto const sle = view.read(keylet::line(account_, lpIssue.account, lpIssue.currency)); - if (xrpLiquid(view, account_, !sle, j_) >= depositAmount) + auto const trustlineExists = + view.exists(keylet::line(account_, lpIssue.account, lpIssue.currency)); + auto const ownerCountAdj = trustlineExists ? 0 : 1; + if (xrpLiquid(view, sponsor.value_or(account_), sponsor ? ownerCountAdj : 0, j_) >= + depositAmount) return tesSUCCESS; } else if ( @@ -552,7 +590,13 @@ AMMDeposit::deposit( } auto res = accountSend( - view, account_, ammAccount, amountDepositActual, ctx_.journal, WaiveTransferFee::Yes); + view, + account_, + ammAccount, + amountDepositActual, + ctx_.journal, + std::nullopt, // don't sponsor for AMM Trustline + WaiveTransferFee::Yes); if (!isTesSuccess(res)) { JLOG(ctx_.journal.debug()) << "AMM Deposit: failed to deposit " << amountDepositActual; @@ -571,7 +615,13 @@ AMMDeposit::deposit( } res = accountSend( - view, account_, ammAccount, *amount2DepositActual, ctx_.journal, WaiveTransferFee::Yes); + view, + account_, + ammAccount, + *amount2DepositActual, + ctx_.journal, + std::nullopt, // don't sponsor for AMM Trustline + WaiveTransferFee::Yes); if (!isTesSuccess(res)) { JLOG(ctx_.journal.debug()) @@ -581,7 +631,7 @@ AMMDeposit::deposit( } // Deposit LP tokens - res = accountSend(view, ammAccount, account_, lpTokensDepositActual, ctx_.journal); + res = accountSend(view, ammAccount, account_, lpTokensDepositActual, ctx_.journal, sponsor); 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 7c46fbb469..9cf2d958da 100644 --- a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp @@ -329,6 +329,7 @@ AMMWithdraw::applyGuts(Sandbox& sb) { return equalWithdrawLimit( sb, + ctx_.tx, *ammSle, ammAccountID, amountBalance, @@ -342,6 +343,7 @@ AMMWithdraw::applyGuts(Sandbox& sb) { return singleWithdrawTokens( sb, + ctx_.tx, *ammSle, ammAccountID, amountBalance, @@ -353,17 +355,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, @@ -414,6 +425,7 @@ AMMWithdraw::doApply() std::pair AMMWithdraw::withdraw( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -427,6 +439,7 @@ AMMWithdraw::withdraw( STAmount newLPTokenBalance; std::tie(ter, newLPTokenBalance, std::ignore, std::ignore) = withdraw( view, + tx, ammSle, ammAccount, account_, @@ -447,6 +460,7 @@ AMMWithdraw::withdraw( std::tuple> AMMWithdraw::withdraw( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, AccountID const& account, @@ -570,6 +584,10 @@ AMMWithdraw::withdraw( } } + // this is also called from AMMClawback, but only AMMWithdraw does sponsor + // the new trustline + auto const sponsor = tx[sfAccount] == account ? getTxReserveSponsorAccountID(tx) : std::nullopt; + // Check the reserve in case a trustline or MPT has to be created bool const enabledFixAMMv1_2 = view.rules().enabled(fixAMMv1_2); // If seated after a call to sufficientReserve() then MPToken must be @@ -595,17 +613,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 sponsorSle = getTxReserveSponsor(view, tx); + + auto const balance = (*sleAccount)[sfBalance]->xrp(); + std::uint32_t const count = ownerCount(sponsorSle ? sponsorSle : sleAccount); // See also TrustSet::doApply() and MPTokenAuthorize::authorize() - XRPAmount const reserve( - (ownerCount < 2) ? XRPAmount(beast::zero) - : view.fees().accountReserve(ownerCount + 1)); - - auto const balance_ = isIssue ? std::max(priorBalance, balance.xrp()) : priorBalance; - if (balance_ < reserve) - return tecINSUFFICIENT_RESERVE; + if (count >= 2) + { + if (auto const ret = checkInsufficientReserve( + view, + tx, + sleAccount, + std::max(priorBalance, balance), + sponsor ? view.read(keylet::account(*sponsor)) + : std::shared_ptr(), + 1); + !isTesSuccess(ret)) + return ret; + } } return tesSUCCESS; }; @@ -620,7 +646,7 @@ AMMWithdraw::withdraw( !isTesSuccess(err)) return err; - if (auto const err = checkCreateMPT(view, mptIssue, account, journal); + if (auto const err = checkCreateMPT(view, mptIssue, account, sponsor, journal); !isTesSuccess(err)) { return err; @@ -637,7 +663,7 @@ AMMWithdraw::withdraw( // Withdraw amountWithdraw auto res = accountSend( - view, ammAccount, account, amountWithdrawActual, journal, WaiveTransferFee::Yes); + view, ammAccount, account, amountWithdrawActual, journal, sponsor, WaiveTransferFee::Yes); if (!isTesSuccess(res)) { // LCOV_EXCL_START @@ -656,7 +682,13 @@ AMMWithdraw::withdraw( return {res, STAmount{}, STAmount{}, STAmount{}}; res = accountSend( - view, ammAccount, account, *amount2WithdrawActual, journal, WaiveTransferFee::Yes); + view, + ammAccount, + account, + *amount2WithdrawActual, + journal, + sponsor, + WaiveTransferFee::Yes); if (!isTesSuccess(res)) { // LCOV_EXCL_START @@ -701,6 +733,7 @@ adjustLPTokensIn( std::pair AMMWithdraw::equalWithdrawTokens( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -714,6 +747,7 @@ AMMWithdraw::equalWithdrawTokens( STAmount newLPTokenBalance; std::tie(ter, newLPTokenBalance, std::ignore, std::ignore) = equalWithdrawTokens( view, + tx, ammSle, account_, ammAccount, @@ -765,6 +799,7 @@ AMMWithdraw::deleteAMMAccountIfEmpty( std::tuple> AMMWithdraw::equalWithdrawTokens( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const account, AccountID const& ammAccount, @@ -787,6 +822,7 @@ AMMWithdraw::equalWithdrawTokens( { return withdraw( view, + tx, ammSle, ammAccount, account, @@ -822,6 +858,7 @@ AMMWithdraw::equalWithdrawTokens( return withdraw( view, + tx, ammSle, ammAccount, account, @@ -874,6 +911,7 @@ AMMWithdraw::equalWithdrawTokens( std::pair AMMWithdraw::equalWithdrawLimit( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -894,6 +932,7 @@ AMMWithdraw::equalWithdrawLimit( { return withdraw( view, + tx, ammSle, ammAccount, amountBalance, @@ -926,6 +965,7 @@ AMMWithdraw::equalWithdrawLimit( } return withdraw( view, + tx, ammSle, ammAccount, amountBalance, @@ -944,6 +984,7 @@ AMMWithdraw::equalWithdrawLimit( std::pair AMMWithdraw::singleWithdraw( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -972,6 +1013,7 @@ AMMWithdraw::singleWithdraw( return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE return withdraw( view, + tx, ammSle, ammAccount, amountBalance, @@ -995,6 +1037,7 @@ AMMWithdraw::singleWithdraw( std::pair AMMWithdraw::singleWithdrawTokens( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -1013,6 +1056,7 @@ AMMWithdraw::singleWithdrawTokens( { return withdraw( view, + tx, ammSle, ammAccount, amountBalance, @@ -1048,6 +1092,7 @@ AMMWithdraw::singleWithdrawTokens( std::pair AMMWithdraw::singleWithdrawEPrice( Sandbox& view, + STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -1092,6 +1137,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 73c1e50e4b..368a538500 100644 --- a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp @@ -780,10 +780,10 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel) return {tefINTERNAL, false}; { - XRPAmount const reserve = - sb.fees().accountReserve(sleCreator->getFieldU32(sfOwnerCount) + 1); - - if (preFeeBalance_ < reserve) + auto const sponsor = getTxReserveSponsor(sb, ctx_.tx); + if (auto const ret = + checkInsufficientReserve(sb, ctx_.tx, sleCreator, preFeeBalance_, sponsor, 1); + !isTesSuccess(ret)) { // If we are here, the signing account had an insufficient reserve // *prior* to our processing. If something actually crossed, then @@ -816,7 +816,8 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel) } // Update owner count. - adjustOwnerCount(sb, sleCreator, 1, viewJ); + auto const sponsor = getTxReserveSponsor(sb, ctx_.tx); + adjustOwnerCount(sb, sleCreator, sponsor, 1, viewJ); JLOG(j_.trace()) << "adding to book: " << to_string(saTakerPays.asset()) << " : " << to_string(saTakerGets.asset()) @@ -885,6 +886,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel) sleOffer->setFlag(lsfSell); if (domainID) sleOffer->setFieldH256(sfDomainID, *domainID); + addSponsorToLedgerEntry(sleOffer, sponsor); // 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 0d5b63635f..c55da695ba 100644 --- a/src/libxrpl/tx/transactors/did/DIDDelete.cpp +++ b/src/libxrpl/tx/transactors/did/DIDDelete.cpp @@ -42,7 +42,8 @@ DIDDelete::deleteSLE( if (!sleOwner) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleOwner, -1, j); + auto const sponsor = getLedgerEntryReserveSponsor(view, sle); + adjustOwnerCount(view, sleOwner, sponsor, -1, j); view.update(sleOwner); // Remove object from ledger diff --git a/src/libxrpl/tx/transactors/did/DIDSet.cpp b/src/libxrpl/tx/transactors/did/DIDSet.cpp index cd5c9bbc96..828b180e5d 100644 --- a/src/libxrpl/tx/transactors/did/DIDSet.cpp +++ b/src/libxrpl/tx/transactors/did/DIDSet.cpp @@ -56,13 +56,12 @@ addSLE(ApplyContext& ctx, std::shared_ptr const& sle, AccountID const& owne 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 sponsor = getTxReserveSponsor(ctx.view(), ctx.tx); + auto const balance = STAmount((*sleAccount)[sfBalance]).xrp(); + if (auto const ret = + checkInsufficientReserve(ctx.view(), ctx.tx, sleAccount, balance, sponsor, 1); + !isTesSuccess(ret)) + return ret; // Add ledger object to ledger ctx.view().insert(sle); @@ -75,7 +74,8 @@ addSLE(ApplyContext& ctx, std::shared_ptr const& sle, AccountID const& owne return tecDIR_FULL; // LCOV_EXCL_LINE (*sle)[sfOwnerNode] = *page; } - adjustOwnerCount(ctx.view(), sleAccount, 1, ctx.journal); + adjustOwnerCount(ctx.view(), sleAccount, sponsor, 1, ctx.journal); + addSponsorToLedgerEntry(sle, sponsor); 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 faa862b424..073f7b9904 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp @@ -167,6 +167,7 @@ EscrowCancel::doApply() [&](T const&) { return escrowUnlockApplyHelper( ctx_.view(), + ctx_.tx, parityRate, slep, preFeeBalance_, @@ -194,7 +195,8 @@ EscrowCancel::doApply() } } - adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); + auto const sponsor = getLedgerEntryReserveSponsor(ctx_.view(), slep); + adjustOwnerCount(ctx_.view(), sle, sponsor, -1, ctx_.journal); ctx_.view().update(sle); // Remove escrow from ledger diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp index 789ca8ab1a..103a3aa570 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp @@ -398,16 +398,18 @@ 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 sponsor = getTxReserveSponsor(view(), ctx_.tx); + if (auto const ret = checkInsufficientReserve(ctx_.view(), ctx_.tx, sle, balance, sponsor, 1); + !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); + !isTesSuccess(ret)) return tecUNFUNDED; } @@ -499,7 +501,8 @@ EscrowCreate::doApply() } // increment owner count - adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); + adjustOwnerCount(ctx_.view(), sle, sponsor, 1, ctx_.journal); + addSponsorToLedgerEntry(slep, sponsor); 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 8714bab5ff..a6b5006bc8 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp @@ -342,6 +342,7 @@ EscrowFinish::doApply() [&](T const&) { return escrowUnlockApplyHelper( ctx_.view(), + ctx_.tx, lockedRate, sled, preFeeBalance_, @@ -373,7 +374,8 @@ EscrowFinish::doApply() // Adjust source owner count auto const sle = ctx_.view().peek(keylet::account(account)); - adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); + auto const sponsor = getLedgerEntryReserveSponsor(ctx_.view(), slep); + adjustOwnerCount(ctx_.view(), sle, sponsor, -1, ctx_.journal); ctx_.view().update(sle); // Remove escrow from ledger diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp index c7493a71cd..d6a32c6ac2 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp @@ -325,7 +325,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); } //------------------------------------------------------------------------------ diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp index 4630e6a360..d5ea0e72a2 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp @@ -109,7 +109,8 @@ LoanBrokerCoverDeposit::doApply() auto const brokerPseudoID = broker->at(sfAccount); // Transfer assets from depositor to pseudo-account. - if (auto ter = accountSend(view(), account_, brokerPseudoID, amount, j_, WaiveTransferFee::Yes)) + if (auto ter = + accountSend(view(), account_, 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 a755db7942..5ea99deb77 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp @@ -128,11 +128,11 @@ LoanBrokerDelete::doApply() { auto const coverAvailable = STAmount{vaultAsset, broker->at(sfCoverAvailable)}; if (auto const ter = accountSend( - view(), brokerPseudoID, account_, coverAvailable, j_, WaiveTransferFee::Yes)) + view(), brokerPseudoID, account_, 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)); @@ -157,10 +157,6 @@ LoanBrokerDelete::doApply() return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE } - view().erase(brokerPseudoSLE); - - view().erase(broker); - { auto owner = view().peek(keylet::account(account_)); if (!owner) @@ -168,9 +164,18 @@ 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 + auto const sponsor = getLedgerEntryReserveSponsor(view(), broker); + adjustOwnerCount(view(), owner, sponsor, -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 f8813ddbef..13ac600444 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp @@ -218,12 +218,27 @@ LoanBrokerSet::doApply() if (auto const ter = dirLink(view, vaultPseudoID, broker, sfVaultNode)) return ter; // LCOV_EXCL_LINE + auto const sponsor = getTxReserveSponsor(view, tx); + + if (auto const ret = + checkInsufficientReserve(view, tx, owner, preFeeBalance_, {}, sponsor ? 1 : 2); + !isTesSuccess(ret)) + return ret; + + if (sponsor) + { + if (auto const ret = + checkInsufficientReserve(view, tx, owner, preFeeBalance_, sponsor, 1); + !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, sponsor, 1, j_); auto maybePseudo = createPseudoAccount(view, broker->key(), sfLoanBrokerID); if (!maybePseudo) @@ -231,7 +246,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: @@ -252,6 +268,8 @@ LoanBrokerSet::doApply() if (auto const coverLiq = tx[~sfCoverRateLiquidation]) broker->at(sfCoverRateLiquidation) = *coverLiq; + addSponsorToLedgerEntry(broker, sponsor); + 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 39b28f5110..61efd76881 100644 --- a/src/libxrpl/tx/transactors/lending/LoanDelete.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanDelete.cpp @@ -91,13 +91,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) @@ -117,7 +115,11 @@ LoanDelete::doApply() } } // Decrement the borrower's owner count - adjustOwnerCount(view, borrowerSle, -1, j_); + auto const sponsor = getLedgerEntryReserveSponsor(view, loanSle); + adjustOwnerCount(view, borrowerSle, sponsor, -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 8c3e625963..c6a4b7044a 100644 --- a/src/libxrpl/tx/transactors/lending/LoanManage.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanManage.cpp @@ -271,6 +271,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 d400fb3630..b4d59c132a 100644 --- a/src/libxrpl/tx/transactors/lending/LoanPay.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanPay.cpp @@ -528,7 +528,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, @@ -546,6 +546,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 9caca6b1f4..998ec0436a 100644 --- a/src/libxrpl/tx/transactors/lending/LoanSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp @@ -474,14 +474,16 @@ LoanSet::doApply() } } - adjustOwnerCount(view, borrowerSle, 1, j_); + auto const sponsorSle = getTxReserveSponsor(view, tx); { - auto const ownerCount = borrowerSle->at(sfOwnerCount); auto const balance = account_ == 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); + !isTesSuccess(ret)) + return ret; } + adjustOwnerCount(view, borrowerSle, sponsorSle, 1, j_); // Account for the origination fee using two payments // @@ -494,7 +496,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 @@ -517,7 +519,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, @@ -535,6 +537,7 @@ LoanSet::doApply() vaultAsset, {{borrower, loanAssetsToBorrower}, {brokerOwner, originationFee}}, j_, + {}, // Vault and Broker cannot be sponsored WaiveTransferFee::Yes)) return ter; @@ -580,6 +583,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 @@ -595,7 +599,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 95955bd0bd..daaa57bc8a 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp @@ -354,7 +354,9 @@ NFTokenAcceptOffer::transferNFToken( std::uint32_t const buyerOwnerCountBefore = sleBuyer->getFieldU32(sfOwnerCount); - auto const insertRet = nft::insertToken(view(), buyer, std::move(tokenAndPage->token)); + auto const sponsor = getTxReserveSponsorAccountID(ctx_.tx); + auto const insertRet = + nft::insertToken(view(), ctx_.tx, buyer, sponsor, std::move(tokenAndPage->token)); // if fixNFTokenReserve is enabled, check if the buyer has sufficient // reserve to own a new object, if their OwnerCount changed. @@ -374,9 +376,12 @@ NFTokenAcceptOffer::transferNFToken( auto const buyerOwnerCountAfter = sleBuyer->getFieldU32(sfOwnerCount); if (buyerOwnerCountAfter > buyerOwnerCountBefore) { - if (auto const reserve = view().fees().accountReserve(buyerOwnerCountAfter); - buyerBalance < reserve) - return tecINSUFFICIENT_RESERVE; + auto const sponsorSle = account_ == buyer ? getTxReserveSponsor(ctx_.view(), ctx_.tx) + : std::shared_ptr(); + if (auto const ret = checkInsufficientReserve( + ctx_.view(), ctx_.tx, sleBuyer, buyerBalance, sponsorSle, 0); + !isTesSuccess(ret)) + return ret; } } diff --git a/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp index 19bf34c560..3341b20902 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp @@ -68,6 +68,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 7bebbd0070..6fb0f0d0f4 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp @@ -285,7 +285,9 @@ NFTokenMint::doApply() object.setFieldVL(sfURI, *uri); }); - if (TER const ret = nft::insertToken(ctx_.view(), account_, std::move(newToken)); + auto const sponsor = getTxReserveSponsorAccountID(ctx_.tx); + if (TER const ret = + nft::insertToken(ctx_.view(), ctx_.tx, account_, sponsor, std::move(newToken)); !isTesSuccess(ret)) return ret; @@ -296,6 +298,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], @@ -316,9 +319,16 @@ NFTokenMint::doApply() view().read(keylet::account(account_))->getFieldU32(sfOwnerCount); ownerCountAfter > ownerCountBefore) { - if (auto const reserve = view().fees().accountReserve(ownerCountAfter); - preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; + auto const sponsor = getTxReserveSponsor(ctx_.view(), ctx_.tx); + if (auto const ret = checkInsufficientReserve( + ctx_.view(), + ctx_.tx, + view().read(keylet::account(account_)), + preFeeBalance_, + sponsor, + 0); + !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 bde403f821..d2a4fdb20c 100644 --- a/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp +++ b/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp @@ -62,7 +62,8 @@ OracleDelete::deleteOracle( auto const count = sle->getFieldArray(sfPriceDataSeries).size() > 5 ? -2 : -1; - adjustOwnerCount(view, sleOwner, count, j); + auto const sponsor = getLedgerEntryReserveSponsor(view, sle); + adjustOwnerCount(view, sleOwner, sponsor, count, j); view.erase(sle); diff --git a/src/libxrpl/tx/transactors/oracle/OracleSet.cpp b/src/libxrpl/tx/transactors/oracle/OracleSet.cpp index 772756ad6d..db1e6ca116 100644 --- a/src/libxrpl/tx/transactors/oracle/OracleSet.cpp +++ b/src/libxrpl/tx/transactors/oracle/OracleSet.cpp @@ -39,6 +39,12 @@ OracleSet::preflight(PreflightContext const& ctx) return tesSUCCESS; } +uint32_t +OracleSet::calculateOracleReserve(std::size_t count) +{ + return count > 5 ? 2 : 1; +} + TER OracleSet::preclaim(PreclaimContext const& ctx) { @@ -131,9 +137,17 @@ 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 { @@ -141,7 +155,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()) @@ -149,28 +163,16 @@ OracleSet::preclaim(PreclaimContext const& ctx) if (pairs.size() > maxOracleDataSeries) 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 sponsor = getTxReserveSponsor(ctx.view, ctx.tx); + if (auto const ret = + checkInsufficientReserve(ctx.view, ctx.tx, sleSetter, balance, sponsor, adjustReserve); + !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) { @@ -209,7 +211,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)) { @@ -247,11 +249,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 currentSponsorSle = getLedgerEntryReserveSponsor(ctx_.view(), sle); + auto const newSponsorSle = getTxReserveSponsor(ctx_.view(), ctx_.tx); + + // decrease current sponsored owner count + adjustOwnerCount(ctx_.view(), accountSle, currentSponsorSle, -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 + auto const sponsorSle = getLedgerEntryReserveSponsor(ctx_.view(), sle); + adjustOwnerCount(ctx_.view(), accountSle, sponsorSle, adjust, ctx_.journal); + } + ctx_.view().update(sle); } else @@ -298,9 +326,14 @@ 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 sponsor = getTxReserveSponsor(ctx_.view(), ctx_.tx); + auto const accountSle = ctx_.view().peek(keylet::account(ctx_.tx[sfAccount])); + if (!accountSle) return tefINTERNAL; // LCOV_EXCL_LINE + adjustOwnerCount(ctx_.view(), accountSle, sponsor, count, ctx_.journal); + + addSponsorToLedgerEntry(sle, sponsor); ctx_.view().insert(sle); } diff --git a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp index 0f4681a3ad..4ed2a79bbe 100644 --- a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp +++ b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp @@ -142,13 +142,11 @@ 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 sponsor = getTxReserveSponsor(view(), ctx_.tx); + if (auto const ret = + checkInsufficientReserve(view(), ctx_.tx, sleOwner, preFeeBalance_, sponsor, 1); + !isTesSuccess(ret)) + return ret; // Preclaim already verified that the Preauth entry does not yet exist. // Create and populate the Preauth entry. @@ -172,7 +170,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, sponsor, 1, j_); + addSponsorToLedgerEntry(slePreauth, sponsor); } else if (ctx_.tx.isFieldPresent(sfUnauthorize)) { @@ -189,13 +188,11 @@ 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 sponsor = getTxReserveSponsor(view(), ctx_.tx); + if (auto const ret = + checkInsufficientReserve(view(), ctx_.tx, sleOwner, preFeeBalance_, sponsor, 1); + !isTesSuccess(ret)) + return ret; // Preclaim already verified that the Preauth entry does not yet exist. // Create and populate the Preauth entry. @@ -233,7 +230,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, sponsor, 1, j_); + addSponsorToLedgerEntry(slePreauth, sponsor); } else if (ctx_.tx.isFieldPresent(sfUnauthorizeCredentials)) { @@ -271,7 +269,8 @@ DepositPreauth::removeFromLedger(ApplyView& view, uint256 const& preauthIndex, b if (!sleOwner) return tefINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(view, sleOwner, -1, j); + auto const sponsor = getLedgerEntryReserveSponsor(view, slePreauth); + adjustOwnerCount(view, sleOwner, sponsor, -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 2eb13d742d..73c30de391 100644 --- a/src/libxrpl/tx/transactors/payment/Payment.cpp +++ b/src/libxrpl/tx/transactors/payment/Payment.cpp @@ -94,6 +94,19 @@ Payment::preflight(PreflightContext const& ctx) std::uint32_t const txFlags = tx.getFlags(); + if ((txFlags & tfSponsorCreatedAccount) != 0u) + { + if (!ctx.rules.enabled(featureSponsor)) + return temDISABLED; + + if ((txFlags & tfNoRippleDirect) != 0u || (txFlags & tfPartialPayment) != 0u || + (txFlags & tfLimitQuality) != 0u) + return temINVALID_FLAG; + + if (!dstAmount.native()) + return temBAD_AMOUNT; + } + if (!MPTokensV2 && isDstMPT && ctx.tx.isFieldPresent(sfPaths)) return temMALFORMED; @@ -300,7 +313,13 @@ Payment::preclaim(PreclaimContext const& ctx) // transaction would succeed. return telNO_DST_PARTIAL; } - if (dstAmount < STAmount(ctx.view.fees().reserve)) + if ((txFlags & tfSponsorCreatedAccount) != 0u) + { + // 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. @@ -313,18 +332,26 @@ Payment::preclaim(PreclaimContext const& ctx) return tecNO_DST_INSUF_XRP; } } - else if ( - ((sleDst->getFlags() & lsfRequireDestTag) != 0u) && - !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 ((txFlags & tfSponsorCreatedAccount) != 0u) + 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->getFlags() & lsfRequireDestTag) != 0u) && + !ctx.tx.isFieldPresent(sfDestinationTag)) + { + // The tag is basically account-specific information we don't + // understand, but we can require someone to fill it in. - return tecDST_TAG_NEEDED; + // 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; + } } // Payment with at least one intermediate step and uses transitive balances. @@ -389,6 +416,19 @@ Payment::doApply() sleDst->setAccountID(sfAccount, dstAccountID); sleDst->setFieldU32(sfSequence, view().seq()); + if ((txFlags & tfSponsorCreatedAccount) != 0u) + { + auto const sponsor = view().peek(keylet::account(account_)); + if (!sponsor) + return tefINTERNAL; // LCOV_EXCL_LINE + auto const currentSponsoringAccountCount = + sponsor->getFieldU32(sfSponsoringAccountCount); + sponsor->setFieldU32(sfSponsoringAccountCount, currentSponsoringAccountCount + 1); + + addSponsorToLedgerEntry(sleDst, sponsor); + view().update(sponsor); + } + view().insert(sleDst); } else @@ -555,12 +595,10 @@ 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 = calculateReserve(sleSrc, view().fees()) + + (((txFlags & tfSponsorCreatedAccount) != 0u) ? view().fees().reserve : beast::zero); // In a delegated payment, the fee payer is the delegated account, // not the source account (account_). diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp index c12169fc0e..4ba0b5529a 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp @@ -65,12 +65,14 @@ 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); + auto const sponsor = getTxReserveSponsor(ctx.view, ctx.tx); + if (auto const ret = checkInsufficientReserve(ctx.view, ctx.tx, sle, balance, sponsor, 1); + !isTesSuccess(ret)) + return ret; - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; - - if (balance < reserve + ctx.tx[sfAmount]) + if (auto const ret = checkInsufficientReserve( + ctx.view, ctx.tx, sle, balance - ctx.tx[sfAmount], sponsor, 1); + !isTesSuccess(ret)) return tecUNFUNDED; } @@ -166,7 +168,9 @@ 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 sponsor = getTxReserveSponsor(ctx_.view(), ctx_.tx); + adjustOwnerCount(ctx_.view(), sle, sponsor, 1, ctx_.journal); + addSponsorToLedgerEntry(slep, sponsor); 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 f8e2399bb1..18b0919f2f 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp @@ -66,12 +66,15 @@ PaymentChannelFund::doApply() { // Check reserve and funds availability auto const balance = (*sle)[sfBalance]; - auto const reserve = ctx_.view().fees().accountReserve((*sle)[sfOwnerCount]); + auto const sponsor = getTxReserveSponsor(ctx_.view(), ctx_.tx); + if (auto const ret = + checkInsufficientReserve(ctx_.view(), ctx_.tx, sle, balance, sponsor, 0); + !isTesSuccess(ret)) + return ret; - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; - - if (balance < reserve + ctx_.tx[sfAmount]) + if (auto const ret = checkInsufficientReserve( + ctx_.view(), ctx_.tx, sle, balance - ctx_.tx[sfAmount], {}, 0); + !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 565631b3fd..931d0d6489 100644 --- a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp +++ b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp @@ -56,7 +56,8 @@ PermissionedDomainDelete::doApply() XRPL_ASSERT( ownerSle && ownerSle->getFieldU32(sfOwnerCount) > 0, "xrpl::PermissionedDomainDelete::doApply : nonzero owner count"); - adjustOwnerCount(view(), ownerSle, -1, ctx_.journal); + auto const sponsor = getLedgerEntryReserveSponsor(view(), slePd); + adjustOwnerCount(view(), ownerSle, sponsor, -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 30f24241aa..df58976bd1 100644 --- a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp +++ b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp @@ -93,9 +93,11 @@ 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 sponsor = getTxReserveSponsor(ctx_.view(), ctx_.tx); + if (auto const ret = + checkInsufficientReserve(ctx_.view(), ctx_.tx, ownerSle, balance, sponsor, 1); + !isTesSuccess(ret)) + return ret; Keylet const pdKeylet = keylet::permissionedDomain(account_, ctx_.tx.getFieldU32(sfSequence)); @@ -111,7 +113,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, sponsor, 1, ctx_.journal); + addSponsorToLedgerEntry(slePd, sponsor); view().insert(slePd); } diff --git a/src/libxrpl/tx/transactors/system/Batch.cpp b/src/libxrpl/tx/transactors/system/Batch.cpp index 612451c764..7c6e62ae40 100644 --- a/src/libxrpl/tx/transactors/system/Batch.cpp +++ b/src/libxrpl/tx/transactors/system/Batch.cpp @@ -204,6 +204,17 @@ Batch::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } + if (ctx.tx.isFieldPresent(sfSponsorFlags)) + { + auto const sponsorFlags = ctx.tx.getFieldU32(sfSponsorFlags); + if (sponsorFlags & spfSponsorReserve) + { + 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) { @@ -310,6 +321,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::zero) diff --git a/src/libxrpl/tx/transactors/system/TicketCreate.cpp b/src/libxrpl/tx/transactors/system/TicketCreate.cpp index c4e281c357..0529b7ffce 100644 --- a/src/libxrpl/tx/transactors/system/TicketCreate.cpp +++ b/src/libxrpl/tx/transactors/system/TicketCreate.cpp @@ -62,13 +62,11 @@ 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 sponsor = getTxReserveSponsor(view(), ctx_.tx); + if (auto const ret = checkInsufficientReserve( + view(), ctx_.tx, sleAccountRoot, preFeeBalance_, sponsor, ticketCount); + !isTesSuccess(ret)) + return ret; beast::Journal const viewJ{ctx_.registry.get().getJournal("View")}; @@ -92,6 +90,7 @@ TicketCreate::doApply() sleTicket->setAccountID(sfAccount, account_); sleTicket->setFieldU32(sfTicketSequence, curTicketSeq); + view().insert(sleTicket); auto const page = @@ -104,6 +103,7 @@ TicketCreate::doApply() return tecDIR_FULL; // LCOV_EXCL_LINE sleTicket->setFieldU64(sfOwnerNode, *page); + addSponsorToLedgerEntry(sleTicket, sponsor); } // Update the record of the number of Tickets this account owns. @@ -112,7 +112,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, sponsor, 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 195df60de4..96079f886c 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp @@ -157,6 +157,7 @@ MPTokenAuthorize::doApply() auto const& tx = ctx_.tx; return authorizeMPToken( ctx_.view(), + tx, preFeeBalance_, tx[sfMPTokenIssuanceID], account_, diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp index c90d1f8f1c..48bddecd7d 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp @@ -87,15 +87,25 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) } 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 Unexpected(tecINTERNAL); // LCOV_EXCL_LINE - if (args.priorBalance && - *(args.priorBalance) < view.fees().accountReserve((*acct)[sfOwnerCount] + 1)) - return Unexpected(tecINSUFFICIENT_RESERVE); + auto const sponsor = + !isPseudoAccount((acct)) ? getTxReserveSponsor(view, tx) : std::shared_ptr(); + if (args.priorBalance) + { + if (auto const ret = + checkInsufficientReserve(view, tx, acct, *(args.priorBalance), sponsor, 1); + !isTesSuccess(ret)) + return Unexpected(ret); // tecINSUFFICIENT_RESERVE + } auto const mptId = makeMptID(args.sequence, args.account); auto const mptIssuanceKeylet = keylet::mptIssuance(mptId); @@ -133,11 +143,13 @@ MPTokenIssuanceCreate::create(ApplyView& view, beast::Journal journal, MPTCreate if (args.mutableFlags) (*mptIssuance)[sfMutableFlags] = *args.mutableFlags; + addSponsorToLedgerEntry(mptIssuance, sponsor); + view.insert(mptIssuance); } // Update owner count. - adjustOwnerCount(view, acct, 1, journal); + adjustOwnerCount(view, acct, sponsor, 1, journal); return mptId; } @@ -148,6 +160,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 8ec1f37886..6e6861b3bf 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp @@ -46,7 +46,8 @@ MPTokenIssuanceDestroy::doApply() view().erase(mpt); - adjustOwnerCount(view(), view().peek(keylet::account(account_)), -1, j_); + auto const sponsor = getLedgerEntryReserveSponsor(view(), mpt); + adjustOwnerCount(view(), view().peek(keylet::account(account_)), sponsor, -1, j_); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/token/TrustSet.cpp b/src/libxrpl/tx/transactors/token/TrustSet.cpp index 761f249785..3447a44d77 100644 --- a/src/libxrpl/tx/transactors/token/TrustSet.cpp +++ b/src/libxrpl/tx/transactors/token/TrustSet.cpp @@ -296,8 +296,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 @@ -315,9 +313,18 @@ TrustSet::doApply() // but the incremental reserve for the trust line as // well. A person with no intention of using the gateway // could use the extra XRP for their own purposes. + auto const txSponsorAcc = getTxReserveSponsorAccountID(ctx_.tx); - XRPAmount const reserveCreate( - (uOwnerCount < 2) ? XRPAmount(beast::zero) : view().fees().accountReserve(uOwnerCount + 1)); + std::shared_ptr txSponsorSle = {}; + if (txSponsorAcc) + txSponsorSle = view().peek(keylet::account(*txSponsorAcc)); + + std::uint32_t const uOwnerCount = ownerCount(txSponsorSle ? txSponsorSle : sle); + + bool const isSponsoredAndPreFunded = txSponsorSle && !isSponsorReserveCoSigning(ctx_.tx); + // If PreFunded Sponsor, it must be checked whether sufficient + // ReserveCount exists. + bool const freeTrustLine = uOwnerCount < 2 && !isSponsoredAndPreFunded; std::uint32_t const uQualityIn(bQualityIn ? ctx_.tx.getFieldU32(sfQualityIn) : 0); std::uint32_t uQualityOut(bQualityOut ? ctx_.tx.getFieldU32(sfQualityOut) : 0); @@ -505,6 +512,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); @@ -512,10 +524,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_, txSponsorSle, 1); + isSponsoredAndPreFunded && !isTesSuccess(ret)) + return tecINSUF_RESERVE_LINE; + // Set reserve for low account. - adjustOwnerCount(view(), sleLowAccount, 1, viewJ); + adjustOwnerCount(view(), sleLowAccount, txSponsorSle, 1, viewJ); uFlagsOut |= lsfLowReserve; + addSponsorToLedgerEntry(sleRippleState, txSponsorSle, sfLowSponsor); + if (!bHigh) bReserveIncrease = true; } @@ -523,16 +545,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_, txSponsorSle, 1); + isSponsoredAndPreFunded && !isTesSuccess(ret)) + return tecINSUF_RESERVE_LINE; + // Set reserve for high account. - adjustOwnerCount(view(), sleHighAccount, 1, viewJ); + adjustOwnerCount(view(), sleHighAccount, txSponsorSle, 1, viewJ); uFlagsOut |= lsfHighReserve; + addSponsorToLedgerEntry(sleRippleState, txSponsorSle, sfHighSponsor); + if (bHigh) bReserveIncrease = true; } @@ -540,8 +574,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) @@ -554,7 +590,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_, txSponsorSle, 0); + !freeTrustLine && bReserveIncrease && !isTesSuccess(ret)) { JLOG(j_.trace()) << "Delay transaction: Insufficent reserve to " "add trust line."; @@ -582,8 +621,15 @@ 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_, + txSponsorSle, + 1); + !freeTrustLine && !isTesSuccess(ret)) // Reserve is not scaled by load. { JLOG(j_.trace()) << "Delay transaction: Line does not exist. " "Insufficent reserve to create line."; @@ -617,6 +663,7 @@ TrustSet::doApply() saLimitAllow, // Limit for who is being charged. uQualityIn, uQualityOut, + txSponsorAcc, viewJ); } diff --git a/src/libxrpl/tx/transactors/vault/VaultClawback.cpp b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp index 095b44e072..1fef0f98fa 100644 --- a/src/libxrpl/tx/transactors/vault/VaultClawback.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp @@ -392,8 +392,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_, std::nullopt, WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; @@ -402,7 +402,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()) // @@ -428,7 +428,13 @@ VaultClawback::doApply() { // Transfer assets from vault to issuer. if (auto const ter = accountSend( - view(), vaultAccount, account_, assetsRecovered, j_, WaiveTransferFee::Yes); + view(), + vaultAccount, + account_, + assetsRecovered, + j_, + std::nullopt, + 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 02f8ecb57b..d0c41aa2ec 100644 --- a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp @@ -142,10 +142,26 @@ VaultCreate::doApply() if (auto ter = dirLink(view(), account_, 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 sponsor = getTxReserveSponsor(view(), tx); + if (!ctx_.view().rules().enabled(featureSponsor)) + { + adjustOwnerCount(view(), owner, sponsor, 2, j_); + addSponsorToLedgerEntry(vault, sponsor); + if (auto const ret = + checkInsufficientReserve(view(), tx, owner, preFeeBalance_, sponsor, 0); + !isTesSuccess(ret)) + return ret; + } + else + { + // after Sponsor Amendment, check insufficient reserve first + if (auto const ret = + checkInsufficientReserve(view(), tx, owner, preFeeBalance_, sponsor, 2); + !isTesSuccess(ret)) + return ret; + adjustOwnerCount(view(), owner, sponsor, 2, j_); + addSponsorToLedgerEntry(vault, sponsor); + } auto maybePseudo = createPseudoAccount(view(), vault->key(), sfVaultID); if (!maybePseudo) @@ -154,7 +170,8 @@ VaultCreate::doApply() auto pseudoId = pseudo->at(sfAccount); auto 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()) @@ -174,6 +191,7 @@ VaultCreate::doApply() // in the vault auto maybeShare = MPTokenIssuanceCreate::create( view(), + tx, j_, { .priorBalance = std::nullopt, @@ -217,7 +235,7 @@ VaultCreate::doApply() // Explicitly create MPToken for the vault owner if (auto const err = - authorizeMPToken(view(), preFeeBalance_, mptIssuanceID, account_, ctx_.journal); + authorizeMPToken(view(), tx, preFeeBalance_, mptIssuanceID, account_, ctx_.journal); !isTesSuccess(err)) return err; @@ -225,7 +243,7 @@ VaultCreate::doApply() if ((txFlags & tfVaultPrivate) != 0u) { if (auto const err = authorizeMPToken( - view(), preFeeBalance_, mptIssuanceID, pseudoId, ctx_.journal, {}, account_); + view(), tx, preFeeBalance_, mptIssuanceID, pseudoId, ctx_.journal, {}, account_); !isTesSuccess(err)) return err; } diff --git a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp index 5a60c0032c..f692915eb8 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp @@ -87,7 +87,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); @@ -115,7 +116,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, account_))) { - if (auto const ter = removeEmptyHolding(view(), account_, MPTIssue(shareMPTID), j_); + if (auto const ter = + removeEmptyHolding(view(), ctx_.tx, account_, MPTIssue(shareMPTID), j_); !isTesSuccess(ter)) { // LCOV_EXCL_START @@ -136,7 +138,7 @@ VaultDelete::doApply() return tefBAD_LEDGER; // LCOV_EXCL_STOP } - adjustOwnerCount(view(), pseudoAcct, -1, j_); + adjustOwnerCount(view(), pseudoAcct, {}, -1, j_); view().erase(mpt); @@ -195,7 +197,8 @@ VaultDelete::doApply() } // We are destroying Vault and PseudoAccount, hence decrease by 2 - adjustOwnerCount(view(), owner, -2, j_); + auto const vaultSponsor = getLedgerEntryReserveSponsor(view(), vault); + adjustOwnerCount(view(), owner, vaultSponsor, -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 04b249d211..2b77d21458 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp @@ -151,7 +151,7 @@ VaultDeposit::doApply() if (vault->isFlag(lsfVaultPrivate) && account_ != vault->at(sfOwner)) { if (auto const err = enforceMPTokenAuthorization( - ctx_.view(), mptIssuanceID, account_, preFeeBalance_, j_); + ctx_.view(), ctx_.tx, mptIssuanceID, account_, preFeeBalance_, j_); !isTesSuccess(err)) return err; } @@ -161,7 +161,12 @@ VaultDeposit::doApply() if (!view().exists(keylet::mptoken(mptIssuanceID, account_))) { if (auto const err = authorizeMPToken( - view(), preFeeBalance_, mptIssuanceID->value(), account_, ctx_.journal); + view(), + ctx_.tx, + preFeeBalance_, + mptIssuanceID->value(), + account_, + ctx_.journal); !isTesSuccess(err)) return err; } @@ -174,6 +179,7 @@ VaultDeposit::doApply() account_ == 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 @@ -239,8 +245,14 @@ VaultDeposit::doApply() return tecLIMIT_EXCEEDED; // Transfer assets from depositor to vault. - if (auto const ter = - accountSend(view(), account_, vaultAccount, assetsDeposited, j_, WaiveTransferFee::Yes); + if (auto const ter = accountSend( + view(), + account_, + vaultAccount, + assetsDeposited, + j_, + std::nullopt, + WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; @@ -259,9 +271,11 @@ VaultDeposit::doApply() // LCOV_EXCL_STOP } + auto const sponsor = getTxReserveSponsorAccountID(ctx_.tx); + // Transfer shares from vault to depositor. - if (auto const ter = - accountSend(view(), vaultAccount, account_, sharesCreated, j_, WaiveTransferFee::Yes); + if (auto const ter = accountSend( + view(), vaultAccount, account_, sharesCreated, j_, sponsor, 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 b3d2864380..a59b083bae 100644 --- a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp @@ -241,9 +241,11 @@ VaultWithdraw::doApply() view().update(vault); auto const& vaultAccount = vault->at(sfAccount); + auto const sponsor = getTxReserveSponsorAccountID(ctx_.tx); + // Transfer shares from depositor to vault. - if (auto const ter = - accountSend(view(), account_, vaultAccount, sharesRedeemed, j_, WaiveTransferFee::Yes); + if (auto const ter = accountSend( + view(), account_, vaultAccount, sharesRedeemed, j_, sponsor, WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; @@ -252,7 +254,8 @@ VaultWithdraw::doApply() // Keep MPToken if holder is the vault owner. if (account_ != vault->at(sfOwner)) { - if (auto const ter = removeEmptyHolding(view(), account_, sharesRedeemed.asset(), j_); + if (auto const ter = + removeEmptyHolding(view(), ctx_.tx, account_, sharesRedeemed.asset(), j_); isTesSuccess(ter)) { JLOG(j_.debug()) // diff --git a/src/test/app/AMMMPT_test.cpp b/src/test/app/AMMMPT_test.cpp index 4fd7b2713a..e04db3a5a2 100644 --- a/src/test/app/AMMMPT_test.cpp +++ b/src/test/app/AMMMPT_test.cpp @@ -925,7 +925,7 @@ private: // Insufficient reserve, XRP/MPT { - Env env(*this); + Env env(*this, features); auto const starting_xrp = reserve(env, 4) + env.current()->fees().base * 4; env.fund(XRP(10'000), gw); env.fund(XRP(10'000), alice); @@ -955,7 +955,13 @@ private: 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 @@ -7005,6 +7011,7 @@ private: 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 246f18c445..55b34384f6 100644 --- a/src/test/app/AccountSet_test.cpp +++ b/src/test/app/AccountSet_test.cpp @@ -389,8 +389,10 @@ public: 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)); @@ -398,6 +400,23 @@ public: 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/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index 288177d6d7..c4d2f7492a 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -2128,7 +2128,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. { @@ -2247,7 +2250,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 = 80; + std::size_t const expectedDelegableCount = 82; BEAST_EXPECTS( delegableCount == expectedDelegableCount, diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 00bf13db22..cbc316a8fa 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -277,7 +277,75 @@ class Invariants_test : public beast::unit_test::suite // check. sleA1->at(sfBalance) = beast::zero; 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 a1 = A1.id(); + auto const sleA1 = ac.view().peek(keylet::account(a1)); + if (!sleA1) + return false; + sleA1->at(sfBalance) = beast::zero; + 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 a1 = A1.id(); + auto const sleA1 = ac.view().peek(keylet::account(a1)); + if (!sleA1) + return false; + sleA1->at(sfBalance) = beast::zero; + 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 a1 = A1.id(); + auto const sleA1 = ac.view().peek(keylet::account(a1)); + if (!sleA1) + return false; + sleA1->at(sfBalance) = beast::zero; + 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 a1 = A1.id(); + auto const sleA1 = ac.view().peek(keylet::account(a1)); + if (!sleA1) + return false; + sleA1->at(sfBalance) = beast::zero; + sleA1->setAccountID(sfSponsor, A2.id()); ac.view().erase(sleA1); @@ -1624,6 +1692,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 { @@ -1650,6 +1719,22 @@ class Invariants_test : public beast::unit_test::suite "pseudo-account has a regular key", [](SLE::pointer& sle) { sle->at(sfRegularKey) = Account("regular").id(); }, }, + { + "pseudo-account has a sponsorship field", + [](SLE::pointer& sle) { sle->at(sfSponsoredOwnerCount) = 1; }, + }, + { + "pseudo-account has a sponsorship field", + [](SLE::pointer& sle) { sle->at(sfSponsoringOwnerCount) = 1; }, + }, + { + "pseudo-account has a sponsorship field", + [](SLE::pointer& sle) { sle->at(sfSponsoringAccountCount) = 1; }, + }, + { + "pseudo-account has a sponsorship field", + [](SLE::pointer& sle) { sle->at(sfSponsor) = Account("sponsor").id(); }, + }, }); for (auto const& mod : mods) @@ -4084,6 +4169,87 @@ class Invariants_test : public beast::unit_test::suite } } + void + testSponsorship() + { + using namespace test::jtx; + using namespace std::string_literals; + testcase << "Sponsorship"; + { + auto const expect_message = + "SponsoredOwnerCount does not equal " + "SponsoringOwnerCount delta."; + + doInvariantCheck( + {{expect_message}}, [&](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( + {{expect_message}}, [&](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 expect_message = + "OwnerCount must be greater than or equal to SponsoredOwnerCount."; + + doInvariantCheck( + {{expect_message}}, [&](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 expect_message = + "Invariant failed: Net delta of SponsoringAccountCount does " + "not match net delta of sfSponsor presence."; + + doInvariantCheck( + {{expect_message}}, [&](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( + {{expect_message}}, [&](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; + }); + } + } + void testConfidentialMPTTransfer() { @@ -4300,6 +4466,7 @@ public: testVault(); testConfidentialMPTTransfer(); testMPT(); + testSponsorship(); } }; diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index fbfcf63197..ae53ca097c 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -2048,6 +2048,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::none); + test(jv, sfFeeAmount.fieldName.c_str()); + } } BEAST_EXPECT(txWithAmounts.empty()); } @@ -3293,8 +3302,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 5bdd686512..3f94df143a 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -378,15 +378,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)); @@ -394,6 +396,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/Oracle_test.cpp b/src/test/app/Oracle_test.cpp index 83b658ac41..6052b31228 100644 --- a/src/test/app/Oracle_test.cpp +++ b/src/test/app/Oracle_test.cpp @@ -1,5 +1,12 @@ #include +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { @@ -451,7 +458,7 @@ private: oracle.remove({.owner = invalid, .fee = baseFee, .err = ter(tecNO_ENTRY)}); // Invalid flags - oracle.remove({.flags = tfSellNFToken, .fee = baseFee, .err = ter(temINVALID_FLAG)}); + oracle.remove({.flags = lsfSellNFToken, .fee = baseFee, .err = ter(temINVALID_FLAG)}); // Bad fee oracle.remove({.fee = -1, .err = ter(temBAD_FEE)}); @@ -687,7 +694,7 @@ private: // alice uses a regular key with the master disabled. Account const alie{"alie", KeyType::secp256k1}; env(regkey(alice, alie)); - env(fset(alice, asfDisableMaster), sig(alice)); + env(fset(alice, lsfDisableMaster), sig(alice)); // Attach signers to alice. env(signers(alice, 2, {{becky, 1}, {bogie, 1}, {ed, 2}}), sig(alie)); diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp new file mode 100644 index 0000000000..d14fe7a298 --- /dev/null +++ b/src/test/app/Sponsor_test.cpp @@ -0,0 +1,5640 @@ +#include +#include +#include +#include +#include + +#include +#include + +#include "test/jtx/check.h" +#include "test/jtx/did.h" + +namespace xrpl { +namespace 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 env.current()->fees().accountReserve(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, testable_amendments() - 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, testable_amendments()}; + 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(); + + // 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(); + } + + void + testPseudoAccountSponsorship() + { + testcase("Pseudo account sponsorship"); + using namespace test::jtx; + Env env{*this, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const invalid("invalid"); + + Account const signer1("signer1"); + Account const signer2("signer2"); + + env.fund(XRP(10000), alice, 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)); + + // Signer account doesn't exist + env(noop(alice), + fee(XRP(1)), + sponsor::as(invalid, 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, testable_amendments()}; + 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_SPONSORSHIP)); + 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, testable_amendments()}; + 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, testable_amendments()}; + 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) == 100); // paybacked + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(99)); + } + + { + // if pre-funded value is not enough, error + Env env{*this, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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::ticket(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, testable_amendments()}; + 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, testable_amendments()}; + 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) == 100); // paybacked + 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) == 100); // paybacked + } + + { + // Dissolve object sponsorship from sponsor(no-ltSponsorship) + Env env{*this, testable_amendments()}; + 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, testable_amendments()}; + 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) == 101); + } + + { + // 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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); + } + } + + { + // pre funded + Env env{*this, testable_amendments()}; + 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); + printf("sponsorAvailableFee: %s\n", to_string(sponsorAvailableFee).c_str()); + 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)); + } + } + + // test lsfSponsorshipRequireSignForFee + { + Env env{*this, testable_amendments()}; + 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, testable_amendments()}; + 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 bob("bob"); + Account const charlie("charlie"); + Account const gw("gw"); + auto const USD = gw["USD"]; + + { + // Disabled + Env env{*this, testable_amendments() - featureSponsor}; + env.fund(XRP(10000), alice, sponsor); + env.close(); + env(pay(alice, bob, XRP(100)), txflags(tfSponsorCreatedAccount), ter(temDISABLED)); + env.close(); + } + + Env env{*this, testable_amendments()}; + env.fund(XRP(10000), alice, sponsor, sponsor2); + 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(env.current()->fees().accountReserve(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)); + } + } + + void + testRequireFlag() + { + using namespace test::jtx; + { + testcase("SponsorshipRequireSignForReserve"); + + Env env{*this, testable_amendments()}; + 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, testable_amendments()}; + 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)); + } + } + + // 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::none); + jv[jss::Amount2] = amount2.getJson(JsonOptions::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::none); + jv[jss::Asset2] = STIssue(sfAsset, amount2.asset()).getJson(JsonOptions::none); + jv[jss::Amount] = amount1.value().getJson(JsonOptions::none); + jv[jss::Amount2] = amount2.value().getJson(JsonOptions::none); + jv[jss::Flags] = tfTwoAsset; + return jv; + }; + + { + // AMMCreate + // - sponsor LPToken + // - doesn't sponsor AMM object + Env env{*this, testable_amendments()}; + 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, testable_amendments()}; + 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))); + }); + } + { + // AMMWithdraw + { + // Single Asset Withdraw + // - sponsor new RippleState + Env env{*this, testable_amendments()}; + 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::none); + jv[jss::Asset2] = STIssue(sfAsset, EUR.issue()).getJson(JsonOptions::none); + jv[jss::Amount] = USD(100).value().getJson(JsonOptions::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, testable_amendments()}; + 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::none); + jv[jss::Asset2] = STIssue(sfAsset, EUR.issue()).getJson(JsonOptions::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, testable_amendments()}; + 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.reference_fee = XRPAmount(1); + return cfg; + }), + testable_amendments()); + 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 < (maxDeletableAMMTrustLines * 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) == maxDeletableAMMTrustLines * 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + + 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(offer_cancel(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, testable_amendments()}; + + 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, testable_amendments()}; + 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, testable_amendments()}; + 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::ticket(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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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::condition(escrow::cb1), + escrow::cancel_time(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::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + 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, testable_amendments()}; + 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::condition(escrow::cb1), + escrow::cancel_time(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::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + 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, testable_amendments()}; + 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::condition(escrow::cb1), + escrow::cancel_time(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::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + + 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::nftpage_max(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, testable_amendments()}; + + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + env.fund(XRP(1000000), alice, sponsor, sponsor2); + env.close(); + + // PermissionedDomainSet + pdomain::Credentials credentials{{alice, "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::testStartTime - epoch_offset); + 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() + + epoch_offset.count() + 100); + jv[jss::PriceDataSeries] = Json::arrayValue; + 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::arrayValue); + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, none)); + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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(bridge_create(doorA, jvb, XRP(1), XRP(1))); + }); + } + // XChainCreateClaimID + { + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(xchain_create_claim_id(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(xchain_commit(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(xchain_commit(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(claim_attestation(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(claim_attestation(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(xchain_create_claim_id(alice, jvb, XRP(1), bob), + sponsor::as(sponsor, spfSponsorReserve), + sig(sfSponsorSignature, sponsor)); + env(xchain_commit(alice, jvb, 2, XRP(100))); // omit destination + env(claim_attestation( + 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(xchain_claim(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); + } + } + + 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"); + + // LoanBrokerSet / LoanBrokerDelete + { + Env env{*this, testable_amendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + 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); + + // LoanBrokerDelete + auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice) - 1); + 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, sponsor) == 0); + } + + // LoanBrokerConverDeposit/Withdraw/Clawback + { + Env env{*this, testable_amendments()}; + env.fund(XRP(1000), alice, bob, issuer, sponsor); + env.close(); + + MPTTester mptt{env, issuer, mptInitNoFund}; + 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::loanBrokerID(brokerKeylet.key), + amount(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, testable_amendments()}; + env.fund(XRP(1000000), alice, bob, issuer, sponsor); + env.close(); + + MPTTester mptt{env, issuer, mptInitNoFund}; + 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); + + // 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"); + + { + // Delete Sponsor/Sponsee Account with ltSponsorship (tecHAS_OBLIGATIONS) + Env env{*this, testable_amendments()}; + 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(); + + incLgrSeqForAccDel(env, sponsor); + + auto const keylet = keylet::sponsor(sponsor, alice); + auto const sponsorObj = env.le(keylet); + BEAST_EXPECT(sponsorObj); + + // AccountDelete + auto const requiredFee = drops(env.current()->fees().increment); + env(acctdelete(alice, bob), fee(requiredFee), ter(tecHAS_OBLIGATIONS)); + env(acctdelete(sponsor, bob), fee(requiredFee), ter(tecHAS_OBLIGATIONS)); + } + + { + // Delete SponsoredAccount + Env env{*this, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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, testable_amendments()}; + 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 inner transaction with co-signing sponsor + Env env{*this, testable_amendments()}; + env.fund(XRP(1000), alice, bob, sponsor); + env.close(); + + auto jt = env.jtnofill( + noop(alice), + sponsor::as(sponsor, spfSponsorReserve | spfSponsorFee), + sig(sfSponsorSignature, sponsor)); + + auto const seq = env.seq(alice); + // should fail because inner transaction cannot include 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_SIGNATURE)); + } + { + // test outer transaction with prefunded sponsor + Env env{*this, testable_amendments()}; + 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); + } + } + + void + testSponsorReserve(bool cosigning) + { + testRequireFlag(); + 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 test +} // namespace xrpl diff --git a/src/test/jtx.h b/src/test/jtx.h index d4b88b0b9e..c416842fb0 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 d638d520ba..f5ae50e510 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -524,6 +524,24 @@ public: 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. + */ + 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. + */ + 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. + */ + 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 bf43d0aa75..ec32afef9c 100644 --- a/src/test/jtx/JTx.h +++ b/src/test/jtx/JTx.h @@ -36,7 +36,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 8f48ad4d37..5bb8b19fdc 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 1dc8fd895c..7438fa3a01 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/Env.cpp b/src/test/jtx/impl/Env.cpp index bba10439ed..a0ffd622d3 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -238,6 +238,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 8010f171f7..655fd4cb07 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -45,6 +45,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 8e3c37f68c..9741071fe4 100644 --- a/src/test/jtx/impl/multisign.cpp +++ b/src/test/jtx/impl/multisign.cpp @@ -48,7 +48,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 855c5b04ff..cda4b044d6 100644 --- a/src/test/jtx/impl/owners.cpp +++ b/src/test/jtx/impl/owners.cpp @@ -37,6 +37,24 @@ owners::operator()(Env& env) const env.test.expect(env.le(account_)->getFieldU32(sfOwnerCount) == value_); } +void +sponsored_owners::operator()(Env& env) const +{ + env.test.expect(env.le(account_)->getFieldU32(sfSponsoredOwnerCount) == value_); +} + +void +sponsoring_owners::operator()(Env& env) const +{ + env.test.expect(env.le(account_)->getFieldU32(sfSponsoringOwnerCount) == value_); +} + +void +sponsoring_account_count::operator()(Env& env) const +{ + env.test.expect(env.le(account_)->getFieldU32(sfSponsoringAccountCount) == value_); +} + } // namespace jtx } // namespace test } // namespace xrpl diff --git a/src/test/jtx/impl/sig.cpp b/src/test/jtx/impl/sig.cpp index 1bdadc0bd3..bb9805ce7f 100644 --- a/src/test/jtx/impl/sig.cpp +++ b/src/test/jtx/impl/sig.cpp @@ -17,7 +17,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 0000000000..59adb58f03 --- /dev/null +++ b/src/test/jtx/impl/sponsor.cpp @@ -0,0 +1,127 @@ +#include +#include + +#include +#include +#include + +namespace xrpl { +namespace test { +namespace jtx { + +namespace 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::none); + if (maxFee) + jv[sfMaxFee.jsonName] = maxFee->getJson(JsonOptions::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::none); + if (maxFee) + jv[sfMaxFee.jsonName] = maxFee->getJson(JsonOptions::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::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 sponsor +} // namespace jtx +} // namespace test +} // namespace xrpl diff --git a/src/test/jtx/owners.h b/src/test/jtx/owners.h index b24f0a686f..f85375f065 100644 --- a/src/test/jtx/owners.h +++ b/src/test/jtx/owners.h @@ -63,6 +63,56 @@ public: operator()(Env& env) const; }; +/** Match the number of items in the account's owner directory */ +class sponsored_owners +{ +private: + Account account_; + std::uint32_t value_; + +public: + sponsored_owners(Account const& account, std::uint32_t value) : account_(account), value_(value) + { + } + + void + operator()(Env& env) const; +}; + +/** Match the number of items in the account's owner directory */ +class sponsoring_owners +{ +private: + Account account_; + std::uint32_t value_; + +public: + sponsoring_owners(Account const& account, std::uint32_t value) + : account_(account), value_(value) + { + } + + void + operator()(Env& env) const; +}; + +/** Match the number of items in the account's owner directory */ +class sponsoring_account_count +{ +private: + Account account_; + std::uint32_t value_; + +public: + sponsoring_account_count(Account const& account, std::uint32_t value) + : account_(account), value_(value) + { + } + + void + operator()(Env& env) const; +}; + /** Match the number of trust lines in the account's owner directory */ using lines = owner_count; diff --git a/src/test/jtx/sponsor.h b/src/test/jtx/sponsor.h new file mode 100644 index 0000000000..8c7b01569a --- /dev/null +++ b/src/test/jtx/sponsor.h @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include + +namespace xrpl { +namespace test { +namespace jtx { + +namespace 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 const& account) : sponsor_(account) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const; +}; + +struct sponseeAcc +{ +private: + jtx::Account sponsee_; + +public: + sponseeAcc(jtx::Account const& account) : sponsee_(account) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const; +}; + +struct as +{ +private: + jtx::Account sponsor_; + std::uint32_t flags; + +public: + as(jtx::Account const& account, std::uint32_t flags = 0) : sponsor_(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 sponsor +} // namespace jtx +} // namespace test +} // namespace xrpl diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index b6e8f6fa76..1aea128954 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -585,6 +586,7 @@ public: 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))); @@ -906,6 +908,30 @@ public: 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; @@ -920,7 +946,8 @@ public: jss::NFTokenPage.c_str(), jss::RippleState.c_str(), jss::PayChannel.c_str(), - jss::PermissionedDomain.c_str()}; + jss::PermissionedDomain.c_str(), + jss::Sponsorship.c_str()}; std::sort(v.begin(), v.end()); return v; }(); @@ -1331,6 +1358,137 @@ public: } } + void + testSponsoredFilter() + { + testcase("SponsoredFilter"); + using namespace jtx; + + Env env(*this, testable_amendments()); + 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::nftpage_max(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 { @@ -1341,6 +1499,7 @@ public: testNFTsMarker(); testAccountNFTs(); testAccountObjectMarker(); + testSponsoredFilter(); } }; diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp index 2470ec3c4c..557383bd0b 100644 --- a/src/test/rpc/AccountTx_test.cpp +++ b/src/test/rpc/AccountTx_test.cpp @@ -851,6 +851,82 @@ 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::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::ticket(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 @@ -859,6 +935,7 @@ public: 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 0bfe8aea31..90ce5ed761 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -536,6 +536,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] = newDomain; + tx[sfSponsor.jsonName] = sponsor.human(); + tx[sfSponsorFlags.jsonName] = spfSponsorFee; + tx[sfSponsorSignature.jsonName] = Json::objectValue; + + // 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 e967b614b7..4da3c83b07 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 a51ce55f6f..de0769793b 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 0000000000..5e805164f4 --- /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 0000000000..339a21cf39 --- /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 0000000000..e9fa9072a6 --- /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 2e8462de2d..5764fd7109 100644 --- a/src/xrpld/rpc/handlers/account/AccountObjects.cpp +++ b/src/xrpld/rpc/handlers/account/AccountObjects.cpp @@ -24,6 +24,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 @@ -34,6 +35,7 @@ getAccountObjects( uint256 dirIndex, uint256 entryIndex, std::uint32_t const limit, + std::optional const sponsored, Json::Value& jvResult) { // check if dirIndex is valid @@ -46,6 +48,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 = @@ -86,7 +95,17 @@ getAccountObjects( while (cp) { - jvObjects.append(cp->getJson(JsonOptions::none)); + bool canAppendNFT = true; + if (sponsored.has_value()) + { + std::optional const nftSponsor = cp->isFieldPresent(sfSponsor) + ? cp->getAccountID(sfSponsor) + : std::optional(std::nullopt); + if (!sponsoredMatchesFilter(sponsored.value(), nftSponsor)) + canAppendNFT = false; + } + if (canAppendNFT) + jvObjects.append(cp->getJson(JsonOptions::none)); auto const npm = (*cp)[~sfNextPageMin]; if (npm) { @@ -175,11 +194,31 @@ getAccountObjects( { auto const sleNode = ledger.read(keylet::child(*iter)); - 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::none)); - } if (++i == mlimit) { @@ -265,6 +304,7 @@ doAccountObjects(RPC::JsonContext& context) {jss::mptoken, ltMPTOKEN}, {jss::permissioned_domain, ltPERMISSIONED_DOMAIN}, {jss::vault, ltVAULT}, + {jss::sponsorship, ltSPONSORSHIP}, }; typeFilter.emplace(); @@ -323,7 +363,18 @@ doAccountObjects(RPC::JsonContext& context) return RPC::invalid_field_error(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::expected_field_error(jss::sponsored, "boolean"); + + sponsored = sponsoredJv.asBool(); + } + + if (!getAccountObjects( + *ledger, accountID, typeFilter, dirIndex, entryIndex, limit, sponsored, result)) return RPC::invalid_field_error(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 1fa88ac34d..60c5c98b64 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp @@ -748,6 +748,30 @@ parseVault( return keylet::vault(*id, *seq).key; } +static 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 Unexpected(sponsorAccountID.error()); + + auto const sponseeAccountID = + LedgerEntryHelpers::requiredAccountID(params, jss::sponsee, "malformedSponsee"); + if (!sponseeAccountID) + return Unexpected(sponseeAccountID.error()); + + return keylet::sponsor(*sponsorAccountID, *sponseeAccountID).key; +} + static Expected parseXChainOwnedClaimID( Json::Value const& claim_id, diff --git a/src/xrpld/rpc/handlers/transaction/Simulate.cpp b/src/xrpld/rpc/handlers/transaction/Simulate.cpp index ba9b49cdca..043deae767 100644 --- a/src/xrpld/rpc/handlers/transaction/Simulate.cpp +++ b/src/xrpld/rpc/handlers/transaction/Simulate.cpp @@ -129,6 +129,12 @@ autofillTx(Json::Value& tx_json, RPC::JsonContext& context) if (auto error = autofillSignature(tx_json)) return error; + if (tx_json.isMember(sfSponsorSignature.jsonName)) + { + if (auto error = autofillSignature(tx_json[sfSponsorSignature.jsonName])) + return error; + } + if (!tx_json.isMember(jss::Sequence)) { auto const seq = getAutofillSequence(tx_json, context);