mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-26 23:19:07 +00:00
Merge remote-tracking branch 'XRPLF-rippled/dangell7/token-paychan-clawback' into develop
# Conflicts: # src/test/app/Delegate_test.cpp
This commit is contained in:
@@ -1246,6 +1246,17 @@ TRANSACTION(ttCONTRACT_CALL, 97, ContractCall,
|
||||
{sfGas, SoeRequired},
|
||||
}))
|
||||
|
||||
/** This transaction claws back tokens held in a payment channel. */
|
||||
#if TRANSACTION_INCLUDE
|
||||
# include <xrpl/tx/transactors/payment_channel/PaymentChannelClawback.h>
|
||||
#endif
|
||||
TRANSACTION(ttPAYCHAN_CLAWBACK, 98, PaymentChannelClawback,
|
||||
({.delegable = Delegation::Delegable, .amendment = featureTokenPaychan}),
|
||||
({
|
||||
{sfChannel, SoeRequired},
|
||||
{sfAmount, SoeOptional, SoeMptSupported},
|
||||
}))
|
||||
|
||||
/** This system-generated transaction type is used to update the status of the various amendments.
|
||||
|
||||
For details, see: https://xrpl.org/amendments.html
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
// This file is auto-generated. Do not edit.
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBase.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::transactions {
|
||||
|
||||
class PaymentChannelClawbackBuilder;
|
||||
|
||||
/**
|
||||
* @brief Transaction: PaymentChannelClawback
|
||||
*
|
||||
* Type: ttPAYCHAN_CLAWBACK (92)
|
||||
* Delegable: Delegation::Delegable
|
||||
* Amendment: featureTokenPaychan
|
||||
* Privileges: NoPriv
|
||||
*
|
||||
* Immutable wrapper around STTx providing type-safe field access.
|
||||
* Use PaymentChannelClawbackBuilder to construct new transactions.
|
||||
*/
|
||||
class PaymentChannelClawback : public TransactionBase
|
||||
{
|
||||
public:
|
||||
static constexpr xrpl::TxType txType = ttPAYCHAN_CLAWBACK;
|
||||
|
||||
/**
|
||||
* @brief Construct a PaymentChannelClawback transaction wrapper from an existing STTx object.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
explicit PaymentChannelClawback(std::shared_ptr<STTx const> tx)
|
||||
: TransactionBase(std::move(tx))
|
||||
{
|
||||
// Verify transaction type
|
||||
if (tx_->getTxnType() != txType)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for PaymentChannelClawback");
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction-specific field getters
|
||||
|
||||
/**
|
||||
* @brief Get sfChannel (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_UINT256::type::value_type
|
||||
getChannel() const
|
||||
{
|
||||
return this->tx_->at(sfChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfAmount (SoeOptional)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_AMOUNT::type::value_type>
|
||||
getAmount() const
|
||||
{
|
||||
if (hasAmount())
|
||||
{
|
||||
return this->tx_->at(sfAmount);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfAmount is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasAmount() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfAmount);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builder for PaymentChannelClawback transactions.
|
||||
*
|
||||
* Provides a fluent interface for constructing transactions with method chaining.
|
||||
* Uses STObject internally for flexible transaction construction.
|
||||
* Inherits common field setters from TransactionBuilderBase.
|
||||
*/
|
||||
class PaymentChannelClawbackBuilder : public TransactionBuilderBase<PaymentChannelClawbackBuilder>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new PaymentChannelClawbackBuilder with required fields.
|
||||
* @param account The account initiating the transaction.
|
||||
* @param channel The sfChannel field value.
|
||||
* @param sequence Optional sequence number for the transaction.
|
||||
* @param fee Optional fee for the transaction.
|
||||
*/
|
||||
PaymentChannelClawbackBuilder(SF_ACCOUNT::type::value_type account,
|
||||
std::decay_t<typename SF_UINT256::type::value_type> const& channel, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
|
||||
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
|
||||
)
|
||||
: TransactionBuilderBase<PaymentChannelClawbackBuilder>(ttPAYCHAN_CLAWBACK, account, sequence, fee)
|
||||
{
|
||||
setChannel(channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a PaymentChannelClawbackBuilder from an existing STTx object.
|
||||
* @param tx The existing transaction to copy from.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
PaymentChannelClawbackBuilder(std::shared_ptr<STTx const> tx)
|
||||
{
|
||||
if (tx->getTxnType() != ttPAYCHAN_CLAWBACK)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for PaymentChannelClawbackBuilder");
|
||||
}
|
||||
object_ = *tx;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Transaction-specific field setters
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Set sfChannel (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PaymentChannelClawbackBuilder&
|
||||
setChannel(std::decay_t<typename SF_UINT256::type::value_type> const& value)
|
||||
{
|
||||
object_[sfChannel] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfAmount (SoeOptional)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PaymentChannelClawbackBuilder&
|
||||
setAmount(std::decay_t<typename SF_AMOUNT::type::value_type> const& value)
|
||||
{
|
||||
object_[sfAmount] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the PaymentChannelClawback wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
* @param secretKey The secret key for signing.
|
||||
* @return The constructed transaction wrapper.
|
||||
*/
|
||||
PaymentChannelClawback
|
||||
build(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
{
|
||||
sign(publicKey, secretKey);
|
||||
return PaymentChannelClawback{std::make_shared<STTx>(std::move(object_))};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::transactions
|
||||
@@ -304,6 +304,28 @@ public:
|
||||
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Invariant: a token payment channel must stay structurally consistent.
|
||||
*
|
||||
* For any non-XRP `PayChannel` (only reachable once `featureTokenPaychan` is
|
||||
* enabled): the paid-out `sfBalance` must never exceed the locked `sfAmount`,
|
||||
* `sfBalance` and `sfAmount` must name the same asset, `sfBalance` must never
|
||||
* decrease, and neither may go negative. This guards every operation that
|
||||
* mutates a channel (create, fund, claim, clawback) against corrupting the
|
||||
* amount/balance relationship.
|
||||
*/
|
||||
class ValidPaymentChannel
|
||||
{
|
||||
bool bad_ = false;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, SLE::const_ref, SLE::const_ref);
|
||||
|
||||
[[nodiscard]] bool
|
||||
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Invariant: a new account root must be the consequence of a payment,
|
||||
* must have the right starting sequence, and the payment
|
||||
@@ -443,6 +465,7 @@ using InvariantChecks = std::tuple<
|
||||
TransfersNotFrozen,
|
||||
NoBadOffers,
|
||||
NoZeroEscrow,
|
||||
ValidPaymentChannel,
|
||||
ValidNewAccountRoot,
|
||||
ValidNFTokenPage,
|
||||
NFTokenCountTracking,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/ApplyContext.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class PaymentChannelClawback : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
|
||||
|
||||
explicit PaymentChannelClawback(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
|
||||
void
|
||||
visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override;
|
||||
|
||||
[[nodiscard]] bool
|
||||
finalizeInvariants(
|
||||
STTx const& tx,
|
||||
TER result,
|
||||
XRPAmount fee,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j) override;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -424,6 +424,60 @@ NoZeroEscrow::finalize(
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
void
|
||||
ValidPaymentChannel::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
|
||||
{
|
||||
// Only token channels are constrained here; XRP channels are covered by the
|
||||
// XRP conservation invariants. A deleted channel has nothing to check.
|
||||
if (isDelete || !after || after->getType() != ltPAYCHAN)
|
||||
return;
|
||||
|
||||
auto const& amount = (*after)[sfAmount];
|
||||
if (isXRP(amount))
|
||||
return;
|
||||
|
||||
auto const& balance = (*after)[sfBalance];
|
||||
|
||||
// Amount and balance must name the same asset, both non-negative, and the
|
||||
// paid-out balance must never exceed the locked amount.
|
||||
if (amount.asset() != balance.asset() || amount < beast::kZero || balance < beast::kZero ||
|
||||
amount < balance)
|
||||
{
|
||||
bad_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// The paid-out balance must never decrease.
|
||||
if (before && before->getType() == ltPAYCHAN && !isXRP((*before)[sfAmount]) &&
|
||||
(*before)[sfBalance].asset() == balance.asset() && balance < (*before)[sfBalance])
|
||||
{
|
||||
bad_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
ValidPaymentChannel::finalize(
|
||||
STTx const&,
|
||||
TER const,
|
||||
XRPAmount const,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j) const
|
||||
{
|
||||
// Token channels only exist once the amendment is active.
|
||||
if (!view.rules().enabled(featureTokenPaychan))
|
||||
return true;
|
||||
|
||||
if (bad_)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: payment channel amount/balance inconsistent";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
void
|
||||
AccountRootsNotDeleted::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
#include <xrpl/tx/transactors/payment_channel/PaymentChannelClawback.h>
|
||||
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/PaymentChannelHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.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/UintTypes.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
NotTEC
|
||||
PaymentChannelClawback::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (auto const amount = ctx.tx[~sfAmount])
|
||||
{
|
||||
if (amount->native() || *amount <= beast::kZero)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (amount->holds<MPTIssue>() && amount->mpt() > MPTAmount{kMaxMpTokenAmount})
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (amount->holds<Issue>() && badCurrency() == amount->get<Issue>().currency)
|
||||
return temBAD_CURRENCY;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
PaymentChannelClawback::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
Keylet const k{ltPAYCHAN, ctx.tx[sfChannel]};
|
||||
auto const slep = ctx.view.read(k);
|
||||
if (!slep)
|
||||
return tecNO_TARGET;
|
||||
|
||||
auto const& amount = slep->getFieldAmount(sfAmount);
|
||||
|
||||
// XRP cannot be clawed back.
|
||||
if (isXRP(amount))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
auto const& issuer = amount.getIssuer();
|
||||
|
||||
// Only the issuer of the locked asset may claw it back.
|
||||
if (ctx.tx[sfAccount] != issuer)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Defensive: the issuer can never be the channel owner.
|
||||
if ((*slep)[sfAccount] == issuer)
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const clawAmount = ctx.tx[~sfAmount];
|
||||
clawAmount && clawAmount->asset() != amount.asset())
|
||||
return tecWRONG_ASSET;
|
||||
|
||||
if (amount.holds<Issue>())
|
||||
{
|
||||
auto const sleIssuer = ctx.view.read(keylet::account(issuer));
|
||||
if (!sleIssuer)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (!sleIssuer->isFlag(lsfAllowTrustLineClawback) || sleIssuer->isFlag(lsfNoFreeze))
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const sleIssuance =
|
||||
ctx.view.read(keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID()));
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND; // LCOV_EXCL_LINE
|
||||
|
||||
if (!sleIssuance->isFlag(lsfMPTCanClawback))
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
PaymentChannelClawback::doApply()
|
||||
{
|
||||
Keylet const k{ltPAYCHAN, ctx_.tx[sfChannel]};
|
||||
auto const slep = ctx_.view().peek(k);
|
||||
if (!slep)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const& chanAmt = slep->getFieldAmount(sfAmount);
|
||||
auto const& chanBalance = slep->getFieldAmount(sfBalance);
|
||||
|
||||
// Only the unclaimed remainder can be clawed; the destination's earned
|
||||
// balance (sfBalance) is untouched.
|
||||
STAmount const lockedRemaining = chanAmt - chanBalance;
|
||||
if (lockedRemaining <= beast::kZero)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const clawAmount = ctx_.tx[~sfAmount];
|
||||
bool const full = !clawAmount || *clawAmount >= lockedRemaining;
|
||||
|
||||
// For a full claw, set the new amount to sfBalance directly rather than
|
||||
// computing chanAmt - lockedRemaining: for IOUs the latter is a - (a - b)
|
||||
// and can round, leaving a dust channel undeleted or pushing sfAmount
|
||||
// below sfBalance. claw is the exact amount removed.
|
||||
STAmount const newAmount = full ? chanBalance : STAmount{chanAmt - *clawAmount};
|
||||
STAmount const claw = full ? lockedRemaining : *clawAmount;
|
||||
|
||||
AccountID const owner = (*slep)[sfAccount];
|
||||
|
||||
// MPT: release the locked accounting back to the issuer (a redemption).
|
||||
// IOU: the locked value already sits with the issuer, so only the channel
|
||||
// obligation shrinks.
|
||||
if (chanAmt.holds<MPTIssue>())
|
||||
{
|
||||
if (auto const ret = unlockEscrowMPT(ctx_.view(), owner, accountID_, claw, claw, j_);
|
||||
!isTesSuccess(ret))
|
||||
return ret; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
(*slep)[sfAmount] = newAmount;
|
||||
|
||||
// Fully drained: nothing remains beyond the paid-out balance, close the
|
||||
// channel (the zero remainder makes closeChannel's refund a no-op).
|
||||
if (full)
|
||||
{
|
||||
return closeChannel(
|
||||
slep,
|
||||
ctx_.getApplyViewContext(),
|
||||
k.key,
|
||||
accountID_,
|
||||
ctx_.registry.get().getJournal("View"));
|
||||
}
|
||||
|
||||
ctx_.view().update(slep);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
PaymentChannelClawback::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
|
||||
{
|
||||
// No transaction-specific invariants yet (future work).
|
||||
}
|
||||
|
||||
bool
|
||||
PaymentChannelClawback::finalizeInvariants(
|
||||
STTx const&,
|
||||
TER,
|
||||
XRPAmount,
|
||||
ReadView const&,
|
||||
beast::Journal const&)
|
||||
{
|
||||
// No transaction-specific invariants yet (future work).
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -2756,7 +2756,7 @@ class Delegate_test : public beast::unit_test::Suite
|
||||
// DO NOT modify expectedDelegableCount unless all scenarios, including
|
||||
// edge cases, have been fully tested and verified.
|
||||
// ====================================================================
|
||||
std::size_t const expectedDelegableCount = 62;
|
||||
std::size_t const expectedDelegableCount = 63;
|
||||
|
||||
BEAST_EXPECTS(
|
||||
delegableCount == expectedDelegableCount,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/delegate.h>
|
||||
#include <test/jtx/fee.h>
|
||||
#include <test/jtx/flags.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
@@ -4579,6 +4580,402 @@ struct PayChanToken_test : public beast::unit_test::Suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testIOUChannelClawback(FeatureBitset features)
|
||||
{
|
||||
testcase("IOU Channel Clawback");
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals;
|
||||
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
auto const gw = Account{"gateway"};
|
||||
auto const usd = gw["USD"];
|
||||
auto const settleDelay = 100s;
|
||||
|
||||
auto const setup = [&](Env& env) {
|
||||
env.fund(XRP(10'000), alice, bob, gw);
|
||||
env(fset(gw, asfAllowTrustLineLocking));
|
||||
env(fset(gw, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.trust(usd(100'000), alice);
|
||||
env.trust(usd(100'000), bob);
|
||||
env.close();
|
||||
env(pay(gw, alice, usd(5'000)));
|
||||
env.close();
|
||||
};
|
||||
|
||||
// Amendment gating: without featureTokenPaychan the transaction type
|
||||
// is disabled (rejected before any channel lookup).
|
||||
{
|
||||
Env env{*this, features - featureTokenPaychan};
|
||||
env.fund(XRP(10'000), gw);
|
||||
env.close();
|
||||
auto const chan = paychan::channel(alice, bob, env.seq(gw));
|
||||
env(paychan::clawback(gw, chan), Ter(temDISABLED));
|
||||
}
|
||||
|
||||
// Full clawback of the unclaimed remainder deletes the channel; the
|
||||
// issuer's trust line is untouched (value already redeemed at lock).
|
||||
{
|
||||
Env env{*this, features};
|
||||
setup(env);
|
||||
auto const seq1 = env.seq(alice);
|
||||
env(paychan::create(alice, bob, usd(4'000), settleDelay, alice.pk()));
|
||||
env.close();
|
||||
auto const chan = paychan::channel(alice, bob, seq1);
|
||||
BEAST_EXPECT(issuerEscrowed(env, gw, usd) == usd(4'000));
|
||||
|
||||
// A non-issuer cannot claw.
|
||||
env(paychan::clawback(alice, chan), Ter(tecNO_PERMISSION));
|
||||
|
||||
env(paychan::clawback(gw, chan));
|
||||
env.close();
|
||||
BEAST_EXPECT(!paychan::channelExists(*env.current(), chan));
|
||||
BEAST_EXPECT(issuerEscrowed(env, gw, usd) == usd(0));
|
||||
BEAST_EXPECT(env.balance(alice, usd) == usd(1'000));
|
||||
}
|
||||
|
||||
// Partial clawback reduces the channel amount in place; a later claim
|
||||
// clamps to what remains.
|
||||
{
|
||||
Env env{*this, features};
|
||||
setup(env);
|
||||
auto const seq1 = env.seq(alice);
|
||||
env(paychan::create(alice, bob, usd(4'000), settleDelay, alice.pk()));
|
||||
env.close();
|
||||
auto const chan = paychan::channel(alice, bob, seq1);
|
||||
|
||||
env(paychan::clawback(gw, chan, usd(1'000)));
|
||||
env.close();
|
||||
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == usd(3'000));
|
||||
BEAST_EXPECT(issuerEscrowed(env, gw, usd) == usd(3'000));
|
||||
|
||||
// Over-claiming the reduced remainder fails.
|
||||
auto const sig = paychan::signClaimAuth(alice.pk(), alice.sk(), chan, usd(4'000));
|
||||
env(paychan::claim(bob, chan, usd(4'000), usd(4'000), Slice(sig), alice.pk()),
|
||||
Ter(tecUNFUNDED_PAYMENT));
|
||||
|
||||
// Claiming within the remainder succeeds.
|
||||
auto const sig2 = paychan::signClaimAuth(alice.pk(), alice.sk(), chan, usd(3'000));
|
||||
env(paychan::claim(bob, chan, usd(3'000), usd(3'000), Slice(sig2), alice.pk()));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(bob, usd) == usd(3'000));
|
||||
}
|
||||
|
||||
// Clawback after a partial claim: only the unclaimed remainder is
|
||||
// taken and the destination's already-claimed balance is untouched.
|
||||
// This is also the regression test for the full-claw amount being set
|
||||
// to sfBalance directly (rather than chanAmt - lockedRemaining, which
|
||||
// rounds for IOUs).
|
||||
{
|
||||
Env env{*this, features};
|
||||
setup(env);
|
||||
auto const seq1 = env.seq(alice);
|
||||
env(paychan::create(alice, bob, usd(4'000), settleDelay, alice.pk()));
|
||||
env.close();
|
||||
auto const chan = paychan::channel(alice, bob, seq1);
|
||||
|
||||
auto const sig = paychan::signClaimAuth(alice.pk(), alice.sk(), chan, usd(1'000));
|
||||
env(paychan::claim(bob, chan, usd(1'000), usd(1'000), Slice(sig), alice.pk()));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(bob, usd) == usd(1'000));
|
||||
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == usd(1'000));
|
||||
BEAST_EXPECT(issuerEscrowed(env, gw, usd) == usd(3'000));
|
||||
|
||||
env(paychan::clawback(gw, chan));
|
||||
env.close();
|
||||
BEAST_EXPECT(!paychan::channelExists(*env.current(), chan));
|
||||
BEAST_EXPECT(env.balance(bob, usd) == usd(1'000));
|
||||
BEAST_EXPECT(issuerEscrowed(env, gw, usd) == usd(0));
|
||||
}
|
||||
|
||||
// An Amount exceeding the remainder claws only the remainder.
|
||||
{
|
||||
Env env{*this, features};
|
||||
setup(env);
|
||||
auto const seq1 = env.seq(alice);
|
||||
env(paychan::create(alice, bob, usd(4'000), settleDelay, alice.pk()));
|
||||
env.close();
|
||||
auto const chan = paychan::channel(alice, bob, seq1);
|
||||
env(paychan::clawback(gw, chan, usd(10'000)));
|
||||
env.close();
|
||||
BEAST_EXPECT(!paychan::channelExists(*env.current(), chan));
|
||||
BEAST_EXPECT(issuerEscrowed(env, gw, usd) == usd(0));
|
||||
}
|
||||
|
||||
// A clawback Amount naming a different asset than the channel fails.
|
||||
{
|
||||
Env env{*this, features};
|
||||
setup(env);
|
||||
auto const seq1 = env.seq(alice);
|
||||
env(paychan::create(alice, bob, usd(4'000), settleDelay, alice.pk()));
|
||||
env.close();
|
||||
auto const chan = paychan::channel(alice, bob, seq1);
|
||||
auto const eur = gw["EUR"];
|
||||
env(paychan::clawback(gw, chan, eur(100)), Ter(tecWRONG_ASSET));
|
||||
}
|
||||
|
||||
// Clawback ignores the channel's timers: an expired channel is still
|
||||
// clawable, and the source is not refunded.
|
||||
{
|
||||
Env env{*this, features};
|
||||
setup(env);
|
||||
auto const seq1 = env.seq(alice);
|
||||
NetClock::time_point const cancelAfter = env.now() + 50s;
|
||||
env(paychan::create(alice, bob, usd(4'000), settleDelay, alice.pk(), cancelAfter));
|
||||
env.close();
|
||||
auto const chan = paychan::channel(alice, bob, seq1);
|
||||
env.close(env.now() + 60s);
|
||||
env(paychan::clawback(gw, chan));
|
||||
env.close();
|
||||
BEAST_EXPECT(!paychan::channelExists(*env.current(), chan));
|
||||
BEAST_EXPECT(issuerEscrowed(env, gw, usd) == usd(0));
|
||||
BEAST_EXPECT(env.balance(alice, usd) == usd(1'000));
|
||||
}
|
||||
|
||||
// An XRP channel can never be clawed.
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(10'000), alice, bob, gw);
|
||||
env.close();
|
||||
auto const seq1 = env.seq(alice);
|
||||
env(paychan::create(alice, bob, XRP(1'000), settleDelay, alice.pk()));
|
||||
env.close();
|
||||
auto const chan = paychan::channel(alice, bob, seq1);
|
||||
env(paychan::clawback(gw, chan), Ter(tecNO_PERMISSION));
|
||||
}
|
||||
|
||||
// The clawback authority can be delegated. The tx is Delegable, which
|
||||
// registers automatically from the macro; only a delegate the issuer
|
||||
// authorized for PaymentChannelClawback may claw on its behalf.
|
||||
if (features[featurePermissionDelegationV1_1])
|
||||
{
|
||||
Env env{*this, features};
|
||||
auto const dan = Account("dan");
|
||||
env.fund(XRP(10'000), alice, bob, gw, dan);
|
||||
env(fset(gw, asfAllowTrustLineLocking));
|
||||
env(fset(gw, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.trust(usd(100'000), alice);
|
||||
env.close();
|
||||
env(pay(gw, alice, usd(5'000)));
|
||||
env.close();
|
||||
auto const seq1 = env.seq(alice);
|
||||
env(paychan::create(alice, bob, usd(4'000), settleDelay, alice.pk()));
|
||||
env.close();
|
||||
auto const chan = paychan::channel(alice, bob, seq1);
|
||||
|
||||
// Unauthorized delegate is rejected.
|
||||
env(paychan::clawback(gw, chan), delegate::As(dan), Ter(terNO_DELEGATE_PERMISSION));
|
||||
|
||||
env(delegate::set(gw, dan, {"PaymentChannelClawback"}));
|
||||
env.close();
|
||||
env(paychan::clawback(gw, chan), delegate::As(dan));
|
||||
env.close();
|
||||
BEAST_EXPECT(!paychan::channelExists(*env.current(), chan));
|
||||
BEAST_EXPECT(issuerEscrowed(env, gw, usd) == usd(0));
|
||||
}
|
||||
|
||||
// Clawback requires the issuer opt-in (lsfAllowTrustLineClawback).
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(10'000), alice, bob, gw);
|
||||
env(fset(gw, asfAllowTrustLineLocking));
|
||||
env.close();
|
||||
env.trust(usd(100'000), alice);
|
||||
env.close();
|
||||
env(pay(gw, alice, usd(5'000)));
|
||||
env.close();
|
||||
auto const seq1 = env.seq(alice);
|
||||
env(paychan::create(alice, bob, usd(4'000), settleDelay, alice.pk()));
|
||||
env.close();
|
||||
auto const chan = paychan::channel(alice, bob, seq1);
|
||||
env(paychan::clawback(gw, chan), Ter(tecNO_PERMISSION));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testMPTChannelClawback(FeatureBitset features)
|
||||
{
|
||||
testcase("MPT Channel Clawback");
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals;
|
||||
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
auto const gw = Account("gw");
|
||||
auto const settleDelay = 100s;
|
||||
|
||||
// Full clawback: the three MPT accounting fields drop and the channel
|
||||
// is deleted.
|
||||
{
|
||||
Env env{*this, features};
|
||||
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
|
||||
mptGw.create(
|
||||
{.ownerCount = 1, .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanClawback});
|
||||
mptGw.authorize({.account = alice});
|
||||
mptGw.authorize({.account = bob});
|
||||
auto const mpt = mptGw["MPT"];
|
||||
env(pay(gw, alice, mpt(5'000)));
|
||||
env.close();
|
||||
|
||||
auto const seq1 = env.seq(alice);
|
||||
env(paychan::create(alice, bob, mpt(4'000), settleDelay, alice.pk()));
|
||||
env.close();
|
||||
auto const chan = paychan::channel(alice, bob, seq1);
|
||||
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 4'000);
|
||||
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 4'000);
|
||||
|
||||
env(paychan::clawback(gw, chan));
|
||||
env.close();
|
||||
BEAST_EXPECT(!paychan::channelExists(*env.current(), chan));
|
||||
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0);
|
||||
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0);
|
||||
}
|
||||
|
||||
// Partial clawback decrements each field by the clawed amount.
|
||||
{
|
||||
Env env{*this, features};
|
||||
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
|
||||
mptGw.create(
|
||||
{.ownerCount = 1, .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanClawback});
|
||||
mptGw.authorize({.account = alice});
|
||||
mptGw.authorize({.account = bob});
|
||||
auto const mpt = mptGw["MPT"];
|
||||
env(pay(gw, alice, mpt(5'000)));
|
||||
env.close();
|
||||
|
||||
auto const seq1 = env.seq(alice);
|
||||
env(paychan::create(alice, bob, mpt(4'000), settleDelay, alice.pk()));
|
||||
env.close();
|
||||
auto const chan = paychan::channel(alice, bob, seq1);
|
||||
|
||||
env(paychan::clawback(gw, chan, mpt(1'500)));
|
||||
env.close();
|
||||
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == mpt(2'500));
|
||||
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 2'500);
|
||||
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 2'500);
|
||||
|
||||
// A second clawback works on the reduced remainder.
|
||||
env(paychan::clawback(gw, chan, mpt(1'000)));
|
||||
env.close();
|
||||
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == mpt(1'500));
|
||||
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 1'500);
|
||||
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 1'500);
|
||||
}
|
||||
|
||||
// Clawback after a partial claim: only the unclaimed remainder is
|
||||
// taken; the three accounting fields drop by exactly that amount and
|
||||
// the destination's claimed balance is untouched.
|
||||
{
|
||||
Env env{*this, features};
|
||||
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
|
||||
mptGw.create(
|
||||
{.ownerCount = 1, .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanClawback});
|
||||
mptGw.authorize({.account = alice});
|
||||
mptGw.authorize({.account = bob});
|
||||
auto const mpt = mptGw["MPT"];
|
||||
env(pay(gw, alice, mpt(5'000)));
|
||||
env.close();
|
||||
|
||||
auto const seq1 = env.seq(alice);
|
||||
env(paychan::create(alice, bob, mpt(4'000), settleDelay, alice.pk()));
|
||||
env.close();
|
||||
auto const chan = paychan::channel(alice, bob, seq1);
|
||||
|
||||
auto const sig = paychan::signClaimAuth(alice.pk(), alice.sk(), chan, mpt(1'000));
|
||||
env(paychan::claim(bob, chan, mpt(1'000), mpt(1'000), Slice(sig), alice.pk()));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(bob, mpt) == mpt(1'000));
|
||||
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == mpt(1'000));
|
||||
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 3'000);
|
||||
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 3'000);
|
||||
|
||||
env(paychan::clawback(gw, chan));
|
||||
env.close();
|
||||
BEAST_EXPECT(!paychan::channelExists(*env.current(), chan));
|
||||
BEAST_EXPECT(env.balance(bob, mpt) == mpt(1'000));
|
||||
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0);
|
||||
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0);
|
||||
}
|
||||
|
||||
// A clawback Amount of the wrong asset is rejected.
|
||||
{
|
||||
Env env{*this, features};
|
||||
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
|
||||
mptGw.create(
|
||||
{.ownerCount = 1, .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanClawback});
|
||||
mptGw.authorize({.account = alice});
|
||||
mptGw.authorize({.account = bob});
|
||||
auto const mpt = mptGw["MPT"];
|
||||
env(pay(gw, alice, mpt(5'000)));
|
||||
env.close();
|
||||
|
||||
auto const seq1 = env.seq(alice);
|
||||
env(paychan::create(alice, bob, mpt(4'000), settleDelay, alice.pk()));
|
||||
env.close();
|
||||
auto const chan = paychan::channel(alice, bob, seq1);
|
||||
auto const usd = gw["USD"];
|
||||
env(paychan::clawback(gw, chan, usd(100)), Ter(tecWRONG_ASSET));
|
||||
}
|
||||
|
||||
// Clawback waives the transfer fee: on a fee-bearing issuance the gross
|
||||
// locked amount is seized (no fee is deducted, and there is no overflow
|
||||
// path since divideRound is never called).
|
||||
{
|
||||
Env env{*this, features};
|
||||
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
|
||||
mptGw.create(
|
||||
{.transferFee = 25'000,
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanClawback});
|
||||
mptGw.authorize({.account = alice});
|
||||
mptGw.authorize({.account = bob});
|
||||
auto const mpt = mptGw["MPT"];
|
||||
env(pay(gw, alice, mpt(5'000)));
|
||||
env.close();
|
||||
|
||||
auto const seq1 = env.seq(alice);
|
||||
env(paychan::create(alice, bob, mpt(4'000), settleDelay, alice.pk()));
|
||||
env.close();
|
||||
auto const chan = paychan::channel(alice, bob, seq1);
|
||||
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 4'000);
|
||||
|
||||
// Partial claw takes exactly the requested gross amount.
|
||||
env(paychan::clawback(gw, chan, mpt(1'000)));
|
||||
env.close();
|
||||
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == mpt(3'000));
|
||||
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 3'000);
|
||||
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 3'000);
|
||||
|
||||
// Full claw of the rest; no fee remains stranded on any field.
|
||||
env(paychan::clawback(gw, chan));
|
||||
env.close();
|
||||
BEAST_EXPECT(!paychan::channelExists(*env.current(), chan));
|
||||
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0);
|
||||
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0);
|
||||
}
|
||||
|
||||
// Without tfMPTCanClawback the issuer cannot claw.
|
||||
{
|
||||
Env env{*this, features};
|
||||
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
|
||||
mptGw.create({.ownerCount = 1, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
|
||||
mptGw.authorize({.account = alice});
|
||||
mptGw.authorize({.account = bob});
|
||||
auto const mpt = mptGw["MPT"];
|
||||
env(pay(gw, alice, mpt(5'000)));
|
||||
env.close();
|
||||
|
||||
auto const seq1 = env.seq(alice);
|
||||
env(paychan::create(alice, bob, mpt(4'000), settleDelay, alice.pk()));
|
||||
env.close();
|
||||
auto const chan = paychan::channel(alice, bob, seq1);
|
||||
env(paychan::clawback(gw, chan), Ter(tecNO_PERMISSION));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testIOUWithFeats(FeatureBitset features)
|
||||
{
|
||||
@@ -4610,6 +5007,7 @@ struct PayChanToken_test : public beast::unit_test::Suite
|
||||
testIOUMultiChannelDrain(features);
|
||||
testIOUPrecisionLoss(features);
|
||||
testIOUClawbackInteraction(features);
|
||||
testIOUChannelClawback(features);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -4637,6 +5035,7 @@ struct PayChanToken_test : public beast::unit_test::Suite
|
||||
testMPTCanEscrowRequired(features);
|
||||
testMPTDestroy(features);
|
||||
testMPTClawbackInteraction(features);
|
||||
testMPTChannelClawback(features);
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
@@ -155,4 +155,17 @@ rate(Env& env, Account const& account, Account const& dest, std::uint32_t const&
|
||||
return Rate{0};
|
||||
}
|
||||
|
||||
json::Value
|
||||
clawback(AccountID const& account, uint256 const& channel, std::optional<STAmount> const& amount)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::TransactionType] = jss::PaymentChannelClawback;
|
||||
jv[jss::Flags] = tfFullyCanonicalSig;
|
||||
jv[jss::Account] = to_string(account);
|
||||
jv["Channel"] = to_string(channel);
|
||||
if (amount)
|
||||
jv[jss::Amount] = amount->getJson(JsonOptions::Values::None);
|
||||
return jv;
|
||||
}
|
||||
|
||||
} // namespace xrpl::test::jtx::paychan
|
||||
|
||||
@@ -90,4 +90,19 @@ signClaimAuth(
|
||||
Rate
|
||||
rate(Env& env, Account const& account, Account const& dest, std::uint32_t const& seq);
|
||||
|
||||
json::Value
|
||||
clawback(
|
||||
AccountID const& account,
|
||||
uint256 const& channel,
|
||||
std::optional<STAmount> const& amount = std::nullopt);
|
||||
|
||||
inline json::Value
|
||||
clawback(
|
||||
Account const& account,
|
||||
uint256 const& channel,
|
||||
std::optional<STAmount> const& amount = std::nullopt)
|
||||
{
|
||||
return clawback(account.id(), channel, amount);
|
||||
}
|
||||
|
||||
} // namespace xrpl::test::jtx::paychan
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
// Auto-generated unit tests for transaction PaymentChannelClawback
|
||||
|
||||
|
||||
#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/PaymentChannelClawback.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(TransactionsPaymentChannelClawbackTests, BuilderSettersRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testPaymentChannelClawback"));
|
||||
|
||||
// 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 channelValue = canonical_UINT256();
|
||||
auto const amountValue = canonical_AMOUNT();
|
||||
|
||||
PaymentChannelClawbackBuilder builder{
|
||||
accountValue,
|
||||
channelValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
// Set optional fields
|
||||
builder.setAmount(amountValue);
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
std::string reason;
|
||||
EXPECT_TRUE(tx.validate(reason)) << reason;
|
||||
|
||||
// Verify signing was applied
|
||||
EXPECT_FALSE(tx.getSigningPubKey().empty());
|
||||
EXPECT_TRUE(tx.hasTxnSignature());
|
||||
|
||||
// Verify common fields
|
||||
EXPECT_EQ(tx.getAccount(), accountValue);
|
||||
EXPECT_EQ(tx.getSequence(), sequenceValue);
|
||||
EXPECT_EQ(tx.getFee(), feeValue);
|
||||
|
||||
// Verify required fields
|
||||
{
|
||||
auto const& expected = channelValue;
|
||||
auto const actual = tx.getChannel();
|
||||
expectEqualField(expected, actual, "sfChannel");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
{
|
||||
auto const& expected = amountValue;
|
||||
auto const actualOpt = tx.getAmount();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAmount should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfAmount");
|
||||
EXPECT_TRUE(tx.hasAmount());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
|
||||
// and verify all fields match.
|
||||
TEST(TransactionsPaymentChannelClawbackTests, BuilderFromStTxRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testPaymentChannelClawbackFromTx"));
|
||||
|
||||
// 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 channelValue = canonical_UINT256();
|
||||
auto const amountValue = canonical_AMOUNT();
|
||||
|
||||
// Build an initial transaction
|
||||
PaymentChannelClawbackBuilder initialBuilder{
|
||||
accountValue,
|
||||
channelValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
initialBuilder.setAmount(amountValue);
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
// Create builder from existing STTx
|
||||
PaymentChannelClawbackBuilder 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 = channelValue;
|
||||
auto const actual = rebuiltTx.getChannel();
|
||||
expectEqualField(expected, actual, "sfChannel");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
{
|
||||
auto const& expected = amountValue;
|
||||
auto const actualOpt = rebuiltTx.getAmount();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAmount should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfAmount");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsPaymentChannelClawbackTests, 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(PaymentChannelClawback{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsPaymentChannelClawbackTests, 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(PaymentChannelClawbackBuilder{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 5) Build with only required fields and verify optional fields return nullopt.
|
||||
TEST(TransactionsPaymentChannelClawbackTests, OptionalFieldsReturnNullopt)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testPaymentChannelClawbackNullopt"));
|
||||
|
||||
// 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 channelValue = canonical_UINT256();
|
||||
|
||||
PaymentChannelClawbackBuilder builder{
|
||||
accountValue,
|
||||
channelValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
// Do NOT set optional fields
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
// Verify optional fields are not present
|
||||
EXPECT_FALSE(tx.hasAmount());
|
||||
EXPECT_FALSE(tx.getAmount().has_value());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user