mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-24 06:00:17 +00:00
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:
@@ -356,6 +356,7 @@ words:
|
||||
- unfund
|
||||
- ungated
|
||||
- unimpair
|
||||
- unmetered
|
||||
- unroutable
|
||||
- unscalable
|
||||
- unserviced
|
||||
|
||||
429
include/xrpl/ledger/helpers/SubscriptionHelpers.h
Normal file
429
include/xrpl/ledger/helpers/SubscriptionHelpers.h
Normal 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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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},
|
||||
}))
|
||||
|
||||
@@ -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
|
||||
|
||||
431
include/xrpl/protocol_autogen/ledger_entries/Subscription.h
Normal file
431
include/xrpl/protocol_autogen/ledger_entries/Subscription.h
Normal 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
|
||||
131
include/xrpl/protocol_autogen/transactions/SubscriptionCancel.h
Normal file
131
include/xrpl/protocol_autogen/transactions/SubscriptionCancel.h
Normal 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
|
||||
157
include/xrpl/protocol_autogen/transactions/SubscriptionClaim.h
Normal file
157
include/xrpl/protocol_autogen/transactions/SubscriptionClaim.h
Normal 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
|
||||
355
include/xrpl/protocol_autogen/transactions/SubscriptionSet.h
Normal file
355
include/xrpl/protocol_autogen/transactions/SubscriptionSet.h
Normal 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
|
||||
@@ -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,
|
||||
|
||||
35
include/xrpl/tx/invariants/SubscriptionInvariant.h
Normal file
35
include/xrpl/tx/invariants/SubscriptionInvariant.h
Normal 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
|
||||
@@ -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
|
||||
44
include/xrpl/tx/transactors/subscription/SubscriptionClaim.h
Normal file
44
include/xrpl/tx/transactors/subscription/SubscriptionClaim.h
Normal 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
|
||||
47
include/xrpl/tx/transactors/subscription/SubscriptionSet.h
Normal file
47
include/xrpl/tx/transactors/subscription/SubscriptionSet.h
Normal 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
|
||||
@@ -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
|
||||
|
||||
66
src/libxrpl/tx/invariants/SubscriptionInvariant.cpp
Normal file
66
src/libxrpl/tx/invariants/SubscriptionInvariant.cpp
Normal 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
|
||||
@@ -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
|
||||
303
src/libxrpl/tx/transactors/subscription/SubscriptionClaim.cpp
Normal file
303
src/libxrpl/tx/transactors/subscription/SubscriptionClaim.cpp
Normal 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
|
||||
358
src/libxrpl/tx/transactors/subscription/SubscriptionSet.cpp
Normal file
358
src/libxrpl/tx/transactors/subscription/SubscriptionSet.cpp
Normal 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
|
||||
4777
src/test/app/Subscription_test.cpp
Normal file
4777
src/test/app/Subscription_test.cpp
Normal file
File diff suppressed because it is too large
Load Diff
88
src/test/jtx/impl/subscription.cpp
Normal file
88
src/test/jtx/impl/subscription.cpp
Normal 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
|
||||
66
src/test/jtx/subscription.h
Normal file
66
src/test/jtx/subscription.h
Normal 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
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user