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

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

View File

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

View File

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

View File

@@ -0,0 +1,91 @@
#include <xrpl/tx/transactors/subscription/SubscriptionCancel.h>
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/SubscriptionHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/Transactor.h>
#include <cstdint>
namespace xrpl {
NotTEC
SubscriptionCancel::preflight(PreflightContext const& ctx)
{
return tesSUCCESS;
}
TER
SubscriptionCancel::preclaim(PreclaimContext const& ctx)
{
auto const sleSub = ctx.view.read(keylet::subscription(ctx.tx.getFieldH256(sfSubscriptionID)));
if (!sleSub)
{
JLOG(ctx.j.debug()) << "SubscriptionCancel: Subscription does not exist.";
return tecNO_ENTRY;
}
// The owner or the destination may cancel at any time; anyone may cancel
// once the subscription has expired.
if (!hasExpired(ctx.view, (*sleSub)[~sfExpiration]))
{
AccountID const account = ctx.tx.getAccountID(sfAccount);
if (account != sleSub->getAccountID(sfAccount) &&
account != sleSub->getAccountID(sfDestination))
{
JLOG(ctx.j.debug()) << "SubscriptionCancel: Account is not the owner "
"or destination of the subscription.";
return tecNO_PERMISSION;
}
}
return tesSUCCESS;
}
TER
SubscriptionCancel::doApply()
{
Sandbox sb(&ctx_.view());
auto const sleSub = sb.peek(keylet::subscription(ctx_.tx.getFieldH256(sfSubscriptionID)));
if (!sleSub)
{
JLOG(ctx_.journal.debug()) << "SubscriptionCancel: Subscription does not exist.";
return tecINTERNAL;
}
auto viewJ = ctx_.registry.get().getJournal("View");
if (auto const ter = deleteSubscription(sb, sleSub, viewJ); !isTesSuccess(ter))
return ter;
sb.apply(ctx_.rawView());
return tesSUCCESS;
}
void
SubscriptionCancel::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
{
// No transaction-specific invariants yet (future work).
}
bool
SubscriptionCancel::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
{
// No transaction-specific invariants yet (future work).
return true;
}
} // namespace xrpl

View File

