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

# Conflicts:
#	include/xrpl/protocol/Indexes.h
#	include/xrpl/protocol/LedgerFormats.h
#	include/xrpl/protocol/TxFlags.h
#	include/xrpl/protocol/jss.h
#	include/xrpl/tx/invariants/InvariantCheck.h
#	src/libxrpl/protocol/Indexes.cpp
This commit is contained in:
Denis Angell
2026-09-13 19:37:19 -04:00
32 changed files with 8542 additions and 1 deletions

View File

@@ -356,6 +356,7 @@ words:
- unfund
- ungated
- unimpair
- unmetered
- unroutable
- unscalable
- unserviced

View File

@@ -0,0 +1,429 @@
#pragma once
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.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/AccountID.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
#include <cstdint>
namespace xrpl {
template <ValidIssueType T>
TER
canTransferTokenHelper(
ReadView const& view,
AccountID const& account,
AccountID const& dest,
STAmount const& amount,
beast::Journal const& j);
template <>
inline TER
canTransferTokenHelper<Issue>(
ReadView const& view,
AccountID const& account,
AccountID const& dest,
STAmount const& amount,
beast::Journal const& j)
{
AccountID issuer = amount.getIssuer();
if (issuer == account)
{
JLOG(j.trace()) << "canTransferTokenHelper: Issuer is the same as the account.";
return tesSUCCESS;
}
// If the issuer does not exist, return tecNO_ISSUER
auto const sleIssuer = view.read(keylet::account(issuer));
if (!sleIssuer)
{
JLOG(j.trace()) << "canTransferTokenHelper: Issuer does not exist.";
return tecNO_ISSUER;
}
// If the account does not have a trustline to the issuer, return tecNO_LINE
auto const sleRippleState =
view.read(keylet::trustLine(account, issuer, amount.get<Issue>().currency));
if (!sleRippleState)
{
JLOG(j.trace()) << "canTransferTokenHelper: Trust line does not exist.";
return tecNO_LINE;
}
STAmount const balance = (*sleRippleState)[sfBalance];
// If balance is positive, issuer must have higher address than account
if (balance > beast::kZero && issuer < account)
{
JLOG(j.trace()) << "canTransferTokenHelper: Invalid trust line state.";
return tecNO_PERMISSION;
}
// If balance is negative, issuer must have lower address than account
if (balance < beast::kZero && issuer > account)
{
JLOG(j.trace()) << "canTransferTokenHelper: Invalid trust line state.";
return tecNO_PERMISSION;
}
// If the issuer has requireAuth set, check if the account is authorized
if (auto const ter = requireAuth(view, amount.get<Issue>(), account); ter != tesSUCCESS)
{
JLOG(j.trace()) << "canTransferTokenHelper: Account is not authorized";
return ter;
}
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(view, amount.get<Issue>(), dest); ter != tesSUCCESS)
{
JLOG(j.trace()) << "canTransferTokenHelper: Destination is not authorized.";
return ter;
}
// If the issuer has frozen the account, return tecFROZEN
if (isFrozen(view, account, amount.get<Issue>()) ||
isDeepFrozen(view, account, amount.get<Issue>().currency, amount.get<Issue>().account))
{
JLOG(j.trace()) << "canTransferTokenHelper: Account is frozen.";
return tecFROZEN;
}
// If the issuer has frozen the destination, return tecFROZEN
if (isFrozen(view, dest, amount.get<Issue>()) ||
isDeepFrozen(view, dest, amount.get<Issue>().currency, amount.get<Issue>().account))
{
JLOG(j.trace()) << "canTransferTokenHelper: Destination is frozen.";
return tecFROZEN;
}
STAmount const spendableAmount = accountHolds(
view, account, amount.get<Issue>().currency, issuer, FreezeHandling::IgnoreFreeze, j);
// If the balance is less than or equal to 0, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::kZero)
{
JLOG(j.trace()) << "canTransferTokenHelper: Spendable amount is less "
"than or equal to 0.";
return tecINSUFFICIENT_FUNDS;
}
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
{
JLOG(j.trace()) << "canTransferTokenHelper: Spendable amount is less "
"than the amount.";
return tecINSUFFICIENT_FUNDS;
}
// If the amount is not addable to the balance, return tecPRECISION_LOSS
if (!canAdd(spendableAmount, amount))
return tecPRECISION_LOSS;
return tesSUCCESS;
}
template <>
inline TER
canTransferTokenHelper<MPTIssue>(
ReadView const& view,
AccountID const& account,
AccountID const& dest,
STAmount const& amount,
beast::Journal const& j)
{
AccountID issuer = amount.getIssuer();
if (issuer == account)
{
JLOG(j.trace()) << "canTransferTokenHelper: Issuer is the same as the account.";
return tesSUCCESS;
}
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = view.read(issuanceKey);
if (!sleIssuance)
{
JLOG(j.trace()) << "canTransferTokenHelper: MPT issuance does not exist.";
return tecOBJECT_NOT_FOUND;
}
// If the issuer is not the same as the issuer of the mpt, return
// tecNO_PERMISSION
if (sleIssuance->getAccountID(sfIssuer) != issuer)
{
JLOG(j.trace()) << "canTransferTokenHelper: Issuer is not the same as "
"the issuer of the MPT.";
return tecNO_PERMISSION;
}
// If the account does not have the mpt, return tecOBJECT_NOT_FOUND
if (!view.exists(keylet::mptoken(issuanceKey.key, account)))
{
JLOG(j.trace()) << "canTransferTokenHelper: Account does not have the MPT.";
return tecOBJECT_NOT_FOUND;
}
// If the issuer has requireAuth set, check if the account is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
ter != tesSUCCESS)
{
JLOG(j.trace()) << "canTransferTokenHelper: Account is not authorized.";
return ter;
}
// If the issuer has requireAuth set, check if the destination is
// authorized
if (auto const ter = requireAuth(view, mptIssue, dest, AuthType::WeakAuth); ter != tesSUCCESS)
{
JLOG(j.trace()) << "canTransferTokenHelper: Destination is not authorized.";
return ter;
}
// If the issuer has locked the account, return tecLOCKED
if (isFrozen(view, account, mptIssue))
{
JLOG(j.trace()) << "canTransferTokenHelper: Account is locked.";
return tecLOCKED;
}
// If the issuer has locked the destination, return tecLOCKED
if (isFrozen(view, dest, mptIssue))
{
JLOG(j.trace()) << "canTransferTokenHelper: Destination is locked.";
return tecLOCKED;
}
// If the mpt cannot be transferred, return tecNO_AUTH
if (auto const ter = canTransfer(view, mptIssue, account, dest); ter != tesSUCCESS)
{
JLOG(j.trace()) << "canTransferTokenHelper: MPT cannot be transferred.";
return ter;
}
STAmount const spendableAmount = accountHolds(
view,
account,
amount.get<MPTIssue>(),
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
j);
// If the balance is less than or equal to 0, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::kZero)
{
JLOG(j.trace()) << "canTransferTokenHelper: Spendable amount is less "
"than or equal to 0.";
return tecINSUFFICIENT_FUNDS;
}
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
{
JLOG(j.trace()) << "canTransferTokenHelper: Spendable amount is less "
"than the amount.";
return tecINSUFFICIENT_FUNDS;
}
// If the amount is not addable to the balance, return tecPRECISION_LOSS
if (!canAdd(spendableAmount, amount))
return tecPRECISION_LOSS;
return tesSUCCESS;
}
template <ValidIssueType T>
TER
doTransferTokenHelper(
ApplyView& view,
SLE::ref sleDest,
STAmount const& xrpBalance,
STAmount const& amount,
AccountID const& issuer,
AccountID const& sender,
AccountID const& receiver,
bool createAsset,
beast::Journal journal);
template <>
inline TER
doTransferTokenHelper<Issue>(
ApplyView& view,
SLE::ref sleDest,
STAmount const& xrpBalance,
STAmount const& amount,
AccountID const& issuer,
AccountID const& sender,
AccountID const& receiver,
bool createAsset,
beast::Journal journal)
{
Keylet const trustLineKey = keylet::trustLine(receiver, amount.get<Issue>());
bool const recvLow = issuer > receiver;
// Review Note: We could remove this and just say to use batch to auth the
// token first
if (!view.exists(trustLineKey) && createAsset && issuer != receiver)
{
// Can the account cover the trust line's reserve?
if (xrpBalance < accountReserve(view, sleDest, journal, {.ownerCountDelta = 1}))
{
JLOG(journal.trace()) << "doTransferTokenHelper: Trust line does not exist. "
"Insufficent reserve to create line.";
return tecNO_LINE_INSUF_RESERVE;
}
Currency const currency = amount.get<Issue>().currency;
STAmount initialBalance(amount.get<Issue>());
initialBalance.get<Issue>().account = noAccount();
// clang-format off
if (TER const ter = trustCreate(
view, // payment sandbox
recvLow, // is dest low?
issuer, // source
receiver, // destination
trustLineKey.key, // ledger index
sleDest, // Account to add to
false, // authorize account
(sleDest->getFlags() & lsfDefaultRipple) == 0,
false, // freeze trust line
false, // deep freeze trust line
initialBalance, // zero initial balance
Issue(currency, receiver), // limit of zero
0, // quality in
0, // quality out
SLE::pointer(), // sponsor
journal); // journal
!isTesSuccess(ter))
{
JLOG(journal.trace()) << "doTransferTokenHelper: Failed to create trust line: " << transToken(ter);
return ter;
}
// clang-format on
view.update(sleDest);
}
if (!view.exists(trustLineKey) && issuer != receiver)
return tecNO_LINE;
auto const ter =
accountSend(view, sender, receiver, amount, journal, SLE::pointer(), WaiveTransferFee::No);
if (ter != tesSUCCESS)
{
JLOG(journal.trace()) << "doTransferTokenHelper: Failed to send token: " << transToken(ter);
return ter; // LCOV_EXCL_LINE
}
return tesSUCCESS;
}
template <>
inline TER
doTransferTokenHelper<MPTIssue>(
ApplyView& view,
SLE::ref sleDest,
STAmount const& xrpBalance,
STAmount const& amount,
AccountID const& issuer,
AccountID const& sender,
AccountID const& receiver,
bool createAsset,
beast::Journal journal)
{
auto const mptID = amount.get<MPTIssue>().getMptID();
auto const issuanceKey = keylet::mptokenIssuance(mptID);
if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && createAsset &&
issuer != receiver)
{
if (xrpBalance < accountReserve(view, sleDest, journal, {.ownerCountDelta = 1}))
{
JLOG(journal.trace()) << "doTransferTokenHelper: MPT does not exist. "
"Insufficent reserve to create MPT.";
return tecINSUFFICIENT_RESERVE;
}
if (auto const ter = createMPToken(view, mptID, receiver, SLE::pointer(), 0);
!isTesSuccess(ter))
{
JLOG(journal.trace()) << "doTransferTokenHelper: Failed to create MPT: "
<< transToken(ter);
return ter;
}
// Update owner count.
increaseOwnerCount(view, sleDest, SLE::pointer(), 1, journal);
}
if (issuer != receiver && !view.exists(keylet::mptoken(issuanceKey.key, receiver)))
{
JLOG(journal.trace()) << "doTransferTokenHelper: MPT does not exist.";
return tecNO_PERMISSION;
}
auto const ter =
accountSend(view, sender, receiver, amount, journal, SLE::pointer(), WaiveTransferFee::No);
if (ter != tesSUCCESS)
{
JLOG(journal.trace()) << "doTransferTokenHelper: Failed to send MPT: " << transToken(ter);
return ter; // LCOV_EXCL_LINE
}
return tesSUCCESS;
}
// Remove a subscription from both owner directories, release the owner's
// reserve, and erase the object. Shared by SubscriptionCancel and the
// single-use claim path so the two never diverge.
inline TER
deleteSubscription(ApplyView& view, SLE::ref sleSub, beast::Journal journal)
{
AccountID const account{sleSub->getAccountID(sfAccount)};
AccountID const dstAcct{sleSub->getAccountID(sfDestination)};
std::uint64_t const ownerPage{(*sleSub)[sfOwnerNode]};
if (!view.dirRemove(keylet::ownerDir(account), ownerPage, sleSub->key(), true))
{
JLOG(journal.fatal()) << "deleteSubscription: Unable to delete from source.";
return tefBAD_LEDGER;
}
std::uint64_t const destPage{(*sleSub)[sfDestinationNode]};
if (!view.dirRemove(keylet::ownerDir(dstAcct), destPage, sleSub->key(), true))
{
JLOG(journal.fatal()) << "deleteSubscription: Unable to delete from destination.";
return tefBAD_LEDGER;
}
auto const sleSrc = view.peek(keylet::account(account));
decreaseOwnerCount(view, sleSrc, SLE::pointer(), 1, journal);
view.erase(sleSub);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -614,6 +614,14 @@ ammBinHolding(uint256 const& ammID, AccountID const& owner, std::int32_t binID)
Keylet
ammBinHolding(uint256 const& key) noexcept;
Keylet
subscription(AccountID const& account, AccountID const& dest, std::uint32_t seq) noexcept;
inline Keylet
subscription(uint256 const& key) noexcept
{
return {ltSUBSCRIPTION, key};
}
} // namespace keylet
// Everything below is deprecated and should be removed in favor of keylets:

