1
2use std::borrow::BorrowMut;
3use std::sync::Arc;
4
5use bdk_wallet::{AddressInfo, TxBuilder, Wallet};
6use bdk_wallet::chain::BlockId;
7use bdk_wallet::coin_selection::InsufficientFunds;
8use bdk_wallet::error::CreateTxError;
9use bitcoin::consensus::encode::serialize_hex;
10use bitcoin::{Amount, BlockHash, FeeRate, OutPoint, Transaction, TxOut, Txid, Weight, Witness};
11use bitcoin::psbt::{ExtractTxError, Input};
12use log::{debug, trace};
13
14use crate::{BlockHeight, TransactionExt};
15use crate::cpfp::MakeCpfpFees;
16use crate::fee::FEE_ANCHOR_SPEND_WEIGHT;
17
18#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
20pub struct TrustedBalance {
21 pub trusted: Amount,
23 pub untrusted: Amount,
25}
26
27impl TrustedBalance {
28 pub fn total(&self) -> Amount {
29 self.trusted + self.untrusted
30 }
31}
32
33pub const KEYCHAIN: bdk_wallet::KeychainKind = bdk_wallet::KeychainKind::External;
35
36
37pub trait TxBuilderExt<'a, A>: BorrowMut<TxBuilder<'a, A>> {
39 fn add_fee_anchor_spend(&mut self, anchor: OutPoint, output: &TxOut)
41 where
42 A: bdk_wallet::coin_selection::CoinSelectionAlgorithm,
43 {
44 let psbt_in = Input {
45 witness_utxo: Some(output.clone()),
46 final_script_witness: Some(Witness::new()),
47 ..Default::default()
48 };
49 self.borrow_mut().add_foreign_utxo(anchor, psbt_in, FEE_ANCHOR_SPEND_WEIGHT)
50 .expect("adding foreign utxo");
51 }
52}
53impl<'a, A> TxBuilderExt<'a, A> for TxBuilder<'a, A> {}
54
55#[derive(Debug, thiserror::Error)]
56pub enum CpfpInternalError {
57 #[error("{0}")]
58 General(String),
59 #[error("Unable to construct transaction: {0}")]
60 Create(CreateTxError),
61 #[error("Unable to extract the final transaction after signing the PSBT: {0}")]
62 Extract(ExtractTxError),
63 #[error("Failed to determine the weight/fee when creating a P2A CPFP")]
64 Fee(),
65 #[error("Unable to finalize CPFP transaction: {0}")]
66 FinalizeError(String),
67 #[error("You need more confirmations on your on-chain funds: {0}")]
68 InsufficientConfirmedFunds(InsufficientFunds),
69 #[error("Transaction has no fee anchor: {0}")]
70 NoFeeAnchor(Txid),
71 #[allow(deprecated)]
72 #[error("Unable to sign transaction: {0}")]
73 Signer(bdk_wallet::signer::SignerError),
74}
75
76pub trait WalletExt: BorrowMut<Wallet> {
78 fn peek_next_address(&self) -> AddressInfo {
80 self.borrow().peek_address(KEYCHAIN, self.borrow().next_derivation_index(KEYCHAIN))
81 }
82
83 fn unconfirmed_txids(&self) -> impl Iterator<Item = Txid> {
85 self.borrow().transactions().filter_map(|tx| {
86 if tx.chain_position.is_unconfirmed() {
87 Some(tx.tx_node.txid)
88 } else {
89 None
90 }
91 })
92 }
93
94 fn unconfirmed_txs(&self) -> impl Iterator<Item = Arc<Transaction>> {
97 self.borrow().transactions().filter_map(|tx| {
98 if tx.chain_position.is_unconfirmed() {
99 Some(tx.tx_node.tx.clone())
100 } else {
101 None
102 }
103 })
104 }
105
106 fn is_trusted_utxo(&self, outpoint: OutPoint, min_confs: u32) -> bool {
110 self.is_trusted_tx(outpoint.txid, min_confs)
111 }
112
113 fn is_trusted_tx(&self, txid: Txid, min_confs: u32) -> bool {
124 let w = self.borrow();
125 let tip = w.latest_checkpoint().height();
126 let mut budget = 100u32;
127 is_trusted_tx_inner(w, txid, min_confs, tip, &mut budget)
128 }
129
130 fn trusted_balance(&self, min_confs: u32) -> TrustedBalance {
132 let mut trusted = Amount::ZERO;
133 let mut untrusted = Amount::ZERO;
134 for utxo in self.borrow().list_unspent() {
135 if self.is_trusted_utxo(utxo.outpoint, min_confs) {
136 trusted += utxo.txout.value;
137 } else {
138 untrusted += utxo.txout.value;
139 }
140 }
141 TrustedBalance { trusted, untrusted }
142 }
143
144 fn untrusted_utxos(&self, min_confs: u32) -> Vec<OutPoint> {
148 self.borrow().list_unspent()
149 .filter(|utxo| !self.is_trusted_utxo(utxo.outpoint, min_confs))
150 .map(|utxo| utxo.outpoint)
151 .collect()
152 }
153
154 fn set_checkpoint(&mut self, height: u32, hash: BlockHash) {
158 let checkpoint = BlockId { height, hash };
159 let wallet = self.borrow_mut();
160 wallet.apply_update(bdk_wallet::Update {
161 chain: Some(wallet.latest_checkpoint().insert(checkpoint)),
162 ..Default::default()
163 }).expect("should work, might fail if tip is genesis");
164 }
165
166 fn make_signed_p2a_cpfp(
167 &mut self,
168 tx: &Transaction,
169 fees: MakeCpfpFees,
170 ) -> Result<Transaction, CpfpInternalError> {
171 let wallet = self.borrow_mut();
172 let (outpoint, txout) = tx.fee_anchor()
173 .ok_or_else(|| CpfpInternalError::NoFeeAnchor(tx.compute_txid()))?;
174
175 let p2a_weight = tx.weight();
178 let extra_fee_needed = p2a_weight * fees.effective();
179
180 let change_addr = wallet.reveal_next_address(KEYCHAIN);
182 let dust_limit = change_addr.address.script_pubkey().minimal_non_dust();
183
184 let mut spend_weight = Weight::ZERO;
187 let mut fee_needed = extra_fee_needed;
188 for i in 0..100 {
189 if fee_needed < txout.value {
196 if txout.value - fee_needed < dust_limit {
197 fee_needed = txout.value + Amount::ONE_SAT;
198 }
199 }
200
201 let mut b = wallet.build_tx();
202 b.only_witness_utxo();
203 b.exclude_unconfirmed();
204 b.version(3); b.add_fee_anchor_spend(outpoint, txout);
206 b.drain_to(change_addr.address.script_pubkey());
207 b.fee_absolute(fee_needed);
208
209 let mut psbt = b.finish().map_err(|e| match e {
211 CreateTxError::CoinSelection(e) => CpfpInternalError::InsufficientConfirmedFunds(e),
212 _ => CpfpInternalError::Create(e),
213 })?;
214 #[allow(deprecated)]
215 let opts = bdk_wallet::SignOptions {
216 trust_witness_utxo: true,
217 ..Default::default()
218 };
219 let finalized = wallet.sign(&mut psbt, opts)
220 .map_err(|e| CpfpInternalError::Signer(e))?;
221 if !finalized {
222 return Err(CpfpInternalError::FinalizeError("finalization failed".into()));
223 }
224 let tx = psbt.extract_tx()
225 .map_err(|e| CpfpInternalError::Extract(e))?;
226 let anchor_weight = FEE_ANCHOR_SPEND_WEIGHT.to_wu();
227 assert!(tx.input.iter().any(|i| i.witness.size() as u64 == anchor_weight),
228 "Missing anchor spend, tx is {}", serialize_hex(&tx),
229 );
230
231 let tx_weight = tx.weight();
233 let total_weight = tx_weight + p2a_weight;
234 if tx_weight != spend_weight {
235 wallet.cancel_tx(&tx);
238 spend_weight = tx_weight;
239 fee_needed = match fees {
240 MakeCpfpFees::Effective(fr) => total_weight * fr,
241 MakeCpfpFees::Rbf { min_effective_fee_rate, current_package_fee } => {
242 let min_tx_relay_fee = FeeRate::from_sat_per_vb(1).unwrap();
246 let min_package_fee = current_package_fee +
247 p2a_weight * min_tx_relay_fee +
248 tx_weight * min_tx_relay_fee;
249
250 let desired_fee = total_weight * min_effective_fee_rate;
255 if desired_fee < min_package_fee {
256 debug!("Using a minimum fee of {} instead of the desired fee of {} for RBF",
257 min_package_fee, desired_fee,
258 );
259 min_package_fee
260 } else {
261 trace!("Attempting to use the desired fee of {} for CPFP RBF",
262 desired_fee,
263 );
264 desired_fee
265 }
266 }
267 }
268 } else {
269 debug!("Created P2A CPFP with weight {} and fee {} in {} iterations",
270 total_weight, fee_needed, i,
271 );
272 return Ok(tx);
273 }
274 }
275 Err(CpfpInternalError::General("Reached max iterations".into()))
276 }
277}
278
279impl WalletExt for Wallet {}
280
281fn is_trusted_tx_inner(
282 w: &Wallet, txid: Txid, min_confs: u32, tip: BlockHeight, budget: &mut u32,
283) -> bool {
284 if *budget == 0 {
285 return false;
286 }
287 *budget -= 1;
288
289 let Some(tx) = w.get_tx(txid) else {
290 return false;
291 };
292
293 let nb_confs = match tx.chain_position.confirmation_height_upper_bound() {
295 Some(h) => tip.saturating_sub(h) + 1,
296 None => 0,
297 };
298 if nb_confs >= min_confs {
299 return true;
300 }
301
302 tx.tx_node.tx.input.iter().all(|input| {
304 let prev = input.previous_output;
305 let Some(prev_tx) = w.get_tx(prev.txid) else {
306 return false;
307 };
308 let Some(txout) = prev_tx.tx_node.tx.output.get(prev.vout as usize) else {
309 return false;
310 };
311 w.is_mine(txout.script_pubkey.clone())
312 && is_trusted_tx_inner(w, prev.txid, min_confs, tip, budget)
313 })
314}