diff --git a/CHANGELOG.md b/CHANGELOG.md
index b3bae91..c374dd0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Changed
+
+- Rename `PsbtParams::version` to `PsbtParams::min_version`.
+- Selected inputs that require CSV now raise the unsigned transaction version to `Version::TWO` instead of erroring.
+- AFS now raises the transaction version to `Version::TWO` when it chooses the `nSequence` path.
+
+### Removed
+
+- Remove `AntiFeeSnipingError::UnsupportedVersion` / the old AFS unsupported-version failure path.
+
## [0.2.0]
diff --git a/src/afs.rs b/src/afs.rs
index 9fd2bb0..85c44e4 100644
--- a/src/afs.rs
+++ b/src/afs.rs
@@ -13,8 +13,6 @@ use rand_core::RngCore;
/// Error returned by `apply_anti_fee_sniping`.
#[derive(Debug, Clone, PartialEq)]
pub enum AntiFeeSnipingError {
- /// Transaction `version` must be >= 2 for AFS to use relative locktimes.
- UnsupportedVersion(Version),
/// AFS only supports height-based locktimes. The transaction's locktime is
/// time-based (MTP), which can originate from either `PsbtParams::min_locktime`
/// or an input's time-based CLTV requirement.
@@ -24,10 +22,6 @@ pub enum AntiFeeSnipingError {
impl core::fmt::Display for AntiFeeSnipingError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
- Self::UnsupportedVersion(version) => write!(
- f,
- "anti-fee-sniping requires tx.version >= 2 (got {version})"
- ),
Self::UnsupportedLockTime(locktime) => write!(
f,
"anti-fee-sniping requires a height-based tx locktime (got time-based {locktime}); \
@@ -53,6 +47,9 @@ impl std::error::Error for AntiFeeSnipingError {}
/// - **nLockTime**: Sets the transaction's lock time to approximately the current height
/// - **nSequence**: Sets one Taproot input's sequence to approximately its confirmation depth
///
+/// If the `nSequence` path is chosen, the transaction version is raised to
+/// `Version::TWO` if necessary.
+///
/// Random offsets (0-99 blocks) are applied with 10% probability to avoid creating
/// a unique fingerprint that could identify transactions from this wallet.
///
@@ -71,7 +68,6 @@ impl std::error::Error for AntiFeeSnipingError {}
/// is implicitly satisfied.
///
/// # Errors
-/// - [`AntiFeeSnipingError::UnsupportedVersion`] if `tx.version < 2`.
/// - [`AntiFeeSnipingError::UnsupportedLockTime`] if `tx.lock_time` is time-based
/// (either from `PsbtParams::min_locktime` or an input's time-based CLTV).
///
@@ -89,10 +85,6 @@ pub(crate) fn apply_anti_fee_sniping(
const TEN_PERCENT_PROBABILITY_RANGE: u32 = 10;
const MAX_RANDOM_OFFSET: u32 = 100;
- if tx.version < Version::TWO {
- return Err(AntiFeeSnipingError::UnsupportedVersion(tx.version));
- }
-
if !tx.lock_time.is_block_height() {
return Err(AntiFeeSnipingError::UnsupportedLockTime(tx.lock_time));
}
@@ -143,6 +135,10 @@ pub(crate) fn apply_anti_fee_sniping(
}
} else {
// Use Sequence
+ if tx.version < Version::TWO {
+ tx.version = Version::TWO;
+ }
+
let random_index = random_range(rng, taproot_inputs.len() as u32);
let (input_index, input) = taproot_inputs[random_index as usize];
let confirmation = input.confirmations(tip_height);
diff --git a/src/selection.rs b/src/selection.rs
index e1dd410..3e87e34 100644
--- a/src/selection.rs
+++ b/src/selection.rs
@@ -24,8 +24,11 @@ pub struct Selection {
/// Parameters for creating a psbt.
#[derive(Debug, Clone)]
pub struct PsbtParams {
- /// Use a specific [`transaction::Version`].
- pub version: transaction::Version,
+ /// Minimum [`transaction::Version`] for the resulting transaction.
+ ///
+ /// The final version may be raised when selected inputs require CSV or when anti-fee-sniping
+ /// selects its `nSequence` strategy.
+ pub min_version: transaction::Version,
/// Minimum tx locktime — a floor on the resulting `tx.lock_time`.
///
@@ -61,11 +64,9 @@ pub struct PsbtParams {
///
/// # Errors
///
- /// When `Some(..)`, [`Selection::create_psbt`] returns [`CreatePsbtError::AntiFeeSniping`] if:
- /// - the transaction version is less than 2
- /// ([`AntiFeeSnipingError::UnsupportedVersion`]) — v2 is required for relative locktimes; or
- /// - a time-based (MTP) locktime is in effect
- /// ([`AntiFeeSnipingError::UnsupportedLockTime`]) — AFS only supports height-based locktimes.
+ /// When `Some(..)`, [`Selection::create_psbt`] returns [`CreatePsbtError::AntiFeeSniping`] if
+ /// a time-based (MTP) locktime is in effect
+ /// ([`AntiFeeSnipingError::UnsupportedLockTime`]) — AFS only supports height-based locktimes.
///
/// See [BIP326](https://github.com/bitcoin/bips/blob/master/bip-0326.mediawiki) for more details.
///
@@ -76,7 +77,7 @@ pub struct PsbtParams {
impl Default for PsbtParams {
fn default() -> Self {
Self {
- version: transaction::Version::TWO,
+ min_version: transaction::Version::TWO,
min_locktime: absolute::LockTime::ZERO,
mandate_full_tx_for_segwit_v0: true,
anti_fee_sniping: None,
@@ -131,6 +132,18 @@ impl core::fmt::Display for CreatePsbtError {
impl std::error::Error for CreatePsbtError {}
impl Selection {
+ fn effective_min_version(&self, min_version: transaction::Version) -> transaction::Version {
+ if self
+ .inputs
+ .iter()
+ .any(|input| input.relative_timelock().is_some())
+ {
+ core::cmp::max(min_version, transaction::Version::TWO)
+ } else {
+ min_version
+ }
+ }
+
pub(crate) fn new(inputs: Vec, outputs: Vec