View File

@@ -211,7 +211,10 @@ enum LedgerEntryType : std::uint16_t {
\
LEDGER_OBJECT(Ballot, \
LSF_FLAG(lsfBallotFinalized, 0x00000001) /* True, results have been published */ \
LSF_FLAG(lsfVoterRecoverable, 0x00000002)) /* True, casts carry a voter self-recovery vector */
LSF_FLAG(lsfVoterRecoverable, 0x00000002)) /* True, casts carry a voter self-recovery vector */ \
\
LEDGER_OBJECT(Subscription, \
LSF_FLAG(lsfSingleUse, 0x00010000)) /* True, delete on first successful claim */
// clang-format on

View File

@@ -261,6 +261,10 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
\
TRANSACTION(BallotCreate, \
TF_FLAG(tfVoterRecoverable, lsfVoterRecoverable), /* casts must carry a voter self-recovery vector */ \
MASK_ADJ(0)) \
\
TRANSACTION(SubscriptionSet, /* True, delete the subscription on the first successful claim */ \
TF_FLAG(tfSingleUse, 0x00010000), \
MASK_ADJ(0))
constexpr std::uint32_t tfSendAmount = 0x00010000;

View File

@@ -159,3 +159,4 @@ XRPL_FEATURE(CouponPayments, Supported::Yes, VoteBehavior::DefaultN
XRPL_FEATURE(ConfidentialVoting, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(AMMCurves, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(OfferQualifiers, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(Subscription, Supported::No, VoteBehavior::DefaultNo)

View File

@@ -865,6 +865,21 @@ LEDGER_ENTRY(ltAMM_BIN_HOLDING, 0x0097, AMMBinHolding, amm_bin_holding, ({
{sfFeeGrowthInsideLast1, SoeRequired},
{sfOwnerNode, SoeRequired},
}))
LEDGER_ENTRY(ltSUBSCRIPTION, 0x008A, Subscription, subscription, ({
{sfPreviousTxnID, SoeRequired},
{sfPreviousTxnLgrSeq, SoeRequired},
{sfSequence, SoeRequired},
{sfOwnerNode, SoeRequired},
{sfAccount, SoeRequired},
{sfDestination, SoeRequired},
{sfDestinationTag, SoeOptional},
{sfAmount, SoeRequired},
{sfBalance, SoeRequired},
{sfFrequency, SoeRequired},
{sfNextClaimTime, SoeRequired},
{sfExpiration, SoeOptional},
{sfDestinationNode, SoeRequired},
}))
#undef EXPAND
#undef LEDGER_ENTRY_DUPLICATE

View File

@@ -561,3 +561,7 @@ TYPED_SFIELD(sfTokensOwed1, AMOUNT, 41)
TYPED_SFIELD(sfReserve0, AMOUNT, 42)
TYPED_SFIELD(sfReserve1, AMOUNT, 43)
TYPED_SFIELD(sfMinQuantity, AMOUNT, 44)
TYPED_SFIELD(sfFrequency, UINT32, 109)
TYPED_SFIELD(sfStartTime, UINT32, 110)
TYPED_SFIELD(sfNextClaimTime, UINT32, 111)
TYPED_SFIELD(sfSubscriptionID, UINT256, 49)

View File

@@ -1499,3 +1499,29 @@ TRANSACTION(ttAMM_BIN_DESTROY, 120, AMMBinDestroy,
{sfAsset2, SoeRequired, SoeMptSupported},
{sfBinID, SoeRequired},
}))
TRANSACTION(ttSUBSCRIPTION_SET, 121, SubscriptionSet,
({.delegable = Delegation::Delegable, .amendment = featureSubscription}),
({
{sfDestination, SoeOptional},
{sfAmount, SoeRequired, SoeMptSupported},
{sfFrequency, SoeOptional},
{sfStartTime, SoeOptional},
{sfExpiration, SoeOptional},
{sfDestinationTag, SoeOptional},
{sfSubscriptionID, SoeOptional},
}))
TRANSACTION(ttSUBSCRIPTION_CANCEL, 122, SubscriptionCancel,
({.delegable = Delegation::Delegable, .amendment = featureSubscription}),
({
{sfSubscriptionID, SoeRequired},
}))
TRANSACTION(ttSUBSCRIPTION_CLAIM, 123, SubscriptionClaim,
({
.delegable = Delegation::Delegable,
.amendment = featureSubscription,
.privileges = Privilege::MayCreateMpt,
}),
({
{sfAmount, SoeRequired, SoeMptSupported},
{sfSubscriptionID, SoeRequired},
}))

View File

@@ -47,6 +47,7 @@ JSS(Destination); // in: TransactionSign; field.
JSS(EPrice); // in: AMM Deposit option
JSS(Fee); // in/out: TransactionSign; field.
JSS(Flags); // in/out: TransactionSign; field.
JSS(Frequency); // in: Subscription transactions
JSS(Holder); // field.
JSS(Invalid); //
JSS(Issuer); // in: Credential transactions
@@ -81,6 +82,7 @@ JSS(Signer); // field.
JSS(Signers); // field.
JSS(SigningPubKey); // field.
JSS(Subject); // in: Credential transactions
JSS(SubscriptionID); // in: Subscription transactions
JSS(TakerGets); // field.
JSS(TakerPays); // field.
JSS(TradingFee); // in/out: AMM trading fee

View File

@@ -0,0 +1,431 @@
// 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 SubscriptionBuilder;
/**
* @brief Ledger Entry: Subscription
*
* Type: ltSUBSCRIPTION (0x008A)
* RPC Name: subscription
*
* Immutable wrapper around SLE providing type-safe field access.
* Use SubscriptionBuilder to construct new ledger entries.
*/
class Subscription : public LedgerEntryBase
{
public:
static constexpr LedgerEntryType entryType = ltSUBSCRIPTION;
/**
* @brief Construct a Subscription ledger entry wrapper from an existing SLE object.
* @throws std::runtime_error if the ledger entry type doesn't match.
*/
explicit Subscription(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 Subscription");
}
}
// 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 sfSequence (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT32::type::value_type
getSequence() const
{
return this->sle_->at(sfSequence);
}
/**
* @brief Get sfOwnerNode (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT64::type::value_type
getOwnerNode() const
{
return this->sle_->at(sfOwnerNode);
}
/**
* @brief Get sfAccount (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_ACCOUNT::type::value_type
getAccount() const
{
return this->sle_->at(sfAccount);
}
/**
* @brief Get sfDestination (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_ACCOUNT::type::value_type
getDestination() const
{
return this->sle_->at(sfDestination);
}
/**
* @brief Get sfDestinationTag (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getDestinationTag() const
{
if (hasDestinationTag())
return this->sle_->at(sfDestinationTag);
return std::nullopt;
}
/**
* @brief Check if sfDestinationTag is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasDestinationTag() const
{
return this->sle_->isFieldPresent(sfDestinationTag);
}
/**
* @brief Get sfAmount (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_AMOUNT::type::value_type
getAmount() const
{
return this->sle_->at(sfAmount);
}
/**
* @brief Get sfBalance (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_AMOUNT::type::value_type
getBalance() const
{
return this->sle_->at(sfBalance);
}
/**
* @brief Get sfFrequency (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT32::type::value_type
getFrequency() const
{
return this->sle_->at(sfFrequency);
}
/**
* @brief Get sfNextClaimTime (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT32::type::value_type
getNextClaimTime() const
{
return this->sle_->at(sfNextClaimTime);
}
/**
* @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 sfDestinationNode (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT64::type::value_type
getDestinationNode() const
{
return this->sle_->at(sfDestinationNode);
}
};
/**
* @brief Builder for Subscription 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 SubscriptionBuilder : public LedgerEntryBuilderBase<SubscriptionBuilder>
{
public:
/**
* @brief Construct a new SubscriptionBuilder with required fields.
* @param previousTxnID The sfPreviousTxnID field value.
* @param previousTxnLgrSeq The sfPreviousTxnLgrSeq field value.
* @param sequence The sfSequence field value.
* @param ownerNode The sfOwnerNode field value.
* @param account The sfAccount field value.
* @param destination The sfDestination field value.
* @param amount The sfAmount field value.
* @param balance The sfBalance field value.
* @param frequency The sfFrequency field value.
* @param nextClaimTime The sfNextClaimTime field value.
* @param destinationNode The sfDestinationNode field value.
*/
SubscriptionBuilder(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_UINT32::type::value_type> const& sequence,std::decay_t<typename SF_UINT64::type::value_type> const& ownerNode,std::decay_t<typename SF_ACCOUNT::type::value_type> const& account,std::decay_t<typename SF_ACCOUNT::type::value_type> const& destination,std::decay_t<typename SF_AMOUNT::type::value_type> const& amount,std::decay_t<typename SF_AMOUNT::type::value_type> const& balance,std::decay_t<typename SF_UINT32::type::value_type> const& frequency,std::decay_t<typename SF_UINT32::type::value_type> const& nextClaimTime,std::decay_t<typename SF_UINT64::type::value_type> const& destinationNode)
: LedgerEntryBuilderBase<SubscriptionBuilder>(ltSUBSCRIPTION)
{
setPreviousTxnID(previousTxnID);
setPreviousTxnLgrSeq(previousTxnLgrSeq);
setSequence(sequence);
setOwnerNode(ownerNode);
setAccount(account);
setDestination(destination);
setAmount(amount);
setBalance(balance);
setFrequency(frequency);
setNextClaimTime(nextClaimTime);
setDestinationNode(destinationNode);
}
/**
* @brief Construct a SubscriptionBuilder 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.
*/
SubscriptionBuilder(SLE::const_pointer sle)
{
if (sle->at(sfLedgerEntryType) != ltSUBSCRIPTION)
{
throw std::runtime_error("Invalid ledger entry type for Subscription");
}
object_ = *sle;
}
/**
* @brief Ledger entry-specific field setters
*/
/**
* @brief Set sfPreviousTxnID (SoeRequired)
* @return Reference to this builder for method chaining.
*/
SubscriptionBuilder&
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.
*/
SubscriptionBuilder&
setPreviousTxnLgrSeq(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfPreviousTxnLgrSeq] = value;
return *this;
}
/**
* @brief Set sfSequence (SoeRequired)
* @return Reference to this builder for method chaining.
*/
SubscriptionBuilder&
setSequence(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfSequence] = value;
return *this;
}
/**
* @brief Set sfOwnerNode (SoeRequired)
* @return Reference to this builder for method chaining.
*/
SubscriptionBuilder&
setOwnerNode(std::decay_t<typename SF_UINT64::type::value_type> const& value)
{
object_[sfOwnerNode] = value;
return *this;
}
/**
* @brief Set sfAccount (SoeRequired)
* @return Reference to this builder for method chaining.
*/
SubscriptionBuilder&
setAccount(std::decay_t<typename SF_ACCOUNT::type::value_type> const& value)
{
object_[sfAccount] = value;
return *this;
}
/**
* @brief Set sfDestination (SoeRequired)
* @return Reference to this builder for method chaining.
*/
SubscriptionBuilder&
setDestination(std::decay_t<typename SF_ACCOUNT::type::value_type> const& value)
{
object_[sfDestination] = value;
return *this;
}
/**
* @brief Set sfDestinationTag (SoeOptional)
* @return Reference to this builder for method chaining.
*/
SubscriptionBuilder&
setDestinationTag(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfDestinationTag] = value;
return *this;
}
/**
* @brief Set sfAmount (SoeRequired)
* @return Reference to this builder for method chaining.
*/
SubscriptionBuilder&
setAmount(std::decay_t<typename SF_AMOUNT::type::value_type> const& value)
{
object_[sfAmount] = value;
return *this;
}
/**
* @brief Set sfBalance (SoeRequired)
* @return Reference to this builder for method chaining.
*/
SubscriptionBuilder&
setBalance(std::decay_t<typename SF_AMOUNT::type::value_type> const& value)
{
object_[sfBalance] = value;
return *this;
}
/**
* @brief Set sfFrequency (SoeRequired)
* @return Reference to this builder for method chaining.
*/
SubscriptionBuilder&
setFrequency(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfFrequency] = value;
return *this;
}
/**
* @brief Set sfNextClaimTime (SoeRequired)
* @return Reference to this builder for method chaining.
*/
SubscriptionBuilder&
setNextClaimTime(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfNextClaimTime] = value;
return *this;
}
/**
* @brief Set sfExpiration (SoeOptional)
* @return Reference to this builder for method chaining.
*/
SubscriptionBuilder&
setExpiration(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfExpiration] = value;
return *this;
}
/**
* @brief Set sfDestinationNode (SoeRequired)
* @return Reference to this builder for method chaining.
*/
SubscriptionBuilder&
setDestinationNode(std::decay_t<typename SF_UINT64::type::value_type> const& value)
{
object_[sfDestinationNode] = value;
return *this;
}
/**
* @brief Build and return the completed Subscription wrapper.
* @param index The ledger entry index.
* @return The constructed ledger entry wrapper.
*/
Subscription
build(uint256 const& index)
{
return Subscription{std::make_shared<SLE>(std::move(object_), index)};
}
};
} // namespace xrpl::ledger_entries

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 SubscriptionCancelBuilder;
/**
* @brief Transaction: SubscriptionCancel
*
* Type: ttSUBSCRIPTION_CANCEL (93)
* Delegable: Delegation::Delegable
* Amendment: featureSubscription
* Privileges: Privilege::NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use SubscriptionCancelBuilder to construct new transactions.
*/
class SubscriptionCancel : public TransactionBase
{
public:
static constexpr xrpl::TxType txType = ttSUBSCRIPTION_CANCEL;
/**
* @brief Construct a SubscriptionCancel transaction wrapper from an existing STTx object.
* @throws std::runtime_error if the transaction type doesn't match.
*/
explicit SubscriptionCancel(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 SubscriptionCancel");
}
}
// Transaction-specific field getters
/**
* @brief Get sfSubscriptionID (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT256::type::value_type
getSubscriptionID() const
{
return this->tx_->at(sfSubscriptionID);
}
};
/**
* @brief Builder for SubscriptionCancel 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 SubscriptionCancelBuilder : public TransactionBuilderBase<SubscriptionCancelBuilder>
{
public:
/**
* @brief Construct a new SubscriptionCancelBuilder with required fields.
* @param account The account initiating the transaction.
* @param subscriptionID The sfSubscriptionID field value.
* @param sequence Optional sequence number for the transaction.
* @param fee Optional fee for the transaction.
*/
SubscriptionCancelBuilder(SF_ACCOUNT::type::value_type account,
std::decay_t<typename SF_UINT256::type::value_type> const& subscriptionID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<SubscriptionCancelBuilder>(ttSUBSCRIPTION_CANCEL, account, sequence, fee)
{
setSubscriptionID(subscriptionID);
}
/**
* @brief Construct a SubscriptionCancelBuilder from an existing STTx object.
* @param tx The existing transaction to copy from.
* @throws std::runtime_error if the transaction type doesn't match.
*/
SubscriptionCancelBuilder(std::shared_ptr<STTx const> tx)
{
if (tx->getTxnType() != ttSUBSCRIPTION_CANCEL)
{
throw std::runtime_error("Invalid transaction type for SubscriptionCancelBuilder");
}
object_ = *tx;
}
/**
* @brief Transaction-specific field setters
*/
/**
* @brief Set sfSubscriptionID (SoeRequired)
* @return Reference to this builder for method chaining.
*/
SubscriptionCancelBuilder&
setSubscriptionID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
{
object_[sfSubscriptionID] = value;
return *this;
}
/**
* @brief Build and return the SubscriptionCancel wrapper.
* @param publicKey The public key for signing.
* @param secretKey The secret key for signing.
* @return The constructed transaction wrapper.
*/
SubscriptionCancel
build(PublicKey const& publicKey, SecretKey const& secretKey)
{
sign(publicKey, secretKey);
return SubscriptionCancel{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 SubscriptionClaimBuilder;
/**
* @brief Transaction: SubscriptionClaim
*
* Type: ttSUBSCRIPTION_CLAIM (94)
* Delegable: Delegation::Delegable
* Amendment: featureSubscription
* Privileges: Privilege::MayCreateMpt
*
* Immutable wrapper around STTx providing type-safe field access.
* Use SubscriptionClaimBuilder to construct new transactions.
*/
class SubscriptionClaim : public TransactionBase
{
public:
static constexpr xrpl::TxType txType = ttSUBSCRIPTION_CLAIM;
/**
* @brief Construct a SubscriptionClaim transaction wrapper from an existing STTx object.
* @throws std::runtime_error if the transaction type doesn't match.
*/
explicit SubscriptionClaim(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 SubscriptionClaim");
}
}
// Transaction-specific field getters
/**
* @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 Get sfSubscriptionID (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT256::type::value_type
getSubscriptionID() const
{
return this->tx_->at(sfSubscriptionID);
}
};
/**
* @brief Builder for SubscriptionClaim 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 SubscriptionClaimBuilder : public TransactionBuilderBase<SubscriptionClaimBuilder>
{
public:
/**
* @brief Construct a new SubscriptionClaimBuilder with required fields.
* @param account The account initiating the transaction.
* @param amount The sfAmount field value.
* @param subscriptionID The sfSubscriptionID field value.
* @param sequence Optional sequence number for the transaction.
* @param fee Optional fee for the transaction.
*/
SubscriptionClaimBuilder(SF_ACCOUNT::type::value_type account,
std::decay_t<typename SF_AMOUNT::type::value_type> const& amount, std::decay_t<typename SF_UINT256::type::value_type> const& subscriptionID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<SubscriptionClaimBuilder>(ttSUBSCRIPTION_CLAIM, account, sequence, fee)
{
setAmount(amount);
setSubscriptionID(subscriptionID);
}
/**
* @brief Construct a SubscriptionClaimBuilder from an existing STTx object.
* @param tx The existing transaction to copy from.
* @throws std::runtime_error if the transaction type doesn't match.
*/
SubscriptionClaimBuilder(std::shared_ptr<STTx const> tx)
{
if (tx->getTxnType() != ttSUBSCRIPTION_CLAIM)
{
throw std::runtime_error("Invalid transaction type for SubscriptionClaimBuilder");
}
object_ = *tx;
}
/**
* @brief Transaction-specific field setters
*/
/**
* @brief Set sfAmount (SoeRequired)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
SubscriptionClaimBuilder&
setAmount(std::decay_t<typename SF_AMOUNT::type::value_type> const& value)
{
object_[sfAmount] = value;
return *this;
}
/**
* @brief Set sfSubscriptionID (SoeRequired)
* @return Reference to this builder for method chaining.
*/
SubscriptionClaimBuilder&
setSubscriptionID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
{
object_[sfSubscriptionID] = value;
return *this;
}
/**
* @brief Build and return the SubscriptionClaim wrapper.
* @param publicKey The public key for signing.
* @param secretKey The secret key for signing.
* @return The constructed transaction wrapper.
*/
SubscriptionClaim
build(PublicKey const& publicKey, SecretKey const& secretKey)
{
sign(publicKey, secretKey);
return SubscriptionClaim{std::make_shared<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions

View File

@@ -0,0 +1,355 @@
// 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 SubscriptionSetBuilder;
/**
* @brief Transaction: SubscriptionSet
*
* Type: ttSUBSCRIPTION_SET (92)
* Delegable: Delegation::Delegable
* Amendment: featureSubscription
* Privileges: Privilege::NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use SubscriptionSetBuilder to construct new transactions.
*/
class SubscriptionSet : public TransactionBase
{
public:
static constexpr xrpl::TxType txType = ttSUBSCRIPTION_SET;
/**
* @brief Construct a SubscriptionSet transaction wrapper from an existing STTx object.
* @throws std::runtime_error if the transaction type doesn't match.
*/
explicit SubscriptionSet(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 SubscriptionSet");
}
}
// Transaction-specific field getters
/**
* @brief Get sfDestination (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_ACCOUNT::type::value_type>
getDestination() const
{
if (hasDestination())
{
return this->tx_->at(sfDestination);
}
return std::nullopt;
}
/**
* @brief Check if sfDestination is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasDestination() const
{
return this->tx_->isFieldPresent(sfDestination);
}
/**
* @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 Get sfFrequency (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getFrequency() const
{
if (hasFrequency())
{
return this->tx_->at(sfFrequency);
}
return std::nullopt;
}
/**
* @brief Check if sfFrequency is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasFrequency() const
{
return this->tx_->isFieldPresent(sfFrequency);
}
/**
* @brief Get sfStartTime (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getStartTime() const
{
if (hasStartTime())
{
return this->tx_->at(sfStartTime);
}
return std::nullopt;
}
/**
* @brief Check if sfStartTime is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasStartTime() const
{
return this->tx_->isFieldPresent(sfStartTime);
}
/**
* @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 sfDestinationTag (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getDestinationTag() const
{
if (hasDestinationTag())
{
return this->tx_->at(sfDestinationTag);
}
return std::nullopt;
}
/**
* @brief Check if sfDestinationTag is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasDestinationTag() const
{
return this->tx_->isFieldPresent(sfDestinationTag);
}
/**
* @brief Get sfSubscriptionID (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT256::type::value_type>
getSubscriptionID() const
{
if (hasSubscriptionID())
{
return this->tx_->at(sfSubscriptionID);
}
return std::nullopt;
}
/**
* @brief Check if sfSubscriptionID is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasSubscriptionID() const
{
return this->tx_->isFieldPresent(sfSubscriptionID);
}
};
/**
* @brief Builder for SubscriptionSet 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 SubscriptionSetBuilder : public TransactionBuilderBase<SubscriptionSetBuilder>
{
public:
/**
* @brief Construct a new SubscriptionSetBuilder with required fields.
* @param account The account initiating the transaction.
* @param amount The sfAmount field value.
* @param sequence Optional sequence number for the transaction.
* @param fee Optional fee for the transaction.
*/
SubscriptionSetBuilder(SF_ACCOUNT::type::value_type account,
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<SubscriptionSetBuilder>(ttSUBSCRIPTION_SET, account, sequence, fee)
{
setAmount(amount);
}
/**
* @brief Construct a SubscriptionSetBuilder from an existing STTx object.
* @param tx The existing transaction to copy from.
* @throws std::runtime_error if the transaction type doesn't match.
*/
SubscriptionSetBuilder(std::shared_ptr<STTx const> tx)
{
if (tx->getTxnType() != ttSUBSCRIPTION_SET)
{
throw std::runtime_error("Invalid transaction type for SubscriptionSetBuilder");
}
object_ = *tx;
}
/**
* @brief Transaction-specific field setters
*/
/**
* @brief Set sfDestination (SoeOptional)
* @return Reference to this builder for method chaining.
*/
SubscriptionSetBuilder&
setDestination(std::decay_t<typename SF_ACCOUNT::type::value_type> const& value)
{
object_[sfDestination] = value;
return *this;
}
/**
* @brief Set sfAmount (SoeRequired)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
SubscriptionSetBuilder&
setAmount(std::decay_t<typename SF_AMOUNT::type::value_type> const& value)
{
object_[sfAmount] = value;
return *this;
}
/**
* @brief Set sfFrequency (SoeOptional)
* @return Reference to this builder for method chaining.
*/
SubscriptionSetBuilder&
setFrequency(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfFrequency] = value;
return *this;
}
/**
* @brief Set sfStartTime (SoeOptional)
* @return Reference to this builder for method chaining.
*/
SubscriptionSetBuilder&
setStartTime(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfStartTime] = value;
return *this;
}
/**
* @brief Set sfExpiration (SoeOptional)
* @return Reference to this builder for method chaining.
*/
SubscriptionSetBuilder&
setExpiration(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfExpiration] = value;
return *this;
}
/**
* @brief Set sfDestinationTag (SoeOptional)
* @return Reference to this builder for method chaining.
*/
SubscriptionSetBuilder&
setDestinationTag(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfDestinationTag] = value;
return *this;
}
/**
* @brief Set sfSubscriptionID (SoeOptional)
* @return Reference to this builder for method chaining.
*/
SubscriptionSetBuilder&
setSubscriptionID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
{
object_[sfSubscriptionID] = value;
return *this;
}
/**
* @brief Build and return the SubscriptionSet wrapper.
* @param publicKey The public key for signing.
* @param secretKey The secret key for signing.
* @return The constructed transaction wrapper.
*/
SubscriptionSet
build(PublicKey const& publicKey, SecretKey const& secretKey)
{
sign(publicKey, secretKey);
return SubscriptionSet{std::make_shared<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions

View File

@@ -18,6 +18,7 @@
#include <xrpl/tx/invariants/PermissionedDEXInvariant.h>
#include <xrpl/tx/invariants/PermissionedDomainInvariant.h>
#include <xrpl/tx/invariants/SponsorshipInvariant.h>
#include <xrpl/tx/invariants/SubscriptionInvariant.h>
#include <xrpl/tx/invariants/TokenIssuanceInvariant.h>
#include <xrpl/tx/invariants/VaultAccrualInvariant.h>
#include <xrpl/tx/invariants/VaultInvariant.h>
@@ -507,6 +508,7 @@ using InvariantChecks = std::tuple<
ValidLoan,
ValidVault,
ValidVaultAccrual,
ValidSubscription,
ValidConfidentialMPToken,
ValidMPTBalanceChanges,
ValidAmounts,

View File

@@ -0,0 +1,35 @@
#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 <memory>
#include <vector>
namespace xrpl {
/**
* @brief Invariant: a Subscription ledger entry holds a well-formed balance and
* distinct parties.
*
* Enforces XLS-78 2.1.1.7 for every Subscription entry the transaction leaves in
* the ledger: Balance is not negative, Balance and Amount are denominated in the
* same asset, and Account differs from Destination.
*/
class ValidSubscription
{
std::vector<std::shared_ptr<STLedgerEntry const>> subscriptions_;
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

@@ -0,0 +1,44 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.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 SubscriptionCancel : public Transactor
{
public:
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
explicit SubscriptionCancel(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,44 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.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 SubscriptionClaim : public Transactor
{
public:
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
explicit SubscriptionClaim(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,47 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.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 SubscriptionSet : public Transactor
{
public:
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
explicit SubscriptionSet(ApplyContext& ctx) : Transactor(ctx)
{
}
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
void
visitInvariantEntry(bool isDelete, 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

@@ -118,6 +118,7 @@ enum class LedgerNameSpace : std::uint16_t {
ContractData = 'b',
Ballot = 'y',
BallotVote = 'v',
Subscription = 'w',
// No longer used or supported. Left here to reserve the space to avoid accidental reuse.
Generator [[deprecated]] = 'g',
@@ -833,6 +834,12 @@ ammBinHolding(uint256 const& key) noexcept
return {ltAMM_BIN_HOLDING, key};
}
Keylet
subscription(AccountID const& account, AccountID const& dest, std::uint32_t seq) noexcept
{
return {ltSUBSCRIPTION, indexHash(LedgerNameSpace::Subscription, account, dest, seq)};
}
} // namespace keylet
} // namespace xrpl

View File

@@ -0,0 +1,66 @@
#include <xrpl/tx/invariants/SubscriptionInvariant.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
namespace xrpl {
void
ValidSubscription::visitEntry(bool isDelete, SLE::const_ref, SLE::const_ref after)
{
// A deleted entry imposes no constraint on the resulting ledger.
if (isDelete || !after || after->getType() != ltSUBSCRIPTION)
return;
subscriptions_.push_back(after);
}
bool
ValidSubscription::finalize(
STTx const&,
TER const result,
XRPAmount const,
ReadView const&,
beast::Journal const& j)
{
if (!isTesSuccess(result))
return true;
for (auto const& sleSub : subscriptions_)
{
STAmount const balance = sleSub->getFieldAmount(sfBalance);
STAmount const amount = sleSub->getFieldAmount(sfAmount);
if (balance.signum() < 0)
{
JLOG(j.fatal()) << "Invariant failed: subscription balance is negative";
return false;
}
if (balance.asset() != amount.asset())
{
JLOG(j.fatal()) << "Invariant failed: subscription balance and amount "
"are denominated in different assets";
return false;
}
if (sleSub->getAccountID(sfAccount) == sleSub->getAccountID(sfDestination))
{
JLOG(j.fatal()) << "Invariant failed: subscription account and "
"destination are the same";
return false;
}
}
return true;
}
} // namespace xrpl

View File

@@ -0,0 +1,91 @@
#include <xrpl/tx/transactors/subscription/SubscriptionCancel.h>
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/SubscriptionHelpers.h>
#include <xrpl/protocol/AccountID.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 <xrpl/tx/Transactor.h>
#include <cstdint>
namespace xrpl {
NotTEC
SubscriptionCancel::preflight(PreflightContext const& ctx)
{
return tesSUCCESS;
}
TER
SubscriptionCancel::preclaim(PreclaimContext const& ctx)
{
auto const sleSub = ctx.view.read(keylet::subscription(ctx.tx.getFieldH256(sfSubscriptionID)));
if (!sleSub)
{
JLOG(ctx.j.debug()) << "SubscriptionCancel: Subscription does not exist.";
return tecNO_ENTRY;
}
// The owner or the destination may cancel at any time; anyone may cancel
// once the subscription has expired.
if (!hasExpired(ctx.view, (*sleSub)[~sfExpiration]))
{
AccountID const account = ctx.tx.getAccountID(sfAccount);
if (account != sleSub->getAccountID(sfAccount) &&
account != sleSub->getAccountID(sfDestination))
{
JLOG(ctx.j.debug()) << "SubscriptionCancel: Account is not the owner "
"or destination of the subscription.";
return tecNO_PERMISSION;
}
}
return tesSUCCESS;
}
TER
SubscriptionCancel::doApply()
{
Sandbox sb(&ctx_.view());
auto const sleSub = sb.peek(keylet::subscription(ctx_.tx.getFieldH256(sfSubscriptionID)));
if (!sleSub)
{
JLOG(ctx_.journal.debug()) << "SubscriptionCancel: Subscription does not exist.";
return tecINTERNAL;
}
auto viewJ = ctx_.registry.get().getJournal("View");
if (auto const ter = deleteSubscription(sb, sleSub, viewJ); !isTesSuccess(ter))
return ter;
sb.apply(ctx_.rawView());
return tesSUCCESS;
}
void
SubscriptionCancel::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
{
// No transaction-specific invariants yet (future work).
}
bool
SubscriptionCancel::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,303 @@
#include <xrpl/tx/transactors/subscription/SubscriptionClaim.h>
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/SubscriptionHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Concepts.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/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/Transactor.h>
#include <cstdint>
#include <variant>
namespace xrpl {
NotTEC
SubscriptionClaim::preflight(PreflightContext const& ctx)
{
return tesSUCCESS;
}
TER
SubscriptionClaim::preclaim(PreclaimContext const& ctx)
{
auto const sleSub = ctx.view.read(keylet::subscription(ctx.tx.getFieldH256(sfSubscriptionID)));
if (!sleSub)
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: Subscription does not exist.";
return tecNO_ENTRY;
}
// Only claim a subscription with this account as the destination.
AccountID const dest = sleSub->getAccountID(sfDestination);
if (ctx.tx[sfAccount] != dest)
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: Cashing a subscription with "
"wrong Destination.";
return tecNO_PERMISSION;
}
AccountID const account = sleSub->getAccountID(sfAccount);
if (account == dest)
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: Malformed transaction: "
"Cashing subscription to self.";
return tecINTERNAL;
}
{
auto const sleSrc = ctx.view.read(keylet::account(account));
auto const sleDst = ctx.view.read(keylet::account(dest));
if (!sleSrc || !sleDst)
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: source or destination not in ledger";
return tecNO_ENTRY;
}
}
{
STAmount const amount = ctx.tx.getFieldAmount(sfAmount);
STAmount const sleAmount = sleSub->getFieldAmount(sfAmount);
if (amount.asset() != sleAmount.asset())
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: Subscription claim does "
"not match subscription currency.";
return tecWRONG_ASSET;
}
if (amount > sleAmount)
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: Claim amount exceeds "
"subscription amount.";
return tecLIMIT_EXCEEDED;
}
// Time/period context
std::uint32_t const currentTime =
ctx.view.header().parentCloseTime.time_since_epoch().count();
std::uint32_t const nextClaimTime = sleSub->getFieldU32(sfNextClaimTime);
std::uint32_t const frequency = sleSub->getFieldU32(sfFrequency);
// Determine effective available balance:
// - If we have crossed into a later period AND the previous period had
// a partial
// balance remaining (carryover not allowed), then the effective
// period rolls forward once and its balance resets to sleAmount.
// - Otherwise we operate on the period at nextClaimTime with its stored
// balance.
STAmount balance = sleSub->getFieldAmount(sfBalance);
bool const arrears = currentTime >= nextClaimTime + frequency;
if (arrears && balance != sleAmount)
{
// We will effectively operate on (nextClaimTime + frequency) with a
// full balance.
balance = sleAmount;
}
if (amount > balance)
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: Claim amount exceeds remaining "
"balance for this period.";
return tecINSUFFICIENT_FUNDS;
}
if (isXRP(amount))
{
if (xrpLiquid(ctx.view, account, 0, ctx.j) < amount)
return tecINSUFFICIENT_FUNDS;
}
else
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return canTransferTokenHelper<T>(ctx.view, account, dest, amount, ctx.j);
},
amount.asset().value());
!isTesSuccess(ret))
return ret;
}
}
// An expired subscription can no longer be claimed; it can only be
// cancelled.
if (hasExpired(ctx.view, (*sleSub)[~sfExpiration]))
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: The subscription has expired.";
return tecEXPIRED;
}
// Must be at or past the start of the effective period.
if (!hasExpired(ctx.view, sleSub->getFieldU32(sfNextClaimTime)))
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: The subscription has not "
"reached the next claim time.";
return tecTOO_SOON;
}
return tesSUCCESS;
}
TER
SubscriptionClaim::doApply()
{
PaymentSandbox psb(&ctx_.view());
auto viewJ = ctx_.registry.get().getJournal("View");
auto sleSub = psb.peek(keylet::subscription(ctx_.tx.getFieldH256(sfSubscriptionID)));
if (!sleSub)
{
JLOG(j_.trace()) << "SubscriptionClaim: Subscription does not exist.";
return tecINTERNAL;
}
AccountID const account = sleSub->getAccountID(sfAccount);
if (!psb.exists(keylet::account(account)))
{
JLOG(j_.trace()) << "SubscriptionClaim: Account does not exist.";
return tecINTERNAL;
}
AccountID const dest = sleSub->getAccountID(sfDestination);
if (!psb.exists(keylet::account(dest)))
{
JLOG(j_.trace()) << "SubscriptionClaim: Account does not exist.";
return tecINTERNAL;
}
if (dest != ctx_.tx.getAccountID(sfAccount))
{
JLOG(j_.trace()) << "SubscriptionClaim: Account is not the "
"destination of the subscription.";
return tecNO_PERMISSION;
}
STAmount const sleAmount = sleSub->getFieldAmount(sfAmount);
STAmount const deliverAmount = ctx_.tx.getFieldAmount(sfAmount);
// Pull current period info
std::uint32_t const currentTime = psb.header().parentCloseTime.time_since_epoch().count();
std::uint32_t nextClaimTime = sleSub->getFieldU32(sfNextClaimTime);
std::uint32_t const frequency = sleSub->getFieldU32(sfFrequency);
STAmount availableBalance = sleSub->getFieldAmount(sfBalance);
bool const arrears = currentTime >= nextClaimTime + frequency;
// If we crossed into a later period and the previous period was partially
// used, forfeit the leftover and roll forward exactly one period; reset the
// balance.
if (arrears && availableBalance != sleAmount)
{
nextClaimTime += frequency;
availableBalance = sleAmount;
// Reflect the rollover immediately in the SLE so subsequent logic is
// consistent.
sleSub->setFieldU32(sfNextClaimTime, nextClaimTime);
sleSub->setFieldAmount(sfBalance, availableBalance);
}
// Enforce available balance for the effective period.
if (deliverAmount > availableBalance)
{
JLOG(j_.trace()) << "SubscriptionClaim: Claim amount exceeds remaining "
<< "balance for this period.";
return tecINTERNAL;
}
// Perform the transfer
if (isXRP(deliverAmount))
{
if (TER const ter{transferXRP(psb, account, dest, deliverAmount, viewJ)}; ter != tesSUCCESS)
{
return ter;
}
}
else
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return doTransferTokenHelper<T>(
psb,
psb.peek(keylet::account(dest)),
preFeeBalance_,
deliverAmount,
deliverAmount.getIssuer(),
account,
dest,
true, // create asset
viewJ);
},
deliverAmount.asset().value());
!isTesSuccess(ret))
return ret;
}
// Metered accounting: advance/reset the period. Unmetered subscriptions
// (Frequency == 0) cap each claim at Amount and never touch Balance or
// NextClaimTime.
if (frequency != 0)
{
STAmount const newBalance = availableBalance - deliverAmount;
if (newBalance == sleAmount.zeroed())
{
// Full period claimed: advance exactly one period and reset next
// period balance.
nextClaimTime += frequency;
sleSub->setFieldU32(sfNextClaimTime, nextClaimTime);
sleSub->setFieldAmount(sfBalance, sleAmount);
}
else
{
// Partial claim within the same effective period.
sleSub->setFieldAmount(sfBalance, newBalance);
// Do not advance nextClaimTime; if we had a rollover-forfeit above,
// we already moved nextClaimTime forward exactly once.
}
}
// Single-use subscriptions are removed on the first successful claim,
// regardless of Frequency or whether the claim was partial.
if (sleSub->isFlag(lsfSingleUse))
{
if (auto const ter = deleteSubscription(psb, sleSub, viewJ); !isTesSuccess(ter))
return ter;
}
else
{
psb.update(sleSub);
}
psb.apply(ctx_.rawView());
return tesSUCCESS;
}
void
SubscriptionClaim::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
{
// No transaction-specific invariants yet (future work).
}
bool
SubscriptionClaim::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,358 @@
#include <xrpl/tx/transactors/subscription/SubscriptionSet.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/SubscriptionHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/tx/Transactor.h>
#include <cstdint>
#include <memory>
#include <variant>
namespace xrpl {
template <ValidIssueType T>
static NotTEC
setPreflightHelper(PreflightContext const& ctx);
template <>
NotTEC
setPreflightHelper<Issue>(PreflightContext const& ctx)
{
STAmount const amount = ctx.tx[sfAmount];
if (amount.native() || amount <= beast::kZero)
return temBAD_AMOUNT;
if (badCurrency() == amount.get<Issue>().currency)
return temBAD_CURRENCY;
return tesSUCCESS;
}
template <>
NotTEC
setPreflightHelper<MPTIssue>(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureMPTokensV1))
return temDISABLED;
auto const amount = ctx.tx[sfAmount];
if (amount.native() || amount.mpt() > MPTAmount{kMaxMpTokenAmount} || amount <= beast::kZero)
return temBAD_AMOUNT;
return tesSUCCESS;
}
std::uint32_t
SubscriptionSet::getFlagsMask(PreflightContext const& ctx)
{
return tfSubscriptionSetMask;
}
NotTEC
SubscriptionSet::preflight(PreflightContext const& ctx)
{
if (ctx.tx.isFieldPresent(sfSubscriptionID))
{
// update
if (!ctx.tx.isFieldPresent(sfAmount))
{
JLOG(ctx.j.trace()) << "SubscriptionSet: Malformed transaction: SubscriptionID "
"is present, but Amount is not.";
return temMALFORMED;
}
if (ctx.tx.isFieldPresent(sfDestination) || ctx.tx.isFieldPresent(sfStartTime))
{
JLOG(ctx.j.trace()) << "SubscriptionSet: Malformed transaction: SubscriptionID "
"is present, but immutable fields are also present.";
return temMALFORMED;
}
// lsfSingleUse is fixed at creation and cannot be changed on update.
if (ctx.tx.getFlags() & tfSingleUse)
{
JLOG(ctx.j.trace()) << "SubscriptionSet: tfSingleUse cannot be set on update.";
return temINVALID_FLAG;
}
}
else
{
// create
if (!ctx.tx.isFieldPresent(sfDestination) || !ctx.tx.isFieldPresent(sfAmount) ||
!ctx.tx.isFieldPresent(sfFrequency))
{
JLOG(ctx.j.trace()) << "SubscriptionSet: Malformed transaction: SubscriptionID "
"is not present, and required fields are not present.";
return temMALFORMED;
}
if (ctx.tx.getAccountID(sfDestination) == ctx.tx.getAccountID(sfAccount))
{
JLOG(ctx.j.trace()) << "SubscriptionSet: Malformed transaction: Account "
"is the same as the destination.";
return temDST_IS_SRC;
}
}
STAmount const amount = ctx.tx.getFieldAmount(sfAmount);
if (amount.native())
{
if (!isLegalNet(amount) || amount <= beast::kZero)
{
JLOG(ctx.j.trace()) << "SubscriptionSet: Malformed transaction: bad amount: "
<< amount.getFullText();
return temBAD_AMOUNT;
}
}
else
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) { return setPreflightHelper<T>(ctx); },
amount.asset().value());
!isTesSuccess(ret))
return ret;
}
return tesSUCCESS;
}
TER
SubscriptionSet::preclaim(PreclaimContext const& ctx)
{
STAmount const amount = ctx.tx.getFieldAmount(sfAmount);
AccountID const account = ctx.tx.getAccountID(sfAccount);
AccountID dest = ctx.tx.getAccountID(sfDestination);
if (ctx.tx.isFieldPresent(sfSubscriptionID))
{
// update
auto sle = ctx.view.read(keylet::subscription(ctx.tx.getFieldH256(sfSubscriptionID)));
if (!sle)
{
JLOG(ctx.j.trace()) << "SubscriptionSet: Subscription does not exist.";
return tecNO_ENTRY;
}
if (sle->getAccountID(sfAccount) != ctx.tx.getAccountID(sfAccount))
{
JLOG(ctx.j.trace()) << "SubscriptionSet: Account is not the "
"owner of the subscription.";
return tecNO_PERMISSION;
}
if (amount.asset() != sle->getFieldAmount(sfAmount).asset())
{
JLOG(ctx.j.trace()) << "SubscriptionSet: Amount asset does not "
"match the subscription asset.";
return tecWRONG_ASSET;
}
dest = sle->getAccountID(sfDestination);
}
else
{
// create
auto const sleDest = ctx.view.read(keylet::account(ctx.tx.getAccountID(sfDestination)));
if (!sleDest)
{
JLOG(ctx.j.trace()) << "SubscriptionSet: Destination account does not exist.";
return tecNO_DST;
}
auto const flags = sleDest->getFlags();
if ((flags & lsfRequireDestTag) && !ctx.tx[~sfDestinationTag])
return tecDST_TAG_NEEDED;
// Frequency == 0 denotes an unmetered subscription: no period
// accounting, each claim capped at Amount.
}
if (!isXRP(amount))
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return canTransferTokenHelper<T>(ctx.view, account, dest, amount, ctx.j);
},
amount.asset().value());
!isTesSuccess(ret))
return ret;
}
return tesSUCCESS;
}
TER
SubscriptionSet::doApply()
{
Sandbox sb(&ctx_.view());
AccountID const account = ctx_.tx.getAccountID(sfAccount);
auto const sleAccount = sb.peek(keylet::account(account));
if (!sleAccount)
{
JLOG(ctx_.journal.trace()) << "SubscriptionSet: Account does not exist.";
return tecINTERNAL;
}
if (ctx_.tx.isFieldPresent(sfSubscriptionID))
{
// update
auto const currentTime = sb.header().parentCloseTime.time_since_epoch().count();
auto sle = sb.peek(keylet::subscription(ctx_.tx.getFieldH256(sfSubscriptionID)));
sle->setFieldAmount(sfAmount, ctx_.tx.getFieldAmount(sfAmount));
// Changing Frequency starts a clean period: reset the anchor to now and
// restore the full balance. This covers metered<->unmetered and
// metered->metered transitions uniformly.
if (ctx_.tx.isFieldPresent(sfFrequency))
{
sle->setFieldU32(sfFrequency, ctx_.tx.getFieldU32(sfFrequency));
sle->setFieldU32(sfNextClaimTime, currentTime);
sle->setFieldAmount(sfBalance, ctx_.tx.getFieldAmount(sfAmount));
}
if (ctx_.tx.isFieldPresent(sfExpiration))
{
auto const expiration = ctx_.tx.getFieldU32(sfExpiration);
// Expiration == 0 removes any existing expiration.
if (expiration == 0)
{
if (sle->isFieldPresent(sfExpiration))
sle->makeFieldAbsent(sfExpiration);
}
else if (expiration < currentTime)
{
JLOG(ctx_.journal.trace())
<< "SubscriptionSet: The expiration time is in the past.";
return tecEXPIRED;
}
else
{
sle->setFieldU32(sfExpiration, expiration);
}
}
sb.update(sle);
}
else
{
auto const currentTime = sb.header().parentCloseTime.time_since_epoch().count();
auto startTime = currentTime;
auto nextClaimTime = currentTime;
// create
{
auto const balance = STAmount((*sleAccount)[sfBalance]).xrp();
auto const reserve =
accountReserve(sb, sleAccount, ctx_.journal, {.ownerCountDelta = 1});
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
}
AccountID const dest = ctx_.tx.getAccountID(sfDestination);
Keylet const subKeylet = keylet::subscription(account, dest, ctx_.tx.getSeqProxy().value());
auto sle = std::make_shared<SLE>(subKeylet);
sle->setAccountID(sfAccount, account);
sle->setAccountID(sfDestination, dest);
sle->setFieldU32(sfSequence, ctx_.tx.getSeqProxy().value());
if (ctx_.tx.getFlags() & tfSingleUse)
sle->setFlag(lsfSingleUse);
if (ctx_.tx.isFieldPresent(sfDestinationTag))
sle->setFieldU32(sfDestinationTag, ctx_.tx.getFieldU32(sfDestinationTag));
sle->setFieldAmount(sfAmount, ctx_.tx.getFieldAmount(sfAmount));
sle->setFieldAmount(sfBalance, ctx_.tx.getFieldAmount(sfAmount));
sle->setFieldU32(sfFrequency, ctx_.tx.getFieldU32(sfFrequency));
if (ctx_.tx.isFieldPresent(sfStartTime))
{
startTime = ctx_.tx.getFieldU32(sfStartTime);
nextClaimTime = startTime;
if (startTime < currentTime)
{
JLOG(ctx_.journal.trace()) << "SubscriptionSet: The start time is in the past.";
return tecNO_PERMISSION;
}
}
sle->setFieldU32(sfNextClaimTime, nextClaimTime);
if (ctx_.tx.isFieldPresent(sfExpiration))
{
auto const expiration = ctx_.tx.getFieldU32(sfExpiration);
if (expiration < currentTime)
{
JLOG(ctx_.journal.trace())
<< "SubscriptionSet: The expiration time is in the past.";
return tecEXPIRED;
}
if (expiration < nextClaimTime)
{
JLOG(ctx_.journal.trace()) << "SubscriptionSet: The expiration time is "
"less than the next claim time.";
return tecEXPIRED;
}
sle->setFieldU32(sfExpiration, expiration);
}
{
auto page =
sb.dirInsert(keylet::ownerDir(account), subKeylet, describeOwnerDir(account));
if (!page)
return tecDIR_FULL;
(*sle)[sfOwnerNode] = *page;
}
{
auto page = sb.dirInsert(keylet::ownerDir(dest), subKeylet, describeOwnerDir(dest));
if (!page)
return tecDIR_FULL;
(*sle)[sfDestinationNode] = *page;
}
increaseOwnerCount(sb, sleAccount, SLE::pointer(), 1, ctx_.journal);
sb.insert(sle);
}
sb.apply(ctx_.rawView());
return tesSUCCESS;
}
void
SubscriptionSet::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
{
// No transaction-specific invariants yet (future work).
}
bool
SubscriptionSet::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
{
// No transaction-specific invariants yet (future work).
return true;
}
} // namespace xrpl

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
#include <test/jtx/subscription.h>
#include <test/jtx/Account.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <optional>
namespace xrpl::test::jtx::subscription {
void
StartTime::operator()(Env& env, JTx& jt) const
{
jt.jv[sfStartTime.jsonName] = value_.time_since_epoch().count();
}
json::Value
create(
jtx::Account const& account,
jtx::Account const& destination,
STAmount const& amount,
NetClock::duration const& frequency,
std::optional<NetClock::time_point> const& expiration,
std::uint32_t flags)
{
json::Value jv;
jv[jss::TransactionType] = jss::SubscriptionSet;
jv[jss::Account] = to_string(account.id());
jv[jss::Destination] = to_string(destination.id());
jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
jv[jss::Frequency] = frequency.count();
jv[jss::Flags] = flags;
if (expiration)
jv[sfExpiration.jsonName] = expiration->time_since_epoch().count();
return jv;
}
json::Value
update(
jtx::Account const& account,
uint256 const& subscriptionId,
STAmount const& amount,
std::optional<NetClock::time_point> const& expiration,
std::optional<NetClock::duration> const& frequency)
{
json::Value jv;
jv[jss::TransactionType] = jss::SubscriptionSet;
jv[jss::Account] = to_string(account.id());
jv[jss::SubscriptionID] = to_string(subscriptionId);
jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
jv[jss::Flags] = tfFullyCanonicalSig;
if (expiration)
jv[sfExpiration.jsonName] = expiration->time_since_epoch().count();
if (frequency)
jv[jss::Frequency] = frequency->count();
return jv;
}
json::Value
cancel(jtx::Account const& account, uint256 const& subscriptionId)
{
json::Value jv;
jv[jss::TransactionType] = jss::SubscriptionCancel;
jv[jss::Account] = to_string(account.id());
jv[jss::SubscriptionID] = to_string(subscriptionId);
jv[jss::Flags] = tfFullyCanonicalSig;
return jv;
}
json::Value
claim(jtx::Account const& account, uint256 const& subscriptionId, STAmount const& amount)
{
json::Value jv;
jv[jss::TransactionType] = jss::SubscriptionClaim;
jv[jss::Account] = to_string(account.id());
jv[jss::SubscriptionID] = to_string(subscriptionId);
jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
jv[jss::Flags] = tfFullyCanonicalSig;
return jv;
}
} // namespace xrpl::test::jtx::subscription

