diff --git a/include/xrpl/ledger/helpers/CouponHelpers.h b/include/xrpl/ledger/helpers/CouponHelpers.h new file mode 100644 index 00000000000..ebfafe8304b --- /dev/null +++ b/include/xrpl/ledger/helpers/CouponHelpers.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +/** + * Settle a holder's accrued coupons on their MPToken. + * + * Adds units * (AccruedPerUnit - CouponIndex) to CouponAccrued, rounded + * once downward to the coupon asset, then advances CouponIndex to + * AccruedPerUnit. Units are MPTAmount + LockedAmount. Increments the + * schedule's ClaimantCount when CouponAccrued becomes non-zero on a + * holder that had none. The caller must view.update() both entries on + * tesSUCCESS. + */ +[[nodiscard]] TER +couponSettleMPToken(SLE::ref schedule, SLE::ref mptoken, beast::Journal j); + +/** + * Settle a holder against the schedule of the issuance their MPToken + * belongs to, if that issuance carries lsfMPTCouponSchedule. Does + * nothing when the flag is clear, which is the case for every issuance + * without a coupon schedule. + * + * This is the entry point called from the MPT unit-change helpers. + */ +[[nodiscard]] TER +couponSettleIfScheduled( + ApplyView& view, + SLE::const_ref issuance, + SLE::ref mptoken, + beast::Journal j); + +/** + * The units a holder is credited for: MPTAmount + LockedAmount. + */ +[[nodiscard]] std::uint64_t +couponHolderUnits(SLE::const_ref mptoken); + +} // namespace xrpl diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 0836cffaf73..da96e52b84e 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -381,6 +381,15 @@ loan(uint256 const& key) return {ltLOAN, key}; } +Keylet +couponSchedule(uint192 const& mptIssuanceID) noexcept; + +inline Keylet +couponSchedule(uint256 const& scheduleKey) +{ + return {ltCOUPON_SCHEDULE, scheduleKey}; +} + Keylet permissionedDomain(AccountID const& account, SeqProxy const& seq) noexcept; diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 68205e27e62..cdcb30ab391 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -188,7 +188,8 @@ enum LedgerEntryType : std::uint16_t { LSF_FLAG(lsfMPTCanTrade, 0x00000010) \ LSF_FLAG(lsfMPTCanTransfer, 0x00000020) \ LSF_FLAG(lsfMPTCanClawback, 0x00000040) \ - LSF_FLAG(lsfMPTCanHoldConfidentialBalance, 0x00000080)) \ + LSF_FLAG(lsfMPTCanHoldConfidentialBalance, 0x00000080) \ + LSF_FLAG(lsfMPTCouponSchedule, 0x00000100)) \ \ LEDGER_OBJECT(MPToken, \ LSF_FLAG2(lsfMPTLocked, 0x00000001) \ diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index e63a7f515dc..f095bf17148 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(CouponPayments, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(SmartEscrow, Supported::No, VoteBehavior::DefaultNo) XRPL_FEATURE(LendingProtocolV1_2, Supported::No, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 18c71b572cd..e5cbb26e894 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -437,6 +437,8 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({ {sfIssuerEncryptedBalance, SoeOptional}, {sfAuditorEncryptedBalance, SoeOptional}, {sfHolderEncryptionKey, SoeOptional}, + {sfCouponAccrued, SoeOptional}, + {sfCouponIndex, SoeOptional}, })) /** A ledger object which tracks Oracle @@ -634,6 +636,33 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfLoanScale, SoeDefault}, })) +/** A ledger object representing a bond coupon schedule: the issuer's + standing offer of fixed periodic coupon payments per unit of a bond + asset, in a settlement asset. One schedule per (Account, BondAsset). + + \sa keylet::couponSchedule + */ +LEDGER_ENTRY(ltCOUPON_SCHEDULE, 0x0091, CouponSchedule, coupon_schedule, ({ + {sfPreviousTxnID, SoeRequired}, + {sfPreviousTxnLgrSeq, SoeRequired}, + {sfOwnerNode, SoeRequired}, + {sfOwner, SoeRequired}, + {sfAccount, SoeRequired}, + {sfMPTokenIssuanceID, SoeRequired}, + {sfCouponAsset, SoeRequired}, + {sfAccruedPerUnit, SoeDefault}, + {sfPoolAmount, SoeDefault}, + {sfClaimantCount, SoeDefault}, + {sfCouponCount, SoeDefault}, + {sfLastCouponTime, SoeOptional}, + {sfCouponAmount, SoeOptional}, + {sfCouponInterval, SoeOptional}, + {sfFirstCouponTime, SoeOptional}, + {sfExpiration, SoeOptional}, + {sfCallNoticePeriod, SoeOptional}, + {sfEarliestCallTime, SoeOptional}, +})) + /** A ledger object representing a sponsorship. \sa keylet::sponsorship */ diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 2cf35743aea..d3222b7a883 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -128,6 +128,13 @@ TYPED_SFIELD(sfBytecodeSizeLimit, UINT32, 82) TYPED_SFIELD(sfGasPrice, UINT32, 83) TYPED_SFIELD(sfGas, UINT32, 84) TYPED_SFIELD(sfGasUsed, UINT32, 85) +TYPED_SFIELD(sfCouponInterval, UINT32, 86) +TYPED_SFIELD(sfFirstCouponTime, UINT32, 87) +TYPED_SFIELD(sfCallNoticePeriod, UINT32, 88) +TYPED_SFIELD(sfEarliestCallTime, UINT32, 89) +TYPED_SFIELD(sfLastCouponTime, UINT32, 90) +TYPED_SFIELD(sfClaimantCount, UINT32, 91) +TYPED_SFIELD(sfCouponCount, UINT32, 92) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -282,6 +289,11 @@ TYPED_SFIELD(sfLPTokenBalance, AMOUNT, 31) TYPED_SFIELD(sfFeeAmount, AMOUNT, 32) TYPED_SFIELD(sfMaxFee, AMOUNT, 33) TYPED_SFIELD(sfFeeAmountDelta, AMOUNT, 34) +TYPED_SFIELD(sfCouponAmount, AMOUNT, 35) +TYPED_SFIELD(sfAccruedPerUnit, AMOUNT, 36) +TYPED_SFIELD(sfPoolAmount, AMOUNT, 37) +TYPED_SFIELD(sfCouponAccrued, AMOUNT, 38) +TYPED_SFIELD(sfCouponIndex, AMOUNT, 39) // variable length (common) TYPED_SFIELD(sfPublicKey, VL, 1) @@ -380,6 +392,7 @@ TYPED_SFIELD(sfLockingChainIssue, ISSUE, 1) TYPED_SFIELD(sfIssuingChainIssue, ISSUE, 2) TYPED_SFIELD(sfAsset, ISSUE, 3) TYPED_SFIELD(sfAsset2, ISSUE, 4) +TYPED_SFIELD(sfCouponAsset, ISSUE, 5) // bridge TYPED_SFIELD(sfXChainBridge, XCHAIN_BRIDGE, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 454aa85ffd0..e98c1456717 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1134,6 +1134,85 @@ TRANSACTION(ttSPONSORSHIP_SET, 91, SponsorshipSet, {sfRemainingOwnerCountDelta, SoeOptional}, })) +/** This transaction creates a coupon schedule for a bond asset. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCOUPON_SCHEDULE_CREATE, 92, CouponScheduleCreate, + ({ + .delegable = Delegation::NotDelegable, + .amendment = featureCouponPayments, + }), + ({ + {sfMPTokenIssuanceID, SoeRequired}, + {sfCouponAsset, SoeRequired, SoeMptSupported}, + {sfCouponAmount, SoeOptional, SoeMptSupported}, + {sfCouponInterval, SoeOptional}, + {sfFirstCouponTime, SoeOptional}, + {sfExpiration, SoeOptional}, + {sfCallNoticePeriod, SoeOptional}, + {sfEarliestCallTime, SoeOptional}, +})) + +/** This transaction exercises the issuer's call option on a callable + coupon schedule by shortening its Expiration. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCOUPON_SCHEDULE_SET, 93, CouponScheduleSet, + ({ + .delegable = Delegation::NotDelegable, + .amendment = featureCouponPayments, + }), + ({ + {sfMPTokenIssuanceID, SoeRequired}, + {sfExpiration, SoeRequired}, +})) + +/** This transaction deletes an empty coupon schedule. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCOUPON_SCHEDULE_DELETE, 94, CouponScheduleDelete, + ({ + .delegable = Delegation::NotDelegable, + .amendment = featureCouponPayments, + }), + ({ + {sfMPTokenIssuanceID, SoeRequired}, +})) + +/** This transaction claims settled coupons, delivered from the + schedule's pool. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCOUPON_CLAIM, 95, CouponClaim, + ({ + .delegable = Delegation::Delegable, + .amendment = featureCouponPayments, + }), + ({ + {sfMPTokenIssuanceID, SoeRequired}, + {sfAmount, SoeOptional, SoeMptSupported}, +})) + +/** This transaction declares a coupon: it moves the coupon amount from the + payer's spendable balance into the schedule's pool and advances the + per-unit accumulator. Codes 95 and 96 are retired and not reused. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCOUPON_PAY, 96, CouponPay, + ({ + .delegable = Delegation::NotDelegable, + .amendment = featureCouponPayments, + }), + ({ + {sfMPTokenIssuanceID, SoeRequired}, + {sfAmount, SoeRequired, SoeMptSupported}, +})) + /** 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 63e877ca311..ebd3e77cf15 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -367,6 +367,7 @@ JSS(load_factor_local); // out: NetworkOPs JSS(load_factor_net); // out: NetworkOPs JSS(load_factor_server); // out: NetworkOPs JSS(load_fee); // out: LoadFeeTrackImp, NetworkOPs +JSS(coupon_schedule_id); // in: LedgerEntry JSS(loan_broker_id); // in: LedgerEntry JSS(loan_seq); // in: LedgerEntry JSS(local); // out: resource/Logic.h diff --git a/include/xrpl/protocol_autogen/ledger_entries/CouponSchedule.h b/include/xrpl/protocol_autogen/ledger_entries/CouponSchedule.h new file mode 100644 index 00000000000..25f35acab43 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/CouponSchedule.h @@ -0,0 +1,650 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::ledger_entries { + +class CouponScheduleBuilder; + +/** + * @brief Ledger Entry: CouponSchedule + * + * Type: ltCOUPON_SCHEDULE (0x0091) + * RPC Name: coupon_schedule + * + * Immutable wrapper around SLE providing type-safe field access. + * Use CouponScheduleBuilder to construct new ledger entries. + */ +class CouponSchedule : public LedgerEntryBase +{ +public: + static constexpr LedgerEntryType entryType = ltCOUPON_SCHEDULE; + + /** + * @brief Construct a CouponSchedule ledger entry wrapper from an existing SLE object. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + explicit CouponSchedule(SLE::const_pointer sle) + : LedgerEntryBase(std::move(sle)) + { + // Verify ledger entry type + if (sle_->getType() != entryType) + { + throw std::runtime_error("Invalid ledger entry type for CouponSchedule"); + } + } + + // 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 sfOwnerNode (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getOwnerNode() const + { + return this->sle_->at(sfOwnerNode); + } + + /** + * @brief Get sfOwner (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getOwner() const + { + return this->sle_->at(sfOwner); + } + + /** + * @brief Get sfAccount (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getAccount() const + { + return this->sle_->at(sfAccount); + } + + /** + * @brief Get sfMPTokenIssuanceID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT192::type::value_type + getMPTokenIssuanceID() const + { + return this->sle_->at(sfMPTokenIssuanceID); + } + + /** + * @brief Get sfCouponAsset (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_ISSUE::type::value_type + getCouponAsset() const + { + return this->sle_->at(sfCouponAsset); + } + + /** + * @brief Get sfAccruedPerUnit (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getAccruedPerUnit() const + { + if (hasAccruedPerUnit()) + return this->sle_->at(sfAccruedPerUnit); + return std::nullopt; + } + + /** + * @brief Check if sfAccruedPerUnit is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasAccruedPerUnit() const + { + return this->sle_->isFieldPresent(sfAccruedPerUnit); + } + + /** + * @brief Get sfPoolAmount (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getPoolAmount() const + { + if (hasPoolAmount()) + return this->sle_->at(sfPoolAmount); + return std::nullopt; + } + + /** + * @brief Check if sfPoolAmount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasPoolAmount() const + { + return this->sle_->isFieldPresent(sfPoolAmount); + } + + /** + * @brief Get sfClaimantCount (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getClaimantCount() const + { + if (hasClaimantCount()) + return this->sle_->at(sfClaimantCount); + return std::nullopt; + } + + /** + * @brief Check if sfClaimantCount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasClaimantCount() const + { + return this->sle_->isFieldPresent(sfClaimantCount); + } + + /** + * @brief Get sfCouponCount (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getCouponCount() const + { + if (hasCouponCount()) + return this->sle_->at(sfCouponCount); + return std::nullopt; + } + + /** + * @brief Check if sfCouponCount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCouponCount() const + { + return this->sle_->isFieldPresent(sfCouponCount); + } + + /** + * @brief Get sfLastCouponTime (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getLastCouponTime() const + { + if (hasLastCouponTime()) + return this->sle_->at(sfLastCouponTime); + return std::nullopt; + } + + /** + * @brief Check if sfLastCouponTime is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasLastCouponTime() const + { + return this->sle_->isFieldPresent(sfLastCouponTime); + } + + /** + * @brief Get sfCouponAmount (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getCouponAmount() const + { + if (hasCouponAmount()) + return this->sle_->at(sfCouponAmount); + return std::nullopt; + } + + /** + * @brief Check if sfCouponAmount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCouponAmount() const + { + return this->sle_->isFieldPresent(sfCouponAmount); + } + + /** + * @brief Get sfCouponInterval (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getCouponInterval() const + { + if (hasCouponInterval()) + return this->sle_->at(sfCouponInterval); + return std::nullopt; + } + + /** + * @brief Check if sfCouponInterval is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCouponInterval() const + { + return this->sle_->isFieldPresent(sfCouponInterval); + } + + /** + * @brief Get sfFirstCouponTime (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getFirstCouponTime() const + { + if (hasFirstCouponTime()) + return this->sle_->at(sfFirstCouponTime); + return std::nullopt; + } + + /** + * @brief Check if sfFirstCouponTime is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasFirstCouponTime() const + { + return this->sle_->isFieldPresent(sfFirstCouponTime); + } + + /** + * @brief Get sfExpiration (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getExpiration() const + { + if (hasExpiration()) + return this->sle_->at(sfExpiration); + return std::nullopt; + } + + /** + * @brief Check if sfExpiration is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasExpiration() const + { + return this->sle_->isFieldPresent(sfExpiration); + } + + /** + * @brief Get sfCallNoticePeriod (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getCallNoticePeriod() const + { + if (hasCallNoticePeriod()) + return this->sle_->at(sfCallNoticePeriod); + return std::nullopt; + } + + /** + * @brief Check if sfCallNoticePeriod is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCallNoticePeriod() const + { + return this->sle_->isFieldPresent(sfCallNoticePeriod); + } + + /** + * @brief Get sfEarliestCallTime (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getEarliestCallTime() const + { + if (hasEarliestCallTime()) + return this->sle_->at(sfEarliestCallTime); + return std::nullopt; + } + + /** + * @brief Check if sfEarliestCallTime is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasEarliestCallTime() const + { + return this->sle_->isFieldPresent(sfEarliestCallTime); + } +}; + +/** + * @brief Builder for CouponSchedule ledger entries. + * + * Provides a fluent interface for constructing ledger entries with method chaining. + * Uses STObject internally for flexible ledger entry construction. + * Inherits common field setters from LedgerEntryBuilderBase. + */ +class CouponScheduleBuilder : public LedgerEntryBuilderBase +{ +public: + /** + * @brief Construct a new CouponScheduleBuilder with required fields. + * @param previousTxnID The sfPreviousTxnID field value. + * @param previousTxnLgrSeq The sfPreviousTxnLgrSeq field value. + * @param ownerNode The sfOwnerNode field value. + * @param owner The sfOwner field value. + * @param account The sfAccount field value. + * @param mPTokenIssuanceID The sfMPTokenIssuanceID field value. + * @param couponAsset The sfCouponAsset field value. + */ + CouponScheduleBuilder(std::decay_t const& previousTxnID,std::decay_t const& previousTxnLgrSeq,std::decay_t const& ownerNode,std::decay_t const& owner,std::decay_t const& account,std::decay_t const& mPTokenIssuanceID,std::decay_t const& couponAsset) + : LedgerEntryBuilderBase(ltCOUPON_SCHEDULE) + { + setPreviousTxnID(previousTxnID); + setPreviousTxnLgrSeq(previousTxnLgrSeq); + setOwnerNode(ownerNode); + setOwner(owner); + setAccount(account); + setMPTokenIssuanceID(mPTokenIssuanceID); + setCouponAsset(couponAsset); + } + + /** + * @brief Construct a CouponScheduleBuilder 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. + */ + CouponScheduleBuilder(SLE::const_pointer sle) + { + if (sle->at(sfLedgerEntryType) != ltCOUPON_SCHEDULE) + { + throw std::runtime_error("Invalid ledger entry type for CouponSchedule"); + } + object_ = *sle; + } + + /** + * @brief Ledger entry-specific field setters + */ + + /** + * @brief Set sfPreviousTxnID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + CouponScheduleBuilder& + setPreviousTxnID(std::decay_t const& value) + { + object_[sfPreviousTxnID] = value; + return *this; + } + + /** + * @brief Set sfPreviousTxnLgrSeq (SoeRequired) + * @return Reference to this builder for method chaining. + */ + CouponScheduleBuilder& + setPreviousTxnLgrSeq(std::decay_t const& value) + { + object_[sfPreviousTxnLgrSeq] = value; + return *this; + } + + /** + * @brief Set sfOwnerNode (SoeRequired) + * @return Reference to this builder for method chaining. + */ + CouponScheduleBuilder& + setOwnerNode(std::decay_t const& value) + { + object_[sfOwnerNode] = value; + return *this; + } + + /** + * @brief Set sfOwner (SoeRequired) + * @return Reference to this builder for method chaining. + */ + CouponScheduleBuilder& + setOwner(std::decay_t const& value) + { + object_[sfOwner] = value; + return *this; + } + + /** + * @brief Set sfAccount (SoeRequired) + * @return Reference to this builder for method chaining. + */ + CouponScheduleBuilder& + setAccount(std::decay_t const& value) + { + object_[sfAccount] = value; + return *this; + } + + /** + * @brief Set sfMPTokenIssuanceID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + CouponScheduleBuilder& + setMPTokenIssuanceID(std::decay_t const& value) + { + object_[sfMPTokenIssuanceID] = value; + return *this; + } + + /** + * @brief Set sfCouponAsset (SoeRequired) + * @return Reference to this builder for method chaining. + */ + CouponScheduleBuilder& + setCouponAsset(std::decay_t const& value) + { + object_[sfCouponAsset] = STIssue(sfCouponAsset, value); + return *this; + } + + /** + * @brief Set sfAccruedPerUnit (SoeDefault) + * @return Reference to this builder for method chaining. + */ + CouponScheduleBuilder& + setAccruedPerUnit(std::decay_t const& value) + { + object_[sfAccruedPerUnit] = value; + return *this; + } + + /** + * @brief Set sfPoolAmount (SoeDefault) + * @return Reference to this builder for method chaining. + */ + CouponScheduleBuilder& + setPoolAmount(std::decay_t const& value) + { + object_[sfPoolAmount] = value; + return *this; + } + + /** + * @brief Set sfClaimantCount (SoeDefault) + * @return Reference to this builder for method chaining. + */ + CouponScheduleBuilder& + setClaimantCount(std::decay_t const& value) + { + object_[sfClaimantCount] = value; + return *this; + } + + /** + * @brief Set sfCouponCount (SoeDefault) + * @return Reference to this builder for method chaining. + */ + CouponScheduleBuilder& + setCouponCount(std::decay_t const& value) + { + object_[sfCouponCount] = value; + return *this; + } + + /** + * @brief Set sfLastCouponTime (SoeOptional) + * @return Reference to this builder for method chaining. + */ + CouponScheduleBuilder& + setLastCouponTime(std::decay_t const& value) + { + object_[sfLastCouponTime] = value; + return *this; + } + + /** + * @brief Set sfCouponAmount (SoeOptional) + * @return Reference to this builder for method chaining. + */ + CouponScheduleBuilder& + setCouponAmount(std::decay_t const& value) + { + object_[sfCouponAmount] = value; + return *this; + } + + /** + * @brief Set sfCouponInterval (SoeOptional) + * @return Reference to this builder for method chaining. + */ + CouponScheduleBuilder& + setCouponInterval(std::decay_t const& value) + { + object_[sfCouponInterval] = value; + return *this; + } + + /** + * @brief Set sfFirstCouponTime (SoeOptional) + * @return Reference to this builder for method chaining. + */ + CouponScheduleBuilder& + setFirstCouponTime(std::decay_t const& value) + { + object_[sfFirstCouponTime] = value; + return *this; + } + + /** + * @brief Set sfExpiration (SoeOptional) + * @return Reference to this builder for method chaining. + */ + CouponScheduleBuilder& + setExpiration(std::decay_t const& value) + { + object_[sfExpiration] = value; + return *this; + } + + /** + * @brief Set sfCallNoticePeriod (SoeOptional) + * @return Reference to this builder for method chaining. + */ + CouponScheduleBuilder& + setCallNoticePeriod(std::decay_t const& value) + { + object_[sfCallNoticePeriod] = value; + return *this; + } + + /** + * @brief Set sfEarliestCallTime (SoeOptional) + * @return Reference to this builder for method chaining. + */ + CouponScheduleBuilder& + setEarliestCallTime(std::decay_t const& value) + { + object_[sfEarliestCallTime] = value; + return *this; + } + + /** + * @brief Build and return the completed CouponSchedule wrapper. + * @param index The ledger entry index. + * @return The constructed ledger entry wrapper. + */ + CouponSchedule + build(uint256 const& index) + { + return CouponSchedule{std::make_shared(std::move(object_), index)}; + } +}; + +} // namespace xrpl::ledger_entries diff --git a/include/xrpl/protocol_autogen/ledger_entries/MPToken.h b/include/xrpl/protocol_autogen/ledger_entries/MPToken.h index 874d779d099..30942cf80d1 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/MPToken.h +++ b/include/xrpl/protocol_autogen/ledger_entries/MPToken.h @@ -291,6 +291,54 @@ class MPToken : public LedgerEntryBase { return this->sle_->isFieldPresent(sfHolderEncryptionKey); } + + /** + * @brief Get sfCouponAccrued (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getCouponAccrued() const + { + if (hasCouponAccrued()) + return this->sle_->at(sfCouponAccrued); + return std::nullopt; + } + + /** + * @brief Check if sfCouponAccrued is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCouponAccrued() const + { + return this->sle_->isFieldPresent(sfCouponAccrued); + } + + /** + * @brief Get sfCouponIndex (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getCouponIndex() const + { + if (hasCouponIndex()) + return this->sle_->at(sfCouponIndex); + return std::nullopt; + } + + /** + * @brief Check if sfCouponIndex is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCouponIndex() const + { + return this->sle_->isFieldPresent(sfCouponIndex); + } }; /** @@ -482,6 +530,28 @@ class MPTokenBuilder : public LedgerEntryBuilderBase return *this; } + /** + * @brief Set sfCouponAccrued (SoeOptional) + * @return Reference to this builder for method chaining. + */ + MPTokenBuilder& + setCouponAccrued(std::decay_t const& value) + { + object_[sfCouponAccrued] = value; + return *this; + } + + /** + * @brief Set sfCouponIndex (SoeOptional) + * @return Reference to this builder for method chaining. + */ + MPTokenBuilder& + setCouponIndex(std::decay_t const& value) + { + object_[sfCouponIndex] = value; + return *this; + } + /** * @brief Build and return the completed MPToken wrapper. * @param index The ledger entry index. diff --git a/include/xrpl/protocol_autogen/transactions/CouponClaim.h b/include/xrpl/protocol_autogen/transactions/CouponClaim.h new file mode 100644 index 00000000000..47a183f17d6 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/CouponClaim.h @@ -0,0 +1,170 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class CouponClaimBuilder; + +/** + * @brief Transaction: CouponClaim + * + * Type: ttCOUPON_CLAIM (95) + * Delegable: Delegation::Delegable + * Amendment: featureCouponPayments + * Privileges: Privilege::NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use CouponClaimBuilder to construct new transactions. + */ +class CouponClaim : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttCOUPON_CLAIM; + + /** + * @brief Construct a CouponClaim transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit CouponClaim(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for CouponClaim"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfMPTokenIssuanceID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT192::type::value_type + getMPTokenIssuanceID() const + { + return this->tx_->at(sfMPTokenIssuanceID); + } + + /** + * @brief Get sfAmount (SoeOptional) + * @note This field supports MPT (Multi-Purpose Token) amounts. + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getAmount() const + { + if (hasAmount()) + { + return this->tx_->at(sfAmount); + } + return std::nullopt; + } + + /** + * @brief Check if sfAmount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasAmount() const + { + return this->tx_->isFieldPresent(sfAmount); + } +}; + +/** + * @brief Builder for CouponClaim transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class CouponClaimBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new CouponClaimBuilder with required fields. + * @param account The account initiating the transaction. + * @param mPTokenIssuanceID The sfMPTokenIssuanceID field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + CouponClaimBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& mPTokenIssuanceID, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttCOUPON_CLAIM, account, sequence, fee) + { + setMPTokenIssuanceID(mPTokenIssuanceID); + } + + /** + * @brief Construct a CouponClaimBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + CouponClaimBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttCOUPON_CLAIM) + { + throw std::runtime_error("Invalid transaction type for CouponClaimBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfMPTokenIssuanceID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + CouponClaimBuilder& + setMPTokenIssuanceID(std::decay_t const& value) + { + object_[sfMPTokenIssuanceID] = value; + return *this; + } + + /** + * @brief Set sfAmount (SoeOptional) + * @note This field supports MPT (Multi-Purpose Token) amounts. + * @return Reference to this builder for method chaining. + */ + CouponClaimBuilder& + setAmount(std::decay_t const& value) + { + object_[sfAmount] = value; + return *this; + } + + /** + * @brief Build and return the CouponClaim wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + CouponClaim + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return CouponClaim{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/CouponPay.h b/include/xrpl/protocol_autogen/transactions/CouponPay.h new file mode 100644 index 00000000000..c4a9591eb0a --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/CouponPay.h @@ -0,0 +1,157 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class CouponPayBuilder; + +/** + * @brief Transaction: CouponPay + * + * Type: ttCOUPON_PAY (96) + * Delegable: Delegation::NotDelegable + * Amendment: featureCouponPayments + * Privileges: Privilege::NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use CouponPayBuilder to construct new transactions. + */ +class CouponPay : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttCOUPON_PAY; + + /** + * @brief Construct a CouponPay transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit CouponPay(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for CouponPay"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfMPTokenIssuanceID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT192::type::value_type + getMPTokenIssuanceID() const + { + return this->tx_->at(sfMPTokenIssuanceID); + } + + /** + * @brief Get sfAmount (SoeRequired) + * @note This field supports MPT (Multi-Purpose Token) amounts. + * @return The field value. + */ + [[nodiscard]] + SF_AMOUNT::type::value_type + getAmount() const + { + return this->tx_->at(sfAmount); + } +}; + +/** + * @brief Builder for CouponPay transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class CouponPayBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new CouponPayBuilder with required fields. + * @param account The account initiating the transaction. + * @param mPTokenIssuanceID The sfMPTokenIssuanceID field value. + * @param amount The sfAmount field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + CouponPayBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& mPTokenIssuanceID, std::decay_t const& amount, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttCOUPON_PAY, account, sequence, fee) + { + setMPTokenIssuanceID(mPTokenIssuanceID); + setAmount(amount); + } + + /** + * @brief Construct a CouponPayBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + CouponPayBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttCOUPON_PAY) + { + throw std::runtime_error("Invalid transaction type for CouponPayBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfMPTokenIssuanceID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + CouponPayBuilder& + setMPTokenIssuanceID(std::decay_t const& value) + { + object_[sfMPTokenIssuanceID] = value; + return *this; + } + + /** + * @brief Set sfAmount (SoeRequired) + * @note This field supports MPT (Multi-Purpose Token) amounts. + * @return Reference to this builder for method chaining. + */ + CouponPayBuilder& + setAmount(std::decay_t const& value) + { + object_[sfAmount] = value; + return *this; + } + + /** + * @brief Build and return the CouponPay wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + CouponPay + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return CouponPay{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/CouponScheduleCreate.h b/include/xrpl/protocol_autogen/transactions/CouponScheduleCreate.h new file mode 100644 index 00000000000..f2414fa1b59 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/CouponScheduleCreate.h @@ -0,0 +1,381 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class CouponScheduleCreateBuilder; + +/** + * @brief Transaction: CouponScheduleCreate + * + * Type: ttCOUPON_SCHEDULE_CREATE (92) + * Delegable: Delegation::NotDelegable + * Amendment: featureCouponPayments + * Privileges: Privilege::NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use CouponScheduleCreateBuilder to construct new transactions. + */ +class CouponScheduleCreate : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttCOUPON_SCHEDULE_CREATE; + + /** + * @brief Construct a CouponScheduleCreate transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit CouponScheduleCreate(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for CouponScheduleCreate"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfMPTokenIssuanceID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT192::type::value_type + getMPTokenIssuanceID() const + { + return this->tx_->at(sfMPTokenIssuanceID); + } + + /** + * @brief Get sfCouponAsset (SoeRequired) + * @note This field supports MPT (Multi-Purpose Token) amounts. + * @return The field value. + */ + [[nodiscard]] + SF_ISSUE::type::value_type + getCouponAsset() const + { + return this->tx_->at(sfCouponAsset); + } + + /** + * @brief Get sfCouponAmount (SoeOptional) + * @note This field supports MPT (Multi-Purpose Token) amounts. + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getCouponAmount() const + { + if (hasCouponAmount()) + { + return this->tx_->at(sfCouponAmount); + } + return std::nullopt; + } + + /** + * @brief Check if sfCouponAmount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCouponAmount() const + { + return this->tx_->isFieldPresent(sfCouponAmount); + } + + /** + * @brief Get sfCouponInterval (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getCouponInterval() const + { + if (hasCouponInterval()) + { + return this->tx_->at(sfCouponInterval); + } + return std::nullopt; + } + + /** + * @brief Check if sfCouponInterval is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCouponInterval() const + { + return this->tx_->isFieldPresent(sfCouponInterval); + } + + /** + * @brief Get sfFirstCouponTime (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getFirstCouponTime() const + { + if (hasFirstCouponTime()) + { + return this->tx_->at(sfFirstCouponTime); + } + return std::nullopt; + } + + /** + * @brief Check if sfFirstCouponTime is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasFirstCouponTime() const + { + return this->tx_->isFieldPresent(sfFirstCouponTime); + } + + /** + * @brief Get sfExpiration (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getExpiration() const + { + if (hasExpiration()) + { + return this->tx_->at(sfExpiration); + } + return std::nullopt; + } + + /** + * @brief Check if sfExpiration is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasExpiration() const + { + return this->tx_->isFieldPresent(sfExpiration); + } + + /** + * @brief Get sfCallNoticePeriod (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getCallNoticePeriod() const + { + if (hasCallNoticePeriod()) + { + return this->tx_->at(sfCallNoticePeriod); + } + return std::nullopt; + } + + /** + * @brief Check if sfCallNoticePeriod is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCallNoticePeriod() const + { + return this->tx_->isFieldPresent(sfCallNoticePeriod); + } + + /** + * @brief Get sfEarliestCallTime (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getEarliestCallTime() const + { + if (hasEarliestCallTime()) + { + return this->tx_->at(sfEarliestCallTime); + } + return std::nullopt; + } + + /** + * @brief Check if sfEarliestCallTime is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasEarliestCallTime() const + { + return this->tx_->isFieldPresent(sfEarliestCallTime); + } +}; + +/** + * @brief Builder for CouponScheduleCreate transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class CouponScheduleCreateBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new CouponScheduleCreateBuilder with required fields. + * @param account The account initiating the transaction. + * @param mPTokenIssuanceID The sfMPTokenIssuanceID field value. + * @param couponAsset The sfCouponAsset field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + CouponScheduleCreateBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& mPTokenIssuanceID, std::decay_t const& couponAsset, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttCOUPON_SCHEDULE_CREATE, account, sequence, fee) + { + setMPTokenIssuanceID(mPTokenIssuanceID); + setCouponAsset(couponAsset); + } + + /** + * @brief Construct a CouponScheduleCreateBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + CouponScheduleCreateBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttCOUPON_SCHEDULE_CREATE) + { + throw std::runtime_error("Invalid transaction type for CouponScheduleCreateBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfMPTokenIssuanceID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + CouponScheduleCreateBuilder& + setMPTokenIssuanceID(std::decay_t const& value) + { + object_[sfMPTokenIssuanceID] = value; + return *this; + } + + /** + * @brief Set sfCouponAsset (SoeRequired) + * @note This field supports MPT (Multi-Purpose Token) amounts. + * @return Reference to this builder for method chaining. + */ + CouponScheduleCreateBuilder& + setCouponAsset(std::decay_t const& value) + { + object_[sfCouponAsset] = STIssue(sfCouponAsset, value); + return *this; + } + + /** + * @brief Set sfCouponAmount (SoeOptional) + * @note This field supports MPT (Multi-Purpose Token) amounts. + * @return Reference to this builder for method chaining. + */ + CouponScheduleCreateBuilder& + setCouponAmount(std::decay_t const& value) + { + object_[sfCouponAmount] = value; + return *this; + } + + /** + * @brief Set sfCouponInterval (SoeOptional) + * @return Reference to this builder for method chaining. + */ + CouponScheduleCreateBuilder& + setCouponInterval(std::decay_t const& value) + { + object_[sfCouponInterval] = value; + return *this; + } + + /** + * @brief Set sfFirstCouponTime (SoeOptional) + * @return Reference to this builder for method chaining. + */ + CouponScheduleCreateBuilder& + setFirstCouponTime(std::decay_t const& value) + { + object_[sfFirstCouponTime] = value; + return *this; + } + + /** + * @brief Set sfExpiration (SoeOptional) + * @return Reference to this builder for method chaining. + */ + CouponScheduleCreateBuilder& + setExpiration(std::decay_t const& value) + { + object_[sfExpiration] = value; + return *this; + } + + /** + * @brief Set sfCallNoticePeriod (SoeOptional) + * @return Reference to this builder for method chaining. + */ + CouponScheduleCreateBuilder& + setCallNoticePeriod(std::decay_t const& value) + { + object_[sfCallNoticePeriod] = value; + return *this; + } + + /** + * @brief Set sfEarliestCallTime (SoeOptional) + * @return Reference to this builder for method chaining. + */ + CouponScheduleCreateBuilder& + setEarliestCallTime(std::decay_t const& value) + { + object_[sfEarliestCallTime] = value; + return *this; + } + + /** + * @brief Build and return the CouponScheduleCreate wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + CouponScheduleCreate + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return CouponScheduleCreate{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/CouponScheduleDelete.h b/include/xrpl/protocol_autogen/transactions/CouponScheduleDelete.h new file mode 100644 index 00000000000..7cdf8e46fe1 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/CouponScheduleDelete.h @@ -0,0 +1,131 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class CouponScheduleDeleteBuilder; + +/** + * @brief Transaction: CouponScheduleDelete + * + * Type: ttCOUPON_SCHEDULE_DELETE (94) + * Delegable: Delegation::NotDelegable + * Amendment: featureCouponPayments + * Privileges: Privilege::NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use CouponScheduleDeleteBuilder to construct new transactions. + */ +class CouponScheduleDelete : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttCOUPON_SCHEDULE_DELETE; + + /** + * @brief Construct a CouponScheduleDelete transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit CouponScheduleDelete(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for CouponScheduleDelete"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfMPTokenIssuanceID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT192::type::value_type + getMPTokenIssuanceID() const + { + return this->tx_->at(sfMPTokenIssuanceID); + } +}; + +/** + * @brief Builder for CouponScheduleDelete transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class CouponScheduleDeleteBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new CouponScheduleDeleteBuilder with required fields. + * @param account The account initiating the transaction. + * @param mPTokenIssuanceID The sfMPTokenIssuanceID field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + CouponScheduleDeleteBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& mPTokenIssuanceID, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttCOUPON_SCHEDULE_DELETE, account, sequence, fee) + { + setMPTokenIssuanceID(mPTokenIssuanceID); + } + + /** + * @brief Construct a CouponScheduleDeleteBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + CouponScheduleDeleteBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttCOUPON_SCHEDULE_DELETE) + { + throw std::runtime_error("Invalid transaction type for CouponScheduleDeleteBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfMPTokenIssuanceID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + CouponScheduleDeleteBuilder& + setMPTokenIssuanceID(std::decay_t const& value) + { + object_[sfMPTokenIssuanceID] = value; + return *this; + } + + /** + * @brief Build and return the CouponScheduleDelete wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + CouponScheduleDelete + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return CouponScheduleDelete{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/CouponScheduleSet.h b/include/xrpl/protocol_autogen/transactions/CouponScheduleSet.h new file mode 100644 index 00000000000..86f736f2064 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/CouponScheduleSet.h @@ -0,0 +1,155 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class CouponScheduleSetBuilder; + +/** + * @brief Transaction: CouponScheduleSet + * + * Type: ttCOUPON_SCHEDULE_SET (93) + * Delegable: Delegation::NotDelegable + * Amendment: featureCouponPayments + * Privileges: Privilege::NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use CouponScheduleSetBuilder to construct new transactions. + */ +class CouponScheduleSet : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttCOUPON_SCHEDULE_SET; + + /** + * @brief Construct a CouponScheduleSet transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit CouponScheduleSet(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for CouponScheduleSet"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfMPTokenIssuanceID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT192::type::value_type + getMPTokenIssuanceID() const + { + return this->tx_->at(sfMPTokenIssuanceID); + } + + /** + * @brief Get sfExpiration (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getExpiration() const + { + return this->tx_->at(sfExpiration); + } +}; + +/** + * @brief Builder for CouponScheduleSet transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class CouponScheduleSetBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new CouponScheduleSetBuilder with required fields. + * @param account The account initiating the transaction. + * @param mPTokenIssuanceID The sfMPTokenIssuanceID field value. + * @param expiration The sfExpiration field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + CouponScheduleSetBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& mPTokenIssuanceID, std::decay_t const& expiration, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttCOUPON_SCHEDULE_SET, account, sequence, fee) + { + setMPTokenIssuanceID(mPTokenIssuanceID); + setExpiration(expiration); + } + + /** + * @brief Construct a CouponScheduleSetBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + CouponScheduleSetBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttCOUPON_SCHEDULE_SET) + { + throw std::runtime_error("Invalid transaction type for CouponScheduleSetBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfMPTokenIssuanceID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + CouponScheduleSetBuilder& + setMPTokenIssuanceID(std::decay_t const& value) + { + object_[sfMPTokenIssuanceID] = value; + return *this; + } + + /** + * @brief Set sfExpiration (SoeRequired) + * @return Reference to this builder for method chaining. + */ + CouponScheduleSetBuilder& + setExpiration(std::decay_t const& value) + { + object_[sfExpiration] = value; + return *this; + } + + /** + * @brief Build and return the CouponScheduleSet wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + CouponScheduleSet + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return CouponScheduleSet{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/tx/invariants/CouponInvariant.h b/include/xrpl/tx/invariants/CouponInvariant.h new file mode 100644 index 00000000000..7932056e56f --- /dev/null +++ b/include/xrpl/tx/invariants/CouponInvariant.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +/** + * @brief Invariants: a coupon schedule's accumulator and pool only move the + * ways the amendment allows. + * + * 1. `AccruedPerUnit` never decreases, and changes only under `CouponPay`. + * 2. `PoolAmount` is never negative. It increases only under `CouponPay` and + * decreases only under `CouponClaim` or `CouponScheduleDelete`. + * 3. A holder's `CouponIndex` never decreases: settlement only advances it + * toward the schedule's `AccruedPerUnit`. + */ +class ValidCouponSchedule +{ + // Pair is ; both are needed to check direction of change. + std::vector> schedules_; + std::vector> mptokens_; + +public: + void + visitEntry(bool, SLE::const_ref, SLE::const_ref); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/InvariantCheck.h b/include/xrpl/tx/invariants/InvariantCheck.h index e8dafbd3017..ea5a5e9d607 100644 --- a/include/xrpl/tx/invariants/InvariantCheck.h +++ b/include/xrpl/tx/invariants/InvariantCheck.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -463,7 +464,8 @@ using InvariantChecks = std::tuple< ValidMPTTransfer, ObjectHasPseudoAccount, SponsorshipOwnerCountsMatch, - SponsorshipAccountCountMatchesField>; + SponsorshipAccountCountMatchesField, + ValidCouponSchedule>; /** * @brief get a tuple of all invariant checks diff --git a/include/xrpl/tx/transactors/coupon/CouponClaim.h b/include/xrpl/tx/transactors/coupon/CouponClaim.h new file mode 100644 index 00000000000..1be22e0c5b0 --- /dev/null +++ b/include/xrpl/tx/transactors/coupon/CouponClaim.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +class CouponClaim : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit CouponClaim(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/coupon/CouponPay.h b/include/xrpl/tx/transactors/coupon/CouponPay.h new file mode 100644 index 00000000000..b06d2f52c5b --- /dev/null +++ b/include/xrpl/tx/transactors/coupon/CouponPay.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +class CouponPay : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit CouponPay(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/coupon/CouponScheduleCreate.h b/include/xrpl/tx/transactors/coupon/CouponScheduleCreate.h new file mode 100644 index 00000000000..55abf9cfb0b --- /dev/null +++ b/include/xrpl/tx/transactors/coupon/CouponScheduleCreate.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +class CouponScheduleCreate : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit CouponScheduleCreate(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/coupon/CouponScheduleDelete.h b/include/xrpl/tx/transactors/coupon/CouponScheduleDelete.h new file mode 100644 index 00000000000..98eaa95ae0e --- /dev/null +++ b/include/xrpl/tx/transactors/coupon/CouponScheduleDelete.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +class CouponScheduleDelete : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit CouponScheduleDelete(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/coupon/CouponScheduleSet.h b/include/xrpl/tx/transactors/coupon/CouponScheduleSet.h new file mode 100644 index 00000000000..ce58f545e70 --- /dev/null +++ b/include/xrpl/tx/transactors/coupon/CouponScheduleSet.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +class CouponScheduleSet : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit CouponScheduleSet(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/src/libxrpl/ledger/helpers/CouponHelpers.cpp b/src/libxrpl/ledger/helpers/CouponHelpers.cpp new file mode 100644 index 00000000000..b40d0200978 --- /dev/null +++ b/src/libxrpl/ledger/helpers/CouponHelpers.cpp @@ -0,0 +1,87 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +std::uint64_t +couponHolderUnits(SLE::const_ref mptoken) +{ + std::uint64_t units = mptoken->at(sfMPTAmount); + if (auto const locked = mptoken->at(~sfLockedAmount)) + units += *locked; + return units; +} + +TER +couponSettleMPToken(SLE::ref schedule, SLE::ref mptoken, beast::Journal j) +{ + STAmount const perUnit = schedule->at(sfAccruedPerUnit); + STAmount const zero{perUnit.asset(), 0}; + auto const indexField = mptoken->at(~sfCouponIndex); + STAmount const index = indexField ? *indexField : zero; + + // Nothing declared since this holder last settled. + if (perUnit <= index) + return tesSUCCESS; + + std::uint64_t const units = couponHolderUnits(mptoken); + if (units != 0) + { + auto const accruedField = mptoken->at(~sfCouponAccrued); + STAmount const accrued = accruedField ? *accruedField : zero; + + NumberRoundModeGuard const mg(Number::RoundingMode::Downward); + Number const addition = Number(units) * (Number(perUnit) - Number(index)); + STAmount const additionAmt{perUnit.asset(), addition}; + + if (!canAdd(accrued, additionAmt)) + { + JLOG(j.debug()) << "couponSettleMPToken: accrual addition loses precision."; + return tecPRECISION_LOSS; + } + + bool const wasEmpty = accrued == beast::kZero; + STAmount const settled = accrued + additionAmt; + mptoken->at(sfCouponAccrued) = settled; + + // ClaimantCount tracks MPTokens holding an unclaimed balance. + if (wasEmpty && settled != beast::kZero) + schedule->at(sfClaimantCount) = schedule->at(sfClaimantCount) + 1; + } + + mptoken->at(sfCouponIndex) = perUnit; + return tesSUCCESS; +} + +TER +couponSettleIfScheduled( + ApplyView& view, + SLE::const_ref issuance, + SLE::ref mptoken, + beast::Journal j) +{ + if (!issuance || !issuance->isFlag(lsfMPTCouponSchedule)) + return tesSUCCESS; + + auto const sleSchedule = view.peek(keylet::couponSchedule(mptoken->at(sfMPTokenIssuanceID))); + if (!sleSchedule) + return tesSUCCESS; + + if (auto const ter = couponSettleMPToken(sleSchedule, mptoken, j); !isTesSuccess(ter)) + return ter; + + view.update(sleSchedule); + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index 27dcd84675b..c5ecd1363e6 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -873,6 +874,14 @@ unlockEscrowMPT( return tecOBJECT_NOT_FOUND; } // LCOV_EXCL_STOP + // Coupons accrue against units held, so settle before they change. + if (sender != receiver) + { + if (auto const ter = couponSettleIfScheduled(view, sleIssuance, sle, j); + !isTesSuccess(ter)) + return ter; + } + auto current = sle->getFieldU64(sfMPTAmount); auto delta = netAmount.mpt().value(); @@ -920,6 +929,13 @@ unlockEscrowMPT( return tecOBJECT_NOT_FOUND; } // LCOV_EXCL_STOP + // Coupons accrue against units held, so settle before they change. + if (sender != receiver) + { + if (auto const ter = couponSettleIfScheduled(view, sleIssuance, sle, j); !isTesSuccess(ter)) + return ter; + } + if (!sle->isFieldPresent(sfLockedAmount)) { // LCOV_EXCL_START JLOG(j.error()) << "unlockEscrowMPT: no locked amount in MPToken for " << to_string(sender); diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp index 2c2c943a9fa..3d64a25c4b3 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -1241,6 +1242,11 @@ directSendNoFeeMPT( auto const mptokenID = keylet::mptoken(mptID.key, uSenderID); if (auto sle = view.peek(mptokenID)) { + // Coupons accrue against units held, so settle before they change. + if (auto const ter = couponSettleIfScheduled(view, sleIssuance, sle, j); + !isTesSuccess(ter)) + return ter; + auto const senderBalance = sle->getFieldU64(sfMPTAmount); if (senderBalance < amt) return tecINSUFFICIENT_FUNDS; @@ -1271,6 +1277,11 @@ directSendNoFeeMPT( auto const mptokenID = keylet::mptoken(mptID.key, uReceiverID); if (auto sle = view.peek(mptokenID)) { + // Coupons accrue against units held, so settle before they change. + if (auto const ter = couponSettleIfScheduled(view, sleIssuance, sle, j); + !isTesSuccess(ter)) + return ter; + if (view.rules().enabled(featureMPTokensV2)) { if ((*sle)[sfMPTAmount] > (std::numeric_limits::max() - amt)) diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 91ed5c893fd..4e28ee5a1b7 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -104,6 +104,7 @@ enum class LedgerNameSpace : std::uint16_t { LoanBroker = 'l', // lower-case L Loan = 'L', Sponsorship = '>', + CouponSchedule = 'b', // No longer used or supported. Left here to reserve the space to avoid accidental reuse. Contract [[deprecated]] = 'c', @@ -596,6 +597,12 @@ loan(uint256 const& loanBrokerID, SeqProxy const& loanSeq) noexcept return loan(indexHash(LedgerNameSpace::Loan, loanBrokerID, loanSeq.value())); } +Keylet +couponSchedule(uint192 const& mptIssuanceID) noexcept +{ + return couponSchedule(indexHash(LedgerNameSpace::CouponSchedule, mptIssuanceID)); +} + Keylet permissionedDomain(AccountID const& account, SeqProxy const& seq) noexcept { diff --git a/src/libxrpl/tx/invariants/CouponInvariant.cpp b/src/libxrpl/tx/invariants/CouponInvariant.cpp new file mode 100644 index 00000000000..142275bb579 --- /dev/null +++ b/src/libxrpl/tx/invariants/CouponInvariant.cpp @@ -0,0 +1,111 @@ +#include + +#include +#include +#include +#include +#include + +namespace xrpl { + +void +ValidCouponSchedule::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) +{ + if (isDelete) + return; + + if (before && before->getType() == ltCOUPON_SCHEDULE) + schedules_.emplace_back(before, after); + else if (!before && after && after->getType() == ltCOUPON_SCHEDULE) + schedules_.emplace_back(before, after); + + if (before && before->getType() == ltMPTOKEN) + mptokens_.emplace_back(before, after); +} + +bool +ValidCouponSchedule::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const&, + beast::Journal const& j) +{ + if (!isTesSuccess(result)) + return true; + + auto const txType = tx.getTxnType(); + bool const isPay = txType == ttCOUPON_PAY; + bool const mayDrainPool = txType == ttCOUPON_CLAIM || txType == ttCOUPON_SCHEDULE_DELETE; + + for (auto const& [before, after] : schedules_) + { + if (!after) + continue; + + if (after->at(sfPoolAmount) < beast::kZero) + { + JLOG(j.fatal()) << "Invariant failed: coupon pool is negative"; + return false; + } + + if (!before) + continue; + + STAmount const perUnitBefore = before->at(sfAccruedPerUnit); + STAmount const perUnitAfter = after->at(sfAccruedPerUnit); + + if (perUnitAfter < perUnitBefore) + { + JLOG(j.fatal()) << "Invariant failed: coupon accumulator decreased"; + return false; + } + + if (perUnitAfter != perUnitBefore && !isPay) + { + JLOG(j.fatal()) << "Invariant failed: coupon accumulator moved outside CouponPay"; + return false; + } + + STAmount const poolBefore = before->at(sfPoolAmount); + STAmount const poolAfter = after->at(sfPoolAmount); + + if (poolAfter > poolBefore && !isPay) + { + JLOG(j.fatal()) << "Invariant failed: coupon pool grew outside CouponPay"; + return false; + } + + if (poolAfter < poolBefore && !mayDrainPool) + { + JLOG(j.fatal()) << "Invariant failed: coupon pool shrank outside a claim or delete"; + return false; + } + } + + for (auto const& [before, after] : mptokens_) + { + if (!before || !after) + continue; + + auto const indexBefore = before->at(~sfCouponIndex); + auto const indexAfter = after->at(~sfCouponIndex); + + if (indexBefore && indexAfter && *indexAfter < *indexBefore) + { + JLOG(j.fatal()) << "Invariant failed: holder coupon index moved backwards"; + return false; + } + + // A holder's index may not be dropped while it carries an accrual. + if (indexBefore && !indexAfter && after->isFieldPresent(sfCouponAccrued)) + { + JLOG(j.fatal()) << "Invariant failed: holder coupon index removed while owed"; + return false; + } + } + + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/coupon/CouponClaim.cpp b/src/libxrpl/tx/transactors/coupon/CouponClaim.cpp new file mode 100644 index 00000000000..6fda039f699 --- /dev/null +++ b/src/libxrpl/tx/transactors/coupon/CouponClaim.cpp @@ -0,0 +1,172 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +NotTEC +CouponClaim::preflight(PreflightContext const& ctx) +{ + if (ctx.tx[sfMPTokenIssuanceID] == beast::kZero) + return temMALFORMED; + + if (auto const amount = ctx.tx[~sfAmount]; amount && *amount <= beast::kZero) + return temBAD_AMOUNT; + + return tesSUCCESS; +} + +TER +CouponClaim::preclaim(PreclaimContext const& ctx) +{ + auto const issuanceID = ctx.tx[sfMPTokenIssuanceID]; + AccountID const holder = ctx.tx[sfAccount]; + + auto const schedule = ctx.view.read(keylet::couponSchedule(issuanceID)); + if (!schedule) + return tecNO_ENTRY; + + if (!ctx.view.exists(keylet::mptoken(issuanceID, holder))) + return tecNO_ENTRY; + + auto const& couponAsset = schedule->at(sfCouponAsset); + + if (auto const amount = ctx.tx[~sfAmount]; amount && amount->asset() != couponAsset) + return tecNO_PERMISSION; + + // Delivery eligibility on the holder's side only. The payer's side was + // settled when CouponPay funded the pool. + if (!couponAsset.native() && holder != couponAsset.getIssuer()) + { + if (auto const ter = requireAuth(ctx.view, couponAsset, holder); !isTesSuccess(ter)) + return ter; + if (isFrozen(ctx.view, holder, couponAsset)) + return couponAsset.holds() ? tecLOCKED : tecFROZEN; + } + + return tesSUCCESS; +} + +TER +CouponClaim::doApply() +{ + auto const issuanceID = ctx_.tx[sfMPTokenIssuanceID]; + + auto schedule = view().peek(keylet::couponSchedule(issuanceID)); + if (!schedule) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto mptoken = view().peek(keylet::mptoken(issuanceID, accountID_)); + if (!mptoken) + return tefINTERNAL; // LCOV_EXCL_LINE + + // Bring the holder current before reading what they are owed. + if (auto const ter = couponSettleMPToken(schedule, mptoken, j_); !isTesSuccess(ter)) + return ter; + + STAmount const zero{schedule->at(sfCouponAsset), 0}; + auto const accruedField = mptoken->at(~sfCouponAccrued); + STAmount const accrued = accruedField ? *accruedField : zero; + if (accrued <= beast::kZero) + return tecNO_PERMISSION; + + auto const requested = ctx_.tx[~sfAmount]; + STAmount const pay = std::min(requested ? *requested : accrued, accrued); + STAmount const pool = schedule->at(sfPoolAmount); + if (pool < pay) + return tefINTERNAL; // LCOV_EXCL_LINE — the pool always covers accruals. + + AccountID const payer = schedule->at(sfAccount); + auto const& couponAsset = pay.asset(); + + // Release from the pool. XRP is credited directly; an IOU comes back off + // its issuer's trust line with the transfer fee deducted and an MPT is + // unlocked, the same release the token escrow performs. + if (couponAsset.native()) + { + auto const sle = view().peek(keylet::account(accountID_)); + if (!sle) + return tefINTERNAL; // LCOV_EXCL_LINE + sle->at(sfBalance) = sle->at(sfBalance) + pay; + view().update(sle); + } + else + { + auto const issuerID = couponAsset.getIssuer(); + auto sleDest = view().peek(keylet::account(accountID_)); + if (!sleDest) + return tefINTERNAL; // LCOV_EXCL_LINE + + Rate const lockedRate = transferRate(view(), issuerID); + if (auto const ret = std::visit( + [&](T const&) { + return escrowUnlockApplyHelper( + ctx_.getApplyViewContext(), + lockedRate, + sleDest, + preFeeBalance_, + pay, + issuerID, + payer, + accountID_, + true, + j_); + }, + couponAsset.value()); + !isTesSuccess(ret)) + return ret; + } + + STAmount const remaining = accrued - pay; + if (remaining <= beast::kZero) + { + mptoken->makeFieldAbsent(sfCouponAccrued); + schedule->at(sfClaimantCount) = schedule->at(sfClaimantCount) - 1; + } + else + { + mptoken->at(sfCouponAccrued) = remaining; + } + view().update(mptoken); + + schedule->at(sfPoolAmount) = pool - pay; + view().update(schedule); + + ctx_.deliver(pay); + + return tesSUCCESS; +} + +void +CouponClaim::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) +{ + // No transaction-specific invariants yet (future work). +} + +bool +CouponClaim::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&) +{ + // No transaction-specific invariants yet (future work). + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/coupon/CouponPay.cpp b/src/libxrpl/tx/transactors/coupon/CouponPay.cpp new file mode 100644 index 00000000000..8ff568cb064 --- /dev/null +++ b/src/libxrpl/tx/transactors/coupon/CouponPay.cpp @@ -0,0 +1,187 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +namespace { + +// The coupon total for one payment: per-unit amount times units outstanding, +// rounded up so the pool is never short of what holders can settle. +STAmount +couponTotal(STAmount const& perUnit, std::uint64_t outstanding) +{ + NumberRoundModeGuard const mg(Number::RoundingMode::Upward); + return STAmount{perUnit.asset(), Number(outstanding) * Number(perUnit)}; +} + +} // namespace + +NotTEC +CouponPay::preflight(PreflightContext const& ctx) +{ + if (ctx.tx[sfMPTokenIssuanceID] == beast::kZero) + return temMALFORMED; + + auto const amount = ctx.tx[sfAmount]; + if (amount <= beast::kZero) + return temBAD_AMOUNT; + + return tesSUCCESS; +} + +TER +CouponPay::preclaim(PreclaimContext const& ctx) +{ + auto const issuanceID = ctx.tx[sfMPTokenIssuanceID]; + + auto const schedule = ctx.view.read(keylet::couponSchedule(issuanceID)); + if (!schedule) + return tecNO_ENTRY; + + // Only the payer named on the schedule may declare a coupon. For a vault + // share issuance that is the vault pseudo-account, not the administrator. + if (ctx.tx[sfAccount] != schedule->at(sfAccount)) + return tecNO_PERMISSION; + + auto const amount = ctx.tx[sfAmount]; + if (amount.asset() != schedule->at(sfCouponAsset)) + return tecWRONG_ASSET; + + auto const closeTime = ctx.view.header().parentCloseTime.time_since_epoch().count(); + if (auto const expiration = schedule->at(~sfExpiration); expiration && closeTime >= *expiration) + return tecEXPIRED; + + auto const issuance = ctx.view.read(keylet::mptokenIssuance(issuanceID)); + if (!issuance) + return tecOBJECT_NOT_FOUND; + + std::uint64_t const outstanding = issuance->at(sfOutstandingAmount); + if (outstanding == 0) + return tecNO_PERMISSION; + + // The pool must be funded in full at declaration, so a claim can never + // fail for want of funds. + STAmount const total = couponTotal(amount, outstanding); + AccountID const payer = ctx.tx[sfAccount]; + if (total.native()) + { + if (xrpLiquid(ctx.view, payer, 0, ctx.j) < total.xrp()) + return tecINSUFFICIENT_FUNDS; + } + else if (payer != total.getIssuer()) + { + STAmount const spendable = std::visit( + [&](T const& issue) { + if constexpr (std::is_same_v) + return accountHolds( + ctx.view, + payer, + issue.currency, + issue.account, + FreezeHandling::ZeroIfFrozen, + ctx.j); + else + return accountHolds( + ctx.view, + payer, + issue, + FreezeHandling::ZeroIfFrozen, + AuthHandling::IgnoreAuth, + ctx.j); + }, + total.asset().value()); + if (spendable < total) + return tecINSUFFICIENT_FUNDS; + } + + return tesSUCCESS; +} + +TER +CouponPay::doApply() +{ + auto const issuanceID = ctx_.tx[sfMPTokenIssuanceID]; + auto schedule = view().peek(keylet::couponSchedule(issuanceID)); + if (!schedule) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const issuance = view().read(keylet::mptokenIssuance(issuanceID)); + if (!issuance) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const perUnit = ctx_.tx[sfAmount]; + STAmount const total = couponTotal(perUnit, issuance->at(sfOutstandingAmount)); + + // Move the coupon out of the payer's spendable balance. XRP is debited + // directly; an IOU parks on its issuer's trust line and an MPT moves into + // LockedAmount, the same custody the token escrow uses. + if (total.native()) + { + auto const sle = view().peek(keylet::account(accountID_)); + if (!sle) + return tefINTERNAL; // LCOV_EXCL_LINE + auto const balance = sle->at(sfBalance); + if (balance < total) + return tecINSUFFICIENT_FUNDS; + sle->at(sfBalance) = balance - total; + view().update(sle); + } + else if (total.holds()) + { + if (auto const ter = lockEscrowMPT(view(), accountID_, total, j_); !isTesSuccess(ter)) + return ter; + } + else + { + auto const issuerID = total.getIssuer(); + if (issuerID == accountID_) + return tecNO_PERMISSION; + if (auto const ter = directSendNoFee(view(), accountID_, issuerID, total, true, j_); + !isTesSuccess(ter)) + return ter; + } + + STAmount const accruedPerUnit = schedule->at(sfAccruedPerUnit); + STAmount const pool = schedule->at(sfPoolAmount); + if (!canAdd(accruedPerUnit, perUnit) || !canAdd(pool, total)) + return tecPRECISION_LOSS; + + schedule->at(sfAccruedPerUnit) = accruedPerUnit + perUnit; + schedule->at(sfPoolAmount) = pool + total; + schedule->at(sfCouponCount) = schedule->at(sfCouponCount) + 1; + schedule->at(sfLastCouponTime) = + static_cast(view().header().parentCloseTime.time_since_epoch().count()); + view().update(schedule); + + return tesSUCCESS; +} + +void +CouponPay::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) +{ + // No transaction-specific invariants yet (future work). +} + +bool +CouponPay::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&) +{ + // No transaction-specific invariants yet (future work). + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/coupon/CouponScheduleCreate.cpp b/src/libxrpl/tx/transactors/coupon/CouponScheduleCreate.cpp new file mode 100644 index 00000000000..5b48b9b358b --- /dev/null +++ b/src/libxrpl/tx/transactors/coupon/CouponScheduleCreate.cpp @@ -0,0 +1,202 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +NotTEC +CouponScheduleCreate::preflight(PreflightContext const& ctx) +{ + if (ctx.tx[sfMPTokenIssuanceID] == beast::kZero) + return temMALFORMED; + + // A bond paying coupons in itself is rebasing, not interest. + if (auto const& couponAsset = ctx.tx[sfCouponAsset]; couponAsset.holds() && + couponAsset.get().getMptID() == ctx.tx[sfMPTokenIssuanceID]) + return temMALFORMED; + + // The declared terms are all-or-nothing: a schedule either publishes a + // coupon date grid or distributes ad hoc. + bool const hasAmount = ctx.tx.isFieldPresent(sfCouponAmount); + bool const hasInterval = ctx.tx.isFieldPresent(sfCouponInterval); + bool const hasFirst = ctx.tx.isFieldPresent(sfFirstCouponTime); + if (hasAmount != hasInterval || hasAmount != hasFirst) + return temMALFORMED; + + if (hasAmount) + { + if (ctx.tx[sfCouponAmount] <= beast::kZero) + return temBAD_AMOUNT; + if (ctx.tx[sfCouponInterval] == 0 || ctx.tx[sfFirstCouponTime] == 0) + return temMALFORMED; + } + + auto const expiration = ctx.tx[~sfExpiration]; + if (expiration && hasFirst && *expiration <= ctx.tx[sfFirstCouponTime]) + return temBAD_EXPIRATION; + + if (auto const notice = ctx.tx[~sfCallNoticePeriod]; notice && *notice == 0) + return temMALFORMED; + + if (auto const earliestCall = ctx.tx[~sfEarliestCallTime]) + { + // Call protection is meaningless on a non-callable schedule. + if (!ctx.tx.isFieldPresent(sfCallNoticePeriod)) + return temMALFORMED; + if (expiration && *earliestCall >= *expiration) + return temBAD_EXPIRATION; + } + + return tesSUCCESS; +} + +TER +CouponScheduleCreate::preclaim(PreclaimContext const& ctx) +{ + auto const issuanceID = ctx.tx[sfMPTokenIssuanceID]; + AccountID const submitter = ctx.tx[sfAccount]; + + // One schedule per issuance: the keylet is the issuance, so a second + // create collides here. + if (ctx.view.exists(keylet::couponSchedule(issuanceID))) + return tecDUPLICATE; + + auto const issuance = ctx.view.read(keylet::mptokenIssuance(issuanceID)); + if (!issuance) + return tecOBJECT_NOT_FOUND; + + // Confidential balances hide the units a coupon would accrue against. + if (issuance->isFlag(lsfMPTCanHoldConfidentialBalance)) + return tecNO_PERMISSION; + + auto const& couponAsset = ctx.tx[sfCouponAsset]; + + if (!couponAsset.native()) + { + if (auto const ter = std::visit( + [&](T const& issue) -> TER { + if constexpr (std::is_same_v) + { + if (!ctx.view.exists(keylet::account(issue.account))) + return tecNO_ISSUER; + } + else + { + if (!ctx.view.exists(keylet::mptokenIssuance(issue.getMptID()))) + return tecOBJECT_NOT_FOUND; + } + return tesSUCCESS; + }, + couponAsset.value()); + !isTesSuccess(ter)) + return ter; + } + + // The payer is the issuance's issuer. For a vault share issuance that is + // the vault pseudo-account, and the vault owner administers the schedule. + AccountID const payer = issuance->at(sfIssuer); + if (payer == submitter) + return tesSUCCESS; + + auto const sleIssuer = ctx.view.read(keylet::account(payer)); + if (!sleIssuer) + return tecNO_ISSUER; // LCOV_EXCL_LINE + + if (auto const vaultID = sleIssuer->at(~sfVaultID)) + { + auto const vault = ctx.view.read(keylet::vault(*vaultID)); + if (!vault) + return tecOBJECT_NOT_FOUND; // LCOV_EXCL_LINE + if (vault->at(sfOwner) != submitter) + return tecNO_PERMISSION; + return tesSUCCESS; + } + + return tecNO_PERMISSION; +} + +TER +CouponScheduleCreate::doApply() +{ + auto const& tx = ctx_.tx; + auto const issuanceID = tx[sfMPTokenIssuanceID]; + + auto const owner = view().peek(keylet::account(accountID_)); + if (!owner) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto issuance = view().peek(keylet::mptokenIssuance(issuanceID)); + if (!issuance) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const& couponAsset = tx[sfCouponAsset]; + auto schedule = std::make_shared(keylet::couponSchedule(issuanceID)); + + schedule->at(sfOwner) = accountID_; + schedule->at(sfAccount) = issuance->at(sfIssuer); + schedule->at(sfMPTokenIssuanceID) = issuanceID; + schedule->setFieldIssue(sfCouponAsset, STIssue{sfCouponAsset, couponAsset}); + schedule->at(sfAccruedPerUnit) = STAmount{couponAsset, 0}; + schedule->at(sfPoolAmount) = STAmount{couponAsset, 0}; + if (auto const couponAmount = tx[~sfCouponAmount]) + { + schedule->at(sfCouponAmount) = *couponAmount; + schedule->at(sfCouponInterval) = tx[sfCouponInterval]; + schedule->at(sfFirstCouponTime) = tx[sfFirstCouponTime]; + } + if (auto const expiration = tx[~sfExpiration]) + schedule->at(sfExpiration) = *expiration; + if (auto const notice = tx[~sfCallNoticePeriod]) + schedule->at(sfCallNoticePeriod) = *notice; + if (auto const earliestCall = tx[~sfEarliestCallTime]) + schedule->at(sfEarliestCallTime) = *earliestCall; + + if (auto const ter = dirLink(view(), accountID_, schedule)) + return ter; // LCOV_EXCL_LINE + increaseOwnerCount(view(), owner, {}, 1, j_); + if (preFeeBalance_ < accountReserve(view(), owner, j_)) + return tecINSUFFICIENT_RESERVE; + + view().insert(schedule); + + // Settlement reads this flag to skip issuances that pay no coupons. + issuance->setFlag(lsfMPTCouponSchedule); + view().update(issuance); + + return tesSUCCESS; +} + +void +CouponScheduleCreate::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) +{ + // No transaction-specific invariants yet (future work). +} + +bool +CouponScheduleCreate::finalizeInvariants( + STTx const&, + TER, + XRPAmount, + ReadView const&, + beast::Journal const&) +{ + // No transaction-specific invariants yet (future work). + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/coupon/CouponScheduleDelete.cpp b/src/libxrpl/tx/transactors/coupon/CouponScheduleDelete.cpp new file mode 100644 index 00000000000..51d50641260 --- /dev/null +++ b/src/libxrpl/tx/transactors/coupon/CouponScheduleDelete.cpp @@ -0,0 +1,93 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +NotTEC +CouponScheduleDelete::preflight(PreflightContext const& ctx) +{ + if (ctx.tx[sfMPTokenIssuanceID] == beast::kZero) + return temMALFORMED; + + return tesSUCCESS; +} + +TER +CouponScheduleDelete::preclaim(PreclaimContext const& ctx) +{ + auto const issuanceID = ctx.tx[sfMPTokenIssuanceID]; + auto const schedule = ctx.view.read(keylet::couponSchedule(issuanceID)); + if (!schedule) + return tecNO_ENTRY; + + if (ctx.tx[sfAccount] != schedule->at(sfOwner)) + return tecNO_PERMISSION; + + auto const issuance = ctx.view.read(keylet::mptokenIssuance(issuanceID)); + if (!issuance) + return tecOBJECT_NOT_FOUND; // LCOV_EXCL_LINE + + if (issuance->at(sfOutstandingAmount) > 0 || schedule->at(sfClaimantCount) > 0) + return tecHAS_OBLIGATIONS; + + return tesSUCCESS; +} + +TER +CouponScheduleDelete::doApply() +{ + auto const issuanceID = ctx_.tx[sfMPTokenIssuanceID]; + auto const k = keylet::couponSchedule(issuanceID); + auto schedule = view().peek(k); + if (!schedule) + return tefINTERNAL; // LCOV_EXCL_LINE + + if (!view().dirRemove(keylet::ownerDir(accountID_), schedule->at(sfOwnerNode), k.key, false)) + { + // LCOV_EXCL_START + JLOG(j_.fatal()) << "CouponScheduleDelete: failed to remove dir link."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + decreaseOwnerCount(view(), accountID_, std::nullopt, 1, j_); + view().erase(schedule); + + // Settlement can stop looking at this issuance. + if (auto issuance = view().peek(keylet::mptokenIssuance(issuanceID))) + { + issuance->clearFlag(lsfMPTCouponSchedule); + view().update(issuance); + } + + return tesSUCCESS; +} + +void +CouponScheduleDelete::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) +{ + // No transaction-specific invariants yet (future work). +} + +bool +CouponScheduleDelete::finalizeInvariants( + STTx const&, + TER, + XRPAmount, + ReadView const&, + beast::Journal const&) +{ + // No transaction-specific invariants yet (future work). + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/coupon/CouponScheduleSet.cpp b/src/libxrpl/tx/transactors/coupon/CouponScheduleSet.cpp new file mode 100644 index 00000000000..03a867a9cd2 --- /dev/null +++ b/src/libxrpl/tx/transactors/coupon/CouponScheduleSet.cpp @@ -0,0 +1,100 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +NotTEC +CouponScheduleSet::preflight(PreflightContext const& ctx) +{ + if (ctx.tx[sfMPTokenIssuanceID] == beast::kZero) + return temMALFORMED; + + if (ctx.tx[sfExpiration] == 0) + return temMALFORMED; + + return tesSUCCESS; +} + +TER +CouponScheduleSet::preclaim(PreclaimContext const& ctx) +{ + auto const schedule = ctx.view.read(keylet::couponSchedule(ctx.tx[sfMPTokenIssuanceID])); + if (!schedule) + return tecNO_ENTRY; + + if (ctx.tx[sfAccount] != schedule->at(sfOwner)) + return tecNO_PERMISSION; + + // Non-callable schedules are fully immutable. + auto const notice = schedule->at(~sfCallNoticePeriod); + if (!notice) + { + JLOG(ctx.j.debug()) << "CouponScheduleSet: schedule is not callable."; + return tecNO_PERMISSION; + } + + std::int64_t const newExpiration = ctx.tx[sfExpiration]; + std::int64_t const now = ctx.view.header().parentCloseTime.time_since_epoch().count(); + + // The notice floor: a call never takes effect with less warning than + // the schedule declared at creation. + if (newExpiration < now + static_cast(*notice)) + return tecNO_PERMISSION; + + // Call protection. + if (auto const earliestCall = schedule->at(~sfEarliestCallTime); + earliestCall && newExpiration < static_cast(*earliestCall)) + return tecNO_PERMISSION; + + // Shorten-only: a call can never extend the coupon liability. A + // perpetual callable schedule may have Expiration set for the first + // time. + if (auto const expiration = schedule->at(~sfExpiration); + expiration && newExpiration >= static_cast(*expiration)) + return tecNO_PERMISSION; + + return tesSUCCESS; +} + +TER +CouponScheduleSet::doApply() +{ + auto schedule = view().peek(keylet::couponSchedule(ctx_.tx[sfMPTokenIssuanceID])); + if (!schedule) + return tefINTERNAL; // LCOV_EXCL_LINE + + schedule->at(sfExpiration) = ctx_.tx[sfExpiration]; + view().update(schedule); + + return tesSUCCESS; +} + +void +CouponScheduleSet::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) +{ + // No transaction-specific invariants yet (future work). +} + +bool +CouponScheduleSet::finalizeInvariants( + STTx const&, + TER, + XRPAmount, + ReadView const&, + beast::Journal const&) +{ + // No transaction-specific invariants yet (future work). + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp index 5be38921517..dfef1defdf4 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -247,6 +248,11 @@ ConfidentialMPTConvert::doApply() if (amtToConvert > kMaxMpTokenAmount - currentCOA) return tecINTERNAL; // LCOV_EXCL_LINE + // Coupons accrue against units held, so settle before they change. + if (auto const ter = couponSettleIfScheduled(view(), sleIssuance, sleMptoken, j_); + !isTesSuccess(ter)) + return ter; + (*sleMptoken)[sfMPTAmount] = amt - amtToConvert; (*sleIssuance)[sfConfidentialOutstandingAmount] = currentCOA + amtToConvert; diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp index 1e3617ffbd7..f38266eedef 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -264,6 +265,11 @@ ConfidentialMPTConvertBack::doApply() // outstanding. This is the inverse of Convert. if (amt > kMaxMpTokenAmount - amtToConvertBack) return tecINTERNAL; // LCOV_EXCL_LINE + // Coupons accrue against units held, so settle before they change. + if (auto const ter = couponSettleIfScheduled(view(), sleIssuance, sleMptoken, j_); + !isTesSuccess(ter)) + return ter; + (*sleMptoken)[sfMPTAmount] = amt + amtToConvertBack; auto const coa = (*sleIssuance)[~sfConfidentialOutstandingAmount].valueOr(0); diff --git a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp index 60b5c6d3afc..d79dcd84c21 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -61,6 +62,10 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) if (!sleMpt) return tecOBJECT_NOT_FOUND; + // Unclaimed coupons are an obligation to this holder. + if ((*sleMpt)[~sfCouponAccrued] && (*sleMpt)[sfCouponAccrued] != beast::kZero) + return tecHAS_OBLIGATIONS; + if ((*sleMpt)[sfMPTAmount] != 0) { if (!sleMptIssuance) diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp index 05d39c3596e..73cc14c4bfc 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -36,6 +37,10 @@ MPTokenIssuanceDestroy::preclaim(PreclaimContext const& ctx) if ((*sleMPT)[~sfLockedAmount].value_or(0) != 0) return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE + // A coupon schedule still references this issuance. + if (sleMPT->isFlag(lsfMPTCouponSchedule)) + return tecHAS_OBLIGATIONS; + return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp index 9c6c41654b8..74529664a70 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -68,6 +69,10 @@ VaultDelete::preclaim(PreclaimContext const& ctx) // Verify we can destroy MPTokenIssuance auto const sleMPT = ctx.view.read(keylet::mptokenIssuance(vault->at(sfShareMPTID))); + // A coupon schedule on the share issuance is an unmet obligation. + if (sleMPT && sleMPT->isFlag(lsfMPTCouponSchedule)) + return tecHAS_OBLIGATIONS; + if (!sleMPT) { // LCOV_EXCL_START diff --git a/src/test/app/Coupon_test.cpp b/src/test/app/Coupon_test.cpp new file mode 100644 index 00000000000..9f0778b5232 --- /dev/null +++ b/src/test/app/Coupon_test.cpp @@ -0,0 +1,526 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace xrpl { + +class Coupon_test : public beast::unit_test::Suite +{ + using MPTTester = test::jtx::MPTTester; + + static json::Value + scheduleCreate( + test::jtx::Account const& admin, + MPTID const& issuanceID, + Asset const& couponAsset) + { + json::Value jv; + jv[jss::TransactionType] = jss::CouponScheduleCreate; + jv[jss::Account] = admin.human(); + jv[sfMPTokenIssuanceID] = to_string(issuanceID); + jv[sfCouponAsset] = toJson(couponAsset); + return jv; + } + + static json::Value + couponPay(test::jtx::Account const& payer, MPTID const& issuanceID, STAmount const& perUnit) + { + json::Value jv; + jv[jss::TransactionType] = jss::CouponPay; + jv[jss::Account] = payer.human(); + jv[sfMPTokenIssuanceID] = to_string(issuanceID); + jv[sfAmount] = toJson(perUnit); + return jv; + } + + static json::Value + couponClaim( + test::jtx::Account const& holder, + MPTID const& issuanceID, + std::optional const& amount = std::nullopt) + { + json::Value jv; + jv[jss::TransactionType] = jss::CouponClaim; + jv[jss::Account] = holder.human(); + jv[sfMPTokenIssuanceID] = to_string(issuanceID); + if (amount) + jv[sfAmount] = toJson(*amount); + return jv; + } + + static json::Value + scheduleDelete(test::jtx::Account const& admin, MPTID const& issuanceID) + { + json::Value jv; + jv[jss::TransactionType] = jss::CouponScheduleDelete; + jv[jss::Account] = admin.human(); + jv[sfMPTokenIssuanceID] = to_string(issuanceID); + return jv; + } + + // The schedule entry, or nullptr when no schedule exists. + static std::shared_ptr + scheduleOf(test::jtx::Env& env, MPTID const& issuanceID) + { + return env.le(keylet::couponSchedule(issuanceID)); + } + + static STAmount + accruedOf(test::jtx::Env& env, MPTID const& issuanceID, test::jtx::Account const& holder) + { + auto const sle = env.le(keylet::mptoken(issuanceID, holder.id())); + if (!sle || !sle->isFieldPresent(sfCouponAccrued)) + return STAmount{}; + return sle->getFieldAmount(sfCouponAccrued); + } + + void + testDisabled() + { + testcase("disabled"); + using namespace test::jtx; + + Env env{*this, testableAmendments() - featureCouponPayments}; + Account const alice{"alice"}; + Account const gw{"gw"}; + env.fund(XRP(10'000), alice, gw); + env.close(); + + MPTID const fake{}; + env(scheduleCreate(alice, fake, gw["USD"]), Ter(temDISABLED)); + env(couponPay(alice, fake, gw["USD"](1)), Ter(temDISABLED)); + env(couponClaim(alice, fake), Ter(temDISABLED)); + env(scheduleDelete(alice, fake), Ter(temDISABLED)); + } + + void + testScheduleCreate() + { + testcase("schedule create"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const gw{"gw"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10'000), gw, alice, bob); + env.close(); + auto const USD = gw["USD"]; + env.trust(USD(10'000), alice, bob); + env(pay(gw, alice, USD(1'000))); + env.close(); + + MPTTester mpt{env, alice, {.holders = {bob}, .fund = false}}; + mpt.create({.flags = tfMPTCanTransfer}); + mpt.authorize({.account = bob}); + env.close(); + + auto const id = mpt.issuanceID(); + + // A stranger cannot create a schedule on someone else's issuance. + env(scheduleCreate(bob, id, USD), Ter(tecNO_PERMISSION)); + env.close(); + + // A bond paying coupons in itself is rebasing, not interest. + env(scheduleCreate(alice, id, mpt), Ter(temMALFORMED)); + env.close(); + + env(scheduleCreate(alice, id, USD), Ter(tesSUCCESS)); + env.close(); + + auto const sle = scheduleOf(env, id); + BEAST_EXPECT(sle); + if (sle) + { + BEAST_EXPECT(sle->getAccountID(sfOwner) == alice.id()); + BEAST_EXPECT(sle->getAccountID(sfAccount) == alice.id()); + BEAST_EXPECT(sle->getFieldAmount(sfPoolAmount) == USD(0)); + BEAST_EXPECT(sle->getFieldAmount(sfAccruedPerUnit) == USD(0)); + BEAST_EXPECT(sle->getFieldU32(sfClaimantCount) == 0); + BEAST_EXPECT(sle->getFieldU32(sfCouponCount) == 0); + } + + // The issuance is flagged so settlement can skip issuances without one. + auto const issuance = env.le(keylet::mptokenIssuance(id)); + BEAST_EXPECT(issuance && issuance->isFlag(lsfMPTCouponSchedule)); + + // One schedule per issuance. + env(scheduleCreate(alice, id, USD), Ter(tecDUPLICATE)); + env.close(); + } + + void + testCouponPay() + { + testcase("coupon pay funds the pool"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const gw{"gw"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10'000), gw, alice, bob); + env.close(); + auto const USD = gw["USD"]; + auto const EUR = gw["EUR"]; + env.trust(USD(10'000), alice, bob); + env.trust(EUR(10'000), alice); + env(pay(gw, alice, USD(1'000))); + env.close(); + + MPTTester mpt{env, alice, {.holders = {bob}, .fund = false}}; + mpt.create({.flags = tfMPTCanTransfer}); + mpt.authorize({.account = bob}); + mpt.pay(alice, bob, 100); + env.close(); + + auto const id = mpt.issuanceID(); + env(scheduleCreate(alice, id, USD), Ter(tesSUCCESS)); + env.close(); + + // Only the payer named on the schedule may declare a coupon. + env(couponPay(bob, id, USD(1)), Ter(tecNO_PERMISSION)); + env.close(); + + // The coupon must be denominated in the schedule's asset. + env(couponPay(alice, id, EUR(1)), Ter(tecWRONG_ASSET)); + env.close(); + + auto const before = env.balance(alice, USD); + env(couponPay(alice, id, USD(2)), Ter(tesSUCCESS)); + env.close(); + + // 100 units outstanding at 2 USD per unit funds 200 USD. + auto const sle = scheduleOf(env, id); + BEAST_EXPECT(sle); + if (sle) + { + BEAST_EXPECT(sle->getFieldAmount(sfPoolAmount) == USD(200)); + BEAST_EXPECT(sle->getFieldAmount(sfAccruedPerUnit) == USD(2)); + BEAST_EXPECT(sle->getFieldU32(sfCouponCount) == 1); + BEAST_EXPECT(sle->isFieldPresent(sfLastCouponTime)); + } + BEAST_EXPECT(env.balance(alice, USD) == before - USD(200)); + } + + void + testAccrualAndClaim() + { + testcase("accrual and claim from the pool"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const gw{"gw"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10'000), gw, alice, bob); + env.close(); + auto const USD = gw["USD"]; + env.trust(USD(10'000), alice, bob); + env(pay(gw, alice, USD(1'000))); + env.close(); + + MPTTester mpt{env, alice, {.holders = {bob}, .fund = false}}; + mpt.create({.flags = tfMPTCanTransfer}); + mpt.authorize({.account = bob}); + mpt.pay(alice, bob, 100); + env.close(); + + auto const id = mpt.issuanceID(); + env(scheduleCreate(alice, id, USD), Ter(tesSUCCESS)); + env.close(); + + // Nothing declared yet: nothing to claim. + env(couponClaim(bob, id), Ter(tecNO_PERMISSION)); + env.close(); + + env(couponPay(alice, id, USD(2)), Ter(tesSUCCESS)); + env.close(); + + auto const bobBefore = env.balance(bob, USD); + env(couponClaim(bob, id), Ter(tesSUCCESS)); + env.close(); + + // 100 units at 2 USD per unit. + BEAST_EXPECT(env.balance(bob, USD) == bobBefore + USD(200)); + BEAST_EXPECT(accruedOf(env, id, bob) == STAmount{}); + + auto const sle = scheduleOf(env, id); + BEAST_EXPECT(sle); + if (sle) + { + BEAST_EXPECT(sle->getFieldAmount(sfPoolAmount) == USD(0)); + BEAST_EXPECT(sle->getFieldU32(sfClaimantCount) == 0); + } + + // A second claim has nothing left. + env(couponClaim(bob, id), Ter(tecNO_PERMISSION)); + env.close(); + } + + void + testArrears() + { + testcase("arrears collect in one claim"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const gw{"gw"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10'000), gw, alice, bob); + env.close(); + auto const USD = gw["USD"]; + env.trust(USD(10'000), alice, bob); + env(pay(gw, alice, USD(1'000))); + env.close(); + + MPTTester mpt{env, alice, {.holders = {bob}, .fund = false}}; + mpt.create({.flags = tfMPTCanTransfer}); + mpt.authorize({.account = bob}); + mpt.pay(alice, bob, 10); + env.close(); + + auto const id = mpt.issuanceID(); + env(scheduleCreate(alice, id, USD), Ter(tesSUCCESS)); + env.close(); + + for (int i = 0; i < 3; ++i) + { + env(couponPay(alice, id, USD(1)), Ter(tesSUCCESS)); + env.close(); + } + + auto const bobBefore = env.balance(bob, USD); + env(couponClaim(bob, id), Ter(tesSUCCESS)); + env.close(); + + // Three coupons of 1 USD on 10 units. + BEAST_EXPECT(env.balance(bob, USD) == bobBefore + USD(30)); + } + + void + testSettlementOnTransfer() + { + testcase("units settle before they move"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const gw{"gw"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const carol{"carol"}; + env.fund(XRP(10'000), gw, alice, bob, carol); + env.close(); + auto const USD = gw["USD"]; + env.trust(USD(10'000), alice, bob, carol); + env(pay(gw, alice, USD(1'000))); + env.close(); + + MPTTester mpt{env, alice, {.holders = {bob, carol}, .fund = false}}; + mpt.create({.flags = tfMPTCanTransfer}); + mpt.authorize({.account = bob}); + mpt.authorize({.account = carol}); + mpt.pay(alice, bob, 100); + env.close(); + + auto const id = mpt.issuanceID(); + env(scheduleCreate(alice, id, USD), Ter(tesSUCCESS)); + env.close(); + + env(couponPay(alice, id, USD(1)), Ter(tesSUCCESS)); + env.close(); + + // bob sells the whole position to carol after the coupon is declared. + mpt.pay(bob, carol, 100); + env.close(); + + // The coupon belongs to bob, who held the units when it was declared. + BEAST_EXPECT(accruedOf(env, id, bob) == USD(100)); + BEAST_EXPECT(accruedOf(env, id, carol) == STAmount{}); + + // A second coupon now belongs entirely to carol. + env(couponPay(alice, id, USD(1)), Ter(tesSUCCESS)); + env.close(); + env(couponClaim(carol, id), Ter(tesSUCCESS)); + env.close(); + + // bob keeps his arrears on a zero-unit MPToken and collects later. + env(couponClaim(bob, id), Ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(accruedOf(env, id, bob) == STAmount{}); + } + + void + testPoolAlwaysCovers() + { + testcase("a declared coupon cannot be spent away"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const gw{"gw"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const dave{"dave"}; + env.fund(XRP(10'000), gw, alice, bob, dave); + env.close(); + auto const USD = gw["USD"]; + env.trust(USD(10'000), alice, bob, dave); + env(pay(gw, alice, USD(500))); + env.close(); + + MPTTester mpt{env, alice, {.holders = {bob}, .fund = false}}; + mpt.create({.flags = tfMPTCanTransfer}); + mpt.authorize({.account = bob}); + mpt.pay(alice, bob, 100); + env.close(); + + auto const id = mpt.issuanceID(); + env(scheduleCreate(alice, id, USD), Ter(tesSUCCESS)); + env.close(); + + env(couponPay(alice, id, USD(2)), Ter(tesSUCCESS)); + env.close(); + + // The payer spends everything else she has. + env(pay(alice, dave, env.balance(alice, USD)), Ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(env.balance(alice, USD) == USD(0)); + + // The claim is still honored: the pool holds it, not the payer. + auto const bobBefore = env.balance(bob, USD); + env(couponClaim(bob, id), Ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(env.balance(bob, USD) == bobBefore + USD(200)); + } + + void + testScheduleDelete() + { + testcase("schedule delete requires no obligations"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const gw{"gw"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10'000), gw, alice, bob); + env.close(); + auto const USD = gw["USD"]; + env.trust(USD(10'000), alice, bob); + env(pay(gw, alice, USD(1'000))); + env.close(); + + MPTTester mpt{env, alice, {.holders = {bob}, .fund = false}}; + mpt.create({.flags = tfMPTCanTransfer}); + mpt.authorize({.account = bob}); + mpt.pay(alice, bob, 100); + env.close(); + + auto const id = mpt.issuanceID(); + env(scheduleCreate(alice, id, USD), Ter(tesSUCCESS)); + env.close(); + + // Units are outstanding. + env(scheduleDelete(alice, id), Ter(tecHAS_OBLIGATIONS)); + env.close(); + + env(couponPay(alice, id, USD(1)), Ter(tesSUCCESS)); + env.close(); + mpt.pay(bob, alice, 100); + env.close(); + + // Nothing outstanding, but bob is still owed his coupon. + env(scheduleDelete(alice, id), Ter(tecHAS_OBLIGATIONS)); + env.close(); + + env(couponClaim(bob, id), Ter(tesSUCCESS)); + env.close(); + + env(scheduleDelete(alice, id), Ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(!scheduleOf(env, id)); + + auto const issuance = env.le(keylet::mptokenIssuance(id)); + BEAST_EXPECT(issuance && !issuance->isFlag(lsfMPTCouponSchedule)); + } + + void + testMPTCouponAsset() + { + testcase("coupons paid in an MPT"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const gw{"gw"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10'000), gw, alice, bob); + env.close(); + + // gw issues the coupon asset; alice holds it and pays coupons with it. + MPTTester coupon{env, gw, {.holders = {alice, bob}, .fund = false}}; + coupon.create({.flags = tfMPTCanTransfer}); + coupon.authorize({.account = alice}); + coupon.authorize({.account = bob}); + coupon.pay(gw, alice, 10'000); + env.close(); + + // alice issues the bond. + MPTTester bond{env, alice, {.holders = {bob}, .fund = false}}; + bond.create({.flags = tfMPTCanTransfer}); + bond.authorize({.account = bob}); + bond.pay(alice, bob, 100); + env.close(); + + auto const bondID = bond.issuanceID(); + env(scheduleCreate(alice, bondID, coupon), Ter(tesSUCCESS)); + env.close(); + + env(couponPay(alice, bondID, STAmount{coupon, 2}), Ter(tesSUCCESS)); + env.close(); + + auto const sle = scheduleOf(env, bondID); + BEAST_EXPECT(sle); + if (sle) + BEAST_EXPECT(sle->getFieldAmount(sfPoolAmount) == STAmount(coupon, 200)); + + env(couponClaim(bob, bondID), Ter(tesSUCCESS)); + env.close(); + + // bob received 100 units at 2 of the coupon MPT each. + coupon.checkMPTokenAmount(bob, 200); + } + +public: + void + run() override + { + testDisabled(); + testScheduleCreate(); + testCouponPay(); + testAccrualAndClaim(); + testArrears(); + testSettlementOnTransfer(); + testPoolAlwaysCovers(); + testScheduleDelete(); + testMPTCouponAsset(); + } +}; + +BEAST_DEFINE_TESTSUITE(Coupon, app, xrpl); + +} // namespace xrpl diff --git a/src/test/app/invariants/InvariantsCoupon_test.cpp b/src/test/app/invariants/InvariantsCoupon_test.cpp new file mode 100644 index 00000000000..dfedc132058 --- /dev/null +++ b/src/test/app/invariants/InvariantsCoupon_test.cpp @@ -0,0 +1,131 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::test { + +class InvariantsCoupon_test : public InvariantsBase +{ + FeatureBitset const all_{test::jtx::testableAmendments()}; + + void + testValidCouponSchedule() + { + using namespace test::jtx; + testcase << "coupon schedule invariants"; + + // Build an issuance with a coupon schedule, then corrupt it. + auto setup = [&](Env& env, Account const& issuer, Account const& holder) { + MPTTester mpt( + {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100}); + auto const id = mpt.issuanceID(); + json::Value jv; + jv[jss::TransactionType] = jss::CouponScheduleCreate; + jv[jss::Account] = issuer.human(); + jv[sfMPTokenIssuanceID] = to_string(id); + jv[sfCouponAsset] = toJson(xrpIssue()); + env(jv); + env.close(); + return id; + }; + + { + Env env(*this, all_); + Account const issuer{"issuer"}; + Account const holder{"holder"}; + env.fund(XRP(10'000), issuer, holder); + auto const id = setup(env, issuer, holder); + + doInvariantCheck( + std::move(env), + issuer, + holder, + {{"Invariant failed: coupon accumulator decreased"}}, + [id](Account const&, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(keylet::couponSchedule(id)); + if (!sle) + return false; + sle->at(sfAccruedPerUnit) = XRP(-1).value(); + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ttCOUPON_PAY, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + } + + { + Env env(*this, all_); + Account const issuer{"issuer"}; + Account const holder{"holder"}; + env.fund(XRP(10'000), issuer, holder); + auto const id = setup(env, issuer, holder); + + doInvariantCheck( + std::move(env), + issuer, + holder, + {{"Invariant failed: coupon accumulator moved outside CouponPay"}}, + [id](Account const&, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(keylet::couponSchedule(id)); + if (!sle) + return false; + sle->at(sfAccruedPerUnit) = XRP(5).value(); + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + } + + { + Env env(*this, all_); + Account const issuer{"issuer"}; + Account const holder{"holder"}; + env.fund(XRP(10'000), issuer, holder); + auto const id = setup(env, issuer, holder); + + doInvariantCheck( + std::move(env), + issuer, + holder, + {{"Invariant failed: coupon pool grew outside CouponPay"}}, + [id](Account const&, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(keylet::couponSchedule(id)); + if (!sle) + return false; + sle->at(sfPoolAmount) = XRP(10).value(); + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + } + } + +public: + void + run() override + { + testValidCouponSchedule(); + } +}; + +BEAST_DEFINE_TESTSUITE(InvariantsCoupon, app, xrpl); + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/CouponScheduleTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/CouponScheduleTests.cpp new file mode 100644 index 00000000000..7f2bff0f0d2 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/CouponScheduleTests.cpp @@ -0,0 +1,567 @@ +// Auto-generated unit tests for ledger entry CouponSchedule + + +#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(CouponScheduleTests, BuilderSettersRoundTrip) +{ + uint256 const index{1u}; + + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + auto const ownerNodeValue = canonical_UINT64(); + auto const ownerValue = canonical_ACCOUNT(); + auto const accountValue = canonical_ACCOUNT(); + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const couponAssetValue = canonical_ISSUE(); + auto const accruedPerUnitValue = canonical_AMOUNT(); + auto const poolAmountValue = canonical_AMOUNT(); + auto const claimantCountValue = canonical_UINT32(); + auto const couponCountValue = canonical_UINT32(); + auto const lastCouponTimeValue = canonical_UINT32(); + auto const couponAmountValue = canonical_AMOUNT(); + auto const couponIntervalValue = canonical_UINT32(); + auto const firstCouponTimeValue = canonical_UINT32(); + auto const expirationValue = canonical_UINT32(); + auto const callNoticePeriodValue = canonical_UINT32(); + auto const earliestCallTimeValue = canonical_UINT32(); + + CouponScheduleBuilder builder{ + previousTxnIDValue, + previousTxnLgrSeqValue, + ownerNodeValue, + ownerValue, + accountValue, + mPTokenIssuanceIDValue, + couponAssetValue + }; + + builder.setAccruedPerUnit(accruedPerUnitValue); + builder.setPoolAmount(poolAmountValue); + builder.setClaimantCount(claimantCountValue); + builder.setCouponCount(couponCountValue); + builder.setLastCouponTime(lastCouponTimeValue); + builder.setCouponAmount(couponAmountValue); + builder.setCouponInterval(couponIntervalValue); + builder.setFirstCouponTime(firstCouponTimeValue); + builder.setExpiration(expirationValue); + builder.setCallNoticePeriod(callNoticePeriodValue); + builder.setEarliestCallTime(earliestCallTimeValue); + + 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 = ownerNodeValue; + auto const actual = entry.getOwnerNode(); + expectEqualField(expected, actual, "sfOwnerNode"); + } + + { + auto const& expected = ownerValue; + auto const actual = entry.getOwner(); + expectEqualField(expected, actual, "sfOwner"); + } + + { + auto const& expected = accountValue; + auto const actual = entry.getAccount(); + expectEqualField(expected, actual, "sfAccount"); + } + + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = entry.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = couponAssetValue; + auto const actual = entry.getCouponAsset(); + expectEqualField(expected, actual, "sfCouponAsset"); + } + + { + auto const& expected = accruedPerUnitValue; + auto const actualOpt = entry.getAccruedPerUnit(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfAccruedPerUnit"); + EXPECT_TRUE(entry.hasAccruedPerUnit()); + } + + { + auto const& expected = poolAmountValue; + auto const actualOpt = entry.getPoolAmount(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfPoolAmount"); + EXPECT_TRUE(entry.hasPoolAmount()); + } + + { + auto const& expected = claimantCountValue; + auto const actualOpt = entry.getClaimantCount(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfClaimantCount"); + EXPECT_TRUE(entry.hasClaimantCount()); + } + + { + auto const& expected = couponCountValue; + auto const actualOpt = entry.getCouponCount(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfCouponCount"); + EXPECT_TRUE(entry.hasCouponCount()); + } + + { + auto const& expected = lastCouponTimeValue; + auto const actualOpt = entry.getLastCouponTime(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfLastCouponTime"); + EXPECT_TRUE(entry.hasLastCouponTime()); + } + + { + auto const& expected = couponAmountValue; + auto const actualOpt = entry.getCouponAmount(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfCouponAmount"); + EXPECT_TRUE(entry.hasCouponAmount()); + } + + { + auto const& expected = couponIntervalValue; + auto const actualOpt = entry.getCouponInterval(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfCouponInterval"); + EXPECT_TRUE(entry.hasCouponInterval()); + } + + { + auto const& expected = firstCouponTimeValue; + auto const actualOpt = entry.getFirstCouponTime(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfFirstCouponTime"); + EXPECT_TRUE(entry.hasFirstCouponTime()); + } + + { + auto const& expected = expirationValue; + auto const actualOpt = entry.getExpiration(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfExpiration"); + EXPECT_TRUE(entry.hasExpiration()); + } + + { + auto const& expected = callNoticePeriodValue; + auto const actualOpt = entry.getCallNoticePeriod(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfCallNoticePeriod"); + EXPECT_TRUE(entry.hasCallNoticePeriod()); + } + + { + auto const& expected = earliestCallTimeValue; + auto const actualOpt = entry.getEarliestCallTime(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfEarliestCallTime"); + EXPECT_TRUE(entry.hasEarliestCallTime()); + } + + 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(CouponScheduleTests, BuilderFromSleRoundTrip) +{ + uint256 const index{2u}; + + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + auto const ownerNodeValue = canonical_UINT64(); + auto const ownerValue = canonical_ACCOUNT(); + auto const accountValue = canonical_ACCOUNT(); + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const couponAssetValue = canonical_ISSUE(); + auto const accruedPerUnitValue = canonical_AMOUNT(); + auto const poolAmountValue = canonical_AMOUNT(); + auto const claimantCountValue = canonical_UINT32(); + auto const couponCountValue = canonical_UINT32(); + auto const lastCouponTimeValue = canonical_UINT32(); + auto const couponAmountValue = canonical_AMOUNT(); + auto const couponIntervalValue = canonical_UINT32(); + auto const firstCouponTimeValue = canonical_UINT32(); + auto const expirationValue = canonical_UINT32(); + auto const callNoticePeriodValue = canonical_UINT32(); + auto const earliestCallTimeValue = canonical_UINT32(); + + auto sle = std::make_shared(CouponSchedule::entryType, index); + + sle->at(sfPreviousTxnID) = previousTxnIDValue; + sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue; + sle->at(sfOwnerNode) = ownerNodeValue; + sle->at(sfOwner) = ownerValue; + sle->at(sfAccount) = accountValue; + sle->at(sfMPTokenIssuanceID) = mPTokenIssuanceIDValue; + sle->at(sfCouponAsset) = STIssue(sfCouponAsset, couponAssetValue); + sle->at(sfAccruedPerUnit) = accruedPerUnitValue; + sle->at(sfPoolAmount) = poolAmountValue; + sle->at(sfClaimantCount) = claimantCountValue; + sle->at(sfCouponCount) = couponCountValue; + sle->at(sfLastCouponTime) = lastCouponTimeValue; + sle->at(sfCouponAmount) = couponAmountValue; + sle->at(sfCouponInterval) = couponIntervalValue; + sle->at(sfFirstCouponTime) = firstCouponTimeValue; + sle->at(sfExpiration) = expirationValue; + sle->at(sfCallNoticePeriod) = callNoticePeriodValue; + sle->at(sfEarliestCallTime) = earliestCallTimeValue; + + CouponScheduleBuilder builderFromSle{sle}; + EXPECT_TRUE(builderFromSle.validate()); + + auto const entryFromBuilder = builderFromSle.build(index); + + CouponSchedule 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 = ownerNodeValue; + + auto const fromSle = entryFromSle.getOwnerNode(); + auto const fromBuilder = entryFromBuilder.getOwnerNode(); + + expectEqualField(expected, fromSle, "sfOwnerNode"); + expectEqualField(expected, fromBuilder, "sfOwnerNode"); + } + + { + 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 = accountValue; + + auto const fromSle = entryFromSle.getAccount(); + auto const fromBuilder = entryFromBuilder.getAccount(); + + expectEqualField(expected, fromSle, "sfAccount"); + expectEqualField(expected, fromBuilder, "sfAccount"); + } + + { + auto const& expected = mPTokenIssuanceIDValue; + + auto const fromSle = entryFromSle.getMPTokenIssuanceID(); + auto const fromBuilder = entryFromBuilder.getMPTokenIssuanceID(); + + expectEqualField(expected, fromSle, "sfMPTokenIssuanceID"); + expectEqualField(expected, fromBuilder, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = couponAssetValue; + + auto const fromSle = entryFromSle.getCouponAsset(); + auto const fromBuilder = entryFromBuilder.getCouponAsset(); + + expectEqualField(expected, fromSle, "sfCouponAsset"); + expectEqualField(expected, fromBuilder, "sfCouponAsset"); + } + + { + auto const& expected = accruedPerUnitValue; + + auto const fromSleOpt = entryFromSle.getAccruedPerUnit(); + auto const fromBuilderOpt = entryFromBuilder.getAccruedPerUnit(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfAccruedPerUnit"); + expectEqualField(expected, *fromBuilderOpt, "sfAccruedPerUnit"); + } + + { + auto const& expected = poolAmountValue; + + auto const fromSleOpt = entryFromSle.getPoolAmount(); + auto const fromBuilderOpt = entryFromBuilder.getPoolAmount(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfPoolAmount"); + expectEqualField(expected, *fromBuilderOpt, "sfPoolAmount"); + } + + { + auto const& expected = claimantCountValue; + + auto const fromSleOpt = entryFromSle.getClaimantCount(); + auto const fromBuilderOpt = entryFromBuilder.getClaimantCount(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfClaimantCount"); + expectEqualField(expected, *fromBuilderOpt, "sfClaimantCount"); + } + + { + auto const& expected = couponCountValue; + + auto const fromSleOpt = entryFromSle.getCouponCount(); + auto const fromBuilderOpt = entryFromBuilder.getCouponCount(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfCouponCount"); + expectEqualField(expected, *fromBuilderOpt, "sfCouponCount"); + } + + { + auto const& expected = lastCouponTimeValue; + + auto const fromSleOpt = entryFromSle.getLastCouponTime(); + auto const fromBuilderOpt = entryFromBuilder.getLastCouponTime(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfLastCouponTime"); + expectEqualField(expected, *fromBuilderOpt, "sfLastCouponTime"); + } + + { + auto const& expected = couponAmountValue; + + auto const fromSleOpt = entryFromSle.getCouponAmount(); + auto const fromBuilderOpt = entryFromBuilder.getCouponAmount(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfCouponAmount"); + expectEqualField(expected, *fromBuilderOpt, "sfCouponAmount"); + } + + { + auto const& expected = couponIntervalValue; + + auto const fromSleOpt = entryFromSle.getCouponInterval(); + auto const fromBuilderOpt = entryFromBuilder.getCouponInterval(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfCouponInterval"); + expectEqualField(expected, *fromBuilderOpt, "sfCouponInterval"); + } + + { + auto const& expected = firstCouponTimeValue; + + auto const fromSleOpt = entryFromSle.getFirstCouponTime(); + auto const fromBuilderOpt = entryFromBuilder.getFirstCouponTime(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfFirstCouponTime"); + expectEqualField(expected, *fromBuilderOpt, "sfFirstCouponTime"); + } + + { + auto const& expected = expirationValue; + + auto const fromSleOpt = entryFromSle.getExpiration(); + auto const fromBuilderOpt = entryFromBuilder.getExpiration(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfExpiration"); + expectEqualField(expected, *fromBuilderOpt, "sfExpiration"); + } + + { + auto const& expected = callNoticePeriodValue; + + auto const fromSleOpt = entryFromSle.getCallNoticePeriod(); + auto const fromBuilderOpt = entryFromBuilder.getCallNoticePeriod(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfCallNoticePeriod"); + expectEqualField(expected, *fromBuilderOpt, "sfCallNoticePeriod"); + } + + { + auto const& expected = earliestCallTimeValue; + + auto const fromSleOpt = entryFromSle.getEarliestCallTime(); + auto const fromBuilderOpt = entryFromBuilder.getEarliestCallTime(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfEarliestCallTime"); + expectEqualField(expected, *fromBuilderOpt, "sfEarliestCallTime"); + } + + EXPECT_EQ(entryFromSle.getKey(), index); + EXPECT_EQ(entryFromBuilder.getKey(), index); +} + +// 3) Verify wrapper throws when constructed from wrong ledger entry type. +TEST(CouponScheduleTests, 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(CouponSchedule{wrongEntry.getSle()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong ledger entry type. +TEST(CouponScheduleTests, 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(CouponScheduleBuilder{wrongEntry.getSle()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(CouponScheduleTests, OptionalFieldsReturnNullopt) +{ + uint256 const index{3u}; + + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + auto const ownerNodeValue = canonical_UINT64(); + auto const ownerValue = canonical_ACCOUNT(); + auto const accountValue = canonical_ACCOUNT(); + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const couponAssetValue = canonical_ISSUE(); + + CouponScheduleBuilder builder{ + previousTxnIDValue, + previousTxnLgrSeqValue, + ownerNodeValue, + ownerValue, + accountValue, + mPTokenIssuanceIDValue, + couponAssetValue + }; + + auto const entry = builder.build(index); + + // Verify optional fields are not present + EXPECT_FALSE(entry.hasAccruedPerUnit()); + EXPECT_FALSE(entry.getAccruedPerUnit().has_value()); + EXPECT_FALSE(entry.hasPoolAmount()); + EXPECT_FALSE(entry.getPoolAmount().has_value()); + EXPECT_FALSE(entry.hasClaimantCount()); + EXPECT_FALSE(entry.getClaimantCount().has_value()); + EXPECT_FALSE(entry.hasCouponCount()); + EXPECT_FALSE(entry.getCouponCount().has_value()); + EXPECT_FALSE(entry.hasLastCouponTime()); + EXPECT_FALSE(entry.getLastCouponTime().has_value()); + EXPECT_FALSE(entry.hasCouponAmount()); + EXPECT_FALSE(entry.getCouponAmount().has_value()); + EXPECT_FALSE(entry.hasCouponInterval()); + EXPECT_FALSE(entry.getCouponInterval().has_value()); + EXPECT_FALSE(entry.hasFirstCouponTime()); + EXPECT_FALSE(entry.getFirstCouponTime().has_value()); + EXPECT_FALSE(entry.hasExpiration()); + EXPECT_FALSE(entry.getExpiration().has_value()); + EXPECT_FALSE(entry.hasCallNoticePeriod()); + EXPECT_FALSE(entry.getCallNoticePeriod().has_value()); + EXPECT_FALSE(entry.hasEarliestCallTime()); + EXPECT_FALSE(entry.getEarliestCallTime().has_value()); +} +} diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp index 7db4b638a7f..2ec9d4e493e 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp @@ -33,6 +33,8 @@ TEST(MPTokenTests, BuilderSettersRoundTrip) auto const issuerEncryptedBalanceValue = canonical_VL(); auto const auditorEncryptedBalanceValue = canonical_VL(); auto const holderEncryptionKeyValue = canonical_VL(); + auto const couponAccruedValue = canonical_AMOUNT(); + auto const couponIndexValue = canonical_AMOUNT(); MPTokenBuilder builder{ accountValue, @@ -50,6 +52,8 @@ TEST(MPTokenTests, BuilderSettersRoundTrip) builder.setIssuerEncryptedBalance(issuerEncryptedBalanceValue); builder.setAuditorEncryptedBalance(auditorEncryptedBalanceValue); builder.setHolderEncryptionKey(holderEncryptionKeyValue); + builder.setCouponAccrued(couponAccruedValue); + builder.setCouponIndex(couponIndexValue); builder.setLedgerIndex(index); builder.setFlags(0x1u); @@ -154,6 +158,22 @@ TEST(MPTokenTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasHolderEncryptionKey()); } + { + auto const& expected = couponAccruedValue; + auto const actualOpt = entry.getCouponAccrued(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfCouponAccrued"); + EXPECT_TRUE(entry.hasCouponAccrued()); + } + + { + auto const& expected = couponIndexValue; + auto const actualOpt = entry.getCouponIndex(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfCouponIndex"); + EXPECT_TRUE(entry.hasCouponIndex()); + } + EXPECT_TRUE(entry.hasLedgerIndex()); auto const ledgerIndex = entry.getLedgerIndex(); ASSERT_TRUE(ledgerIndex.has_value()); @@ -180,6 +200,8 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip) auto const issuerEncryptedBalanceValue = canonical_VL(); auto const auditorEncryptedBalanceValue = canonical_VL(); auto const holderEncryptionKeyValue = canonical_VL(); + auto const couponAccruedValue = canonical_AMOUNT(); + auto const couponIndexValue = canonical_AMOUNT(); auto sle = std::make_shared(MPToken::entryType, index); @@ -196,6 +218,8 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip) sle->at(sfIssuerEncryptedBalance) = issuerEncryptedBalanceValue; sle->at(sfAuditorEncryptedBalance) = auditorEncryptedBalanceValue; sle->at(sfHolderEncryptionKey) = holderEncryptionKeyValue; + sle->at(sfCouponAccrued) = couponAccruedValue; + sle->at(sfCouponIndex) = couponIndexValue; MPTokenBuilder builderFromSle{sle}; EXPECT_TRUE(builderFromSle.validate()); @@ -360,6 +384,32 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfHolderEncryptionKey"); } + { + auto const& expected = couponAccruedValue; + + auto const fromSleOpt = entryFromSle.getCouponAccrued(); + auto const fromBuilderOpt = entryFromBuilder.getCouponAccrued(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfCouponAccrued"); + expectEqualField(expected, *fromBuilderOpt, "sfCouponAccrued"); + } + + { + auto const& expected = couponIndexValue; + + auto const fromSleOpt = entryFromSle.getCouponIndex(); + auto const fromBuilderOpt = entryFromBuilder.getCouponIndex(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfCouponIndex"); + expectEqualField(expected, *fromBuilderOpt, "sfCouponIndex"); + } + EXPECT_EQ(entryFromSle.getKey(), index); EXPECT_EQ(entryFromBuilder.getKey(), index); } @@ -438,5 +488,9 @@ TEST(MPTokenTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getAuditorEncryptedBalance().has_value()); EXPECT_FALSE(entry.hasHolderEncryptionKey()); EXPECT_FALSE(entry.getHolderEncryptionKey().has_value()); + EXPECT_FALSE(entry.hasCouponAccrued()); + EXPECT_FALSE(entry.getCouponAccrued().has_value()); + EXPECT_FALSE(entry.hasCouponIndex()); + EXPECT_FALSE(entry.getCouponIndex().has_value()); } } diff --git a/src/tests/libxrpl/protocol_autogen/transactions/CouponClaimTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/CouponClaimTests.cpp new file mode 100644 index 00000000000..f855cbe54f1 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/CouponClaimTests.cpp @@ -0,0 +1,195 @@ +// Auto-generated unit tests for transaction CouponClaim + + +#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(TransactionsCouponClaimTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testCouponClaim")); + + // 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 mPTokenIssuanceIDValue = canonical_UINT192(); + auto const amountValue = canonical_AMOUNT(); + + CouponClaimBuilder builder{ + accountValue, + mPTokenIssuanceIDValue, + sequenceValue, + feeValue + }; + + // Set optional fields + builder.setAmount(amountValue); + + 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 + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = tx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + // Verify optional fields + { + auto const& expected = amountValue; + auto const actualOpt = tx.getAmount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAmount should be present"; + expectEqualField(expected, *actualOpt, "sfAmount"); + EXPECT_TRUE(tx.hasAmount()); + } + +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsCouponClaimTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testCouponClaimFromTx")); + + // 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 mPTokenIssuanceIDValue = canonical_UINT192(); + auto const amountValue = canonical_AMOUNT(); + + // Build an initial transaction + CouponClaimBuilder initialBuilder{ + accountValue, + mPTokenIssuanceIDValue, + sequenceValue, + feeValue + }; + + initialBuilder.setAmount(amountValue); + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + CouponClaimBuilder 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 + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = rebuiltTx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + // Verify optional fields + { + auto const& expected = amountValue; + auto const actualOpt = rebuiltTx.getAmount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAmount should be present"; + expectEqualField(expected, *actualOpt, "sfAmount"); + } + +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsCouponClaimTests, 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(CouponClaim{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsCouponClaimTests, 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(CouponClaimBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsCouponClaimTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testCouponClaimNullopt")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 3; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific required field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + + CouponClaimBuilder builder{ + accountValue, + mPTokenIssuanceIDValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasAmount()); + EXPECT_FALSE(tx.getAmount().has_value()); +} + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/CouponPayTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/CouponPayTests.cpp new file mode 100644 index 00000000000..0d925d8ca6c --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/CouponPayTests.cpp @@ -0,0 +1,162 @@ +// Auto-generated unit tests for transaction CouponPay + + +#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(TransactionsCouponPayTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testCouponPay")); + + // 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 mPTokenIssuanceIDValue = canonical_UINT192(); + auto const amountValue = canonical_AMOUNT(); + + CouponPayBuilder builder{ + accountValue, + mPTokenIssuanceIDValue, + amountValue, + sequenceValue, + feeValue + }; + + // Set optional fields + + 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 + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = tx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = amountValue; + auto const actual = tx.getAmount(); + expectEqualField(expected, actual, "sfAmount"); + } + + // Verify optional fields +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsCouponPayTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testCouponPayFromTx")); + + // 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 mPTokenIssuanceIDValue = canonical_UINT192(); + auto const amountValue = canonical_AMOUNT(); + + // Build an initial transaction + CouponPayBuilder initialBuilder{ + accountValue, + mPTokenIssuanceIDValue, + amountValue, + sequenceValue, + feeValue + }; + + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + CouponPayBuilder 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 + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = rebuiltTx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = amountValue; + auto const actual = rebuiltTx.getAmount(); + expectEqualField(expected, actual, "sfAmount"); + } + + // Verify optional fields +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsCouponPayTests, 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(CouponPay{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsCouponPayTests, 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(CouponPayBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/CouponScheduleCreateTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/CouponScheduleCreateTests.cpp new file mode 100644 index 00000000000..5f65650ee51 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/CouponScheduleCreateTests.cpp @@ -0,0 +1,318 @@ +// Auto-generated unit tests for transaction CouponScheduleCreate + + +#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(TransactionsCouponScheduleCreateTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testCouponScheduleCreate")); + + // 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 mPTokenIssuanceIDValue = canonical_UINT192(); + auto const couponAssetValue = canonical_ISSUE(); + auto const couponAmountValue = canonical_AMOUNT(); + auto const couponIntervalValue = canonical_UINT32(); + auto const firstCouponTimeValue = canonical_UINT32(); + auto const expirationValue = canonical_UINT32(); + auto const callNoticePeriodValue = canonical_UINT32(); + auto const earliestCallTimeValue = canonical_UINT32(); + + CouponScheduleCreateBuilder builder{ + accountValue, + mPTokenIssuanceIDValue, + couponAssetValue, + sequenceValue, + feeValue + }; + + // Set optional fields + builder.setCouponAmount(couponAmountValue); + builder.setCouponInterval(couponIntervalValue); + builder.setFirstCouponTime(firstCouponTimeValue); + builder.setExpiration(expirationValue); + builder.setCallNoticePeriod(callNoticePeriodValue); + builder.setEarliestCallTime(earliestCallTimeValue); + + 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 + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = tx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = couponAssetValue; + auto const actual = tx.getCouponAsset(); + expectEqualField(expected, actual, "sfCouponAsset"); + } + + // Verify optional fields + { + auto const& expected = couponAmountValue; + auto const actualOpt = tx.getCouponAmount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCouponAmount should be present"; + expectEqualField(expected, *actualOpt, "sfCouponAmount"); + EXPECT_TRUE(tx.hasCouponAmount()); + } + + { + auto const& expected = couponIntervalValue; + auto const actualOpt = tx.getCouponInterval(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCouponInterval should be present"; + expectEqualField(expected, *actualOpt, "sfCouponInterval"); + EXPECT_TRUE(tx.hasCouponInterval()); + } + + { + auto const& expected = firstCouponTimeValue; + auto const actualOpt = tx.getFirstCouponTime(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFirstCouponTime should be present"; + expectEqualField(expected, *actualOpt, "sfFirstCouponTime"); + EXPECT_TRUE(tx.hasFirstCouponTime()); + } + + { + auto const& expected = expirationValue; + auto const actualOpt = tx.getExpiration(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfExpiration should be present"; + expectEqualField(expected, *actualOpt, "sfExpiration"); + EXPECT_TRUE(tx.hasExpiration()); + } + + { + auto const& expected = callNoticePeriodValue; + auto const actualOpt = tx.getCallNoticePeriod(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCallNoticePeriod should be present"; + expectEqualField(expected, *actualOpt, "sfCallNoticePeriod"); + EXPECT_TRUE(tx.hasCallNoticePeriod()); + } + + { + auto const& expected = earliestCallTimeValue; + auto const actualOpt = tx.getEarliestCallTime(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfEarliestCallTime should be present"; + expectEqualField(expected, *actualOpt, "sfEarliestCallTime"); + EXPECT_TRUE(tx.hasEarliestCallTime()); + } + +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsCouponScheduleCreateTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testCouponScheduleCreateFromTx")); + + // 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 mPTokenIssuanceIDValue = canonical_UINT192(); + auto const couponAssetValue = canonical_ISSUE(); + auto const couponAmountValue = canonical_AMOUNT(); + auto const couponIntervalValue = canonical_UINT32(); + auto const firstCouponTimeValue = canonical_UINT32(); + auto const expirationValue = canonical_UINT32(); + auto const callNoticePeriodValue = canonical_UINT32(); + auto const earliestCallTimeValue = canonical_UINT32(); + + // Build an initial transaction + CouponScheduleCreateBuilder initialBuilder{ + accountValue, + mPTokenIssuanceIDValue, + couponAssetValue, + sequenceValue, + feeValue + }; + + initialBuilder.setCouponAmount(couponAmountValue); + initialBuilder.setCouponInterval(couponIntervalValue); + initialBuilder.setFirstCouponTime(firstCouponTimeValue); + initialBuilder.setExpiration(expirationValue); + initialBuilder.setCallNoticePeriod(callNoticePeriodValue); + initialBuilder.setEarliestCallTime(earliestCallTimeValue); + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + CouponScheduleCreateBuilder 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 + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = rebuiltTx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = couponAssetValue; + auto const actual = rebuiltTx.getCouponAsset(); + expectEqualField(expected, actual, "sfCouponAsset"); + } + + // Verify optional fields + { + auto const& expected = couponAmountValue; + auto const actualOpt = rebuiltTx.getCouponAmount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCouponAmount should be present"; + expectEqualField(expected, *actualOpt, "sfCouponAmount"); + } + + { + auto const& expected = couponIntervalValue; + auto const actualOpt = rebuiltTx.getCouponInterval(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCouponInterval should be present"; + expectEqualField(expected, *actualOpt, "sfCouponInterval"); + } + + { + auto const& expected = firstCouponTimeValue; + auto const actualOpt = rebuiltTx.getFirstCouponTime(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFirstCouponTime should be present"; + expectEqualField(expected, *actualOpt, "sfFirstCouponTime"); + } + + { + auto const& expected = expirationValue; + auto const actualOpt = rebuiltTx.getExpiration(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfExpiration should be present"; + expectEqualField(expected, *actualOpt, "sfExpiration"); + } + + { + auto const& expected = callNoticePeriodValue; + auto const actualOpt = rebuiltTx.getCallNoticePeriod(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCallNoticePeriod should be present"; + expectEqualField(expected, *actualOpt, "sfCallNoticePeriod"); + } + + { + auto const& expected = earliestCallTimeValue; + auto const actualOpt = rebuiltTx.getEarliestCallTime(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfEarliestCallTime should be present"; + expectEqualField(expected, *actualOpt, "sfEarliestCallTime"); + } + +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsCouponScheduleCreateTests, 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(CouponScheduleCreate{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsCouponScheduleCreateTests, 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(CouponScheduleCreateBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsCouponScheduleCreateTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testCouponScheduleCreateNullopt")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 3; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific required field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const couponAssetValue = canonical_ISSUE(); + + CouponScheduleCreateBuilder builder{ + accountValue, + mPTokenIssuanceIDValue, + couponAssetValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasCouponAmount()); + EXPECT_FALSE(tx.getCouponAmount().has_value()); + EXPECT_FALSE(tx.hasCouponInterval()); + EXPECT_FALSE(tx.getCouponInterval().has_value()); + EXPECT_FALSE(tx.hasFirstCouponTime()); + EXPECT_FALSE(tx.getFirstCouponTime().has_value()); + EXPECT_FALSE(tx.hasExpiration()); + EXPECT_FALSE(tx.getExpiration().has_value()); + EXPECT_FALSE(tx.hasCallNoticePeriod()); + EXPECT_FALSE(tx.getCallNoticePeriod().has_value()); + EXPECT_FALSE(tx.hasEarliestCallTime()); + EXPECT_FALSE(tx.getEarliestCallTime().has_value()); +} + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/CouponScheduleDeleteTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/CouponScheduleDeleteTests.cpp new file mode 100644 index 00000000000..41b6621136a --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/CouponScheduleDeleteTests.cpp @@ -0,0 +1,146 @@ +// Auto-generated unit tests for transaction CouponScheduleDelete + + +#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(TransactionsCouponScheduleDeleteTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testCouponScheduleDelete")); + + // 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 mPTokenIssuanceIDValue = canonical_UINT192(); + + CouponScheduleDeleteBuilder builder{ + accountValue, + mPTokenIssuanceIDValue, + sequenceValue, + feeValue + }; + + // Set optional fields + + 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 + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = tx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + // Verify optional fields +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsCouponScheduleDeleteTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testCouponScheduleDeleteFromTx")); + + // 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 mPTokenIssuanceIDValue = canonical_UINT192(); + + // Build an initial transaction + CouponScheduleDeleteBuilder initialBuilder{ + accountValue, + mPTokenIssuanceIDValue, + sequenceValue, + feeValue + }; + + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + CouponScheduleDeleteBuilder 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 + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = rebuiltTx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + // Verify optional fields +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsCouponScheduleDeleteTests, 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(CouponScheduleDelete{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsCouponScheduleDeleteTests, 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(CouponScheduleDeleteBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/CouponScheduleSetTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/CouponScheduleSetTests.cpp new file mode 100644 index 00000000000..79d34013a75 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/CouponScheduleSetTests.cpp @@ -0,0 +1,162 @@ +// Auto-generated unit tests for transaction CouponScheduleSet + + +#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(TransactionsCouponScheduleSetTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testCouponScheduleSet")); + + // 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 mPTokenIssuanceIDValue = canonical_UINT192(); + auto const expirationValue = canonical_UINT32(); + + CouponScheduleSetBuilder builder{ + accountValue, + mPTokenIssuanceIDValue, + expirationValue, + sequenceValue, + feeValue + }; + + // Set optional fields + + 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 + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = tx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = expirationValue; + auto const actual = tx.getExpiration(); + expectEqualField(expected, actual, "sfExpiration"); + } + + // Verify optional fields +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsCouponScheduleSetTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testCouponScheduleSetFromTx")); + + // 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 mPTokenIssuanceIDValue = canonical_UINT192(); + auto const expirationValue = canonical_UINT32(); + + // Build an initial transaction + CouponScheduleSetBuilder initialBuilder{ + accountValue, + mPTokenIssuanceIDValue, + expirationValue, + sequenceValue, + feeValue + }; + + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + CouponScheduleSetBuilder 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 + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = rebuiltTx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = expirationValue; + auto const actual = rebuiltTx.getExpiration(); + expectEqualField(expected, actual, "sfExpiration"); + } + + // Verify optional fields +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsCouponScheduleSetTests, 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(CouponScheduleSet{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsCouponScheduleSetTests, 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(CouponScheduleSetBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + + +} diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp index 5271720b34f..6c7a540622a 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp @@ -479,6 +479,25 @@ parseLedgerHashes( return parseFixed(keylet::skip(), params, fieldName, apiVersion); } +static std::expected +parseCouponSchedule( + json::Value const& params, + json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) +{ + if (!params.isObject()) + { + return parseObjectID(params, fieldName); + } + + auto const issuanceID = + ledger_entry_helpers::requiredUInt192(params, jss::mpt_issuance_id, "malformedRequest"); + if (!issuanceID) + return std::unexpected(issuanceID.error()); + + return keylet::couponSchedule(*issuanceID).key; +} + static std::expected parseLoanBroker( json::Value const& params,