Merge remote-tracking branch 'Transia-RnD-rippled/dangell7/coupon-payments' into develop

# Conflicts:
#	include/xrpl/protocol/detail/ledger_entries.macro
#	include/xrpl/protocol/jss.h
#	include/xrpl/tx/invariants/InvariantCheck.h
#	src/libxrpl/protocol/Indexes.cpp
#	src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp
This commit is contained in:
Denis Angell
2026-09-13 19:29:24 -04:00
47 changed files with 5418 additions and 2 deletions

View File

@@ -0,0 +1,49 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
#include <cstdint>
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

View File

@@ -396,6 +396,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;

View File

@@ -179,7 +179,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) \

View File

@@ -155,3 +155,4 @@ XRPL_FEATURE(Passkey, Supported::Yes, VoteBehavior::DefaultN
XRPL_FEATURE(MPTStructuredData, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(TokenIssuance, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(VaultContinuousAccrual, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(CouponPayments, Supported::Yes, VoteBehavior::DefaultNo)

View File

@@ -443,6 +443,8 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
{sfAuditorEncryptedBalance, SoeOptional},
{sfHolderEncryptionKey, SoeOptional},
{sfRedemptionAfter, SoeDefault},
{sfCouponAccrued, SoeOptional},
{sfCouponIndex, SoeOptional},
}))
/** A ledger object which tracks Oracle
@@ -732,5 +734,32 @@ LEDGER_ENTRY(ltTOKEN_ISSUANCE, 0x0092, TokenIssuance, token_issuance, ({
{sfPreviousTxnLgrSeq, SoeRequired},
}))
/** 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, 0x0093, 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},
}))
#undef EXPAND
#undef LEDGER_ENTRY_DUPLICATE

View File

@@ -495,3 +495,16 @@ TYPED_SFIELD(sfRedemptionAfter, UINT32, 95)
TYPED_SFIELD(sfUnearnedInterest, NUMBER, 19, SField::kSmdNeedsAsset | SField::kSmdDefault)
TYPED_SFIELD(sfAccrualRate, NUMBER, 20, SField::kSmdDefault)
TYPED_SFIELD(sfStruckPrice, NUMBER, 21, SField::kSmdDefault)
TYPED_SFIELD(sfCouponInterval, UINT32, 96)
TYPED_SFIELD(sfFirstCouponTime, UINT32, 97)
TYPED_SFIELD(sfCallNoticePeriod, UINT32, 98)
TYPED_SFIELD(sfEarliestCallTime, UINT32, 99)
TYPED_SFIELD(sfLastCouponTime, UINT32, 100)
TYPED_SFIELD(sfClaimantCount, UINT32, 101)
TYPED_SFIELD(sfCouponCount, UINT32, 102)
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)
TYPED_SFIELD(sfCouponAsset, ISSUE, 5)

View File

@@ -1363,3 +1363,53 @@ TRANSACTION(ttTOKEN_CONVERT, 107, TokenConvert,
({
{sfAmount, SoeRequired, SoeMptSupported},
}))
TRANSACTION(ttCOUPON_SCHEDULE_CREATE, 108, CouponScheduleCreate,
({
.delegable = Delegation::NotDelegable,
.amendment = featureCouponPayments,
}),
({
{sfMPTokenIssuanceID, SoeRequired},
{sfCouponAsset, SoeRequired, SoeMptSupported},
{sfCouponAmount, SoeOptional, SoeMptSupported},
{sfCouponInterval, SoeOptional},
{sfFirstCouponTime, SoeOptional},
{sfExpiration, SoeOptional},
{sfCallNoticePeriod, SoeOptional},
{sfEarliestCallTime, SoeOptional},
}))
TRANSACTION(ttCOUPON_SCHEDULE_SET, 109, CouponScheduleSet,
({
.delegable = Delegation::NotDelegable,
.amendment = featureCouponPayments,
}),
({
{sfMPTokenIssuanceID, SoeRequired},
{sfExpiration, SoeRequired},
}))
TRANSACTION(ttCOUPON_SCHEDULE_DELETE, 110, CouponScheduleDelete,
({
.delegable = Delegation::NotDelegable,
.amendment = featureCouponPayments,
}),
({
{sfMPTokenIssuanceID, SoeRequired},
}))
TRANSACTION(ttCOUPON_CLAIM, 111, CouponClaim,
({
.delegable = Delegation::Delegable,
.amendment = featureCouponPayments,
}),
({
{sfMPTokenIssuanceID, SoeRequired},
{sfAmount, SoeOptional, SoeMptSupported},
}))
TRANSACTION(ttCOUPON_PAY, 112, CouponPay,
({
.delegable = Delegation::NotDelegable,
.amendment = featureCouponPayments,
}),
({
{sfMPTokenIssuanceID, SoeRequired},
{sfAmount, SoeRequired, SoeMptSupported},
}))

View File

@@ -374,6 +374,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

View File

@@ -0,0 +1,650 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/LedgerEntryBase.h>
#include <xrpl/protocol_autogen/LedgerEntryBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::ledger_entries {
class CouponScheduleBuilder;
/**
* @brief Ledger Entry: CouponSchedule
*
* Type: ltCOUPON_SCHEDULE (0x0093)
* 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<SF_AMOUNT::type::value_type>
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<SF_AMOUNT::type::value_type>
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<SF_UINT32::type::value_type>
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<SF_UINT32::type::value_type>
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<SF_UINT32::type::value_type>
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<SF_AMOUNT::type::value_type>
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<SF_UINT32::type::value_type>
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<SF_UINT32::type::value_type>
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<SF_UINT32::type::value_type>
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<SF_UINT32::type::value_type>
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<SF_UINT32::type::value_type>
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<CouponScheduleBuilder>
{
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<typename SF_UINT256::type::value_type> const& previousTxnID,std::decay_t<typename SF_UINT32::type::value_type> const& previousTxnLgrSeq,std::decay_t<typename SF_UINT64::type::value_type> const& ownerNode,std::decay_t<typename SF_ACCOUNT::type::value_type> const& owner,std::decay_t<typename SF_ACCOUNT::type::value_type> const& account,std::decay_t<typename SF_UINT192::type::value_type> const& mPTokenIssuanceID,std::decay_t<typename SF_ISSUE::type::value_type> const& couponAsset)
: LedgerEntryBuilderBase<CouponScheduleBuilder>(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<typename SF_UINT256::type::value_type> const& value)
{
object_[sfPreviousTxnID] = value;
return *this;
}
/**
* @brief Set sfPreviousTxnLgrSeq (SoeRequired)
* @return Reference to this builder for method chaining.
*/
CouponScheduleBuilder&
setPreviousTxnLgrSeq(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfPreviousTxnLgrSeq] = value;
return *this;
}
/**
* @brief Set sfOwnerNode (SoeRequired)
* @return Reference to this builder for method chaining.
*/
CouponScheduleBuilder&
setOwnerNode(std::decay_t<typename SF_UINT64::type::value_type> const& value)
{
object_[sfOwnerNode] = value;
return *this;
}
/**
* @brief Set sfOwner (SoeRequired)
* @return Reference to this builder for method chaining.
*/
CouponScheduleBuilder&
setOwner(std::decay_t<typename SF_ACCOUNT::type::value_type> const& value)
{
object_[sfOwner] = value;
return *this;
}
/**
* @brief Set sfAccount (SoeRequired)
* @return Reference to this builder for method chaining.
*/
CouponScheduleBuilder&
setAccount(std::decay_t<typename SF_ACCOUNT::type::value_type> const& value)
{
object_[sfAccount] = value;
return *this;
}
/**
* @brief Set sfMPTokenIssuanceID (SoeRequired)
* @return Reference to this builder for method chaining.
*/
CouponScheduleBuilder&
setMPTokenIssuanceID(std::decay_t<typename SF_UINT192::type::value_type> const& value)
{
object_[sfMPTokenIssuanceID] = value;
return *this;
}
/**
* @brief Set sfCouponAsset (SoeRequired)
* @return Reference to this builder for method chaining.
*/
CouponScheduleBuilder&
setCouponAsset(std::decay_t<typename SF_ISSUE::type::value_type> 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<typename SF_AMOUNT::type::value_type> const& value)
{
object_[sfAccruedPerUnit] = value;
return *this;
}
/**
* @brief Set sfPoolAmount (SoeDefault)
* @return Reference to this builder for method chaining.
*/
CouponScheduleBuilder&
setPoolAmount(std::decay_t<typename SF_AMOUNT::type::value_type> const& value)
{
object_[sfPoolAmount] = value;
return *this;
}
/**
* @brief Set sfClaimantCount (SoeDefault)
* @return Reference to this builder for method chaining.
*/
CouponScheduleBuilder&
setClaimantCount(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfClaimantCount] = value;
return *this;
}
/**
* @brief Set sfCouponCount (SoeDefault)
* @return Reference to this builder for method chaining.
*/
CouponScheduleBuilder&
setCouponCount(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfCouponCount] = value;
return *this;
}
/**
* @brief Set sfLastCouponTime (SoeOptional)
* @return Reference to this builder for method chaining.
*/
CouponScheduleBuilder&
setLastCouponTime(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfLastCouponTime] = value;
return *this;
}
/**
* @brief Set sfCouponAmount (SoeOptional)
* @return Reference to this builder for method chaining.
*/
CouponScheduleBuilder&
setCouponAmount(std::decay_t<typename SF_AMOUNT::type::value_type> const& value)
{
object_[sfCouponAmount] = value;
return *this;
}
/**
* @brief Set sfCouponInterval (SoeOptional)
* @return Reference to this builder for method chaining.
*/
CouponScheduleBuilder&
setCouponInterval(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfCouponInterval] = value;
return *this;
}
/**
* @brief Set sfFirstCouponTime (SoeOptional)
* @return Reference to this builder for method chaining.
*/
CouponScheduleBuilder&
setFirstCouponTime(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfFirstCouponTime] = value;
return *this;
}
/**
* @brief Set sfExpiration (SoeOptional)
* @return Reference to this builder for method chaining.
*/
CouponScheduleBuilder&
setExpiration(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfExpiration] = value;
return *this;
}
/**
* @brief Set sfCallNoticePeriod (SoeOptional)
* @return Reference to this builder for method chaining.
*/
CouponScheduleBuilder&
setCallNoticePeriod(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfCallNoticePeriod] = value;
return *this;
}
/**
* @brief Set sfEarliestCallTime (SoeOptional)
* @return Reference to this builder for method chaining.
*/
CouponScheduleBuilder&
setEarliestCallTime(std::decay_t<typename SF_UINT32::type::value_type> 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<SLE>(std::move(object_), index)};
}
};
} // namespace xrpl::ledger_entries