View File

@@ -0,0 +1,66 @@
#pragma once
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/JTx.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/TxFlags.h>
#include <optional>
namespace xrpl::test::jtx {
/** Subscription operations. */
namespace subscription {
/** Create a subscription. Pass a frequency of zero for an unmetered
subscription and set tfSingleUse in flags for a one-shot subscription. */
json::Value
create(
jtx::Account const& account,
jtx::Account const& destination,
STAmount const& amount,
NetClock::duration const& frequency,
std::optional<NetClock::time_point> const& expiration = std::nullopt,
std::uint32_t flags = tfFullyCanonicalSig);
/** Update a subscription. An engaged expiration of zero removes any existing
expiration; an engaged frequency changes it and resets the period. */
json::Value
update(
jtx::Account const& account,
uint256 const& subscriptionId,
STAmount const& amount,
std::optional<NetClock::time_point> const& expiration = std::nullopt,
std::optional<NetClock::duration> const& frequency = std::nullopt);
/** Cancel a subscription. */
json::Value
cancel(jtx::Account const& account, uint256 const& subscriptionId);
/** Claim a subscription payment. */
json::Value
claim(jtx::Account const& account, uint256 const& subscriptionId, STAmount const& amount);
/** Set the "StartTime" time tag on a JTx. */
class StartTime
{
private:
NetClock::time_point value_;
public:
explicit StartTime(NetClock::time_point const& value) : value_(value)
{
}
void
operator()(Env&, JTx& jt) const;
};
} // namespace subscription
} // namespace xrpl::test::jtx

