mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-28 07:48:01 +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:
@@ -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