View File

@@ -291,6 +291,54 @@ public:
{
return this->sle_->isFieldPresent(sfHolderEncryptionKey);
}
/**
* @brief Get sfCouponAccrued (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_AMOUNT::type::value_type>
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<SF_AMOUNT::type::value_type>
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 @@ public:
return *this;
}
/**
* @brief Set sfCouponAccrued (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenBuilder&
setCouponAccrued(std::decay_t<typename SF_AMOUNT::type::value_type> const& value)
{
object_[sfCouponAccrued] = value;
return *this;
}
/**
* @brief Set sfCouponIndex (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenBuilder&
setCouponIndex(std::decay_t<typename SF_AMOUNT::type::value_type> const& value)
{
object_[sfCouponIndex] = value;
return *this;
}
/**
* @brief Build and return the completed MPToken wrapper.
* @param index The ledger entry index.

View File

@@ -0,0 +1,170 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
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<STTx const> 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<SF_AMOUNT::type::value_type>
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<CouponClaimBuilder>
{
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<typename SF_UINT192::type::value_type> const& mPTokenIssuanceID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<CouponClaimBuilder>(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<STTx const> 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<typename SF_UINT192::type::value_type> 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<typename SF_AMOUNT::type::value_type> 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<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions

View File

@@ -0,0 +1,157 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
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<STTx const> 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<CouponPayBuilder>
{
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<typename SF_UINT192::type::value_type> const& mPTokenIssuanceID, std::decay_t<typename SF_AMOUNT::type::value_type> const& amount, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<CouponPayBuilder>(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<STTx const> 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<typename SF_UINT192::type::value_type> 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<typename SF_AMOUNT::type::value_type> 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<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions

View File

@@ -0,0 +1,381 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
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<STTx const> 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<SF_AMOUNT::type::value_type>
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<SF_UINT32::type::value_type>
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<SF_UINT32::type::value_type>
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<SF_UINT32::type::value_type>
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<SF_UINT32::type::value_type>
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<SF_UINT32::type::value_type>
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<CouponScheduleCreateBuilder>
{
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<typename SF_UINT192::type::value_type> const& mPTokenIssuanceID, std::decay_t<typename SF_ISSUE::type::value_type> const& couponAsset, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<CouponScheduleCreateBuilder>(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<STTx const> 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<typename SF_UINT192::type::value_type> 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<typename SF_ISSUE::type::value_type> 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<typename SF_AMOUNT::type::value_type> const& value)
{
object_[sfCouponAmount] = value;
return *this;
}
/**
* @brief Set sfCouponInterval (SoeOptional)
* @return Reference to this builder for method chaining.
*/
CouponScheduleCreateBuilder&
setCouponInterval(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfCouponInterval] = value;
return *this;
}
/**
* @brief Set sfFirstCouponTime (SoeOptional)
* @return Reference to this builder for method chaining.
*/
CouponScheduleCreateBuilder&
setFirstCouponTime(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfFirstCouponTime] = value;
return *this;
}
/**
* @brief Set sfExpiration (SoeOptional)
* @return Reference to this builder for method chaining.
*/
CouponScheduleCreateBuilder&
setExpiration(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfExpiration] = value;
return *this;
}
/**
* @brief Set sfCallNoticePeriod (SoeOptional)
* @return Reference to this builder for method chaining.
*/
CouponScheduleCreateBuilder&
setCallNoticePeriod(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfCallNoticePeriod] = value;
return *this;
}
/**
* @brief Set sfEarliestCallTime (SoeOptional)
* @return Reference to this builder for method chaining.
*/
CouponScheduleCreateBuilder&
setEarliestCallTime(std::decay_t<typename SF_UINT32::type::value_type> 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<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions

View File

@@ -0,0 +1,131 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
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<STTx const> 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<CouponScheduleDeleteBuilder>
{
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<typename SF_UINT192::type::value_type> const& mPTokenIssuanceID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<CouponScheduleDeleteBuilder>(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<STTx const> 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<typename SF_UINT192::type::value_type> 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<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions

View File

@@ -0,0 +1,155 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
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<STTx const> 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<CouponScheduleSetBuilder>
{
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<typename SF_UINT192::type::value_type> const& mPTokenIssuanceID, std::decay_t<typename SF_UINT32::type::value_type> const& expiration, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<CouponScheduleSetBuilder>(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<STTx const> 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<typename SF_UINT192::type::value_type> const& value)
{
object_[sfMPTokenIssuanceID] = value;
return *this;
}
/**
* @brief Set sfExpiration (SoeRequired)
* @return Reference to this builder for method chaining.
*/
CouponScheduleSetBuilder&
setExpiration(std::decay_t<typename SF_UINT32::type::value_type> 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<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions

View File

@@ -0,0 +1,39 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <utility>
#include <vector>
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 <before, after>; both are needed to check direction of change.
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> schedules_;
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> 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

View File

@@ -7,6 +7,7 @@
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/AMMInvariant.h>
#include <xrpl/tx/invariants/CouponInvariant.h>
#include <xrpl/tx/invariants/DirectoryInvariant.h>
#include <xrpl/tx/invariants/FreezeInvariant.h>
#include <xrpl/tx/invariants/LoanBrokerInvariant.h>
@@ -490,7 +491,8 @@ using InvariantChecks = std::tuple<
ObjectHasPseudoAccount,
SponsorshipOwnerCountsMatch,
SponsorshipAccountCountMatchesField,
ValidTokenIssuance>;
ValidTokenIssuance,
ValidCouponSchedule>;
/**
* @brief get a tuple of all invariant checks

View File

@@ -0,0 +1,43 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
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

View File

@@ -0,0 +1,43 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
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

View File

@@ -0,0 +1,43 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
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

View File

@@ -0,0 +1,43 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
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

View File

@@ -0,0 +1,43 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
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

View File

@@ -0,0 +1,87 @@
#include <xrpl/ledger/helpers/CouponHelpers.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/TER.h>
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

View File

@@ -8,6 +8,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CouponHelpers.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
@@ -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);

View File

@@ -8,6 +8,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CouponHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
@@ -1278,6 +1279,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;
@@ -1308,6 +1314,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<std::uint64_t>::max() - amt))

View File

@@ -106,6 +106,7 @@ enum class LedgerNameSpace : std::uint16_t {
PasskeyList = 'k',
Sponsorship = '>',
TokenIssuance = 'F',
CouponSchedule = 'J',
ContractSource = 'Z',
Contract = 'c',
@@ -607,6 +608,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
{

View File

@@ -0,0 +1,111 @@
#include <xrpl/tx/invariants/CouponInvariant.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TxFormats.h>
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

View File

@@ -0,0 +1,172 @@
#include <xrpl/tx/transactors/coupon/CouponClaim.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CouponHelpers.h>
#include <xrpl/ledger/helpers/EscrowHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <algorithm>
#include <variant>
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<MPTIssue>() ? 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(
[&]<typename T>(T const&) {
return escrowUnlockApplyHelper<T>(
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

View File

@@ -0,0 +1,187 @@
#include <xrpl/tx/transactors/coupon/CouponPay.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <variant>
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(
[&]<typename T>(T const& issue) {
if constexpr (std::is_same_v<T, Issue>)
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<MPTIssue>())
{
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<std::uint32_t>(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

View File

@@ -0,0 +1,202 @@
#include <xrpl/tx/transactors/coupon/CouponScheduleCreate.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STIssue.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <variant>
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<MPTIssue>() &&
couponAsset.get<MPTIssue>().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(
[&]<typename T>(T const& issue) -> TER {
if constexpr (std::is_same_v<T, Issue>)
{
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<SLE>(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

View File

@@ -0,0 +1,93 @@
#include <xrpl/tx/transactors/coupon/CouponScheduleDelete.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
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

View File

@@ -0,0 +1,100 @@
#include <xrpl/tx/transactors/coupon/CouponScheduleSet.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <cstdint>
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<std::int64_t>(*notice))
return tecNO_PERMISSION;
// Call protection.
if (auto const earliestCall = schedule->at(~sfEarliestCallTime);
earliestCall && newExpiration < static_cast<std::int64_t>(*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<std::int64_t>(*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

View File

@@ -6,6 +6,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/CouponHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Indexes.h>
@@ -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;

View File

@@ -5,6 +5,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/CouponHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Indexes.h>
@@ -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);

View File

@@ -1,5 +1,6 @@
#include <xrpl/tx/transactors/token/MPTokenAuthorize.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
@@ -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)

View File

@@ -2,6 +2,7 @@
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
@@ -41,6 +42,10 @@ MPTokenIssuanceDestroy::preclaim(PreclaimContext const& ctx)
if (sleMPT->isFieldPresent(sfTokenIssuanceID))
return tecHAS_OBLIGATIONS;
// A coupon schedule still references this issuance.
if (sleMPT->isFlag(lsfMPTCouponSchedule))
return tecHAS_OBLIGATIONS;
return tesSUCCESS;
}

View File

@@ -9,6 +9,7 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
@@ -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

View File

@@ -0,0 +1,526 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <test/jtx/balance.h>
#include <test/jtx/fee.h>
#include <test/jtx/flags.h>
#include <test/jtx/mpt.h>
#include <test/jtx/pay.h>
#include <test/jtx/ter.h>
#include <test/jtx/trust.h>
#include <test/jtx/utility.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/jss.h>
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<STAmount> 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<SLE const>
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

View File

@@ -0,0 +1,131 @@
#include <test/app/invariants/InvariantsBase.h>
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/mpt.h>
#include <test/jtx/pay.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/tx/ApplyContext.h>
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

View File

@@ -0,0 +1,567 @@
// Auto-generated unit tests for ledger entry CouponSchedule
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol_autogen/ledger_entries/CouponSchedule.h>
#include <xrpl/protocol_autogen/ledger_entries/Ticket.h>
#include <string>
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<SLE>(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());
}
}

View File

@@ -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<SLE>(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());
}
}

View File

@@ -0,0 +1,195 @@
// Auto-generated unit tests for transaction CouponClaim
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/CouponClaim.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
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());
}
}

View File

@@ -0,0 +1,162 @@
// Auto-generated unit tests for transaction CouponPay
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/CouponPay.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
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);
}
}

View File

@@ -0,0 +1,318 @@
// Auto-generated unit tests for transaction CouponScheduleCreate
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/CouponScheduleCreate.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
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());
}
}

View File

@@ -0,0 +1,146 @@
// Auto-generated unit tests for transaction CouponScheduleDelete
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/CouponScheduleDelete.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
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);
}
}

View File

@@ -0,0 +1,162 @@
// Auto-generated unit tests for transaction CouponScheduleSet
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/CouponScheduleSet.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
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);
}
}

View File

@@ -479,6 +479,25 @@ parseLedgerHashes(
return parseFixed(keylet::skip(), params, fieldName, apiVersion);
}
static std::expected<uint256, json::Value>
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<uint256, json::Value>
parseLoanBroker(
json::Value const& params,