Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions include/xrpl/protocol/detail/transactions.macro
Original file line number Diff line number Diff line change
Expand Up @@ -1208,6 +1208,22 @@ TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 92, TransactionProposalCreate,
{sfExpiration, SoeRequired},
}))

// Transaction type 93 is reserved for ttTRANSACTION_PROPOSAL_SIGN.

/** This transaction deletes a TransactionProposal, releasing its owner's
reserve. While the proposal is live only its Owner or its target account
may cancel; once it is terminal anyone may (XLS-0103 §7). */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/proposal/TransactionProposalCancel.h>
#endif
TRANSACTION(ttTRANSACTION_PROPOSAL_CANCEL, 94, TransactionProposalCancel,
Delegation::NotDelegable,
featureCosign,
NoPriv,
({
{sfProposalID, SoeRequired},
}))

/** This system-generated transaction type is used to update the status of the various amendments.

For details, see: https://xrpl.org/amendments.html
Expand Down
131 changes: 131 additions & 0 deletions include/xrpl/protocol_autogen/transactions/TransactionProposalCancel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// This file is auto-generated. Do not edit.
#pragma once

#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>

#include <stdexcept>
#include <optional>

namespace xrpl::transactions {

class TransactionProposalCancelBuilder;

/**
* @brief Transaction: TransactionProposalCancel
*
* Type: ttTRANSACTION_PROPOSAL_CANCEL (94)
* Delegable: Delegation::NotDelegable
* Amendment: featureCosign
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use TransactionProposalCancelBuilder to construct new transactions.
*/
class TransactionProposalCancel : public TransactionBase
{
public:
static constexpr xrpl::TxType txType = ttTRANSACTION_PROPOSAL_CANCEL;

/**
* @brief Construct a TransactionProposalCancel transaction wrapper from an existing STTx object.
* @throws std::runtime_error if the transaction type doesn't match.
*/
explicit TransactionProposalCancel(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 TransactionProposalCancel");
}
}

// Transaction-specific field getters

/**
* @brief Get sfProposalID (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT256::type::value_type
getProposalID() const
{
return this->tx_->at(sfProposalID);
}
};

/**
* @brief Builder for TransactionProposalCancel 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 TransactionProposalCancelBuilder : public TransactionBuilderBase<TransactionProposalCancelBuilder>
{
public:
/**
* @brief Construct a new TransactionProposalCancelBuilder with required fields.
* @param account The account initiating the transaction.
* @param proposalID The sfProposalID field value.
* @param sequence Optional sequence number for the transaction.
* @param fee Optional fee for the transaction.
*/
TransactionProposalCancelBuilder(SF_ACCOUNT::type::value_type account,
std::decay_t<typename SF_UINT256::type::value_type> const& proposalID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<TransactionProposalCancelBuilder>(ttTRANSACTION_PROPOSAL_CANCEL, account, sequence, fee)
{
setProposalID(proposalID);
}

/**
* @brief Construct a TransactionProposalCancelBuilder from an existing STTx object.
* @param tx The existing transaction to copy from.
* @throws std::runtime_error if the transaction type doesn't match.
*/
TransactionProposalCancelBuilder(std::shared_ptr<STTx const> tx)
{
if (tx->getTxnType() != ttTRANSACTION_PROPOSAL_CANCEL)
{
throw std::runtime_error("Invalid transaction type for TransactionProposalCancelBuilder");
}
object_ = *tx;
}

/**
* @brief Transaction-specific field setters
*/

/**
* @brief Set sfProposalID (SoeRequired)
* @return Reference to this builder for method chaining.
*/
TransactionProposalCancelBuilder&
setProposalID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
{
object_[sfProposalID] = value;
return *this;
}

/**
* @brief Build and return the TransactionProposalCancel wrapper.
* @param publicKey The public key for signing.
* @param secretKey The secret key for signing.
* @return The constructed transaction wrapper.
*/
TransactionProposalCancel
build(PublicKey const& publicKey, SecretKey const& secretKey)
{
sign(publicKey, secretKey);
return TransactionProposalCancel{std::make_shared<STTx>(std::move(object_))};
}
};

} // namespace xrpl::transactions
84 changes: 78 additions & 6 deletions include/xrpl/tx/transactors/proposal/ProposalHelpers.h
Original file line number Diff line number Diff line change
@@ -1,24 +1,51 @@
#pragma once

#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h> // IWYU pragma: keep (range-for over getFieldArray)
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>

#include <cstdint>
#include <optional>

