mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-24 07:40:53 +00:00
Compare commits
5 Commits
pratik/std
...
ripple/cos
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0810163743 | ||
|
|
9024c6e159 | ||
|
|
7f8c43285f | ||
|
|
86135323b8 | ||
|
|
3b7cfca750 |
@@ -43,7 +43,14 @@ enum ApplyFlags : std::uint32_t {
|
||||
|
||||
// Transaction shouldn't be applied
|
||||
// Signatures shouldn't be checked
|
||||
TapDryRun = 0x1000
|
||||
TapDryRun = 0x1000,
|
||||
|
||||
// Transaction is being preflighted as the payload of a
|
||||
// TransactionProposalCreate. Its signatures are collected on-ledger
|
||||
// afterward, so signature-presence checks (e.g. Batch signer matching)
|
||||
// are skipped at proposal-creation time (On-Chain Cosigner spec
|
||||
// §5.3.1.2).
|
||||
TapProposal = 0x2000
|
||||
};
|
||||
|
||||
constexpr ApplyFlags
|
||||
|
||||
87
include/xrpl/ledger/helpers/ProposalHelpers.h
Normal file
87
include/xrpl/ledger/helpers/ProposalHelpers.h
Normal file
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl::proposal {
|
||||
|
||||
/**
|
||||
* Owner-reserve increments held by a proposal of an ordinary transaction.
|
||||
*/
|
||||
constexpr std::uint32_t kProposalOwnerCount = 5;
|
||||
|
||||
/**
|
||||
* Owner-reserve increments held by a proposal of a Batch transaction. A
|
||||
* proposed Batch stores up to eight inner transactions plus multi-account
|
||||
* signatures, so it reserves more than an ordinary proposed transaction.
|
||||
*/
|
||||
constexpr std::uint32_t kBatchProposalOwnerCount = 10;
|
||||
|
||||
/**
|
||||
* Owner-reserve increments held by a proposal of the given transaction.
|
||||
*/
|
||||
inline std::uint32_t
|
||||
proposalOwnerCount(STObject const& proposedTx)
|
||||
{
|
||||
return proposedTx.getFieldU16(sfTransactionType) == ttBATCH ? kBatchProposalOwnerCount
|
||||
: kProposalOwnerCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
inline bool
|
||||
isProposalTx(STObject const& proposedTx)
|
||||
{
|
||||
return proposedTx.getFieldU16(sfTransactionType) == ttTRANSACTION_PROPOSAL_CREATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the proposed transaction is independently submittable through the
|
||||
* ordinary multi-sign path: not a nested proposal, not a pseudo-transaction,
|
||||
* not itself flagged as someone else's inner batch transaction, and — if it
|
||||
* is a Batch — none of its own inner transactions is a nested proposal or a
|
||||
* pseudo-transaction either. A Batch inner transaction cannot itself be
|
||||
* pseudo (preflight0 rejects the pseudo/tfInnerBatchTxn combination
|
||||
* generically), but that guard lives outside this feature, so it is checked
|
||||
* again here rather than relied upon.
|
||||
*/
|
||||
bool
|
||||
isValidProposal(STObject const& proposedTx);
|
||||
|
||||
/**
|
||||
* Whether the proposed transaction carries any signature field.
|
||||
*
|
||||
* A proposal is stored in unsigned canonical form; signatures may only ever
|
||||
* arrive through TransactionProposalSign. Shared by the create-time check and
|
||||
* the invariant that guards the stored entry, so the two cannot drift apart.
|
||||
*/
|
||||
inline bool
|
||||
hasSignatureField(STObject const& proposedTx)
|
||||
{
|
||||
return proposedTx.isFieldPresent(sfTxnSignature) || proposedTx.isFieldPresent(sfSigners) ||
|
||||
proposedTx.isFieldPresent(sfBatchSigners) ||
|
||||
proposedTx.isFieldPresent(sfCounterpartySignature) ||
|
||||
proposedTx.isFieldPresent(sfSponsorSignature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the proposed transaction's SigningPubKey is present and empty, as
|
||||
* unsigned canonical form requires. An absent field is not the same as an
|
||||
* empty one, and a populated one means the payload was already signed.
|
||||
*/
|
||||
inline bool
|
||||
hasEmptySigningPubKey(STObject const& proposedTx)
|
||||
{
|
||||
return proposedTx.isFieldPresent(sfSigningPubKey) &&
|
||||
proposedTx.getFieldVL(sfSigningPubKey).empty();
|
||||
}
|
||||
|
||||
} // namespace xrpl::proposal
|
||||
@@ -187,6 +187,20 @@ check(uint256 const& key) noexcept
|
||||
}
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* A TransactionProposal
|
||||
*/
|
||||
/** @{ */
|
||||
Keylet
|
||||
txProposal(AccountID const& target, std::uint32_t ticketSequence) noexcept;
|
||||
|
||||
inline Keylet
|
||||
txProposal(uint256 const& key) noexcept
|
||||
{
|
||||
return {ltTRANSACTION_PROPOSAL, key};
|
||||
}
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* A DepositPreauth
|
||||
*/
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
// Add new amendments to the top of this list.
|
||||
// Keep it sorted in reverse chronological order.
|
||||
|
||||
XRPL_FEATURE(Cosign, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (Cleanup3_4_0, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(Sponsor, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(BatchV1_1, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -636,5 +636,24 @@ LEDGER_ENTRY(ltSPONSORSHIP, 0x0090, Sponsorship, sponsorship, ({
|
||||
{sfSponseeNode, SoeRequired},
|
||||
}))
|
||||
|
||||
/** A ledger object holding a pending transaction proposal.
|
||||
|
||||
The proposed transaction is stored unsigned in ProposedTransaction, and
|
||||
signatures accumulate on it over time. It becomes fully signed either from
|
||||
a single signature by its own Account (or that account's Delegate), or once
|
||||
the weight collected in its Signers field meets that account's quorum. At
|
||||
that point the stored transaction is one that anyone may copy and submit.
|
||||
|
||||
\sa keylet::txProposal
|
||||
*/
|
||||
LEDGER_ENTRY(ltTRANSACTION_PROPOSAL, 0x0091, TransactionProposal, transaction_proposal, ({
|
||||
{sfPreviousTxnID, SoeRequired},
|
||||
{sfPreviousTxnLgrSeq, SoeRequired},
|
||||
{sfOwner, SoeRequired},
|
||||
{sfProposedTransaction, SoeRequired},
|
||||
{sfExpiration, SoeRequired},
|
||||
{sfOwnerNode, SoeRequired},
|
||||
}))
|
||||
|
||||
#undef EXPAND
|
||||
#undef LEDGER_ENTRY_DUPLICATE
|
||||
|
||||
@@ -216,6 +216,7 @@ TYPED_SFIELD(sfLoanID, UINT256, 38)
|
||||
TYPED_SFIELD(sfReferenceHolding, UINT256, 39)
|
||||
TYPED_SFIELD(sfBlindingFactor, UINT256, 40)
|
||||
TYPED_SFIELD(sfObjectID, UINT256, 41)
|
||||
TYPED_SFIELD(sfProposalID, UINT256, 42)
|
||||
|
||||
// number (common)
|
||||
TYPED_SFIELD(sfNumber, NUMBER, 1)
|
||||
@@ -357,6 +358,7 @@ TYPED_SFIELD(sfHighSponsor, ACCOUNT, 28)
|
||||
TYPED_SFIELD(sfLowSponsor, ACCOUNT, 29)
|
||||
TYPED_SFIELD(sfCounterpartySponsor, ACCOUNT, 30)
|
||||
TYPED_SFIELD(sfSponsee, ACCOUNT, 31)
|
||||
TYPED_SFIELD(sfSigningFor, ACCOUNT, 32)
|
||||
|
||||
// vector of 256-bit
|
||||
TYPED_SFIELD(sfIndexes, VECTOR256, 1, SField::kSmdNever)
|
||||
@@ -422,6 +424,7 @@ UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35)
|
||||
UNTYPED_SFIELD(sfBook, OBJECT, 36)
|
||||
UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 37, SField::kSmdDefault, SField::kNotSigning)
|
||||
UNTYPED_SFIELD(sfSponsorSignature, OBJECT, 38, SField::kSmdDefault, SField::kNotSigning)
|
||||
UNTYPED_SFIELD(sfProposedTransaction, OBJECT, 39)
|
||||
|
||||
// array of objects (common)
|
||||
// ARRAY/1 is reserved for end of array
|
||||
|
||||
@@ -1194,6 +1194,20 @@ TRANSACTION(ttSPONSORSHIP_SET, 91, SponsorshipSet,
|
||||
{sfRemainingOwnerCount, SoeOptional},
|
||||
}))
|
||||
|
||||
/** This transaction posts an unsigned transaction on-ledger as a
|
||||
TransactionProposal, pending multi-signature collection. */
|
||||
#if TRANSACTION_INCLUDE
|
||||
# include <xrpl/tx/transactors/proposal/TransactionProposalCreate.h>
|
||||
#endif
|
||||
TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 92, TransactionProposalCreate,
|
||||
Delegation::NotDelegable,
|
||||
featureCosign,
|
||||
NoPriv,
|
||||
({
|
||||
{sfProposedTransaction, SoeRequired},
|
||||
{sfExpiration, SoeRequired},
|
||||
}))
|
||||
|
||||
/** 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,242 @@
|
||||
// This file is auto-generated. Do not edit.
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol_autogen/LedgerEntryBase.h>
|
||||
#include <xrpl/protocol_autogen/LedgerEntryBuilderBase.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::ledger_entries {
|
||||
|
||||
class TransactionProposalBuilder;
|
||||
|
||||
/**
|
||||
* @brief Ledger Entry: TransactionProposal
|
||||
*
|
||||
* Type: ltTRANSACTION_PROPOSAL (0x0091)
|
||||
* RPC Name: transaction_proposal
|
||||
*
|
||||
* Immutable wrapper around SLE providing type-safe field access.
|
||||
* Use TransactionProposalBuilder to construct new ledger entries.
|
||||
*/
|
||||
class TransactionProposal : public LedgerEntryBase
|
||||
{
|
||||
public:
|
||||
static constexpr LedgerEntryType entryType = ltTRANSACTION_PROPOSAL;
|
||||
|
||||
/**
|
||||
* @brief Construct a TransactionProposal ledger entry wrapper from an existing SLE object.
|
||||
* @throws std::runtime_error if the ledger entry type doesn't match.
|
||||
*/
|
||||
explicit TransactionProposal(SLE::const_pointer sle)
|
||||
: LedgerEntryBase(std::move(sle))
|
||||
{
|
||||
// Verify ledger entry type
|
||||
if (sle_->getType() != entryType)
|
||||
{
|
||||
throw std::runtime_error("Invalid ledger entry type for TransactionProposal");
|
||||
}
|
||||
}
|
||||
|
||||
// Ledger entry-specific field getters
|
||||
|
||||
/**
|
||||
* @brief Get sfPreviousTxnID (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_UINT256::type::value_type
|
||||
getPreviousTxnID() const
|
||||
{
|
||||
return this->sle_->at(sfPreviousTxnID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfPreviousTxnLgrSeq (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_UINT32::type::value_type
|
||||
getPreviousTxnLgrSeq() const
|
||||
{
|
||||
return this->sle_->at(sfPreviousTxnLgrSeq);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfOwner (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_ACCOUNT::type::value_type
|
||||
getOwner() const
|
||||
{
|
||||
return this->sle_->at(sfOwner);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfProposedTransaction (SoeRequired)
|
||||
* @note This is an untyped field (unknown).
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
STObject
|
||||
getProposedTransaction() const
|
||||
{
|
||||
return this->sle_->getFieldObject(sfProposedTransaction);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfExpiration (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_UINT32::type::value_type
|
||||
getExpiration() const
|
||||
{
|
||||
return this->sle_->at(sfExpiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfOwnerNode (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_UINT64::type::value_type
|
||||
getOwnerNode() const
|
||||
{
|
||||
return this->sle_->at(sfOwnerNode);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builder for TransactionProposal ledger entries.
|
||||
*
|
||||
* Provides a fluent interface for constructing ledger entries with method chaining.
|
||||
* Uses STObject internally for flexible ledger entry construction.
|
||||
* Inherits common field setters from LedgerEntryBuilderBase.
|
||||
*/
|
||||
class TransactionProposalBuilder : public LedgerEntryBuilderBase<TransactionProposalBuilder>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new TransactionProposalBuilder with required fields.
|
||||
* @param previousTxnID The sfPreviousTxnID field value.
|
||||
* @param previousTxnLgrSeq The sfPreviousTxnLgrSeq field value.
|
||||
* @param owner The sfOwner field value.
|
||||
* @param proposedTransaction The sfProposedTransaction field value.
|
||||
* @param expiration The sfExpiration field value.
|
||||
* @param ownerNode The sfOwnerNode field value.
|
||||
*/
|
||||
TransactionProposalBuilder(std::decay_t<typename SF_UINT256::type::value_type> const& previousTxnID,std::decay_t<typename SF_UINT32::type::value_type> const& previousTxnLgrSeq,std::decay_t<typename SF_ACCOUNT::type::value_type> const& owner,STObject const& proposedTransaction,std::decay_t<typename SF_UINT32::type::value_type> const& expiration,std::decay_t<typename SF_UINT64::type::value_type> const& ownerNode)
|
||||
: LedgerEntryBuilderBase<TransactionProposalBuilder>(ltTRANSACTION_PROPOSAL)
|
||||
{
|
||||
setPreviousTxnID(previousTxnID);
|
||||
setPreviousTxnLgrSeq(previousTxnLgrSeq);
|
||||
setOwner(owner);
|
||||
setProposedTransaction(proposedTransaction);
|
||||
setExpiration(expiration);
|
||||
setOwnerNode(ownerNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a TransactionProposalBuilder from an existing SLE object.
|
||||
* @param sle The existing ledger entry to copy from.
|
||||
* @throws std::runtime_error if the ledger entry type doesn't match.
|
||||
*/
|
||||
TransactionProposalBuilder(SLE::const_pointer sle)
|
||||
{
|
||||
if (sle->at(sfLedgerEntryType) != ltTRANSACTION_PROPOSAL)
|
||||
{
|
||||
throw std::runtime_error("Invalid ledger entry type for TransactionProposal");
|
||||
}
|
||||
object_ = *sle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Ledger entry-specific field setters
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Set sfPreviousTxnID (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
TransactionProposalBuilder&
|
||||
setPreviousTxnID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
|
||||
{
|
||||
object_[sfPreviousTxnID] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfPreviousTxnLgrSeq (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
TransactionProposalBuilder&
|
||||
setPreviousTxnLgrSeq(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfPreviousTxnLgrSeq] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfOwner (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
TransactionProposalBuilder&
|
||||
setOwner(std::decay_t<typename SF_ACCOUNT::type::value_type> const& value)
|
||||
{
|
||||
object_[sfOwner] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfProposedTransaction (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
TransactionProposalBuilder&
|
||||
setProposedTransaction(STObject const& value)
|
||||
{
|
||||
object_.setFieldObject(sfProposedTransaction, value);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfExpiration (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
TransactionProposalBuilder&
|
||||
setExpiration(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfExpiration] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfOwnerNode (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
TransactionProposalBuilder&
|
||||
setOwnerNode(std::decay_t<typename SF_UINT64::type::value_type> const& value)
|
||||
{
|
||||
object_[sfOwnerNode] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the completed TransactionProposal wrapper.
|
||||
* @param index The ledger entry index.
|
||||
* @return The constructed ledger entry wrapper.
|
||||
*/
|
||||
TransactionProposal
|
||||
build(uint256 const& index)
|
||||
{
|
||||
return TransactionProposal{std::make_shared<SLE>(std::move(object_), index)};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::ledger_entries
|
||||
@@ -0,0 +1,155 @@
|
||||
// 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 TransactionProposalCreateBuilder;
|
||||
|
||||
/**
|
||||
* @brief Transaction: TransactionProposalCreate
|
||||
*
|
||||
* Type: ttTRANSACTION_PROPOSAL_CREATE (92)
|
||||
* Delegable: Delegation::NotDelegable
|
||||
* Amendment: featureCosign
|
||||
* Privileges: NoPriv
|
||||
*
|
||||
* Immutable wrapper around STTx providing type-safe field access.
|
||||
* Use TransactionProposalCreateBuilder to construct new transactions.
|
||||
*/
|
||||
class TransactionProposalCreate : public TransactionBase
|
||||
{
|
||||
public:
|
||||
static constexpr xrpl::TxType txType = ttTRANSACTION_PROPOSAL_CREATE;
|
||||
|
||||
/**
|
||||
* @brief Construct a TransactionProposalCreate transaction wrapper from an existing STTx object.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
explicit TransactionProposalCreate(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 TransactionProposalCreate");
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction-specific field getters
|
||||
/**
|
||||
* @brief Get sfProposedTransaction (SoeRequired)
|
||||
* @note This is an untyped field.
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
STObject
|
||||
getProposedTransaction() const
|
||||
{
|
||||
return this->tx_->getFieldObject(sfProposedTransaction);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfExpiration (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_UINT32::type::value_type
|
||||
getExpiration() const
|
||||
{
|
||||
return this->tx_->at(sfExpiration);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builder for TransactionProposalCreate 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 TransactionProposalCreateBuilder : public TransactionBuilderBase<TransactionProposalCreateBuilder>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new TransactionProposalCreateBuilder with required fields.
|
||||
* @param account The account initiating the transaction.
|
||||
* @param proposedTransaction The sfProposedTransaction field value.
|
||||
* @param expiration The sfExpiration field value.
|
||||
* @param sequence Optional sequence number for the transaction.
|
||||
* @param fee Optional fee for the transaction.
|
||||
*/
|
||||
TransactionProposalCreateBuilder(SF_ACCOUNT::type::value_type account,
|
||||
STObject const& proposedTransaction, std::decay_t<typename SF_UINT32::type::value_type> const& expiration, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
|
||||
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
|
||||
)
|
||||
: TransactionBuilderBase<TransactionProposalCreateBuilder>(ttTRANSACTION_PROPOSAL_CREATE, account, sequence, fee)
|
||||
{
|
||||
setProposedTransaction(proposedTransaction);
|
||||
setExpiration(expiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a TransactionProposalCreateBuilder from an existing STTx object.
|
||||
* @param tx The existing transaction to copy from.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
TransactionProposalCreateBuilder(std::shared_ptr<STTx const> tx)
|
||||
{
|
||||
if (tx->getTxnType() != ttTRANSACTION_PROPOSAL_CREATE)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for TransactionProposalCreateBuilder");
|
||||
}
|
||||
object_ = *tx;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Transaction-specific field setters
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Set sfProposedTransaction (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
TransactionProposalCreateBuilder&
|
||||
setProposedTransaction(STObject const& value)
|
||||
{
|
||||
object_.setFieldObject(sfProposedTransaction, value);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfExpiration (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
TransactionProposalCreateBuilder&
|
||||
setExpiration(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfExpiration] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the TransactionProposalCreate wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
* @param secretKey The secret key for signing.
|
||||
* @return The constructed transaction wrapper.
|
||||
*/
|
||||
TransactionProposalCreate
|
||||
build(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
{
|
||||
sign(publicKey, secretKey);
|
||||
return TransactionProposalCreate{std::make_shared<STTx>(std::move(object_))};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::transactions
|
||||
@@ -59,6 +59,10 @@ public:
|
||||
, j(j)
|
||||
{
|
||||
XRPL_ASSERT((flags & TapBatch) == TapBatch, "Batch apply flag should be set");
|
||||
XRPL_ASSERT_IF(
|
||||
(flags & TapProposal) != TapNone,
|
||||
(flags & TapDryRun) != TapNone,
|
||||
"xrpl::PreflightContext : proposal preflight implies dry run");
|
||||
}
|
||||
|
||||
PreflightContext(
|
||||
@@ -70,6 +74,10 @@ public:
|
||||
: registry(registry), tx(tx), rules(std::move(rules)), flags(flags), j(j)
|
||||
{
|
||||
XRPL_ASSERT((flags & TapBatch) == 0, "Batch apply flag should not be set");
|
||||
XRPL_ASSERT_IF(
|
||||
(flags & TapProposal) != TapNone,
|
||||
(flags & TapDryRun) != TapNone,
|
||||
"xrpl::PreflightContext : proposal preflight implies dry run");
|
||||
}
|
||||
|
||||
PreflightContext&
|
||||
|
||||
@@ -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 TransactionProposalCreate : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
|
||||
|
||||
explicit TransactionProposalCreate(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
|
||||
39
src/libxrpl/ledger/helpers/ProposalHelpers.cpp
Normal file
39
src/libxrpl/ledger/helpers/ProposalHelpers.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#include <xrpl/ledger/helpers/ProposalHelpers.h>
|
||||
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
|
||||
namespace xrpl::proposal {
|
||||
|
||||
bool
|
||||
isValidProposal(STObject const& proposedTx)
|
||||
{
|
||||
if (isProposalTx(proposedTx))
|
||||
return false;
|
||||
|
||||
if (isPseudoTx(proposedTx))
|
||||
return false;
|
||||
|
||||
if (proposedTx.isFieldPresent(sfFlags) &&
|
||||
(proposedTx.getFieldU32(sfFlags) & tfInnerBatchTxn) != 0u)
|
||||
return false;
|
||||
|
||||
if (proposedTx.getFieldU16(sfTransactionType) == ttBATCH &&
|
||||
proposedTx.isFieldPresent(sfRawTransactions))
|
||||
{
|
||||
STArray const& innerTxns = proposedTx.getFieldArray(sfRawTransactions);
|
||||
for (STObject const& inner : innerTxns)
|
||||
{
|
||||
if (isProposalTx(inner) || isPseudoTx(inner))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl::proposal
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/OracleHelpers.h>
|
||||
#include <xrpl/ledger/helpers/ProposalHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -56,6 +57,7 @@ isReserveSponsorAllowed(TxType txType)
|
||||
ttACCOUNT_SET,
|
||||
ttREGULAR_KEY_SET,
|
||||
ttSPONSORSHIP_TRANSFER,
|
||||
ttTRANSACTION_PROPOSAL_CREATE,
|
||||
};
|
||||
return kReserveSponsorAllowed.contains(txType);
|
||||
}
|
||||
@@ -255,6 +257,8 @@ isLedgerEntryOwner(ReadView const& view, SLE const& sle, AccountID const& accoun
|
||||
// to tecNO_PERMISSION.
|
||||
return false;
|
||||
}
|
||||
case ltTRANSACTION_PROPOSAL:
|
||||
return sle.getAccountID(sfOwner) == account;
|
||||
default:
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::isLedgerEntryOwner : object is not supported by sponsorship.");
|
||||
@@ -278,6 +282,7 @@ isLedgerEntrySupportedBySponsorship(SLE const& sle)
|
||||
case ltSIGNER_LIST:
|
||||
case ltCREDENTIAL:
|
||||
case ltRIPPLE_STATE:
|
||||
case ltTRANSACTION_PROPOSAL:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -304,6 +309,11 @@ getLedgerEntryOwnerCount(SLE const& sle)
|
||||
return 1;
|
||||
return 2 + static_cast<std::uint32_t>(sle.getFieldArray(sfSignerEntries).size());
|
||||
}
|
||||
case ltTRANSACTION_PROPOSAL:
|
||||
// Mirror TransactionProposalCreate's own reserve sizing so that
|
||||
// creation and sponsorship accounting agree: a proposed Batch
|
||||
// reserves more than an ordinary proposal.
|
||||
return proposal::proposalOwnerCount(sle.getFieldObject(sfProposedTransaction));
|
||||
case ltACCOUNT_ROOT:
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("AccountRoots are not supported by object sponsorship.");
|
||||
|
||||
@@ -104,6 +104,7 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
LoanBroker = 'l', // lower-case L
|
||||
Loan = 'L',
|
||||
Sponsorship = '>',
|
||||
TransactionProposal = 'y',
|
||||
|
||||
// No longer used or supported. Left here to reserve the space to avoid accidental reuse.
|
||||
Contract [[deprecated]] = 'c',
|
||||
@@ -358,6 +359,14 @@ check(AccountID const& id, std::uint32_t seq) noexcept
|
||||
return {ltCHECK, indexHash(LedgerNameSpace::Check, id, seq)};
|
||||
}
|
||||
|
||||
Keylet
|
||||
txProposal(AccountID const& target, std::uint32_t ticketSequence) noexcept
|
||||
{
|
||||
return {
|
||||
ltTRANSACTION_PROPOSAL,
|
||||
indexHash(LedgerNameSpace::TransactionProposal, target, ticketSequence)};
|
||||
}
|
||||
|
||||
Keylet
|
||||
depositPreauth(AccountID const& owner, AccountID const& preauthorized) noexcept
|
||||
{
|
||||
|
||||
@@ -324,6 +324,17 @@ preflight(
|
||||
beast::Journal j)
|
||||
{
|
||||
PreflightContext const pfCtx(registry, tx, rules, flags, j);
|
||||
|
||||
// XRPL_ASSERT_IF in the PreflightContext constructor only fires in debug
|
||||
// builds; re-check the same invariant here so a release build can't
|
||||
// silently skip a proposed transaction's signature-presence checks
|
||||
// outside of a dry run.
|
||||
if ((flags & TapProposal) != TapNone && (flags & TapDryRun) == TapNone)
|
||||
{
|
||||
JLOG(j.fatal()) << "apply (preflight): TapProposal set without TapDryRun.";
|
||||
return {pfCtx, {tefEXCEPTION, TxConsequences{tx}}};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return {pfCtx, invokePreflight(pfCtx)};
|
||||
@@ -345,6 +356,14 @@ preflight(
|
||||
beast::Journal j)
|
||||
{
|
||||
PreflightContext const pfCtx(registry, tx, parentBatchId, rules, flags, j);
|
||||
|
||||
// See the comment in the other preflight() overload above.
|
||||
if ((flags & TapProposal) != TapNone && (flags & TapDryRun) == TapNone)
|
||||
{
|
||||
JLOG(j.fatal()) << "apply (preflight): TapProposal set without TapDryRun.";
|
||||
return {pfCtx, {tefEXCEPTION, TxConsequences{tx}}};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return {pfCtx, invokePreflight(pfCtx)};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
@@ -79,7 +80,10 @@ LoanSet::preflight(PreflightContext const& ctx)
|
||||
return tx.getFieldObject(sfCounterpartySignature);
|
||||
return std::nullopt;
|
||||
}();
|
||||
if (!tx.isFlag(tfInnerBatchTxn) && !counterPartySig)
|
||||
// A proposed LoanSet is stored unsigned; its CounterpartySignature is
|
||||
// collected on-ledger afterward, so its absence here is expected, not an
|
||||
// error (On-Chain Cosigner spec §5.3.1.2).
|
||||
if (!tx.isFlag(tfInnerBatchTxn) && !counterPartySig && (ctx.flags & TapProposal) == 0)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanSet transaction must have a CounterpartySignature.";
|
||||
return temBAD_SIGNER;
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
#include <xrpl/tx/transactors/proposal/TransactionProposalCreate.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DelegateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/ProposalHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/SignerEntries.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
#include <xrpl/tx/applySteps.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
NotTEC
|
||||
TransactionProposalCreate::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (ctx.tx[sfExpiration] == 0)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "TransactionProposalCreate: zero expiration.";
|
||||
return temBAD_EXPIRATION;
|
||||
}
|
||||
|
||||
STObject const proposedTx = ctx.tx.getFieldObject(sfProposedTransaction);
|
||||
|
||||
if (!proposedTx.isFieldPresent(sfTransactionType) || !proposedTx.isFieldPresent(sfAccount))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn "
|
||||
"lacks TransactionType or Account.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// The proposed transaction must be independently submittable through the
|
||||
// ordinary multi-sign path: no nested proposals, no pseudo-transactions,
|
||||
// no batch inner transactions — and, if it is a Batch, none of its own
|
||||
// inner transactions may be a nested proposal or a pseudo-transaction
|
||||
// either.
|
||||
if (!proposal::isValidProposal(proposedTx))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn is not "
|
||||
"independently submittable.";
|
||||
return temINVALID;
|
||||
}
|
||||
|
||||
// The proposed transaction is stored in its unsigned canonical form; the
|
||||
// ledger populates its signature fields as contributions arrive.
|
||||
if (proposal::hasSignatureField(proposedTx))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn "
|
||||
"carries signature fields.";
|
||||
return temBAD_SIGNER;
|
||||
}
|
||||
|
||||
if (!proposal::hasEmptySigningPubKey(proposedTx))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn "
|
||||
"SigningPubKey must be present and empty.";
|
||||
return temBAD_SIGNER;
|
||||
}
|
||||
|
||||
// The proposed transaction's fee is charged to the target account when
|
||||
// the completed transaction is submitted, so it must be fixed now.
|
||||
if (!proposedTx.isFieldPresent(sfFee) || !proposedTx.isFieldPresent(sfSequence))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn "
|
||||
"lacks Fee or Sequence.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// The proposed transaction must be ticket-based: it must carry a
|
||||
// TicketSequence and must not use a live Sequence. Sequence is a required
|
||||
// common field, so "no Sequence" is expressed as a Sequence of 0 rather
|
||||
// than an absent field. A ticket decouples the proposal from the target
|
||||
// account's live sequence, so unrelated target-account activity cannot
|
||||
// invalidate it while signatures are collected (On-Chain Cosigner spec
|
||||
// §4.2.1).
|
||||
if (!proposedTx.isFieldPresent(sfTicketSequence) || proposedTx.getFieldU32(sfSequence) != 0)
|
||||
return temSEQ_AND_TICKET;
|
||||
|
||||
// The proposed transaction must pass its own static checks under the
|
||||
// current rules, so no statically-dead proposal can be stored. TapDryRun
|
||||
// accepts the unsigned canonical form without a signature check;
|
||||
// TapProposal additionally skips signature-presence checks (e.g. Batch
|
||||
// signer matching), which are deferred to submission time (On-Chain
|
||||
// Cosigner spec §5.3.1.2).
|
||||
try
|
||||
{
|
||||
STTx const stx{STObject{proposedTx}};
|
||||
auto const inner =
|
||||
xrpl::preflight(ctx.registry, ctx.rules, stx, TapDryRun | TapProposal, ctx.j);
|
||||
if (!isTesSuccess(inner.ter))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn "
|
||||
"failed preflight: "
|
||||
<< transHuman(inner.ter);
|
||||
// Surface the proposed transaction type's own preflight code
|
||||
// rather than collapsing it to a generic error (On-Chain Cosigner
|
||||
// spec §5.3.1).
|
||||
return inner.ter;
|
||||
}
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn is "
|
||||
"malformed: "
|
||||
<< e.what();
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
TransactionProposalCreate::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
if (hasExpired(ctx.view, ctx.tx[~sfExpiration]))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "TransactionProposalCreate: already expired.";
|
||||
return tecEXPIRED;
|
||||
}
|
||||
|
||||
auto const proposedTx = ctx.tx.getFieldObject(sfProposedTransaction);
|
||||
|
||||
// Once the proposed transaction's own ledger bound has passed it can never
|
||||
// be applied, so the proposal is dead on arrival. The bound is the one the
|
||||
// ordinary path uses for tefMAX_LEDGER: the last ledger in which the
|
||||
// proposed transaction may still be submitted (On-Chain Cosigner spec
|
||||
// §4.5).
|
||||
if (proposedTx.isFieldPresent(sfLastLedgerSequence) &&
|
||||
proposedTx.getFieldU32(sfLastLedgerSequence) <= ctx.view.seq())
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn "
|
||||
"LastLedgerSequence has passed.";
|
||||
return tecEXPIRED;
|
||||
}
|
||||
|
||||
AccountID const target = proposedTx.getAccountID(sfAccount);
|
||||
auto const sleTarget = ctx.view.read(keylet::account(target));
|
||||
if (!sleTarget)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "TransactionProposalCreate: target account "
|
||||
"does not exist.";
|
||||
return tecNO_TARGET;
|
||||
}
|
||||
|
||||
// A pseudo-account cannot authorize a transaction through a SignerList.
|
||||
if (isPseudoAccount(sleTarget))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Only the target account itself, an account on its SignerList, or (if
|
||||
// the proposed transaction's own type has been delegated by the target,
|
||||
// Permission Delegation / XLS-75) that delegate or an account on the
|
||||
// delegate's own SignerList, may create a proposal against it. Otherwise
|
||||
// any account could spam or squat the target's Tickets with unwanted
|
||||
// proposals (On-Chain Cosigner V1 scope).
|
||||
if (AccountID const proposer = ctx.tx.getAccountID(sfAccount); proposer != target)
|
||||
{
|
||||
// Whether `proposer` is `account` itself or an entry on `account`'s
|
||||
// applicable SignerList.
|
||||
auto isAuthorizedFor = [&](AccountID const& account) -> std::expected<bool, TER> {
|
||||
if (proposer == account)
|
||||
return true;
|
||||
|
||||
auto const sleSigners = ctx.view.read(keylet::signerList(account));
|
||||
if (!sleSigners)
|
||||
return false;
|
||||
|
||||
auto const accountSigners = SignerEntries::deserialize(*sleSigners, ctx.j, "ledger");
|
||||
if (!accountSigners)
|
||||
return std::unexpected(TER{accountSigners.error()});
|
||||
|
||||
return std::ranges::any_of(
|
||||
*accountSigners, [&](auto const& entry) { return entry.account == proposer; });
|
||||
};
|
||||
|
||||
auto isSigner = isAuthorizedFor(target);
|
||||
if (!isSigner)
|
||||
return isSigner.error();
|
||||
|
||||
// A delegate that the target has granted permission over the
|
||||
// proposed transaction's own type — or one of that delegate's own
|
||||
// signers — is equally authorized: it will need to help complete
|
||||
// the proposed transaction's own authorization anyway once the
|
||||
// proposal is submitted.
|
||||
if (!*isSigner && proposedTx.isFieldPresent(sfDelegate))
|
||||
{
|
||||
AccountID const delegateAccount = proposedTx.getAccountID(sfDelegate);
|
||||
// NOLINTNEXTLINE(readability-suspicious-call-argument)
|
||||
auto const sleDelegate = ctx.view.read(keylet::delegate(target, delegateAccount));
|
||||
if (sleDelegate &&
|
||||
isTesSuccess(checkTxPermission(sleDelegate, STTx{STObject{proposedTx}})))
|
||||
{
|
||||
isSigner = isAuthorizedFor(delegateAccount);
|
||||
if (!isSigner)
|
||||
return isSigner.error();
|
||||
}
|
||||
}
|
||||
|
||||
if (!*isSigner)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposer is "
|
||||
"not the target account, one of its "
|
||||
"signers, or an authorized delegate.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
}
|
||||
|
||||
std::uint32_t const ticketSequence = proposedTx.getFieldU32(sfTicketSequence);
|
||||
|
||||
// The proposal reserves the ticket for as long as it exists (On-Chain
|
||||
// Cosigner spec §4.2.1, §5.3.2): a ticket that doesn't exist yet can't be
|
||||
// reserved.
|
||||
if (!ctx.view.exists(keylet::ticket(target, ticketSequence)))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "TransactionProposalCreate: target ticket "
|
||||
"does not exist.";
|
||||
return tefNO_TICKET;
|
||||
}
|
||||
|
||||
if (ctx.view.exists(keylet::txProposal(target, ticketSequence)))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "TransactionProposalCreate: duplicate proposal.";
|
||||
return tecDUPLICATE;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
TransactionProposalCreate::doApply()
|
||||
{
|
||||
auto const sle = view().peek(keylet::account(accountID_));
|
||||
if (!sle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const proposedTx = ctx_.tx.getFieldObject(sfProposedTransaction);
|
||||
std::uint32_t const ownerCount = proposal::proposalOwnerCount(proposedTx);
|
||||
|
||||
// The proposal holds a full transaction plus its collected signatures, so
|
||||
// it reserves more than a typical ledger entry (5 increments; 10 for a
|
||||
// proposed Batch).
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
sle,
|
||||
preFeeBalance_,
|
||||
{.ownerCountDelta = static_cast<int>(ownerCount)},
|
||||
ctx_.journal);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
AccountID const target = proposedTx.getAccountID(sfAccount);
|
||||
std::uint32_t const ticketSequence = proposedTx.getFieldU32(sfTicketSequence);
|
||||
|
||||
Keylet const proposalKeylet = keylet::txProposal(target, ticketSequence);
|
||||
auto sleProposal = std::make_shared<SLE>(proposalKeylet);
|
||||
sleProposal->setAccountID(sfOwner, accountID_);
|
||||
sleProposal->setFieldObject(sfProposedTransaction, proposedTx);
|
||||
sleProposal->setFieldU32(sfExpiration, ctx_.tx[sfExpiration]);
|
||||
|
||||
view().insert(sleProposal);
|
||||
|
||||
auto viewJ = ctx_.registry.get().getJournal("View");
|
||||
{
|
||||
auto const page = view().dirInsert(
|
||||
keylet::ownerDir(accountID_), proposalKeylet, describeOwnerDir(accountID_));
|
||||
if (!page)
|
||||
return tecDIR_FULL; // LCOV_EXCL_LINE
|
||||
sleProposal->setFieldU64(sfOwnerNode, *page);
|
||||
}
|
||||
|
||||
increaseOwnerCount(ctx_.getApplyViewContext(), sle, ownerCount, viewJ);
|
||||
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sleProposal);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
TransactionProposalCreate::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
|
||||
{
|
||||
// No transaction-specific invariants yet (future work). Object-level
|
||||
// invariants for the TransactionProposal ledger entry (unsigned canonical
|
||||
// form, non-zero Expiration, correct ProposalID key, sorted/unique signer
|
||||
// arrays) belong in a protocol-level ValidTransactionProposal check.
|
||||
}
|
||||
|
||||
bool
|
||||
TransactionProposalCreate::finalizeInvariants(
|
||||
STTx const&,
|
||||
TER,
|
||||
XRPAmount,
|
||||
ReadView const&,
|
||||
beast::Journal const&)
|
||||
{
|
||||
// No transaction-specific invariants yet (future work).
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -199,7 +199,11 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx)
|
||||
bool const isAccountReserveSponsorship =
|
||||
isCreateOrReassign && reserveSponsor && !ctx.tx.isFieldPresent(sfObjectID);
|
||||
|
||||
if (isAccountReserveSponsorship && !ctx.tx.isFieldPresent(sfSponsorSignature))
|
||||
// A proposed SponsorshipTransfer is stored unsigned; its SponsorSignature
|
||||
// is collected on-ledger afterward, so its absence here is expected, not
|
||||
// an error (On-Chain Cosigner spec §5.3.1.2).
|
||||
if (isAccountReserveSponsorship && !ctx.tx.isFieldPresent(sfSponsorSignature) &&
|
||||
(ctx.flags & TapProposal) == 0)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "preflight: account sponsorship requires sfSponsorSignature";
|
||||
return temMALFORMED;
|
||||
|
||||
@@ -406,6 +406,13 @@ Batch::preflightSigValidated(PreflightContext const& ctx)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
ctx.tx.getTxnType() == ttBATCH, "xrpl::Batch::preflightSigValidated : batch transaction");
|
||||
|
||||
// A proposed Batch is stored unsigned; its BatchSigners are collected
|
||||
// on-ledger afterward, so the signer-presence match belongs to submission
|
||||
// time, not proposal creation (On-Chain Cosigner spec §5.3.1.2).
|
||||
if ((ctx.flags & TapProposal) != 0)
|
||||
return tesSUCCESS;
|
||||
|
||||
auto const parentBatchId = ctx.tx.getTransactionID();
|
||||
auto const outerAccount = ctx.tx.getAccountID(sfAccount);
|
||||
// Accounts that must sign the batch: each inner authorizer and counterparty
|
||||
|
||||
1210
src/test/app/TransactionProposalCreate_test.cpp
Normal file
1210
src/test/app/TransactionProposalCreate_test.cpp
Normal file
File diff suppressed because it is too large
Load Diff
135
src/test/jtx/impl/proposal.cpp
Normal file
135
src/test/jtx/impl/proposal.cpp
Normal file
@@ -0,0 +1,135 @@
|
||||
#include <test/jtx/proposal.h>
|
||||
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/batch.h>
|
||||
#include <test/jtx/multisign.h>
|
||||
#include <test/jtx/ticket.h>
|
||||
#include <test/jtx/utility.h>
|
||||
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test::jtx::proposal {
|
||||
|
||||
json::Value
|
||||
create(Account const& proposer, json::Value const& proposedTx, std::uint32_t expiration)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::TransactionType] = "TransactionProposalCreate";
|
||||
jv[jss::Account] = proposer.human();
|
||||
jv[sfProposedTransaction.jsonName] = proposedTx;
|
||||
jv[sfExpiration.jsonName] = expiration;
|
||||
return jv;
|
||||
}
|
||||
|
||||
json::Value
|
||||
unsignedPayload(Env const& env, json::Value tx, std::uint32_t ticketSeq)
|
||||
{
|
||||
// Unsigned canonical form: an empty SigningPubKey and no signature fields
|
||||
// at all. Signatures may only ever arrive through TransactionProposalSign.
|
||||
tx[jss::SigningPubKey] = "";
|
||||
|
||||
// Ticket-based rather than sequence-based. Sequence is a required common
|
||||
// field, so "no Sequence" is expressed as a Sequence of 0.
|
||||
tx[jss::Sequence] = 0;
|
||||
tx[sfTicketSequence.jsonName] = ticketSeq;
|
||||
|
||||
// The target account pays this fee when the completed transaction is
|
||||
// submitted, so it is fixed now. A fee already chosen by the caller stands.
|
||||
fillFee(tx, *env.current());
|
||||
|
||||
return tx;
|
||||
}
|
||||
|
||||
json::Value
|
||||
innerTx(json::Value tx, std::uint32_t seq)
|
||||
{
|
||||
return batch::Inner{std::move(tx), seq}.getTxn();
|
||||
}
|
||||
|
||||
json::Value
|
||||
unsignedBatch(
|
||||
Env const& env,
|
||||
Account const& target,
|
||||
std::uint32_t ticketSeq,
|
||||
std::uint32_t flags,
|
||||
std::vector<json::Value> const& inners,
|
||||
std::optional<std::uint32_t> numSigners)
|
||||
{
|
||||
// Each inner account other than the outer one will contribute one
|
||||
// BatchSigners entry once the signatures are collected, and the outer fee
|
||||
// has to cover them from the start (Batch::calculateBaseFee).
|
||||
std::uint32_t const signers = numSigners ? *numSigners : [&]() {
|
||||
std::set<std::string> participants;
|
||||
for (auto const& inner : inners)
|
||||
{
|
||||
if (auto const account = inner[jss::Account].asString(); account != target.human())
|
||||
participants.insert(account);
|
||||
}
|
||||
return static_cast<std::uint32_t>(participants.size());
|
||||
}();
|
||||
|
||||
json::Value jv = batch::outer(
|
||||
target,
|
||||
0,
|
||||
batch::calcBatchFee(env, signers, static_cast<std::uint32_t>(inners.size())),
|
||||
flags);
|
||||
|
||||
json::Value& rawTransactions = jv[jss::RawTransactions];
|
||||
for (auto const& inner : inners)
|
||||
rawTransactions[rawTransactions.size()][jss::RawTransaction] = inner;
|
||||
|
||||
return unsignedPayload(env, std::move(jv), ticketSeq);
|
||||
}
|
||||
|
||||
void
|
||||
authorizeProposer(Env& env, Account const& target, Account const& proposer)
|
||||
{
|
||||
env(signers(target, 1, {{proposer, 1}}));
|
||||
env.close();
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
createTicket(Env& env, Account const& account, std::uint32_t count)
|
||||
{
|
||||
// The tickets a TicketCreate makes are numbered from the sequence that
|
||||
// follows the one it consumes.
|
||||
std::uint32_t const firstTicketSeq = env.seq(account) + 1;
|
||||
env(ticket::create(account, count));
|
||||
env.close();
|
||||
return firstTicketSeq;
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
expiration(Env& env, NetClock::duration delta)
|
||||
{
|
||||
return (env.now() + delta).time_since_epoch().count();
|
||||
}
|
||||
|
||||
SLE::const_pointer
|
||||
entry(Env const& env, AccountID const& target, std::uint32_t ticketSeq)
|
||||
{
|
||||
return env.le(keylet::txProposal(target, ticketSeq));
|
||||
}
|
||||
|
||||
SLE::const_pointer
|
||||
entry(Env const& env, Account const& target, std::uint32_t ticketSeq)
|
||||
{
|
||||
return entry(env, target.id(), ticketSeq);
|
||||
}
|
||||
|
||||
} // namespace xrpl::test::jtx::proposal
|
||||
150
src/test/jtx/proposal.h
Normal file
150
src/test/jtx/proposal.h
Normal file
@@ -0,0 +1,150 @@
|
||||
#pragma once
|
||||
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/ledger/helpers/ProposalHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* @brief Helpers for constructing TransactionProposal test transactions.
|
||||
*/
|
||||
namespace xrpl::test::jtx::proposal {
|
||||
|
||||
// The owner-reserve increments a proposal holds against its proposer. Tests
|
||||
// spend these rather than repeating their values, so they follow the transactor
|
||||
// instead of checking it; TransactionProposalCreate_test pins the values
|
||||
// themselves, so a change to them has to be a deliberate one.
|
||||
using xrpl::proposal::kBatchProposalOwnerCount;
|
||||
using xrpl::proposal::kProposalOwnerCount;
|
||||
|
||||
/**
|
||||
* @brief Build a TransactionProposalCreate carrying an unsigned proposed
|
||||
* transaction.
|
||||
*
|
||||
* @param proposer The account creating and paying the reserve for the proposal.
|
||||
* @param proposedTx The proposed transaction, in unsigned canonical form; see
|
||||
* unsignedPayload().
|
||||
* @param expiration Absolute time after which the proposal may no longer be
|
||||
* completed.
|
||||
* @return The TransactionProposalCreate JSON object.
|
||||
*/
|
||||
json::Value
|
||||
create(Account const& proposer, json::Value const& proposedTx, std::uint32_t expiration);
|
||||
|
||||
/**
|
||||
* @brief Put a transaction of any type into the form a proposal stores it in:
|
||||
* unsigned and ticket-based, with the fee the target account will pay fixed
|
||||
* now.
|
||||
*
|
||||
* This takes whatever any jtx generator produces — @c pay(), @c loan::set(),
|
||||
* @c sponsor::transfer(), an outer @c Batch — and applies only what the
|
||||
* proposal itself demands of the payload, so a test never hand-rolls those
|
||||
* fields. It leaves the rest of @p tx untouched, which is what lets a test
|
||||
* build one valid payload and then break exactly one rule of it.
|
||||
*
|
||||
* The payload is ticket-based so unrelated activity on the target account
|
||||
* cannot invalidate it while signatures are collected. A Fee already set on
|
||||
* @p tx is left alone, so a payload whose fee is not the base fee — a Batch,
|
||||
* say — can carry its own; otherwise the fee is filled in the same way as for
|
||||
* an ordinary submission.
|
||||
*
|
||||
* @param env The test environment providing ledger fee settings.
|
||||
* @param tx The transaction to propose.
|
||||
* @param ticketSeq A ticket sequence owned by @p tx's account.
|
||||
* @return The proposed transaction JSON object.
|
||||
*/
|
||||
json::Value
|
||||
unsignedPayload(Env const& env, json::Value tx, std::uint32_t ticketSeq);
|
||||
|
||||
/**
|
||||
* @brief Put a transaction into the form an inner transaction of a proposed
|
||||
* Batch takes, as @c batch::Inner does for an ordinary Batch.
|
||||
*
|
||||
* @param tx The transaction to nest.
|
||||
* @param seq The sequence number of @p tx's own account.
|
||||
* @return The inner transaction JSON object.
|
||||
*/
|
||||
json::Value
|
||||
innerTx(json::Value tx, std::uint32_t seq);
|
||||
|
||||
/**
|
||||
* @brief An unsigned outer Batch payload holding @p inners.
|
||||
*
|
||||
* A proposed Batch stores no BatchSigners — the participants' signatures are
|
||||
* collected on-ledger afterwards — but its fee is fixed now and must already
|
||||
* cover them. By default one signer is assumed for each inner account other
|
||||
* than @p target, which is what those participants will contribute.
|
||||
*
|
||||
* @param env The test environment providing ledger fee settings.
|
||||
* @param target The outer account of the Batch, and the proposal's target.
|
||||
* @param ticketSeq A ticket sequence owned by @p target.
|
||||
* @param flags The Batch mode flags, e.g. @c tfAllOrNothing.
|
||||
* @param inners The inner transactions; see innerTx().
|
||||
* @param numSigners Overrides the number of signatures the fee accounts for.
|
||||
* @return The proposed Batch JSON object.
|
||||
*/
|
||||
json::Value
|
||||
unsignedBatch(
|
||||
Env const& env,
|
||||
Account const& target,
|
||||
std::uint32_t ticketSeq,
|
||||
std::uint32_t flags,
|
||||
std::vector<json::Value> const& inners,
|
||||
std::optional<std::uint32_t> numSigners = std::nullopt);
|
||||
|
||||
/**
|
||||
* @brief Give @p proposer a place on @p target's SignerList, so it may
|
||||
* create proposals against @p target.
|
||||
*
|
||||
* Only the target account itself, or an account on its SignerList, may
|
||||
* create a TransactionProposalCreate against it (On-Chain Cosigner V1
|
||||
* authorization). Sets a minimal one-signer, quorum-1 list; the quorum
|
||||
* itself is irrelevant here since it only governs the proposed
|
||||
* transaction's own completion, not who may propose it.
|
||||
*
|
||||
* @param env The test environment.
|
||||
* @param target The account whose SignerList is set.
|
||||
* @param proposer The account to add to it.
|
||||
*/
|
||||
void
|
||||
authorizeProposer(Env& env, Account const& target, Account const& proposer);
|
||||
|
||||
/**
|
||||
* @brief Create tickets for a proposal to be built against, and close the
|
||||
* ledger.
|
||||
*
|
||||
* @param env The test environment.
|
||||
* @param account The account that will own the tickets, i.e. the target of the
|
||||
* proposals to come.
|
||||
* @param count How many tickets to create.
|
||||
* @return The first ticket sequence created; the rest follow it.
|
||||
*/
|
||||
std::uint32_t
|
||||
createTicket(Env& env, Account const& account, std::uint32_t count = 1);
|
||||
|
||||
/**
|
||||
* @brief An absolute expiration @p delta past the environment's current time.
|
||||
*
|
||||
* @c expiration(env, 0s) is an expiration that has already passed.
|
||||
*/
|
||||
std::uint32_t
|
||||
expiration(Env& env, NetClock::duration delta);
|
||||
|
||||
/**
|
||||
* @brief The proposal stored against a target account's ticket.
|
||||
* @return empty if no such proposal exists.
|
||||
*/
|
||||
[[nodiscard]] SLE::const_pointer
|
||||
entry(Env const& env, AccountID const& target, std::uint32_t ticketSeq);
|
||||
[[nodiscard]] SLE::const_pointer
|
||||
entry(Env const& env, Account const& target, std::uint32_t ticketSeq);
|
||||
|
||||
} // namespace xrpl::test::jtx::proposal
|
||||
@@ -0,0 +1,223 @@
|
||||
// Auto-generated unit tests for ledger entry TransactionProposal
|
||||
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <protocol_autogen/TestHelpers.h>
|
||||
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol_autogen/ledger_entries/TransactionProposal.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(TransactionProposalTests, BuilderSettersRoundTrip)
|
||||
{
|
||||
uint256 const index{1u};
|
||||
|
||||
auto const previousTxnIDValue = canonical_UINT256();
|
||||
auto const previousTxnLgrSeqValue = canonical_UINT32();
|
||||
auto const ownerValue = canonical_ACCOUNT();
|
||||
auto const proposedTransactionValue = canonical_OBJECT();
|
||||
auto const expirationValue = canonical_UINT32();
|
||||
auto const ownerNodeValue = canonical_UINT64();
|
||||
|
||||
TransactionProposalBuilder builder{
|
||||
previousTxnIDValue,
|
||||
previousTxnLgrSeqValue,
|
||||
ownerValue,
|
||||
proposedTransactionValue,
|
||||
expirationValue,
|
||||
ownerNodeValue
|
||||
};
|
||||
|
||||
|
||||
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 = ownerValue;
|
||||
auto const actual = entry.getOwner();
|
||||
expectEqualField(expected, actual, "sfOwner");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = proposedTransactionValue;
|
||||
auto const actual = entry.getProposedTransaction();
|
||||
expectEqualField(expected, actual, "sfProposedTransaction");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = expirationValue;
|
||||
auto const actual = entry.getExpiration();
|
||||
expectEqualField(expected, actual, "sfExpiration");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = ownerNodeValue;
|
||||
auto const actual = entry.getOwnerNode();
|
||||
expectEqualField(expected, actual, "sfOwnerNode");
|
||||
}
|
||||
|
||||
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(TransactionProposalTests, BuilderFromSleRoundTrip)
|
||||
{
|
||||
uint256 const index{2u};
|
||||
|
||||
auto const previousTxnIDValue = canonical_UINT256();
|
||||
auto const previousTxnLgrSeqValue = canonical_UINT32();
|
||||
auto const ownerValue = canonical_ACCOUNT();
|
||||
auto const proposedTransactionValue = canonical_OBJECT();
|
||||
auto const expirationValue = canonical_UINT32();
|
||||
auto const ownerNodeValue = canonical_UINT64();
|
||||
|
||||
auto sle = std::make_shared<SLE>(TransactionProposal::entryType, index);
|
||||
|
||||
sle->at(sfPreviousTxnID) = previousTxnIDValue;
|
||||
sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue;
|
||||
sle->at(sfOwner) = ownerValue;
|
||||
sle->setFieldObject(sfProposedTransaction, proposedTransactionValue);
|
||||
sle->at(sfExpiration) = expirationValue;
|
||||
sle->at(sfOwnerNode) = ownerNodeValue;
|
||||
|
||||
TransactionProposalBuilder builderFromSle{sle};
|
||||
EXPECT_TRUE(builderFromSle.validate());
|
||||
|
||||
auto const entryFromBuilder = builderFromSle.build(index);
|
||||
|
||||
TransactionProposal 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 = ownerValue;
|
||||
|
||||
auto const fromSle = entryFromSle.getOwner();
|
||||
auto const fromBuilder = entryFromBuilder.getOwner();
|
||||
|
||||
expectEqualField(expected, fromSle, "sfOwner");
|
||||
expectEqualField(expected, fromBuilder, "sfOwner");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = proposedTransactionValue;
|
||||
|
||||
auto const fromSle = entryFromSle.getProposedTransaction();
|
||||
auto const fromBuilder = entryFromBuilder.getProposedTransaction();
|
||||
|
||||
expectEqualField(expected, fromSle, "sfProposedTransaction");
|
||||
expectEqualField(expected, fromBuilder, "sfProposedTransaction");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = expirationValue;
|
||||
|
||||
auto const fromSle = entryFromSle.getExpiration();
|
||||
auto const fromBuilder = entryFromBuilder.getExpiration();
|
||||
|
||||
expectEqualField(expected, fromSle, "sfExpiration");
|
||||
expectEqualField(expected, fromBuilder, "sfExpiration");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = ownerNodeValue;
|
||||
|
||||
auto const fromSle = entryFromSle.getOwnerNode();
|
||||
auto const fromBuilder = entryFromBuilder.getOwnerNode();
|
||||
|
||||
expectEqualField(expected, fromSle, "sfOwnerNode");
|
||||
expectEqualField(expected, fromBuilder, "sfOwnerNode");
|
||||
}
|
||||
|
||||
EXPECT_EQ(entryFromSle.getKey(), index);
|
||||
EXPECT_EQ(entryFromBuilder.getKey(), index);
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong ledger entry type.
|
||||
TEST(TransactionProposalTests, 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(TransactionProposal{wrongEntry.getSle()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong ledger entry type.
|
||||
TEST(TransactionProposalTests, 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(TransactionProposalBuilder{wrongEntry.getSle()}, std::runtime_error);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// Auto-generated unit tests for transaction TransactionProposalCreate
|
||||
|
||||
|
||||
#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/TransactionProposalCreate.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(TransactionsTransactionProposalCreateTests, BuilderSettersRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testTransactionProposalCreate"));
|
||||
|
||||
// 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 proposedTransactionValue = canonical_OBJECT();
|
||||
auto const expirationValue = canonical_UINT32();
|
||||
|
||||
TransactionProposalCreateBuilder builder{
|
||||
accountValue,
|
||||
proposedTransactionValue,
|
||||
expirationValue,
|
||||
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 = proposedTransactionValue;
|
||||
auto const actual = tx.getProposedTransaction();
|
||||
expectEqualField(expected, actual, "sfProposedTransaction");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = expirationValue;
|
||||
auto const actual = tx.getExpiration();
|
||||
expectEqualField(expected, actual, "sfExpiration");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
|
||||
// and verify all fields match.
|
||||
TEST(TransactionsTransactionProposalCreateTests, BuilderFromStTxRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testTransactionProposalCreateFromTx"));
|
||||
|
||||
// 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 proposedTransactionValue = canonical_OBJECT();
|
||||
auto const expirationValue = canonical_UINT32();
|
||||
|
||||
// Build an initial transaction
|
||||
TransactionProposalCreateBuilder initialBuilder{
|
||||
accountValue,
|
||||
proposedTransactionValue,
|
||||
expirationValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
// Create builder from existing STTx
|
||||
TransactionProposalCreateBuilder 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 = proposedTransactionValue;
|
||||
auto const actual = rebuiltTx.getProposedTransaction();
|
||||
expectEqualField(expected, actual, "sfProposedTransaction");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = expirationValue;
|
||||
auto const actual = rebuiltTx.getExpiration();
|
||||
expectEqualField(expected, actual, "sfExpiration");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsTransactionProposalCreateTests, 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(TransactionProposalCreate{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsTransactionProposalCreateTests, 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(TransactionProposalCreateBuilder{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -741,6 +741,30 @@ parseSponsorship(
|
||||
return keylet::sponsorship(*sponsorID, *sponseeID).key;
|
||||
}
|
||||
|
||||
static std::expected<uint256, json::Value>
|
||||
parseTransactionProposal(
|
||||
json::Value const& params,
|
||||
json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
|
||||
auto const targetID =
|
||||
LedgerEntryHelpers::requiredAccountID(params, jss::account, "malformedAddress");
|
||||
if (!targetID)
|
||||
return std::unexpected(targetID.error());
|
||||
|
||||
// The proposed transaction's TicketSequence (a proposed transaction is
|
||||
// ticket-only), mirroring how parseTicket looks up a Ticket object.
|
||||
auto const ticketSequence =
|
||||
LedgerEntryHelpers::requiredUInt32(params, jss::ticket_seq, "malformedRequest");
|
||||
if (!ticketSequence)
|
||||
return std::unexpected(ticketSequence.error());
|
||||
|
||||
return keylet::txProposal(*targetID, *ticketSequence).key;
|
||||
}
|
||||
|
||||
static std::expected<uint256, json::Value>
|
||||
parseTicket(
|
||||
json::Value const& params,
|
||||
|
||||
Reference in New Issue
Block a user