|
| 1 | +//! The resolved spendable candidate set (PSBT-building stage 1 output). |
| 2 | +
|
| 3 | +use bdk_tx::{Input, InputCandidates, RbfParams}; |
| 4 | +use bdk_wallet::LocalOutput; |
| 5 | +use bitcoin::Txid; |
| 6 | +use std::collections::HashSet; |
| 7 | + |
| 8 | +use crate::CandidatesError; |
| 9 | + |
| 10 | +/// A resolved set of spendable input candidates (output of PSBT-building stage 1). |
| 11 | +/// |
| 12 | +/// Produced by [`WalletTxExt::candidates_with`] from [`CandidateParams`]: every owned UTXO has been |
| 13 | +/// planned against the wallet's descriptors and spendability filters applied. It owns its inputs |
| 14 | +/// (no wallet borrow), so it can be held as a snapshot and used to build one or more PSBTs via |
| 15 | +/// [`WalletTxExt::select`]. |
| 16 | +/// |
| 17 | +/// Add foreign (non-wallet) inputs with [`push_must_select`](Self::push_must_select) / |
| 18 | +/// [`push_can_select`](Self::push_can_select), and apply your own post-resolution filters with |
| 19 | +/// [`filter`](Self::filter) / [`regroup`](Self::regroup). |
| 20 | +/// |
| 21 | +/// If the [`CandidateParams`] had a non-empty [`replace`](crate::CandidateParams::replace) list, |
| 22 | +/// the set carries the [`RbfParams`] (replaced-tx fee statistics) forward so stage 2 applies the |
| 23 | +/// correct fee floor, and exposes the wallet-owned outputs being stripped by the replacement via |
| 24 | +/// [`replaced_unspent`](Self::replaced_unspent). |
| 25 | +/// |
| 26 | +/// [`WalletTxExt::candidates_with`]: crate::WalletTxExt::candidates_with |
| 27 | +/// [`WalletTxExt::select`]: crate::WalletTxExt::select |
| 28 | +/// [`CandidateParams`]: crate::CandidateParams |
| 29 | +/// [`CandidateParams::replace`]: crate::CandidateParams::replace |
| 30 | +#[derive(Debug, Clone)] |
| 31 | +pub struct CandidateSet { |
| 32 | + pub(crate) candidates: InputCandidates, |
| 33 | + pub(crate) rbf: Option<RbfParams>, |
| 34 | + /// Txids being replaced/evicted (direct conflicts + descendants). A pushed input may not spend |
| 35 | + /// an output of any of these. |
| 36 | + pub(crate) replaced: HashSet<Txid>, |
| 37 | + /// Wallet-owned UTXOs stripped from the canonical view by the replacement. |
| 38 | + pub(crate) replaced_unspent: Vec<LocalOutput>, |
| 39 | +} |
| 40 | + |
| 41 | +impl CandidateSet { |
| 42 | + /// Iterate over all resolved input candidates (both must-select and optional). |
| 43 | + pub fn inputs(&self) -> impl Iterator<Item = &Input> + '_ { |
| 44 | + self.candidates.inputs() |
| 45 | + } |
| 46 | + |
| 47 | + /// Whether the set contains no candidates at all. |
| 48 | + pub fn is_empty(&self) -> bool { |
| 49 | + self.candidates.inputs().next().is_none() |
| 50 | + } |
| 51 | + |
| 52 | + /// Whether this set is a Replace-By-Fee set (built from a non-empty |
| 53 | + /// [`CandidateParams::replace`](crate::CandidateParams::replace) list). |
| 54 | + pub fn is_rbf(&self) -> bool { |
| 55 | + self.rbf.is_some() |
| 56 | + } |
| 57 | + |
| 58 | + /// Wallet-owned UTXOs that the replacement strips out of the canonical view -- the outputs of |
| 59 | + /// the replaced (and descendant) txs that were unspent in the wallet's view before the replace. |
| 60 | + /// |
| 61 | + /// These are the still-live payments of the txs being replaced; a caller batching several txs |
| 62 | + /// into one replacement can use them to decide which payments to re-create. Empty for a |
| 63 | + /// non-Replace-By-Fee set. |
| 64 | + pub fn replaced_unspent(&self) -> &[LocalOutput] { |
| 65 | + &self.replaced_unspent |
| 66 | + } |
| 67 | + |
| 68 | + /// Add a foreign [`Input`] to the must-select group (always spent). |
| 69 | + /// |
| 70 | + /// Use this for a UTXO that did not originate from the wallet, supplied with a pre-built |
| 71 | + /// plan -- its validity (UTXO existence, satisfaction weight, ...) relies on the |
| 72 | + /// caller-supplied values, so only push inputs you trust. |
| 73 | + /// |
| 74 | + /// # Errors |
| 75 | + /// |
| 76 | + /// Returns [`ConflictingInput`](CandidatesError::ConflictingInput) if the input spends an |
| 77 | + /// output of a transaction being replaced (RBF). |
| 78 | + pub fn push_must_select(mut self, input: Input) -> Result<Self, CandidatesError> { |
| 79 | + self.ensure_not_replaced(&input)?; |
| 80 | + self.candidates = self.candidates.push_must_select(input); |
| 81 | + Ok(self) |
| 82 | + } |
| 83 | + |
| 84 | + /// Add a foreign [`Input`] as an optional (can-select) candidate. |
| 85 | + /// |
| 86 | + /// # Errors |
| 87 | + /// |
| 88 | + /// Returns [`ConflictingInput`](CandidatesError::ConflictingInput) if the input spends an |
| 89 | + /// output of a transaction being replaced (RBF). |
| 90 | + pub fn push_can_select(mut self, input: Input) -> Result<Self, CandidatesError> { |
| 91 | + self.ensure_not_replaced(&input)?; |
| 92 | + self.candidates = self.candidates.push_can_select(input); |
| 93 | + Ok(self) |
| 94 | + } |
| 95 | + |
| 96 | + /// Reject an input that spends an output of a replaced (evicted) transaction. |
| 97 | + fn ensure_not_replaced(&self, input: &Input) -> Result<(), CandidatesError> { |
| 98 | + let op = input.prev_outpoint(); |
| 99 | + if self.replaced.contains(&op.txid) { |
| 100 | + return Err(CandidatesError::ConflictingInput(op)); |
| 101 | + } |
| 102 | + Ok(()) |
| 103 | + } |
| 104 | + |
| 105 | + /// Keep only the candidates for which `policy` returns `true`. |
| 106 | + /// |
| 107 | + /// Forwards to [`bdk_tx::InputCandidates::filter`]. |
| 108 | + pub fn filter<P>(mut self, policy: P) -> Self |
| 109 | + where |
| 110 | + P: FnMut(&Input) -> bool, |
| 111 | + { |
| 112 | + self.candidates = self.candidates.filter(policy); |
| 113 | + self |
| 114 | + } |
| 115 | + |
| 116 | + /// Regroup the candidates by the group key returned by `policy`. |
| 117 | + /// |
| 118 | + /// Forwards to [`bdk_tx::InputCandidates::regroup`]. |
| 119 | + pub fn regroup<P, G>(mut self, policy: P) -> Self |
| 120 | + where |
| 121 | + P: FnMut(&Input) -> G, |
| 122 | + G: Ord + Clone, |
| 123 | + { |
| 124 | + self.candidates = self.candidates.regroup(policy); |
| 125 | + self |
| 126 | + } |
| 127 | + |
| 128 | + /// Consume into the underlying `bdk_tx` parts: the [`InputCandidates`] and, if this is a |
| 129 | + /// Replace-By-Fee set (see [`is_rbf`](Self::is_rbf)), the [`RbfParams`] carrying the |
| 130 | + /// replaced-tx fee floor. |
| 131 | + /// |
| 132 | + /// Pass both on to `bdk_tx` (e.g. via |
| 133 | + /// [`SelectionParams::replace`](bdk_tx::SelectionParams::replace)) to build a `TxTemplate` |
| 134 | + /// directly while still enforcing the RBF minimum fee. |
| 135 | + pub fn into_parts(self) -> (InputCandidates, Option<RbfParams>) { |
| 136 | + (self.candidates, self.rbf) |
| 137 | + } |
| 138 | +} |
0 commit comments