@@ -0,0 +1,303 @@
#include <xrpl/tx/transactors/subscription/SubscriptionClaim.h>
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/SubscriptionHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/Transactor.h>
#include <cstdint>
#include <variant>
namespace xrpl {
NotTEC
SubscriptionClaim::preflight(PreflightContext const& ctx)
{
return tesSUCCESS;
}
TER
SubscriptionClaim::preclaim(PreclaimContext const& ctx)
{
auto const sleSub = ctx.view.read(keylet::subscription(ctx.tx.getFieldH256(sfSubscriptionID)));
if (!sleSub)
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: Subscription does not exist.";
return tecNO_ENTRY;
}
// Only claim a subscription with this account as the destination.
AccountID const dest = sleSub->getAccountID(sfDestination);
if (ctx.tx[sfAccount] != dest)
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: Cashing a subscription with "
"wrong Destination.";
return tecNO_PERMISSION;
}
AccountID const account = sleSub->getAccountID(sfAccount);
if (account == dest)
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: Malformed transaction: "
"Cashing subscription to self.";
return tecINTERNAL;
}
{
auto const sleSrc = ctx.view.read(keylet::account(account));
auto const sleDst = ctx.view.read(keylet::account(dest));
if (!sleSrc || !sleDst)
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: source or destination not in ledger";
return tecNO_ENTRY;
}
}
{
STAmount const amount = ctx.tx.getFieldAmount(sfAmount);
STAmount const sleAmount = sleSub->getFieldAmount(sfAmount);
if (amount.asset() != sleAmount.asset())
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: Subscription claim does "
"not match subscription currency.";
return tecWRONG_ASSET;
}
if (amount > sleAmount)
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: Claim amount exceeds "
"subscription amount.";
return tecLIMIT_EXCEEDED;
}
// Time/period context
std::uint32_t const currentTime =
ctx.view.header().parentCloseTime.time_since_epoch().count();
std::uint32_t const nextClaimTime = sleSub->getFieldU32(sfNextClaimTime);
std::uint32_t const frequency = sleSub->getFieldU32(sfFrequency);
// Determine effective available balance:
// - If we have crossed into a later period AND the previous period had
// a partial
// balance remaining (carryover not allowed), then the effective
// period rolls forward once and its balance resets to sleAmount.
// - Otherwise we operate on the period at nextClaimTime with its stored
// balance.
STAmount balance = sleSub->getFieldAmount(sfBalance);
bool const arrears = currentTime >= nextClaimTime + frequency;
if (arrears && balance != sleAmount)
{
// We will effectively operate on (nextClaimTime + frequency) with a
// full balance.
balance = sleAmount;
}
if (amount > balance)
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: Claim amount exceeds remaining "
"balance for this period.";
return tecINSUFFICIENT_FUNDS;
}
if (isXRP(amount))
{
if (xrpLiquid(ctx.view, account, 0, ctx.j) < amount)
return tecINSUFFICIENT_FUNDS;
}
else
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return canTransferTokenHelper<T>(ctx.view, account, dest, amount, ctx.j);
},
amount.asset().value());
!isTesSuccess(ret))
return ret;
}
}
// An expired subscription can no longer be claimed; it can only be
// cancelled.
if (hasExpired(ctx.view, (*sleSub)[~sfExpiration]))
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: The subscription has expired.";
return tecEXPIRED;
}
// Must be at or past the start of the effective period.
if (!hasExpired(ctx.view, sleSub->getFieldU32(sfNextClaimTime)))
{
JLOG(ctx.j.trace()) << "SubscriptionClaim: The subscription has not "
"reached the next claim time.";
return tecTOO_SOON;
}
return tesSUCCESS;
}
TER
SubscriptionClaim::doApply()
{
PaymentSandbox psb(&ctx_.view());
auto viewJ = ctx_.registry.get().getJournal("View");
auto sleSub = psb.peek(keylet::subscription(ctx_.tx.getFieldH256(sfSubscriptionID)));
if (!sleSub)
{
JLOG(j_.trace()) << "SubscriptionClaim: Subscription does not exist.";
return tecINTERNAL;
}
AccountID const account = sleSub->getAccountID(sfAccount);
if (!psb.exists(keylet::account(account)))
{
JLOG(j_.trace()) << "SubscriptionClaim: Account does not exist.";
return tecINTERNAL;
}
AccountID const dest = sleSub->getAccountID(sfDestination);
if (!psb.exists(keylet::account(dest)))
{
JLOG(j_.trace()) << "SubscriptionClaim: Account does not exist.";
return tecINTERNAL;
}
if (dest != ctx_.tx.getAccountID(sfAccount))
{
JLOG(j_.trace()) << "SubscriptionClaim: Account is not the "
"destination of the subscription.";
return tecNO_PERMISSION;
}
STAmount const sleAmount = sleSub->getFieldAmount(sfAmount);
STAmount const deliverAmount = ctx_.tx.getFieldAmount(sfAmount);
// Pull current period info
std::uint32_t const currentTime = psb.header().parentCloseTime.time_since_epoch().count();
std::uint32_t nextClaimTime = sleSub->getFieldU32(sfNextClaimTime);
std::uint32_t const frequency = sleSub->getFieldU32(sfFrequency);
STAmount availableBalance = sleSub->getFieldAmount(sfBalance);
bool const arrears = currentTime >= nextClaimTime + frequency;
// If we crossed into a later period and the previous period was partially
// used, forfeit the leftover and roll forward exactly one period; reset the
// balance.
if (arrears && availableBalance != sleAmount)
{
nextClaimTime += frequency;
availableBalance = sleAmount;
// Reflect the rollover immediately in the SLE so subsequent logic is
// consistent.
sleSub->setFieldU32(sfNextClaimTime, nextClaimTime);
sleSub->setFieldAmount(sfBalance, availableBalance);
}
// Enforce available balance for the effective period.
if (deliverAmount > availableBalance)
{
JLOG(j_.trace()) << "SubscriptionClaim: Claim amount exceeds remaining "
<< "balance for this period.";
return tecINTERNAL;
}
// Perform the transfer
if (isXRP(deliverAmount))
{
if (TER const ter{transferXRP(psb, account, dest, deliverAmount, viewJ)}; ter != tesSUCCESS)
{
return ter;
}
}
else
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return doTransferTokenHelper<T>(
psb,
psb.peek(keylet::account(dest)),
preFeeBalance_,
deliverAmount,
deliverAmount.getIssuer(),
account,
dest,
true, // create asset
viewJ);
},
deliverAmount.asset().value());
!isTesSuccess(ret))
return ret;
}
// Metered accounting: advance/reset the period. Unmetered subscriptions
// (Frequency == 0) cap each claim at Amount and never touch Balance or
// NextClaimTime.
if (frequency != 0)
{
STAmount const newBalance = availableBalance - deliverAmount;
if (newBalance == sleAmount.zeroed())
{
// Full period claimed: advance exactly one period and reset next
// period balance.
nextClaimTime += frequency;
sleSub->setFieldU32(sfNextClaimTime, nextClaimTime);
sleSub->setFieldAmount(sfBalance, sleAmount);
}
else
{
// Partial claim within the same effective period.
sleSub->setFieldAmount(sfBalance, newBalance);
// Do not advance nextClaimTime; if we had a rollover-forfeit above,
// we already moved nextClaimTime forward exactly once.
}
}
// Single-use subscriptions are removed on the first successful claim,
// regardless of Frequency or whether the claim was partial.
if (sleSub->isFlag(lsfSingleUse))
{
if (auto const ter = deleteSubscription(psb, sleSub, viewJ); !isTesSuccess(ter))
return ter;
}
else
{
psb.update(sleSub);
}
psb.apply(ctx_.rawView());
return tesSUCCESS;
}
void
SubscriptionClaim::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
{
// No transaction-specific invariants yet (future work).
}
bool
SubscriptionClaim::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
{
// No transaction-specific invariants yet (future work).
return true;
}
} // namespace xrpl

View File