namespace xrpl::proposal {

/**
* Whether the proposed transaction is itself a proposal transaction, which
* would nest one proposal inside another.
*
* TODO: cover ttTRANSACTION_PROPOSAL_SIGN and ttTRANSACTION_PROPOSAL_CANCEL
* once those transactions exist.
* Whether the proposed transaction is a proposal transaction, or a Batch
* containing one. A proposal must not nest another proposal (On-Chain
* Cosigner spec §5.3.1), and without the one-level walk into a proposed
* Batch's inner transactions, a proposal transaction could be hidden there.
* TODO: cover ttTRANSACTION_PROPOSAL_SIGN once that transaction exists.
*/
inline bool
isProposalTx(STObject const& proposedTx)
{
return proposedTx.getFieldU16(sfTransactionType) == ttTRANSACTION_PROPOSAL_CREATE;
auto const isProposalType = [](std::uint16_t type) {
return type == ttTRANSACTION_PROPOSAL_CREATE || type == ttTRANSACTION_PROPOSAL_CANCEL;
};

auto const type = proposedTx.getFieldU16(sfTransactionType);
if (isProposalType(type))
return true;

if (type == ttBATCH && proposedTx.isFieldPresent(sfRawTransactions))
{
for (STObject const& inner : proposedTx.getFieldArray(sfRawTransactions))
{
if (!inner.isFieldPresent(sfTransactionType))
continue;

Check warning on line 41 in include/xrpl/tx/transactors/proposal/ProposalHelpers.h

View check run for this annotation

Codecov / codecov/patch

include/xrpl/tx/transactors/proposal/ProposalHelpers.h#L41

Added line #L41 was not covered by tests
auto const innerType = inner.getFieldU16(sfTransactionType);
if (isProposalType(innerType) || innerType == ttBATCH)
return true;
}
}

return false;
}

/**
Expand Down Expand Up @@ -71,4 +98,49 @@
: kProposalOwnerCount;
}

/**
* Whether the proposal is terminal (XLS-0103 §4.5). A terminal proposal can
* never complete: it stops accepting signatures and anyone may delete it.
*
* A proposal is terminal when either:
* - its Expiration has passed (the parent ledger closed at or after it), or
* - the proposed transaction carries a LastLedgerSequence that is at or
* below the current ledger sequence. This matches the dead-on-arrival
* check in TransactionProposalCreate::preclaim: a proposal whose bound
* leaves no future ledger to collect signatures in is already dead.
*
* @param view The ledger the deciding transaction is being applied to.
* @param expiration The proposal's Expiration field.
* @param proposedTx The proposal's ProposedTransaction field.
*/
bool
isTerminal(
ReadView const& view,
std::optional<std::uint32_t> expiration,
STObject const& proposedTx);

/**
* Delete a TransactionProposal ledger entry.
*
* Removes the entry from its Owner's directory, releases the reserve the
* proposal holds against the Owner, and erases the entry. Shared by every
* deletion path the spec defines (XLS-0103 §4.5): automatic cleanup when the
* proposed transaction's TicketSequence is consumed, and the
* TransactionProposalCancel / TransactionProposalSign cleanup paths once
* those transactions exist.
*
* A TransactionProposal cannot carry a reserve sponsor today (its type is
* not sponsorship-supported), so the release always lands on the Owner; it
* goes through decreaseOwnerCountForObject regardless, matching ticketDelete,
* so it would follow an sfSponsor recorded on the entry if the type ever
* becomes sponsorable.
*
* @param view The apply view for making changes
* @param sleProposal The TransactionProposal ledger entry to delete
* @param j Journal for logging
* @return tesSUCCESS, or tefBAD_LEDGER if the ledger contradicts the entry
*/
TER
deleteProposal(ApplyView& view, SLE::pointer const& sleProposal, beast::Journal j);

} // namespace xrpl::proposal
44 changes: 44 additions & 0 deletions include/xrpl/tx/transactors/proposal/TransactionProposalCancel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#pragma once

#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>

namespace xrpl {

class TransactionProposalCancel : public Transactor
{
public:
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;

explicit TransactionProposalCancel(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
82 changes: 82 additions & 0 deletions src/libxrpl/tx/transactors/proposal/ProposalHelpers.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#include <xrpl/tx/transactors/proposal/ProposalHelpers.h>

#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/TER.h>

#include <cstdint>
#include <optional>

namespace xrpl::proposal {

bool
isTerminal(
ReadView const& view,
std::optional<std::uint32_t> expiration,
STObject const& proposedTx)
{
if (hasExpired(view, expiration))
return true;

return proposedTx.isFieldPresent(sfLastLedgerSequence) &&
view.seq() >= proposedTx.getFieldU32(sfLastLedgerSequence);
}

TER
deleteProposal(ApplyView& view, SLE::pointer const& sleProposal, beast::Journal j)
{
// view carries no null contract (a reference), but the two parameters are
// bound to each other: sleProposal must be a live entry of this same
// view, since the directory removal, owner-root peek, and erase below all
// mutate that view assuming they see the entry's state.
XRPL_ASSERT(
sleProposal && sleProposal->getType() == ltTRANSACTION_PROPOSAL &&
view.exists(Keylet{ltTRANSACTION_PROPOSAL, sleProposal->key()}),
"xrpl::proposal::deleteProposal : valid proposal sle of this view");

AccountID const owner = sleProposal->getAccountID(sfOwner);

std::uint64_t const page{(*sleProposal)[sfOwnerNode]};
if (!view.dirRemove(keylet::ownerDir(owner), page, sleProposal->key(), true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete TransactionProposal from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}

auto const sleOwner = view.peek(keylet::account(owner));
if (!sleOwner)
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not find TransactionProposal owner account root.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}

// Release the reserve against the Owner or, if the entry carries a
// reserve sponsor, against that sponsor.
decreaseOwnerCountForObject(
view,
sleOwner,
sleProposal,
proposalOwnerCount(sleProposal->getFieldObject(sfProposedTransaction)),
j);

view.erase(sleProposal);
return tesSUCCESS;
}

} // namespace xrpl::proposal
Loading
Loading