diff --git a/examples/anti_fee_sniping.rs b/examples/anti_fee_sniping.rs index da318b93..cedac7d2 100644 --- a/examples/anti_fee_sniping.rs +++ b/examples/anti_fee_sniping.rs @@ -2,7 +2,7 @@ use bdk_testenv::{bitcoincore_rpc::RpcApi, TestEnv}; use bdk_tx::{ filter_unspendable, group_by_spk, selection_algorithm_lowest_fee_bnb, Output, PsbtParams, - SelectorParams, + SelectionParams, }; use bitcoin::{absolute::LockTime, key::Secp256k1, Amount, FeeRate}; use miniscript::Descriptor; @@ -73,11 +73,11 @@ fn main() -> anyhow::Result<()> { .regroup(group_by_spk()) .filter(filter_unspendable(tip_height, Some(tip_time))) .into_selection( - selection_algorithm_lowest_fee_bnb(longterm_feerate, 100_000), - SelectorParams { - // For waste optimization when deciding change. - change_longterm_feerate: Some(longterm_feerate), - ..SelectorParams::new( + selection_algorithm_lowest_fee_bnb(100_000), + SelectionParams { + // Drives waste optimization for both change and the bnb metric. + longterm_feerate: Some(longterm_feerate), + ..SelectionParams::new( FeeRate::from_sat_per_vb(10).expect("valid fee rate"), vec![Output::with_script( recipient_addr.script_pubkey(), diff --git a/examples/synopsis.rs b/examples/synopsis.rs index 2d629ba6..1559cb3a 100644 --- a/examples/synopsis.rs +++ b/examples/synopsis.rs @@ -1,7 +1,7 @@ use bdk_testenv::{bitcoincore_rpc::RpcApi, TestEnv}; use bdk_tx::{ filter_unspendable, group_by_spk, selection_algorithm_lowest_fee_bnb, Output, PsbtParams, - SelectorParams, Signer, + SelectionParams, Signer, }; use bitcoin::{key::Secp256k1, Amount, FeeRate}; use miniscript::Descriptor; @@ -53,11 +53,11 @@ fn main() -> anyhow::Result<()> { .regroup(group_by_spk()) .filter(filter_unspendable(tip_height, Some(tip_mtp))) .into_selection( - selection_algorithm_lowest_fee_bnb(longterm_feerate, 100_000), - SelectorParams { - // For waste-optimization when deciding change. - change_longterm_feerate: Some(longterm_feerate), - ..SelectorParams::new( + selection_algorithm_lowest_fee_bnb(100_000), + SelectionParams { + // Drives waste optimization for both change and the bnb metric. + longterm_feerate: Some(longterm_feerate), + ..SelectionParams::new( FeeRate::from_sat_per_vb(10).expect("valid fee rate"), vec![Output::with_script( recipient_addr.script_pubkey(), @@ -124,8 +124,8 @@ fn main() -> anyhow::Result<()> { // Do coin selection. .into_selection( // Coin selection algorithm. - selection_algorithm_lowest_fee_bnb(longterm_feerate, 100_000), - SelectorParams { + selection_algorithm_lowest_fee_bnb(100_000), + SelectionParams { // This is just a lower-bound feerate. The actual result will be much higher to // satisfy mempool-replacement policy. target_feerate: FeeRate::from_sat_per_vb(1).expect("valid fee rate"), @@ -137,8 +137,8 @@ fn main() -> anyhow::Result<()> { change_script: bdk_tx::ChangeScript::from_descriptor( internal.at_derivation_index(1)?, ), - // For waste optimization when deciding change. - change_longterm_feerate: Some(longterm_feerate), + // Drives waste optimization for both change and the bnb metric. + longterm_feerate: Some(longterm_feerate), change_min_value: None, change_dust_relay_feerate: None, // This ensures that we satisfy mempool-replacement policy rules 4 and 6. diff --git a/src/input_candidates.rs b/src/input_candidates.rs index 131f6af8..318e2672 100644 --- a/src/input_candidates.rs +++ b/src/input_candidates.rs @@ -1,14 +1,14 @@ use alloc::{vec, vec::Vec}; use core::fmt; -use bdk_coin_select::{metrics::LowestFee, Candidate, NoBnbSolution}; -use bitcoin::{absolute, FeeRate, OutPoint}; +use bdk_coin_select::{metrics::LowestFee, Candidate, CoinSelector, NoBnbSolution}; +use bitcoin::{absolute, Amount, OutPoint}; use miniscript::bitcoin; use crate::collections::{BTreeMap, HashSet}; use crate::{ - CannotMeetTarget, FeeRateExt, Input, InputGroup, Selection, Selector, SelectorError, - SelectorParams, + ChangePolicyError, FeeRateExt, Input, InputGroup, Output, Selection, SelectionContext, + SelectionParams, }; /// Input candidates. @@ -191,48 +191,151 @@ impl InputCandidates { self } - /// Attempt to convert the input candidates into a valid [`Selection`] with a given - /// `algorithm` and selector `params`. + /// Attempt to convert the input candidates into a valid [`Selection`]. + /// + /// This drives the whole selection lifecycle: it resolves `params`, validates the candidates, + /// runs the provided `algorithm` against a [`CoinSelector`], then finalizes the result. The + /// `algorithm` is handed the [`CoinSelector`] to drive and a [`SelectionContext`] describing + /// the resolved target, change policy and long-term feerate. + /// + /// # Errors + /// + /// - [`IntoSelectionError::ChangePolicy`] if the change policy cannot be built from the params. + /// - [`IntoSelectionError::LockTypeMismatch`] if the candidates have incompatible absolute + /// timelock units. + /// - [`IntoSelectionError::CannotMeetTarget`] if the target is unreachable even when selecting + /// every effective input at the target feerate - i.e. genuinely impossible. + /// - [`IntoSelectionError::Algorithm`] if the `algorithm` itself errors. + /// - [`IntoSelectionError::AlgorithmFellShort`] if the `algorithm` returns successfully but + /// its selection still falls short of the target. pub fn into_selection( self, algorithm: A, - params: SelectorParams, + params: SelectionParams, ) -> Result> where - A: FnMut(&mut Selector) -> Result<(), E>, + A: FnOnce(&mut CoinSelector, SelectionContext) -> Result<(), E>, { - let mut selector = Selector::new(&self, params).map_err(IntoSelectionError::Selector)?; - selector - .select_with_algorithm(algorithm) - .map_err(IntoSelectionError::SelectionAlgorithm)?; - let selection = selector - .try_finalize() - .ok_or(IntoSelectionError::CannotMeetTarget(CannotMeetTarget))?; - Ok(selection) + let target = params.to_cs_target(); + let change_policy = params + .to_cs_change_policy() + .map_err(IntoSelectionError::ChangePolicy)?; + let longterm_feerate = params + .longterm_feerate + .unwrap_or(params.target_feerate) + .into_cs_feerate(); + let change_script = params.change_script.source(); + let target_outputs = params.target_outputs; + + // Verify that all inputs agree on absolute timelock unit (height vs time). Downstream + // stages (create_psbt, apply_anti_fee_sniping) rely on this invariant. + let mut unit: Option = None; + for lt in self.inputs().filter_map(Input::absolute_timelock) { + match unit { + Some(existing_unit) => { + if !existing_unit.is_same_unit(lt) { + return Err(IntoSelectionError::LockTypeMismatch); + } + } + None => unit = Some(lt), + } + } + + let mut cs = CoinSelector::new(self.coin_select_candidates()); + if self.must_select().is_some() { + cs.select_next(); + } + + // Reachability pre-check. + { + let mut check = cs.clone(); + check.select_all_effective(target.fee.rate); + let max_excess = check.excess(target, bdk_coin_select::Drain::NONE); + if max_excess < 0 { + return Err(IntoSelectionError::CannotMeetTarget { + missing: max_excess.unsigned_abs(), + }); + } + } + + algorithm( + &mut cs, + SelectionContext { + target, + change_policy, + longterm_feerate, + }, + ) + .map_err(IntoSelectionError::Algorithm)?; + + // Ensure target is actually met after selection. The target was already proven reachable, + // so a shortfall here is the algorithm under-selecting. + let drain = cs.drain(target, change_policy); + if cs.excess(target, drain) < 0 { + return Err(IntoSelectionError::AlgorithmFellShort); + } + + let to_apply = self.groups().collect::>(); + let inputs = cs + .apply_selection(&to_apply) + .copied() + .flat_map(InputGroup::inputs) + .cloned() + .collect(); + let mut outputs = target_outputs; + if drain.is_some() { + outputs.push(Output::from((change_script, Amount::from_sat(drain.value)))); + } + Ok(Selection::new(inputs, outputs)) } } -/// Occurs when we cannot find a solution for selection. +/// Error returned by [`InputCandidates::into_selection`]. +/// +/// Covers every way the lifecycle can fail: an unbuildable change policy, incompatible candidate +/// timelocks, an impossible target, a failing algorithm, or an algorithm that finished without +/// meeting the target. #[derive(Debug)] pub enum IntoSelectionError { - /// Coin selector returned an error - Selector(SelectorError), - /// Selection algorithm failed. - SelectionAlgorithm(E), - /// The target cannot be met - CannotMeetTarget(CannotMeetTarget), + /// The change policy could not be built from the params (see [`ChangePolicyError`]). + ChangePolicy(ChangePolicyError), + /// Input candidates have absolute timelocks of mixed units (some height-based, others + /// time-based), which is unbuildable since `nLockTime` is a single field on a transaction. + LockTypeMismatch, + /// The target is impossible: unreachable even when selecting every effective input at the + /// target feerate. + CannotMeetTarget { + /// The shortfall in satoshis at best-case (all effective inputs selected). + missing: u64, + }, + /// The selection algorithm itself returned an error. + Algorithm(E), + /// The algorithm returned successfully but its selection still falls short of the target. + /// + /// This is an algorithm contract violation as the target *was* reachable — the algorithm simply + /// did not select enough inputs. A correct algorithm either meets the target or returns its own + /// error. + AlgorithmFellShort, } impl fmt::Display for IntoSelectionError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - IntoSelectionError::Selector(error) => { - write!(f, "{error}") + IntoSelectionError::ChangePolicy(error) => write!(f, "{error}"), + IntoSelectionError::LockTypeMismatch => { + write!(f, "input candidates have absolute timelocks of mixed units") } - IntoSelectionError::SelectionAlgorithm(error) => { + IntoSelectionError::CannotMeetTarget { missing } => write!( + f, + "meeting the target is not possible with the input candidates; {missing} sats missing" + ), + IntoSelectionError::Algorithm(error) => { write!(f, "selection algorithm failed: {error}") } - IntoSelectionError::CannotMeetTarget(error) => write!(f, "{error}"), + IntoSelectionError::AlgorithmFellShort => write!( + f, + "the selection algorithm returned successfully but did not meet the target" + ), } } } @@ -240,26 +343,24 @@ impl fmt::Display for IntoSelectionError { #[cfg(feature = "std")] impl std::error::Error for IntoSelectionError {} -/// Select for lowest fee with bnb +/// Select for lowest fee with bnb. +/// +/// The long-term feerate is taken from the [`SelectionContext`] (resolved from +/// [`SelectionParams::longterm_feerate`](crate::SelectionParams::longterm_feerate)), so the same +/// estimate drives both this metric and the change policy. pub fn selection_algorithm_lowest_fee_bnb( - longterm_feerate: FeeRate, max_rounds: usize, -) -> impl FnMut(&mut Selector) -> Result<(), NoBnbSolution> { - let long_term_feerate = longterm_feerate.into_cs_feerate(); - move |selector| { - let target = selector.target(); - let change_policy = selector.cs_change_policy(); - selector - .inner_mut() - .run_bnb( - LowestFee { - target, - long_term_feerate, - change_policy, - }, - max_rounds, - ) - .map(|_| ()) +) -> impl FnOnce(&mut CoinSelector, SelectionContext) -> Result<(), NoBnbSolution> { + move |cs, cx| { + cs.run_bnb( + LowestFee { + target: cx.target, + long_term_feerate: cx.longterm_feerate, + change_policy: cx.change_policy, + }, + max_rounds, + ) + .map(|_| ()) } } diff --git a/src/lib.rs b/src/lib.rs index b60312ec..7474c1d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ mod no_std_rand; mod output; mod rbf; mod selection; -mod selector; +mod selection_params; mod signer; pub use afs::*; @@ -35,7 +35,7 @@ use no_std_rand::*; pub use output::*; pub use rbf::*; pub use selection::*; -pub use selector::*; +pub use selection_params::*; pub use signer::*; #[cfg(feature = "std")] diff --git a/src/selection.rs b/src/selection.rs index e1dd4100..0656eea9 100644 --- a/src/selection.rs +++ b/src/selection.rs @@ -210,20 +210,20 @@ impl Selection { /// # Panics /// /// In debug builds, panics if `locktimes` contains values with different units (height vs. - /// time). `Selector::new` rejects such candidates upstream, so this should never fire in - /// practice. + /// time). [`InputCandidates::into_selection`] rejects such candidates upstream, so this should + /// never fire in practice. fn accumulate_max_locktime( locktimes: impl IntoIterator, min_locktime: absolute::LockTime, ) -> absolute::LockTime { // Accumulate locktimes required by inputs. An input-vs-input unit mismatch is rejected - // upstream by `Selector::new`. `min_locktime` is only used when it is compatible with + // upstream in `into_selection`. `min_locktime` is only used when it is compatible with // the input requirements; a different unit is intentionally ignored so that, e.g., a // height-based `min_locktime` does not conflict with a time-based CLTV requirement. let inputs_max = locktimes.into_iter().reduce(|a, b| { debug_assert!( a.is_same_unit(b), - "Selector::new should reject mixed-unit candidates", + "into_selection should reject mixed-unit candidates", ); if a.is_implied_by(b) { b diff --git a/src/selector.rs b/src/selection_params.rs similarity index 60% rename from src/selector.rs rename to src/selection_params.rs index 4c98ec86..2c51da17 100644 --- a/src/selector.rs +++ b/src/selection_params.rs @@ -1,24 +1,29 @@ -use bdk_coin_select::{InsufficientFunds, Replace, Target, TargetFee, TargetOutputs}; +use bdk_coin_select::{Replace, Target, TargetFee, TargetOutputs}; use bitcoin::{Amount, FeeRate, ScriptBuf, Transaction, Weight}; use miniscript::bitcoin; -use crate::{ - DefiniteDescriptor, FeeRateExt, Input, InputCandidates, InputGroup, Output, ScriptSource, - Selection, -}; +use crate::{DefiniteDescriptor, FeeRateExt, Output, ScriptSource}; use alloc::boxed::Box; use alloc::vec::Vec; -use core::fmt::{self, Debug}; +use core::fmt; -/// A coin selector -#[derive(Debug, Clone)] -pub struct Selector<'c> { - candidates: &'c InputCandidates, - target_outputs: Vec, - target: Target, - change_policy: bdk_coin_select::ChangePolicy, - change_script: ScriptSource, - inner: bdk_coin_select::CoinSelector<'c>, +/// Context handed to a selection algorithm. +/// +/// This is pure data describing the resolved coin-selection target and the parameters an algorithm +/// needs to make change/waste decisions. +#[derive(Debug, Clone, Copy)] +#[non_exhaustive] +pub struct SelectionContext { + /// The resolved coin-selection target (recipient value, weight and feerate). + pub target: Target, + /// The change policy derived from the params, used to decide whether to add a change output. + pub change_policy: bdk_coin_select::ChangePolicy, + /// Long-term feerate used for waste calculations, e.g. by metrics such as + /// [`LowestFee`](bdk_coin_select::metrics::LowestFee). + /// + /// Resolved from [`SelectionParams::longterm_feerate`]; when that is `None` it defaults to the + /// target feerate (i.e. "assume future conditions match the present"). + pub longterm_feerate: bdk_coin_select::FeeRate, } /// Parameters for creating tx. @@ -30,7 +35,7 @@ pub struct Selector<'c> { /// If the caller wants to create non-mempool-policy conforming txs, they can just fill in the /// fields directly. #[derive(Debug)] -pub struct SelectorParams { +pub struct SelectionParams { /// Target feerate. /// /// The actual feerate of the resulting transaction may be higher due to RBF requirements or @@ -56,11 +61,13 @@ pub struct SelectorParams { /// A change value below this is forgone as fee. `None` means only the dust threshold applies. pub change_min_value: Option, - /// Long-term feerate for waste optimization when deciding whether to include change. + /// Long-term feerate used for waste optimization across the whole selection - both the change + /// policy and metrics such as [`LowestFee`](bdk_coin_select::metrics::LowestFee). /// - /// `None` means no waste optimization - just enforce `change_min_value` (if specified) and the - /// dust threshold. - pub change_longterm_feerate: Option, + /// Represents the feerate at which the resulting coins are expected to be spent in the future. + /// `None` means "assume it matches `target_feerate`", the neutral default when no estimate is + /// available. + pub longterm_feerate: Option, /// Params for replacing tx(s). pub replace: Option, @@ -69,8 +76,8 @@ pub struct SelectorParams { /// Source of the change output script and its spending cost. /// /// For a [`DefiniteDescriptor`], the satisfaction weight is derived automatically. For a raw -/// script (e.g. silent payments), the caller may provide it. It can be omitted if the change -/// policy does not require waste calculations. +/// script (e.g. silent payments), the caller must provide it: the change policy is always +/// waste-aware, so an accurate spend cost is needed to decide whether a change output is worth it. #[derive(Debug)] pub enum ChangeScript { /// A raw script pubkey. @@ -84,7 +91,8 @@ pub enum ChangeScript { /// [`Plan::satisfaction_weight`](miniscript::plan::Plan::satisfaction_weight) and is used /// by coin selection to estimate the cost of spending the change output. /// - /// Can be `Weight::ZERO` if `SelectorParams::change_longterm_feerate` is unspecified. + /// This always feeds the waste calculation, so it should reflect the real spend cost; a + /// `Weight::ZERO` here will skew the change/no-change decision. satisfaction_weight: Weight, }, /// A definite descriptor from which the script and satisfaction weight are both derived. @@ -144,7 +152,7 @@ impl ChangeScript { } } - fn satisfaction_weight(&self) -> Result { + fn satisfaction_weight(&self) -> Result { match &self { ChangeScript::Script { satisfaction_weight, @@ -158,10 +166,10 @@ impl ChangeScript { .clone() .plan(assets) .map(|p| Weight::from_wu_usize(p.satisfaction_weight())) - .map_err(|_| SelectorError::InsufficientAssets), + .map_err(|_| ChangePolicyError::InsufficientAssets), None => descriptor .max_weight_to_satisfy() - .map_err(SelectorError::Miniscript), + .map_err(ChangePolicyError::Miniscript), }, } } @@ -246,7 +254,7 @@ impl RbfParams { } } -impl SelectorParams { +impl SelectionParams { /// With default params. pub fn new( target_feerate: FeeRate, @@ -258,7 +266,7 @@ impl SelectorParams { target_outputs, change_script, change_min_value: None, - change_longterm_feerate: None, + longterm_feerate: None, replace: None, change_dust_relay_feerate: None, } @@ -287,11 +295,12 @@ impl SelectorParams { /// /// # Errors /// - /// Returns [`SelectorError::InsufficientAssets`] if the provided assets cannot satisfy the + /// Returns [`ChangePolicyError::InsufficientAssets`] if the provided assets cannot satisfy the /// change descriptor. /// - /// Returns [`SelectorError::Miniscript`] if the change descriptor is inherently unsatisfiable. - pub fn to_cs_change_policy(&self) -> Result { + /// Returns [`ChangePolicyError::Miniscript`] if the change descriptor is inherently + /// unsatisfiable. + pub fn to_cs_change_policy(&self) -> Result { let change_script = self.change_script.source().script(); let min_non_dust = self.change_dust_relay_feerate.map_or_else( || change_script.minimal_non_dust(), @@ -316,199 +325,46 @@ impl SelectorParams { .max(self.change_min_value.unwrap_or(Amount::ZERO)) .to_sat(); - Ok( - if let Some(longterm_feerate) = self.change_longterm_feerate { - bdk_coin_select::ChangePolicy::min_value_and_waste( - change_weights, - min_value, - self.target_feerate.into_cs_feerate(), - longterm_feerate.into_cs_feerate(), - ) - } else { - bdk_coin_select::ChangePolicy::min_value(change_weights, min_value) - }, - ) + // The change policy is always waste-aware. When no long-term feerate is configured we fall + // back to the target feerate, i.e. "assume the change will be spent under today's + // conditions". Note this does not collapse to a plain dust threshold: the change output + // still has to clear its own lifetime cost (creation now plus spending later). + Ok(bdk_coin_select::ChangePolicy::min_value_and_waste( + change_weights, + min_value, + self.target_feerate.into_cs_feerate(), + self.longterm_feerate + .unwrap_or(self.target_feerate) + .into_cs_feerate(), + )) } } -/// Error when the selection is impossible with the input candidates -#[derive(Debug)] -pub struct CannotMeetTarget; - -impl fmt::Display for CannotMeetTarget { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "meeting the target is not possible with the input candidates" - ) - } -} - -#[cfg(feature = "std")] -impl std::error::Error for CannotMeetTarget {} - -/// Selector error +/// Error building the change policy from [`SelectionParams`]. +/// +/// Returned by [`SelectionParams::to_cs_change_policy`]; every variant stems from the change +/// descriptor being unsatisfiable with the available assets. #[derive(Debug)] -pub enum SelectorError { +pub enum ChangePolicyError { /// Miniscript error (e.g. the change descriptor is inherently unsatisfiable). Miniscript(miniscript::Error), - /// Meeting the target is not possible with the input candidates. - CannotMeetTarget(CannotMeetTarget), /// The provided assets cannot satisfy the change descriptor. InsufficientAssets, - /// Input candidates have absolute timelocks of mixed units (some height-based, others - /// time-based). - /// - /// Such a set is unbuildable since `nLockTime` is a single field on a transaction. - /// Filter the [`InputCandidates`] down to a single-unit subset before constructing the - /// [`Selector`]. - LockTypeMismatch, } -impl fmt::Display for SelectorError { +impl fmt::Display for ChangePolicyError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Miniscript(err) => write!(f, "{err}"), - Self::CannotMeetTarget(err) => write!(f, "{err}"), Self::InsufficientAssets => { write!(f, "provided assets cannot satisfy the change descriptor") } - Self::LockTypeMismatch => { - write!(f, "input candidates have absolute timelocks of mixed units") - } } } } #[cfg(feature = "std")] -impl std::error::Error for SelectorError {} - -impl<'c> Selector<'c> { - /// Create new input selector. - /// - /// # Errors - /// - /// - If we are unable to create a change policy from the `params`. - /// - If the target is unreachable given the total input value. - pub fn new( - candidates: &'c InputCandidates, - params: SelectorParams, - ) -> Result { - let target = params.to_cs_target(); - let change_policy = params.to_cs_change_policy()?; - let target_outputs = params.target_outputs; - let change_script = params.change_script.source(); - - if target.value() > candidates.groups().map(|grp| grp.value().to_sat()).sum() { - return Err(SelectorError::CannotMeetTarget(CannotMeetTarget)); - } - - // Verify that all inputs agree on absolute timelock unit (height vs time). - // Downstream stages (create_psbt, apply_anti_fee_sniping) rely on this invariant. - let mut unit: Option = None; - for lt in candidates.inputs().filter_map(Input::absolute_timelock) { - match unit { - Some(existing_unit) => { - if !existing_unit.is_same_unit(lt) { - return Err(SelectorError::LockTypeMismatch); - } - } - None => unit = Some(lt), - } - } - - let mut inner = bdk_coin_select::CoinSelector::new(candidates.coin_select_candidates()); - if candidates.must_select().is_some() { - inner.select_next(); - } - Ok(Self { - candidates, - target, - target_outputs, - change_policy, - change_script, - inner, - }) - } - - /// Get the inner coin selector. - pub fn inner(&self) -> &bdk_coin_select::CoinSelector<'c> { - &self.inner - } - - /// Get a mutable reference to the inner coin selector. - pub fn inner_mut(&mut self) -> &mut bdk_coin_select::CoinSelector<'c> { - &mut self.inner - } - - /// Coin selection target. - pub fn target(&self) -> Target { - self.target - } - - /// Coin selection change policy. - pub fn cs_change_policy(&self) -> bdk_coin_select::ChangePolicy { - self.change_policy - } - - /// Select with the provided `algorithm`. - pub fn select_with_algorithm(&mut self, mut algorithm: F) -> Result<(), E> - where - F: FnMut(&mut Selector) -> Result<(), E>, - { - algorithm(self) - } - - /// Select all. - pub fn select_all(&mut self) { - self.inner.select_all(); - } - - /// Select in order until target is met. - pub fn select_until_target_met(&mut self) -> Result<(), InsufficientFunds> { - self.inner.select_until_target_met(self.target) - } - - /// Whether we added the change output to the selection. - /// - /// Return `None` if target is not met yet. - pub fn has_change(&self) -> Option { - if !self.inner.is_target_met(self.target) { - return None; - } - let has_drain = self - .inner - .drain_value(self.target, self.change_policy) - .is_some(); - Some(has_drain) - } - - /// Try get final selection. - /// - /// Return `None` if target is not met yet. - pub fn try_finalize(&self) -> Option { - if !self.inner.is_target_met(self.target) { - return None; - } - let maybe_change = self.inner.drain(self.target, self.change_policy); - let to_apply = self.candidates.groups().collect::>(); - let inputs = self - .inner - .apply_selection(&to_apply) - .copied() - .flat_map(InputGroup::inputs) - .cloned() - .collect(); - let mut outputs = self.target_outputs.clone(); - if maybe_change.is_some() { - outputs.push(Output::from(( - self.change_script.clone(), - Amount::from_sat(maybe_change.value), - ))); - } - Some(Selection::new(inputs, outputs)) - } -} +impl std::error::Error for ChangePolicyError {} #[cfg_attr(coverage_nightly, coverage(off))] #[cfg(test)] @@ -545,19 +401,20 @@ mod tests { } #[test] - fn test_selector_rejects_mixed_absolute_locktime_units() -> anyhow::Result<()> { + fn test_selection_rejects_mixed_absolute_locktime_units() -> anyhow::Result<()> { let height_locked_input = setup_cltv_input(absolute::LockTime::from_consensus(10_000))?; let time_locked_input = setup_cltv_input(absolute::LockTime::from_consensus(500_000_001))?; let candidates = InputCandidates::new([], [height_locked_input, time_locked_input]); - let params = SelectorParams::new( + let params = SelectionParams::new( FeeRate::ZERO, vec![], ChangeScript::from_script(ScriptBuf::new(), Weight::ZERO), ); - assert!(matches!( - Selector::new(&candidates, params), - Err(SelectorError::LockTypeMismatch) - )); + let result = candidates.into_selection( + |_cs, _cx| Result::<(), core::convert::Infallible>::Ok(()), + params, + ); + assert!(matches!(result, Err(IntoSelectionError::LockTypeMismatch))); Ok(()) } }