@@ -0,0 +1,358 @@
#include <xrpl/tx/transactors/subscription/SubscriptionSet.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/SubscriptionHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/tx/Transactor.h>
#include <cstdint>
#include <memory>
#include <variant>
namespace xrpl {
template <ValidIssueType T>
static NotTEC
setPreflightHelper(PreflightContext const& ctx);
template <>
NotTEC
setPreflightHelper<Issue>(PreflightContext const& ctx)
{
STAmount const amount = ctx.tx[sfAmount];
if (amount.native() || amount <= beast::kZero)
return temBAD_AMOUNT;
if (badCurrency() == amount.get<Issue>().currency)
return temBAD_CURRENCY;
return tesSUCCESS;
}
template <>
NotTEC
setPreflightHelper<MPTIssue>(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureMPTokensV1))
return temDISABLED;
auto const amount = ctx.tx[sfAmount];
if (amount.native() || amount.mpt() > MPTAmount{kMaxMpTokenAmount} || amount <= beast::kZero)
return temBAD_AMOUNT;
return tesSUCCESS;
}
std::uint32_t
SubscriptionSet::getFlagsMask(PreflightContext const& ctx)
{
return tfSubscriptionSetMask;
}
NotTEC
SubscriptionSet::preflight(PreflightContext const& ctx)
{
if (ctx.tx.isFieldPresent(sfSubscriptionID))
{
// update
if (!ctx.tx.isFieldPresent(sfAmount))
{
JLOG(ctx.j.trace()) << "SubscriptionSet: Malformed transaction: SubscriptionID "
"is present, but Amount is not.";
return temMALFORMED;
}
if (ctx.tx.isFieldPresent(sfDestination) || ctx.tx.isFieldPresent(sfStartTime))
{
JLOG(ctx.j.trace()) << "SubscriptionSet: Malformed transaction: SubscriptionID "
"is present, but immutable fields are also present.";
return temMALFORMED;
}
// lsfSingleUse is fixed at creation and cannot be changed on update.
if (ctx.tx.getFlags() & tfSingleUse)
{
JLOG(ctx.j.trace()) << "SubscriptionSet: tfSingleUse cannot be set on update.";
return temINVALID_FLAG;
}
}
else
{
// create
if (!ctx.tx.isFieldPresent(sfDestination) || !ctx.tx.isFieldPresent(sfAmount) ||
!ctx.tx.isFieldPresent(sfFrequency))
{
JLOG(ctx.j.trace()) << "SubscriptionSet: Malformed transaction: SubscriptionID "
"is not present, and required fields are not present.";
return temMALFORMED;
}
if (ctx.tx.getAccountID(sfDestination) == ctx.tx.getAccountID(sfAccount))
{
JLOG(ctx.j.trace()) << "SubscriptionSet: Malformed transaction: Account "
"is the same as the destination.";
return temDST_IS_SRC;
}
}
STAmount const amount = ctx.tx.getFieldAmount(sfAmount);
if (amount.native())
{
if (!isLegalNet(amount) || amount <= beast::kZero)
{
JLOG(ctx.j.trace()) << "SubscriptionSet: Malformed transaction: bad amount: "
<< amount.getFullText();
return temBAD_AMOUNT;
}
}
else
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) { return setPreflightHelper<T>(ctx); },
amount.asset().value());
!isTesSuccess(ret))
return ret;
}
return tesSUCCESS;
}
TER
SubscriptionSet::preclaim(PreclaimContext const& ctx)
{
STAmount const amount = ctx.tx.getFieldAmount(sfAmount);
AccountID const account = ctx.tx.getAccountID(sfAccount);
AccountID dest = ctx.tx.getAccountID(sfDestination);
if (ctx.tx.isFieldPresent(sfSubscriptionID))
{
// update
auto sle = ctx.view.read(keylet::subscription(ctx.tx.getFieldH256(sfSubscriptionID)));
if (!sle)
{
JLOG(ctx.j.trace()) << "SubscriptionSet: Subscription does not exist.";
return tecNO_ENTRY;
}
if (sle->getAccountID(sfAccount) != ctx.tx.getAccountID(sfAccount))
{
JLOG(ctx.j.trace()) << "SubscriptionSet: Account is not the "
"owner of the subscription.";
return tecNO_PERMISSION;
}
if (amount.asset() != sle->getFieldAmount(sfAmount).asset())
{
JLOG(ctx.j.trace()) << "SubscriptionSet: Amount asset does not "
"match the subscription asset.";
return tecWRONG_ASSET;
}
dest = sle->getAccountID(sfDestination);
}
else
{
// create
auto const sleDest = ctx.view.read(keylet::account(ctx.tx.getAccountID(sfDestination)));
if (!sleDest)
{
JLOG(ctx.j.trace()) << "SubscriptionSet: Destination account does not exist.";
return tecNO_DST;
}
auto const flags = sleDest->getFlags();
if ((flags & lsfRequireDestTag) && !ctx.tx[~sfDestinationTag])
return tecDST_TAG_NEEDED;
// Frequency == 0 denotes an unmetered subscription: no period
// accounting, each claim capped at Amount.
}
if (!isXRP(amount))
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return canTransferTokenHelper<T>(ctx.view, account, dest, amount, ctx.j);
},
amount.asset().value());
!isTesSuccess(ret))
return ret;
}
return tesSUCCESS;
}
TER
SubscriptionSet::doApply()
{
Sandbox sb(&ctx_.view());
AccountID const account = ctx_.tx.getAccountID(sfAccount);
auto const sleAccount = sb.peek(keylet::account(account));
if (!sleAccount)
{
JLOG(ctx_.journal.trace()) << "SubscriptionSet: Account does not exist.";
return tecINTERNAL;
}
if (ctx_.tx.isFieldPresent(sfSubscriptionID))
{
// update
auto const currentTime = sb.header().parentCloseTime.time_since_epoch().count();
auto sle = sb.peek(keylet::subscription(ctx_.tx.getFieldH256(sfSubscriptionID)));
sle->setFieldAmount(sfAmount, ctx_.tx.getFieldAmount(sfAmount));
// Changing Frequency starts a clean period: reset the anchor to now and
// restore the full balance. This covers metered<->unmetered and
// metered->metered transitions uniformly.
if (ctx_.tx.isFieldPresent(sfFrequency))
{
sle->setFieldU32(sfFrequency, ctx_.tx.getFieldU32(sfFrequency));
sle->setFieldU32(sfNextClaimTime, currentTime);
sle->setFieldAmount(sfBalance, ctx_.tx.getFieldAmount(sfAmount));
}
if (ctx_.tx.isFieldPresent(sfExpiration))
{
auto const expiration = ctx_.tx.getFieldU32(sfExpiration);
// Expiration == 0 removes any existing expiration.
if (expiration == 0)
{
if (sle->isFieldPresent(sfExpiration))
sle->makeFieldAbsent(sfExpiration);
}
else if (expiration < currentTime)
{
JLOG(ctx_.journal.trace())
<< "SubscriptionSet: The expiration time is in the past.";
return tecEXPIRED;
}
else
{
sle->setFieldU32(sfExpiration, expiration);
}
}
sb.update(sle);
}
else
{
auto const currentTime = sb.header().parentCloseTime.time_since_epoch().count();
auto startTime = currentTime;
auto nextClaimTime = currentTime;
// create
{
auto const balance = STAmount((*sleAccount)[sfBalance]).xrp();
auto const reserve =
accountReserve(sb, sleAccount, ctx_.journal, {.ownerCountDelta = 1});
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
}
AccountID const dest = ctx_.tx.getAccountID(sfDestination);
Keylet const subKeylet = keylet::subscription(account, dest, ctx_.tx.getSeqProxy().value());
auto sle = std::make_shared<SLE>(subKeylet);
sle->setAccountID(sfAccount, account);
sle->setAccountID(sfDestination, dest);
sle->setFieldU32(sfSequence, ctx_.tx.getSeqProxy().value());
if (ctx_.tx.getFlags() & tfSingleUse)
sle->setFlag(lsfSingleUse);
if (ctx_.tx.isFieldPresent(sfDestinationTag))
sle->setFieldU32(sfDestinationTag, ctx_.tx.getFieldU32(sfDestinationTag));
sle->setFieldAmount(sfAmount, ctx_.tx.getFieldAmount(sfAmount));
sle->setFieldAmount(sfBalance, ctx_.tx.getFieldAmount(sfAmount));
sle->setFieldU32(sfFrequency, ctx_.tx.getFieldU32(sfFrequency));
if (ctx_.tx.isFieldPresent(sfStartTime))
{
startTime = ctx_.tx.getFieldU32(sfStartTime);
nextClaimTime = startTime;
if (startTime < currentTime)
{
JLOG(ctx_.journal.trace()) << "SubscriptionSet: The start time is in the past.";
return tecNO_PERMISSION;
}
}
sle->setFieldU32(sfNextClaimTime, nextClaimTime);
if (ctx_.tx.isFieldPresent(sfExpiration))
{
auto const expiration = ctx_.tx.getFieldU32(sfExpiration);
if (expiration < currentTime)
{
JLOG(ctx_.journal.trace())
<< "SubscriptionSet: The expiration time is in the past.";
return tecEXPIRED;
}
if (expiration < nextClaimTime)
{
JLOG(ctx_.journal.trace()) << "SubscriptionSet: The expiration time is "
"less than the next claim time.";
return tecEXPIRED;
}
sle->setFieldU32(sfExpiration, expiration);
}
{
auto page =
sb.dirInsert(keylet::ownerDir(account), subKeylet, describeOwnerDir(account));
if (!page)
return tecDIR_FULL;
(*sle)[sfOwnerNode] = *page;
}
{
auto page = sb.dirInsert(keylet::ownerDir(dest), subKeylet, describeOwnerDir(dest));
if (!page)
return tecDIR_FULL;
(*sle)[sfDestinationNode] = *page;
}
increaseOwnerCount(sb, sleAccount, SLE::pointer(), 1, ctx_.journal);
sb.insert(sle);
}
sb.apply(ctx_.rawView());
return tesSUCCESS;
}
void
SubscriptionSet::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
{
// No transaction-specific invariants yet (future work).
}
bool
SubscriptionSet::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
{
// No transaction-specific invariants yet (future work).
return true;
}
} // namespace xrpl

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -0,0 +1,412 @@
// Auto-generated unit tests for ledger entry Subscription
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol_autogen/ledger_entries/Subscription.h>
#include <xrpl/protocol_autogen/ledger_entries/Ticket.h>
#include <string>
namespace xrpl::ledger_entries {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed for both the
// builder's STObject and the wrapper's SLE.
TEST(SubscriptionTests, BuilderSettersRoundTrip)
{
uint256 const index{1u};
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto const sequenceValue = canonical_UINT32();
auto const ownerNodeValue = canonical_UINT64();
auto const accountValue = canonical_ACCOUNT();
auto const destinationValue = canonical_ACCOUNT();
auto const destinationTagValue = canonical_UINT32();
auto const amountValue = canonical_AMOUNT();
auto const balanceValue = canonical_AMOUNT();
auto const frequencyValue = canonical_UINT32();
auto const nextClaimTimeValue = canonical_UINT32();
auto const expirationValue = canonical_UINT32();
auto const destinationNodeValue = canonical_UINT64();
SubscriptionBuilder builder{
previousTxnIDValue,
previousTxnLgrSeqValue,
sequenceValue,
ownerNodeValue,
accountValue,
destinationValue,
amountValue,
balanceValue,
frequencyValue,
nextClaimTimeValue,
destinationNodeValue
};
builder.setDestinationTag(destinationTagValue);
builder.setExpiration(expirationValue);
builder.setLedgerIndex(index);
builder.setFlags(0x1u);
EXPECT_TRUE(builder.validate());
auto const entry = builder.build(index);
EXPECT_TRUE(entry.validate());
{
auto const& expected = previousTxnIDValue;
auto const actual = entry.getPreviousTxnID();
expectEqualField(expected, actual, "sfPreviousTxnID");
}
{
auto const& expected = previousTxnLgrSeqValue;
auto const actual = entry.getPreviousTxnLgrSeq();
expectEqualField(expected, actual, "sfPreviousTxnLgrSeq");
}
{
auto const& expected = sequenceValue;
auto const actual = entry.getSequence();
expectEqualField(expected, actual, "sfSequence");
}
{
auto const& expected = ownerNodeValue;
auto const actual = entry.getOwnerNode();
expectEqualField(expected, actual, "sfOwnerNode");
}
{
auto const& expected = accountValue;
auto const actual = entry.getAccount();
expectEqualField(expected, actual, "sfAccount");
}
{
auto const& expected = destinationValue;
auto const actual = entry.getDestination();
expectEqualField(expected, actual, "sfDestination");
}
{
auto const& expected = amountValue;
auto const actual = entry.getAmount();
expectEqualField(expected, actual, "sfAmount");
}
{
auto const& expected = balanceValue;
auto const actual = entry.getBalance();
expectEqualField(expected, actual, "sfBalance");
}
{
auto const& expected = frequencyValue;
auto const actual = entry.getFrequency();
expectEqualField(expected, actual, "sfFrequency");
}
{
auto const& expected = nextClaimTimeValue;
auto const actual = entry.getNextClaimTime();
expectEqualField(expected, actual, "sfNextClaimTime");
}
{
auto const& expected = destinationNodeValue;
auto const actual = entry.getDestinationNode();
expectEqualField(expected, actual, "sfDestinationNode");
}
{
auto const& expected = destinationTagValue;
auto const actualOpt = entry.getDestinationTag();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfDestinationTag");
EXPECT_TRUE(entry.hasDestinationTag());
}
{
auto const& expected = expirationValue;
auto const actualOpt = entry.getExpiration();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfExpiration");
EXPECT_TRUE(entry.hasExpiration());
}
EXPECT_TRUE(entry.hasLedgerIndex());
auto const ledgerIndex = entry.getLedgerIndex();
ASSERT_TRUE(ledgerIndex.has_value());
EXPECT_EQ(*ledgerIndex, index);
EXPECT_EQ(entry.getKey(), index);
}
// 2 & 4) Start from an SLE, set fields directly on it, construct a builder
// from that SLE, build a new wrapper, and verify all fields (and validate()).
TEST(SubscriptionTests, BuilderFromSleRoundTrip)
{
uint256 const index{2u};
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto const sequenceValue = canonical_UINT32();
auto const ownerNodeValue = canonical_UINT64();
auto const accountValue = canonical_ACCOUNT();
auto const destinationValue = canonical_ACCOUNT();
auto const destinationTagValue = canonical_UINT32();
auto const amountValue = canonical_AMOUNT();
auto const balanceValue = canonical_AMOUNT();
auto const frequencyValue = canonical_UINT32();
auto const nextClaimTimeValue = canonical_UINT32();
auto const expirationValue = canonical_UINT32();
auto const destinationNodeValue = canonical_UINT64();
auto sle = std::make_shared<SLE>(Subscription::entryType, index);
sle->at(sfPreviousTxnID) = previousTxnIDValue;
sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue;
sle->at(sfSequence) = sequenceValue;
sle->at(sfOwnerNode) = ownerNodeValue;
sle->at(sfAccount) = accountValue;
sle->at(sfDestination) = destinationValue;
sle->at(sfDestinationTag) = destinationTagValue;
sle->at(sfAmount) = amountValue;
sle->at(sfBalance) = balanceValue;
sle->at(sfFrequency) = frequencyValue;
sle->at(sfNextClaimTime) = nextClaimTimeValue;
sle->at(sfExpiration) = expirationValue;
sle->at(sfDestinationNode) = destinationNodeValue;
SubscriptionBuilder builderFromSle{sle};
EXPECT_TRUE(builderFromSle.validate());
auto const entryFromBuilder = builderFromSle.build(index);
Subscription entryFromSle{sle};
EXPECT_TRUE(entryFromBuilder.validate());
EXPECT_TRUE(entryFromSle.validate());
{
auto const& expected = previousTxnIDValue;
auto const fromSle = entryFromSle.getPreviousTxnID();
auto const fromBuilder = entryFromBuilder.getPreviousTxnID();
expectEqualField(expected, fromSle, "sfPreviousTxnID");
expectEqualField(expected, fromBuilder, "sfPreviousTxnID");
}
{
auto const& expected = previousTxnLgrSeqValue;
auto const fromSle = entryFromSle.getPreviousTxnLgrSeq();
auto const fromBuilder = entryFromBuilder.getPreviousTxnLgrSeq();
expectEqualField(expected, fromSle, "sfPreviousTxnLgrSeq");
expectEqualField(expected, fromBuilder, "sfPreviousTxnLgrSeq");
}
{
auto const& expected = sequenceValue;
auto const fromSle = entryFromSle.getSequence();
auto const fromBuilder = entryFromBuilder.getSequence();
expectEqualField(expected, fromSle, "sfSequence");
expectEqualField(expected, fromBuilder, "sfSequence");
}
{
auto const& expected = ownerNodeValue;
auto const fromSle = entryFromSle.getOwnerNode();
auto const fromBuilder = entryFromBuilder.getOwnerNode();
expectEqualField(expected, fromSle, "sfOwnerNode");
expectEqualField(expected, fromBuilder, "sfOwnerNode");
}
{
auto const& expected = accountValue;
auto const fromSle = entryFromSle.getAccount();
auto const fromBuilder = entryFromBuilder.getAccount();
expectEqualField(expected, fromSle, "sfAccount");
expectEqualField(expected, fromBuilder, "sfAccount");
}
{
auto const& expected = destinationValue;
auto const fromSle = entryFromSle.getDestination();
auto const fromBuilder = entryFromBuilder.getDestination();
expectEqualField(expected, fromSle, "sfDestination");
expectEqualField(expected, fromBuilder, "sfDestination");
}
{
auto const& expected = amountValue;
auto const fromSle = entryFromSle.getAmount();
auto const fromBuilder = entryFromBuilder.getAmount();
expectEqualField(expected, fromSle, "sfAmount");
expectEqualField(expected, fromBuilder, "sfAmount");
}
{
auto const& expected = balanceValue;
auto const fromSle = entryFromSle.getBalance();
auto const fromBuilder = entryFromBuilder.getBalance();
expectEqualField(expected, fromSle, "sfBalance");
expectEqualField(expected, fromBuilder, "sfBalance");
}
{
auto const& expected = frequencyValue;
auto const fromSle = entryFromSle.getFrequency();
auto const fromBuilder = entryFromBuilder.getFrequency();
expectEqualField(expected, fromSle, "sfFrequency");
expectEqualField(expected, fromBuilder, "sfFrequency");
}
{
auto const& expected = nextClaimTimeValue;
auto const fromSle = entryFromSle.getNextClaimTime();
auto const fromBuilder = entryFromBuilder.getNextClaimTime();
expectEqualField(expected, fromSle, "sfNextClaimTime");
expectEqualField(expected, fromBuilder, "sfNextClaimTime");
}
{
auto const& expected = destinationNodeValue;
auto const fromSle = entryFromSle.getDestinationNode();
auto const fromBuilder = entryFromBuilder.getDestinationNode();
expectEqualField(expected, fromSle, "sfDestinationNode");
expectEqualField(expected, fromBuilder, "sfDestinationNode");
}
{
auto const& expected = destinationTagValue;
auto const fromSleOpt = entryFromSle.getDestinationTag();
auto const fromBuilderOpt = entryFromBuilder.getDestinationTag();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfDestinationTag");
expectEqualField(expected, *fromBuilderOpt, "sfDestinationTag");
}
{
auto const& expected = expirationValue;
auto const fromSleOpt = entryFromSle.getExpiration();
auto const fromBuilderOpt = entryFromBuilder.getExpiration();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfExpiration");
expectEqualField(expected, *fromBuilderOpt, "sfExpiration");
}
EXPECT_EQ(entryFromSle.getKey(), index);
EXPECT_EQ(entryFromBuilder.getKey(), index);
}
// 3) Verify wrapper throws when constructed from wrong ledger entry type.
TEST(SubscriptionTests, WrapperThrowsOnWrongEntryType)
{
uint256 const index{3u};
// Build a valid ledger entry of a different type
// Ticket requires: Account, OwnerNode, TicketSequence, PreviousTxnID, PreviousTxnLgrSeq
// Check requires: Account, Destination, SendMax, Sequence, OwnerNode, DestinationNode, PreviousTxnID, PreviousTxnLgrSeq
TicketBuilder wrongBuilder{
canonical_ACCOUNT(),
canonical_UINT64(),
canonical_UINT32(),
canonical_UINT256(),
canonical_UINT32()};
auto wrongEntry = wrongBuilder.build(index);
EXPECT_THROW(Subscription{wrongEntry.getSle()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong ledger entry type.
TEST(SubscriptionTests, BuilderThrowsOnWrongEntryType)
{
uint256 const index{4u};
// Build a valid ledger entry of a different type
TicketBuilder wrongBuilder{
canonical_ACCOUNT(),
canonical_UINT64(),
canonical_UINT32(),
canonical_UINT256(),
canonical_UINT32()};
auto wrongEntry = wrongBuilder.build(index);
EXPECT_THROW(SubscriptionBuilder{wrongEntry.getSle()}, std::runtime_error);
}
// 5) Build with only required fields and verify optional fields return nullopt.
TEST(SubscriptionTests, OptionalFieldsReturnNullopt)
{
uint256 const index{3u};
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto const sequenceValue = canonical_UINT32();
auto const ownerNodeValue = canonical_UINT64();
auto const accountValue = canonical_ACCOUNT();
auto const destinationValue = canonical_ACCOUNT();
auto const amountValue = canonical_AMOUNT();
auto const balanceValue = canonical_AMOUNT();
auto const frequencyValue = canonical_UINT32();
auto const nextClaimTimeValue = canonical_UINT32();
auto const destinationNodeValue = canonical_UINT64();
SubscriptionBuilder builder{
previousTxnIDValue,
previousTxnLgrSeqValue,
sequenceValue,
ownerNodeValue,
accountValue,
destinationValue,
amountValue,
balanceValue,
frequencyValue,
nextClaimTimeValue,
destinationNodeValue
};
auto const entry = builder.build(index);
// Verify optional fields are not present
EXPECT_FALSE(entry.hasDestinationTag());
EXPECT_FALSE(entry.getDestinationTag().has_value());
EXPECT_FALSE(entry.hasExpiration());
EXPECT_FALSE(entry.getExpiration().has_value());
}
}

View File

@@ -0,0 +1,146 @@
// Auto-generated unit tests for transaction SubscriptionCancel
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/SubscriptionCancel.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(TransactionsSubscriptionCancelTests, BuilderSettersRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSubscriptionCancel"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 1;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const subscriptionIDValue = canonical_UINT256();
SubscriptionCancelBuilder builder{
accountValue,
subscriptionIDValue,
sequenceValue,
feeValue
};
// Set optional fields
auto tx = builder.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(tx.validate(reason)) << reason;
// Verify signing was applied
EXPECT_FALSE(tx.getSigningPubKey().empty());
EXPECT_TRUE(tx.hasTxnSignature());
// Verify common fields
EXPECT_EQ(tx.getAccount(), accountValue);
EXPECT_EQ(tx.getSequence(), sequenceValue);
EXPECT_EQ(tx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = subscriptionIDValue;
auto const actual = tx.getSubscriptionID();
expectEqualField(expected, actual, "sfSubscriptionID");
}
// Verify optional fields
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
// and verify all fields match.
TEST(TransactionsSubscriptionCancelTests, BuilderFromStTxRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSubscriptionCancelFromTx"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 2;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const subscriptionIDValue = canonical_UINT256();
// Build an initial transaction
SubscriptionCancelBuilder initialBuilder{
accountValue,
subscriptionIDValue,
sequenceValue,
feeValue
};
auto initialTx = initialBuilder.build(publicKey, secretKey);
// Create builder from existing STTx
SubscriptionCancelBuilder builderFromTx{initialTx.getSTTx()};
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
// Verify common fields
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = subscriptionIDValue;
auto const actual = rebuiltTx.getSubscriptionID();
expectEqualField(expected, actual, "sfSubscriptionID");
}
// Verify optional fields
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
TEST(TransactionsSubscriptionCancelTests, WrapperThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(SubscriptionCancel{wrongTx.getSTTx()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong transaction type.
TEST(TransactionsSubscriptionCancelTests, BuilderThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(SubscriptionCancelBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
}

View File

@@ -0,0 +1,162 @@
// Auto-generated unit tests for transaction SubscriptionClaim
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/SubscriptionClaim.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(TransactionsSubscriptionClaimTests, BuilderSettersRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSubscriptionClaim"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 1;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const amountValue = canonical_AMOUNT();
auto const subscriptionIDValue = canonical_UINT256();
SubscriptionClaimBuilder builder{
accountValue,
amountValue,
subscriptionIDValue,
sequenceValue,
feeValue
};
// Set optional fields
auto tx = builder.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(tx.validate(reason)) << reason;
// Verify signing was applied
EXPECT_FALSE(tx.getSigningPubKey().empty());
EXPECT_TRUE(tx.hasTxnSignature());
// Verify common fields
EXPECT_EQ(tx.getAccount(), accountValue);
EXPECT_EQ(tx.getSequence(), sequenceValue);
EXPECT_EQ(tx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = amountValue;
auto const actual = tx.getAmount();
expectEqualField(expected, actual, "sfAmount");
}
{
auto const& expected = subscriptionIDValue;
auto const actual = tx.getSubscriptionID();
expectEqualField(expected, actual, "sfSubscriptionID");
}
// Verify optional fields
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
// and verify all fields match.
TEST(TransactionsSubscriptionClaimTests, BuilderFromStTxRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSubscriptionClaimFromTx"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 2;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const amountValue = canonical_AMOUNT();
auto const subscriptionIDValue = canonical_UINT256();
// Build an initial transaction
SubscriptionClaimBuilder initialBuilder{
accountValue,
amountValue,
subscriptionIDValue,
sequenceValue,
feeValue
};
auto initialTx = initialBuilder.build(publicKey, secretKey);
// Create builder from existing STTx
SubscriptionClaimBuilder builderFromTx{initialTx.getSTTx()};
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
// Verify common fields
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = amountValue;
auto const actual = rebuiltTx.getAmount();
expectEqualField(expected, actual, "sfAmount");
}
{
auto const& expected = subscriptionIDValue;
auto const actual = rebuiltTx.getSubscriptionID();
expectEqualField(expected, actual, "sfSubscriptionID");
}
// Verify optional fields
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
TEST(TransactionsSubscriptionClaimTests, WrapperThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(SubscriptionClaim{wrongTx.getSTTx()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong transaction type.
TEST(TransactionsSubscriptionClaimTests, BuilderThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(SubscriptionClaimBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
}

View File

@@ -0,0 +1,300 @@
// Auto-generated unit tests for transaction SubscriptionSet
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/SubscriptionSet.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(TransactionsSubscriptionSetTests, BuilderSettersRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSubscriptionSet"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 1;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const destinationValue = canonical_ACCOUNT();
auto const amountValue = canonical_AMOUNT();
auto const frequencyValue = canonical_UINT32();
auto const startTimeValue = canonical_UINT32();
auto const expirationValue = canonical_UINT32();
auto const destinationTagValue = canonical_UINT32();
auto const subscriptionIDValue = canonical_UINT256();
SubscriptionSetBuilder builder{
accountValue,
amountValue,
sequenceValue,
feeValue
};
// Set optional fields
builder.setDestination(destinationValue);
builder.setFrequency(frequencyValue);
builder.setStartTime(startTimeValue);
builder.setExpiration(expirationValue);
builder.setDestinationTag(destinationTagValue);
builder.setSubscriptionID(subscriptionIDValue);
auto tx = builder.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(tx.validate(reason)) << reason;
// Verify signing was applied
EXPECT_FALSE(tx.getSigningPubKey().empty());
EXPECT_TRUE(tx.hasTxnSignature());
// Verify common fields
EXPECT_EQ(tx.getAccount(), accountValue);
EXPECT_EQ(tx.getSequence(), sequenceValue);
EXPECT_EQ(tx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = amountValue;
auto const actual = tx.getAmount();
expectEqualField(expected, actual, "sfAmount");
}
// Verify optional fields
{
auto const& expected = destinationValue;
auto const actualOpt = tx.getDestination();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestination should be present";
expectEqualField(expected, *actualOpt, "sfDestination");
EXPECT_TRUE(tx.hasDestination());
}
{
auto const& expected = frequencyValue;
auto const actualOpt = tx.getFrequency();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFrequency should be present";
expectEqualField(expected, *actualOpt, "sfFrequency");
EXPECT_TRUE(tx.hasFrequency());
}
{
auto const& expected = startTimeValue;
auto const actualOpt = tx.getStartTime();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfStartTime should be present";
expectEqualField(expected, *actualOpt, "sfStartTime");
EXPECT_TRUE(tx.hasStartTime());
}
{
auto const& expected = expirationValue;
auto const actualOpt = tx.getExpiration();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfExpiration should be present";
expectEqualField(expected, *actualOpt, "sfExpiration");
EXPECT_TRUE(tx.hasExpiration());
}
{
auto const& expected = destinationTagValue;
auto const actualOpt = tx.getDestinationTag();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
expectEqualField(expected, *actualOpt, "sfDestinationTag");
EXPECT_TRUE(tx.hasDestinationTag());
}
{
auto const& expected = subscriptionIDValue;
auto const actualOpt = tx.getSubscriptionID();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSubscriptionID should be present";
expectEqualField(expected, *actualOpt, "sfSubscriptionID");
EXPECT_TRUE(tx.hasSubscriptionID());
}
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
// and verify all fields match.
TEST(TransactionsSubscriptionSetTests, BuilderFromStTxRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSubscriptionSetFromTx"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 2;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const destinationValue = canonical_ACCOUNT();
auto const amountValue = canonical_AMOUNT();
auto const frequencyValue = canonical_UINT32();
auto const startTimeValue = canonical_UINT32();
auto const expirationValue = canonical_UINT32();
auto const destinationTagValue = canonical_UINT32();
auto const subscriptionIDValue = canonical_UINT256();
// Build an initial transaction
SubscriptionSetBuilder initialBuilder{
accountValue,
amountValue,
sequenceValue,
feeValue
};
initialBuilder.setDestination(destinationValue);
initialBuilder.setFrequency(frequencyValue);
initialBuilder.setStartTime(startTimeValue);
initialBuilder.setExpiration(expirationValue);
initialBuilder.setDestinationTag(destinationTagValue);
initialBuilder.setSubscriptionID(subscriptionIDValue);
auto initialTx = initialBuilder.build(publicKey, secretKey);
// Create builder from existing STTx
SubscriptionSetBuilder builderFromTx{initialTx.getSTTx()};
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
// Verify common fields
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = amountValue;
auto const actual = rebuiltTx.getAmount();
expectEqualField(expected, actual, "sfAmount");
}
// Verify optional fields
{
auto const& expected = destinationValue;
auto const actualOpt = rebuiltTx.getDestination();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestination should be present";
expectEqualField(expected, *actualOpt, "sfDestination");
}
{
auto const& expected = frequencyValue;
auto const actualOpt = rebuiltTx.getFrequency();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFrequency should be present";
expectEqualField(expected, *actualOpt, "sfFrequency");
}
{
auto const& expected = startTimeValue;
auto const actualOpt = rebuiltTx.getStartTime();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfStartTime should be present";
expectEqualField(expected, *actualOpt, "sfStartTime");
}
{
auto const& expected = expirationValue;
auto const actualOpt = rebuiltTx.getExpiration();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfExpiration should be present";
expectEqualField(expected, *actualOpt, "sfExpiration");
}
{
auto const& expected = destinationTagValue;
auto const actualOpt = rebuiltTx.getDestinationTag();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
expectEqualField(expected, *actualOpt, "sfDestinationTag");
}
{
auto const& expected = subscriptionIDValue;
auto const actualOpt = rebuiltTx.getSubscriptionID();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSubscriptionID should be present";
expectEqualField(expected, *actualOpt, "sfSubscriptionID");
}
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
TEST(TransactionsSubscriptionSetTests, WrapperThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(SubscriptionSet{wrongTx.getSTTx()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong transaction type.
TEST(TransactionsSubscriptionSetTests, BuilderThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(SubscriptionSetBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
// 5) Build with only required fields and verify optional fields return nullopt.
TEST(TransactionsSubscriptionSetTests, OptionalFieldsReturnNullopt)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSubscriptionSetNullopt"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 3;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific required field values
auto const amountValue = canonical_AMOUNT();
SubscriptionSetBuilder builder{
accountValue,
amountValue,
sequenceValue,
feeValue
};
// Do NOT set optional fields
auto tx = builder.build(publicKey, secretKey);
// Verify optional fields are not present
EXPECT_FALSE(tx.hasDestination());
EXPECT_FALSE(tx.getDestination().has_value());
EXPECT_FALSE(tx.hasFrequency());
EXPECT_FALSE(tx.getFrequency().has_value());
EXPECT_FALSE(tx.hasStartTime());
EXPECT_FALSE(tx.getStartTime().has_value());
EXPECT_FALSE(tx.hasExpiration());
EXPECT_FALSE(tx.getExpiration().has_value());
EXPECT_FALSE(tx.hasDestinationTag());
EXPECT_FALSE(tx.getDestinationTag().has_value());
EXPECT_FALSE(tx.hasSubscriptionID());
EXPECT_FALSE(tx.getSubscriptionID().has_value());
}
}

View File

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