1
2use std::borrow::BorrowMut;
3use std::collections::{HashMap, HashSet};
4use std::sync::Arc;
5
6use bdk_wallet::{AddressInfo, TxBuilder, Wallet};
7use bdk_wallet::chain::{BlockId, CanonicalizationParams, ChainPosition, ConfirmationBlockTime};
8use bdk_wallet::coin_selection::InsufficientFunds;
9use bdk_wallet::error::CreateTxError;
10use bitcoin::consensus::encode::serialize_hex;
11use bitcoin::{Amount, BlockHash, FeeRate, OutPoint, Transaction, TxOut, Txid, Weight, Witness};
12use bitcoin::psbt::{ExtractTxError, Input};
13use log::{debug, trace};
14
15use crate::TransactionExt;
16use crate::cpfp::MakeCpfpFees;
17use crate::fee::FEE_ANCHOR_SPEND_WEIGHT;
18
19#[derive(Debug, Clone)]
21pub struct LocalTransaction {
22 pub tx: Arc<Transaction>,
24 pub chain_position: ChainPosition<ConfirmationBlockTime>,
25 pub is_trusted: bool,
26}
27
28pub struct TrustedUtxo<'a> {
32 pub outpoint: OutPoint,
33 pub txout: &'a TxOut,
34 pub chain_position: &'a ChainPosition<ConfirmationBlockTime>,
35 pub is_trusted: bool,
36}
37
38pub struct TrustedCanonicalization {
55 txs: HashMap<Txid, LocalTransaction>,
56 unspent: Vec<OutPoint>,
57}
58
59impl TrustedCanonicalization {
60 pub fn from_wallet(w: &Wallet, min_confs: u32) -> Self {
64 let tip = w.latest_checkpoint().height();
65 let chain = w.local_chain();
66 let chain_tip = w.latest_checkpoint().block_id();
67
68 let mut txs: HashMap<Txid, LocalTransaction> = HashMap::new();
69 let mut spent: HashSet<OutPoint> = HashSet::new();
70
71 for ctx in w.tx_graph().list_ordered_canonical_txs(
72 chain, chain_tip, CanonicalizationParams::default(),
73 ) {
74 let txid = ctx.tx_node.txid;
75 let tx = ctx.tx_node.tx.clone();
76 let chain_position = ctx.chain_position.clone();
77
78 for input in tx.input.iter() {
79 spent.insert(input.previous_output);
80 }
81
82 let nb_confs = match chain_position.confirmation_height_upper_bound() {
83 Some(h) => tip.saturating_sub(h) + 1,
84 None => 0,
85 };
86 let is_trusted = nb_confs >= min_confs || tx.input.iter().all(|input| {
87 let prev = input.previous_output;
88 let Some(prev_entry) = txs.get(&prev.txid) else { return false };
89 let Some(prev_out) = prev_entry.tx.output.get(prev.vout as usize) else { return false };
90 w.is_mine(prev_out.script_pubkey.clone()) && prev_entry.is_trusted
95 });
96
97 txs.insert(txid, LocalTransaction { tx, chain_position, is_trusted });
98 }
99
100 let unspent = w.spk_index().outpoints().iter()
105 .map(|(_, op)| *op)
106 .filter(|op| !spent.contains(op))
107 .filter(|op| txs.contains_key(&op.txid))
108 .collect();
109
110 Self { txs, unspent }
111 }
112
113 pub fn is_trusted(&self, txid: Txid) -> bool {
116 self.txs.get(&txid).map(|e| e.is_trusted).unwrap_or(false)
117 }
118
119 pub fn list_unspent(&self) -> impl Iterator<Item = TrustedUtxo<'_>> + '_ {
122 self.unspent.iter().map(move |op| {
123 let lt = &self.txs[&op.txid];
124 TrustedUtxo {
125 outpoint: *op,
126 txout: <.tx.output[op.vout as usize],
127 chain_position: <.chain_position,
128 is_trusted: lt.is_trusted,
129 }
130 })
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
136pub struct TrustedBalance {
137 pub trusted: Amount,
139 pub untrusted: Amount,
141}
142
143impl TrustedBalance {
144 pub fn total(&self) -> Amount {
145 self.trusted + self.untrusted
146 }
147}
148
149pub const KEYCHAIN: bdk_wallet::KeychainKind = bdk_wallet::KeychainKind::External;
151
152
153pub trait TxBuilderExt<'a, A>: BorrowMut<TxBuilder<'a, A>> {
155 fn add_fee_anchor_spend(&mut self, anchor: OutPoint, output: &TxOut)
157 where
158 A: bdk_wallet::coin_selection::CoinSelectionAlgorithm,
159 {
160 let psbt_in = Input {
161 witness_utxo: Some(output.clone()),
162 final_script_witness: Some(Witness::new()),
163 ..Default::default()
164 };
165 self.borrow_mut().add_foreign_utxo(anchor, psbt_in, FEE_ANCHOR_SPEND_WEIGHT)
166 .expect("adding foreign utxo");
167 }
168}
169impl<'a, A> TxBuilderExt<'a, A> for TxBuilder<'a, A> {}
170
171#[derive(Debug, thiserror::Error)]
172pub enum CpfpInternalError {
173 #[error("{0}")]
174 General(String),
175 #[error("Unable to construct transaction: {0}")]
176 Create(CreateTxError),
177 #[error("Unable to extract the final transaction after signing the PSBT: {0}")]
178 Extract(ExtractTxError),
179 #[error("Failed to determine the weight/fee when creating a P2A CPFP")]
180 Fee(),
181 #[error("Unable to finalize CPFP transaction: {0}")]
182 FinalizeError(String),
183 #[error("You need more confirmations on your on-chain funds: {0}")]
184 InsufficientConfirmedFunds(InsufficientFunds),
185 #[error("Transaction has no fee anchor: {0}")]
186 NoFeeAnchor(Txid),
187 #[allow(deprecated)]
188 #[error("Unable to sign transaction: {0}")]
189 Signer(bdk_wallet::signer::SignerError),
190}
191
192pub trait WalletExt: BorrowMut<Wallet> {
194 fn peek_next_address(&self) -> AddressInfo {
196 self.borrow().peek_address(KEYCHAIN, self.borrow().next_derivation_index(KEYCHAIN))
197 }
198
199 fn unconfirmed_txids(&self) -> impl Iterator<Item = Txid> {
201 self.borrow().transactions().filter_map(|tx| {
202 if tx.chain_position.is_unconfirmed() {
203 Some(tx.tx_node.txid)
204 } else {
205 None
206 }
207 })
208 }
209
210 fn unconfirmed_txs(&self) -> impl Iterator<Item = Arc<Transaction>> {
213 self.borrow().transactions().filter_map(|tx| {
214 if tx.chain_position.is_unconfirmed() {
215 Some(tx.tx_node.tx.clone())
216 } else {
217 None
218 }
219 })
220 }
221
222 fn trusted_balance(&self, min_confs: u32) -> TrustedBalance {
224 let canon = TrustedCanonicalization::from_wallet(self.borrow(), min_confs);
225 let mut trusted = Amount::ZERO;
226 let mut untrusted = Amount::ZERO;
227 for utxo in canon.list_unspent() {
228 if utxo.is_trusted {
229 trusted += utxo.txout.value;
230 } else {
231 untrusted += utxo.txout.value;
232 }
233 }
234 TrustedBalance { trusted, untrusted }
235 }
236
237 fn untrusted_utxos(&self, min_confs: u32) -> Vec<OutPoint> {
239 TrustedCanonicalization::from_wallet(self.borrow(), min_confs)
240 .list_unspent()
241 .filter(|u| !u.is_trusted)
242 .map(|u| u.outpoint)
243 .collect()
244 }
245
246 fn is_fully_owned_tx(&self, txid: Txid) -> bool {
249 let wallet = self.borrow();
250 let graph = wallet.tx_graph();
251 match graph.get_tx(txid) {
252 Some(tx) => {
253 tx.input.iter().all(|input| {
254 let prev = input.previous_output;
255 graph.get_tx(prev.txid)
256 .and_then(|prev_tx| prev_tx.output.get(prev.vout as usize).cloned())
257 .map(|out| wallet.is_mine(out.script_pubkey))
258 .unwrap_or(false)
259 })
260 }, None => false
261 }
262
263 }
264
265 fn set_checkpoint(&mut self, height: u32, hash: BlockHash) {
269 let checkpoint = BlockId { height, hash };
270 let wallet = self.borrow_mut();
271 wallet.apply_update(bdk_wallet::Update {
272 chain: Some(wallet.latest_checkpoint().insert(checkpoint)),
273 ..Default::default()
274 }).expect("should work, might fail if tip is genesis");
275 }
276
277 fn mark_output_keys_unused(&mut self, tx: &Transaction) {
282 let wallet = self.borrow_mut();
283 for txout in &tx.output {
284 if let Some((keychain, index)) = wallet.spk_index().index_of_spk(txout.script_pubkey.clone()) {
285 wallet.unmark_used(*keychain, *index);
288 }
289 }
290 }
291
292 fn make_signed_p2a_cpfp(
293 &mut self,
294 tx: &Transaction,
295 fees: MakeCpfpFees,
296 ) -> Result<Transaction, CpfpInternalError> {
297 let wallet = self.borrow_mut();
298 let (fee_anchor_point, fee_anchor_txout) = tx.fee_anchor()
299 .ok_or_else(|| CpfpInternalError::NoFeeAnchor(tx.compute_txid()))?;
300
301 let parent_weight = tx.weight();
304 let extra_fee_needed = parent_weight * fees.effective();
305
306 let change_addr = wallet.next_unused_address(KEYCHAIN);
308 let dust_limit = change_addr.address.script_pubkey().minimal_non_dust();
309
310 let mut final_child_weight = Weight::ZERO;
313 let mut fee_needed = extra_fee_needed;
314 for i in 0..100 {
315 if fee_needed < fee_anchor_txout.value {
322 if fee_anchor_txout.value - fee_needed < dust_limit {
323 fee_needed = fee_anchor_txout.value + Amount::ONE_SAT;
324 }
325 }
326
327 let mut b = wallet.build_tx();
328 b.only_witness_utxo();
329 b.exclude_unconfirmed();
330 b.version(3); b.add_fee_anchor_spend(fee_anchor_point, fee_anchor_txout);
332 b.drain_to(change_addr.address.script_pubkey());
333 b.fee_absolute(fee_needed);
334
335 let mut psbt = b.finish().map_err(|e| match e {
337 CreateTxError::CoinSelection(e) => CpfpInternalError::InsufficientConfirmedFunds(e),
338 _ => CpfpInternalError::Create(e),
339 })?;
340 #[allow(deprecated)]
341 let opts = bdk_wallet::SignOptions {
342 trust_witness_utxo: true,
343 ..Default::default()
344 };
345 let finalized = wallet.sign(&mut psbt, opts)
346 .map_err(|e| CpfpInternalError::Signer(e))?;
347 if !finalized {
348 return Err(CpfpInternalError::FinalizeError("finalization failed".into()));
349 }
350 let tx = psbt.extract_tx()
351 .map_err(|e| CpfpInternalError::Extract(e))?;
352 assert!(tx.input.iter().any(|i| i.previous_output == fee_anchor_point),
353 "Missing anchor spend, tx is {}", serialize_hex(&tx),
354 );
355
356 let tx_weight = tx.weight();
358 let total_weight = tx_weight + parent_weight;
359 if tx_weight != final_child_weight {
360 wallet.mark_output_keys_unused(&tx);
363 final_child_weight = tx_weight;
364 fee_needed = match fees {
365 MakeCpfpFees::Effective(fr) => total_weight * fr,
366 MakeCpfpFees::Rbf { min_effective_fee_rate, current_package_fee } => {
367 let min_tx_relay_fee = FeeRate::from_sat_per_vb(1).unwrap();
371 let min_package_fee = current_package_fee +
372 parent_weight * min_tx_relay_fee +
373 tx_weight * min_tx_relay_fee;
374
375 let desired_fee = total_weight * min_effective_fee_rate;
380 if desired_fee < min_package_fee {
381 debug!("Using a minimum fee of {} instead of the desired fee of {} for RBF",
382 min_package_fee, desired_fee,
383 );
384 min_package_fee
385 } else {
386 trace!("Attempting to use the desired fee of {} for CPFP RBF",
387 desired_fee,
388 );
389 desired_fee
390 }
391 }
392 }
393 } else {
394 debug!("Created P2A CPFP with weight {} and fee {} in {} iterations",
395 total_weight, fee_needed, i,
396 );
397 return Ok(tx);
398 }
399 }
400 Err(CpfpInternalError::General("Reached max iterations".into()))
401 }
402}
403
404impl WalletExt for Wallet {}