View File

@@ -0,0 +1,412 @@
// Auto-generated unit tests for ledger entry Subscription
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol_autogen/ledger_entries/Subscription.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(SubscriptionTests, BuilderSettersRoundTrip)
{
uint256 const index{1u};
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto const sequenceValue = canonical_UINT32();
auto const ownerNodeValue = canonical_UINT64();
auto const accountValue = canonical_ACCOUNT();
auto const destinationValue = canonical_ACCOUNT();
auto const destinationTagValue = canonical_UINT32();
auto const amountValue = canonical_AMOUNT();
auto const balanceValue = canonical_AMOUNT();
auto const frequencyValue = canonical_UINT32();
auto const nextClaimTimeValue = canonical_UINT32();
auto const expirationValue = canonical_UINT32();
auto const destinationNodeValue = canonical_UINT64();
SubscriptionBuilder builder{
previousTxnIDValue,
previousTxnLgrSeqValue,
sequenceValue,
ownerNodeValue,
accountValue,
destinationValue,
amountValue,
balanceValue,
frequencyValue,
nextClaimTimeValue,
destinationNodeValue
};
builder.setDestinationTag(destinationTagValue);
builder.setExpiration(expirationValue);
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 = sequenceValue;
auto const actual = entry.getSequence();
expectEqualField(expected, actual, "sfSequence");
}
{
auto const& expected = ownerNodeValue;
auto const actual = entry.getOwnerNode();
expectEqualField(expected, actual, "sfOwnerNode");
}
{
auto const& expected = accountValue;
auto const actual = entry.getAccount();
expectEqualField(expected, actual, "sfAccount");
}
{
auto const& expected = destinationValue;
auto const actual = entry.getDestination();
expectEqualField(expected, actual, "sfDestination");
}
{
auto const& expected = amountValue;
auto const actual = entry.getAmount();
expectEqualField(expected, actual, "sfAmount");
}
{
auto const& expected = balanceValue;
auto const actual = entry.getBalance();
expectEqualField(expected, actual, "sfBalance");
}
{
auto const& expected = frequencyValue;
auto const actual = entry.getFrequency();
expectEqualField(expected, actual, "sfFrequency");
}
{
auto const& expected = nextClaimTimeValue;
auto const actual = entry.getNextClaimTime();
expectEqualField(expected, actual, "sfNextClaimTime");
}
{
auto const& expected = destinationNodeValue;
auto const actual = entry.getDestinationNode();
expectEqualField(expected, actual, "sfDestinationNode");
}
{
auto const& expected = destinationTagValue;
auto const actualOpt = entry.getDestinationTag();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfDestinationTag");
EXPECT_TRUE(entry.hasDestinationTag());
}
{
auto const& expected = expirationValue;
auto const actualOpt = entry.getExpiration();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfExpiration");
EXPECT_TRUE(entry.hasExpiration());
}
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(SubscriptionTests, BuilderFromSleRoundTrip)
{
uint256 const index{2u};
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto const sequenceValue = canonical_UINT32();
auto const ownerNodeValue = canonical_UINT64();
auto const accountValue = canonical_ACCOUNT();
auto const destinationValue = canonical_ACCOUNT();
auto const destinationTagValue = canonical_UINT32();
auto const amountValue = canonical_AMOUNT();
auto const balanceValue = canonical_AMOUNT();
auto const frequencyValue = canonical_UINT32();
auto const nextClaimTimeValue = canonical_UINT32();
auto const expirationValue = canonical_UINT32();
auto const destinationNodeValue = canonical_UINT64();
auto sle = std::make_shared<SLE>(Subscription::entryType, index);
sle->at(sfPreviousTxnID) = previousTxnIDValue;
sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue;
sle->at(sfSequence) = sequenceValue;
sle->at(sfOwnerNode) = ownerNodeValue;
sle->at(sfAccount) = accountValue;
sle->at(sfDestination) = destinationValue;
sle->at(sfDestinationTag) = destinationTagValue;
sle->at(sfAmount) = amountValue;
sle->at(sfBalance) = balanceValue;
sle->at(sfFrequency) = frequencyValue;
sle->at(sfNextClaimTime) = nextClaimTimeValue;
sle->at(sfExpiration) = expirationValue;
sle->at(sfDestinationNode) = destinationNodeValue;
SubscriptionBuilder builderFromSle{sle};
EXPECT_TRUE(builderFromSle.validate());
auto const entryFromBuilder = builderFromSle.build(index);
Subscription 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 = sequenceValue;
auto const fromSle = entryFromSle.getSequence();
auto const fromBuilder = entryFromBuilder.getSequence();
expectEqualField(expected, fromSle, "sfSequence");
expectEqualField(expected, fromBuilder, "sfSequence");
}
{
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 = accountValue;
auto const fromSle = entryFromSle.getAccount();
auto const fromBuilder = entryFromBuilder.getAccount();
expectEqualField(expected, fromSle, "sfAccount");
expectEqualField(expected, fromBuilder, "sfAccount");
}
{
auto const& expected = destinationValue;
auto const fromSle = entryFromSle.getDestination();
auto const fromBuilder = entryFromBuilder.getDestination();
expectEqualField(expected, fromSle, "sfDestination");
expectEqualField(expected, fromBuilder, "sfDestination");
}
{
auto const& expected = amountValue;
auto const fromSle = entryFromSle.getAmount();
auto const fromBuilder = entryFromBuilder.getAmount();
expectEqualField(expected, fromSle, "sfAmount");
expectEqualField(expected, fromBuilder, "sfAmount");
}
{
auto const& expected = balanceValue;
auto const fromSle = entryFromSle.getBalance();
auto const fromBuilder = entryFromBuilder.getBalance();
expectEqualField(expected, fromSle, "sfBalance");
expectEqualField(expected, fromBuilder, "sfBalance");
}
{
auto const& expected = frequencyValue;
auto const fromSle = entryFromSle.getFrequency();
auto const fromBuilder = entryFromBuilder.getFrequency();
expectEqualField(expected, fromSle, "sfFrequency");
expectEqualField(expected, fromBuilder, "sfFrequency");
}
{
auto const& expected = nextClaimTimeValue;
auto const fromSle = entryFromSle.getNextClaimTime();
auto const fromBuilder = entryFromBuilder.getNextClaimTime();
expectEqualField(expected, fromSle, "sfNextClaimTime");
expectEqualField(expected, fromBuilder, "sfNextClaimTime");
}
{
auto const& expected = destinationNodeValue;
auto const fromSle = entryFromSle.getDestinationNode();
auto const fromBuilder = entryFromBuilder.getDestinationNode();
expectEqualField(expected, fromSle, "sfDestinationNode");
expectEqualField(expected, fromBuilder, "sfDestinationNode");
}
{
auto const& expected = destinationTagValue;
auto const fromSleOpt = entryFromSle.getDestinationTag();
auto const fromBuilderOpt = entryFromBuilder.getDestinationTag();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfDestinationTag");
expectEqualField(expected, *fromBuilderOpt, "sfDestinationTag");
}
{
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");
}
EXPECT_EQ(entryFromSle.getKey(), index);
EXPECT_EQ(entryFromBuilder.getKey(), index);
}
// 3) Verify wrapper throws when constructed from wrong ledger entry type.
TEST(SubscriptionTests, 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(Subscription{wrongEntry.getSle()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong ledger entry type.
TEST(SubscriptionTests, 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(SubscriptionBuilder{wrongEntry.getSle()}, std::runtime_error);
}
// 5) Build with only required fields and verify optional fields return nullopt.
TEST(SubscriptionTests, OptionalFieldsReturnNullopt)
{
uint256 const index{3u};
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto const sequenceValue = canonical_UINT32();
auto const ownerNodeValue = canonical_UINT64();
auto const accountValue = canonical_ACCOUNT();
auto const destinationValue = canonical_ACCOUNT();
auto const amountValue = canonical_AMOUNT();
auto const balanceValue = canonical_AMOUNT();
auto const frequencyValue = canonical_UINT32();
auto const nextClaimTimeValue = canonical_UINT32();
auto const destinationNodeValue = canonical_UINT64();
SubscriptionBuilder builder{
previousTxnIDValue,
previousTxnLgrSeqValue,
sequenceValue,
ownerNodeValue,
accountValue,
destinationValue,
amountValue,
balanceValue,
frequencyValue,
nextClaimTimeValue,
destinationNodeValue
};
auto const entry = builder.build(index);
// Verify optional fields are not present
EXPECT_FALSE(entry.hasDestinationTag());
EXPECT_FALSE(entry.getDestinationTag().has_value());
EXPECT_FALSE(entry.hasExpiration());
EXPECT_FALSE(entry.getExpiration().has_value());
}
}

View File

@@ -0,0 +1,146 @@
// Auto-generated unit tests for transaction SubscriptionCancel
#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/SubscriptionCancel.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(TransactionsSubscriptionCancelTests, BuilderSettersRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSubscriptionCancel"));
// 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 subscriptionIDValue = canonical_UINT256();
SubscriptionCancelBuilder builder{
accountValue,
subscriptionIDValue,
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 = subscriptionIDValue;
auto const actual = tx.getSubscriptionID();
expectEqualField(expected, actual, "sfSubscriptionID");
}
// Verify optional fields
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
// and verify all fields match.
TEST(TransactionsSubscriptionCancelTests, BuilderFromStTxRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSubscriptionCancelFromTx"));
// 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 subscriptionIDValue = canonical_UINT256();
// Build an initial transaction
SubscriptionCancelBuilder initialBuilder{
accountValue,
subscriptionIDValue,
sequenceValue,
feeValue
};
auto initialTx = initialBuilder.build(publicKey, secretKey);
// Create builder from existing STTx
SubscriptionCancelBuilder 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 = subscriptionIDValue;
auto const actual = rebuiltTx.getSubscriptionID();
expectEqualField(expected, actual, "sfSubscriptionID");
}
// Verify optional fields
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
TEST(TransactionsSubscriptionCancelTests, 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(SubscriptionCancel{wrongTx.getSTTx()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong transaction type.
TEST(TransactionsSubscriptionCancelTests, 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(SubscriptionCancelBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
}

View File

@@ -0,0 +1,162 @@
// Auto-generated unit tests for transaction SubscriptionClaim
#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/SubscriptionClaim.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(TransactionsSubscriptionClaimTests, BuilderSettersRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSubscriptionClaim"));
// 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 amountValue = canonical_AMOUNT();
auto const subscriptionIDValue = canonical_UINT256();
SubscriptionClaimBuilder builder{
accountValue,
amountValue,
subscriptionIDValue,
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 = amountValue;
auto const actual = tx.getAmount();
expectEqualField(expected, actual, "sfAmount");
}
{
auto const& expected = subscriptionIDValue;
auto const actual = tx.getSubscriptionID();
expectEqualField(expected, actual, "sfSubscriptionID");
}
// Verify optional fields
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
// and verify all fields match.
TEST(TransactionsSubscriptionClaimTests, BuilderFromStTxRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSubscriptionClaimFromTx"));
// 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 amountValue = canonical_AMOUNT();
auto const subscriptionIDValue = canonical_UINT256();
// Build an initial transaction
SubscriptionClaimBuilder initialBuilder{
accountValue,
amountValue,
subscriptionIDValue,
sequenceValue,
feeValue
};
auto initialTx = initialBuilder.build(publicKey, secretKey);
// Create builder from existing STTx
SubscriptionClaimBuilder 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 = amountValue;
auto const actual = rebuiltTx.getAmount();
expectEqualField(expected, actual, "sfAmount");
}
{
auto const& expected = subscriptionIDValue;
auto const actual = rebuiltTx.getSubscriptionID();
expectEqualField(expected, actual, "sfSubscriptionID");
}
// Verify optional fields
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
TEST(TransactionsSubscriptionClaimTests, 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(SubscriptionClaim{wrongTx.getSTTx()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong transaction type.
TEST(TransactionsSubscriptionClaimTests, 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(SubscriptionClaimBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
}

View File

@@ -0,0 +1,300 @@
// Auto-generated unit tests for transaction SubscriptionSet
#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/SubscriptionSet.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(TransactionsSubscriptionSetTests, BuilderSettersRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSubscriptionSet"));
// 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 destinationValue = canonical_ACCOUNT();
auto const amountValue = canonical_AMOUNT();
auto const frequencyValue = canonical_UINT32();
auto const startTimeValue = canonical_UINT32();
auto const expirationValue = canonical_UINT32();
auto const destinationTagValue = canonical_UINT32();
auto const subscriptionIDValue = canonical_UINT256();
SubscriptionSetBuilder builder{
accountValue,
amountValue,
sequenceValue,
feeValue
};
// Set optional fields
builder.setDestination(destinationValue);
builder.setFrequency(frequencyValue);
builder.setStartTime(startTimeValue);
builder.setExpiration(expirationValue);
builder.setDestinationTag(destinationTagValue);
builder.setSubscriptionID(subscriptionIDValue);
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 = amountValue;
auto const actual = tx.getAmount();
expectEqualField(expected, actual, "sfAmount");
}
// Verify optional fields
{
auto const& expected = destinationValue;
auto const actualOpt = tx.getDestination();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestination should be present";
expectEqualField(expected, *actualOpt, "sfDestination");
EXPECT_TRUE(tx.hasDestination());
}
{
auto const& expected = frequencyValue;
auto const actualOpt = tx.getFrequency();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFrequency should be present";
expectEqualField(expected, *actualOpt, "sfFrequency");
EXPECT_TRUE(tx.hasFrequency());
}
{
auto const& expected = startTimeValue;
auto const actualOpt = tx.getStartTime();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfStartTime should be present";
expectEqualField(expected, *actualOpt, "sfStartTime");
EXPECT_TRUE(tx.hasStartTime());
}
{
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 = destinationTagValue;
auto const actualOpt = tx.getDestinationTag();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
expectEqualField(expected, *actualOpt, "sfDestinationTag");
EXPECT_TRUE(tx.hasDestinationTag());
}
{
auto const& expected = subscriptionIDValue;
auto const actualOpt = tx.getSubscriptionID();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSubscriptionID should be present";
expectEqualField(expected, *actualOpt, "sfSubscriptionID");
EXPECT_TRUE(tx.hasSubscriptionID());
}
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
// and verify all fields match.
TEST(TransactionsSubscriptionSetTests, BuilderFromStTxRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSubscriptionSetFromTx"));
// 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 destinationValue = canonical_ACCOUNT();
auto const amountValue = canonical_AMOUNT();
auto const frequencyValue = canonical_UINT32();
auto const startTimeValue = canonical_UINT32();
auto const expirationValue = canonical_UINT32();
auto const destinationTagValue = canonical_UINT32();
auto const subscriptionIDValue = canonical_UINT256();
// Build an initial transaction
SubscriptionSetBuilder initialBuilder{
accountValue,
amountValue,
sequenceValue,
feeValue
};
initialBuilder.setDestination(destinationValue);
initialBuilder.setFrequency(frequencyValue);
initialBuilder.setStartTime(startTimeValue);
initialBuilder.setExpiration(expirationValue);
initialBuilder.setDestinationTag(destinationTagValue);
initialBuilder.setSubscriptionID(subscriptionIDValue);
auto initialTx = initialBuilder.build(publicKey, secretKey);
// Create builder from existing STTx
SubscriptionSetBuilder 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 = amountValue;
auto const actual = rebuiltTx.getAmount();
expectEqualField(expected, actual, "sfAmount");
}
// Verify optional fields
{
auto const& expected = destinationValue;
auto const actualOpt = rebuiltTx.getDestination();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestination should be present";
expectEqualField(expected, *actualOpt, "sfDestination");
}
{
auto const& expected = frequencyValue;
auto const actualOpt = rebuiltTx.getFrequency();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFrequency should be present";
expectEqualField(expected, *actualOpt, "sfFrequency");
}
{
auto const& expected = startTimeValue;
auto const actualOpt = rebuiltTx.getStartTime();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfStartTime should be present";
expectEqualField(expected, *actualOpt, "sfStartTime");
}
{
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 = destinationTagValue;
auto const actualOpt = rebuiltTx.getDestinationTag();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
expectEqualField(expected, *actualOpt, "sfDestinationTag");
}
{
auto const& expected = subscriptionIDValue;
auto const actualOpt = rebuiltTx.getSubscriptionID();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSubscriptionID should be present";
expectEqualField(expected, *actualOpt, "sfSubscriptionID");
}
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
TEST(TransactionsSubscriptionSetTests, 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(SubscriptionSet{wrongTx.getSTTx()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong transaction type.
TEST(TransactionsSubscriptionSetTests, 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(SubscriptionSetBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
// 5) Build with only required fields and verify optional fields return nullopt.
TEST(TransactionsSubscriptionSetTests, OptionalFieldsReturnNullopt)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSubscriptionSetNullopt"));
// 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 amountValue = canonical_AMOUNT();
SubscriptionSetBuilder builder{
accountValue,
amountValue,
sequenceValue,
feeValue
};
// Do NOT set optional fields
auto tx = builder.build(publicKey, secretKey);
// Verify optional fields are not present
EXPECT_FALSE(tx.hasDestination());
EXPECT_FALSE(tx.getDestination().has_value());
EXPECT_FALSE(tx.hasFrequency());
EXPECT_FALSE(tx.getFrequency().has_value());
EXPECT_FALSE(tx.hasStartTime());
EXPECT_FALSE(tx.getStartTime().has_value());
EXPECT_FALSE(tx.hasExpiration());
EXPECT_FALSE(tx.getExpiration().has_value());
EXPECT_FALSE(tx.hasDestinationTag());
EXPECT_FALSE(tx.getDestinationTag().has_value());
EXPECT_FALSE(tx.hasSubscriptionID());
EXPECT_FALSE(tx.getSubscriptionID().has_value());
}
}

View File

@@ -907,6 +907,32 @@ parseSponsorship(
return keylet::sponsorship(*sponsorID, *sponseeID).key;
}
static std::expected<uint256, json::Value>
parseSubscription(
json::Value const& params,
json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (!params.isObject())
return parseObjectID(params, fieldName);
auto const account =
ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAccount");
if (!account)
return std::unexpected(account.error());
auto const destination =
ledger_entry_helpers::requiredAccountID(params, jss::destination, "malformedDestination");
if (!destination)
return std::unexpected(destination.error());
auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest");
if (!seq)
return std::unexpected(seq.error());
return keylet::subscription(*account, *destination, *seq).key;
}
static std::expected<uint256, json::Value>
parseTicket(
json::Value const& params,