1
2use std::borrow::BorrowMut;
3use std::sync::Arc;
4
5use bdk_wallet::{AddressInfo, SignOptions, TxBuilder, Wallet};
6use bdk_wallet::chain::BlockId;
7use bdk_wallet::coin_selection::InsufficientFunds;
8use bdk_wallet::error::CreateTxError;
9use bdk_wallet::signer::SignerError;
10use bitcoin::consensus::encode::serialize_hex;
11use bitcoin::{BlockHash, FeeRate, OutPoint, Transaction, TxOut, Txid, Weight, Witness};
12use bitcoin::psbt::{ExtractTxError, Input};
13use log::{debug, trace};
14
15use crate::{BlockHeight, TransactionExt};
16use crate::cpfp::MakeCpfpFees;
17use crate::fee::FEE_ANCHOR_SPEND_WEIGHT;
18
19pub const KEYCHAIN: bdk_wallet::KeychainKind = bdk_wallet::KeychainKind::External;
21
22
23pub trait TxBuilderExt<'a, A>: BorrowMut<TxBuilder<'a, A>> {
25 fn add_fee_anchor_spend(&mut self, anchor: OutPoint, output: &TxOut)
27 where
28 A: bdk_wallet::coin_selection::CoinSelectionAlgorithm,
29 {
30 let psbt_in = Input {
31 witness_utxo: Some(output.clone()),
32 final_script_witness: Some(Witness::new()),
33 ..Default::default()
34 };
35 self.borrow_mut().add_foreign_utxo(anchor, psbt_in, FEE_ANCHOR_SPEND_WEIGHT)
36 .expect("adding foreign utxo");
37 }
38}
39impl<'a, A> TxBuilderExt<'a, A> for TxBuilder<'a, A> {}
40
41#[derive(Debug, thiserror::Error)]
42pub enum CpfpInternalError {
43 #[error("{0}")]
44 General(String),
45 #[error("Unable to construct transaction: {0}")]
46 Create(CreateTxError),
47 #[error("Unable to extract the final transaction after signing the PSBT: {0}")]
48 Extract(ExtractTxError),
49 #[error("Failed to determine the weight/fee when creating a P2A CPFP")]
50 Fee(),
51 #[error("Unable to finalize CPFP transaction: {0}")]
52 FinalizeError(String),
53 #[error("You need more confirmations on your on-chain funds: {0}")]
54 InsufficientConfirmedFunds(InsufficientFunds),
55 #[error("Transaction has no fee anchor: {0}")]
56 NoFeeAnchor(Txid),
57 #[error("Unable to sign transaction: {0}")]
58 Signer(SignerError),
59}
60
61pub trait WalletExt: BorrowMut<Wallet> {
63 fn peek_next_address(&self) -> AddressInfo {
65 self.borrow().peek_address(KEYCHAIN, self.borrow().next_derivation_index(KEYCHAIN))
66 }
67
68 fn unconfirmed_txids(&self) -> impl Iterator<Item = Txid> {
70 self.borrow().transactions().filter_map(|tx| {
71 if tx.chain_position.is_unconfirmed() {
72 Some(tx.tx_node.txid)
73 } else {
74 None
75 }
76 })
77 }
78
79 fn unconfirmed_txs(&self) -> impl Iterator<Item = Arc<Transaction>> {
82 self.borrow().transactions().filter_map(|tx| {
83 if tx.chain_position.is_unconfirmed() {
84 Some(tx.tx_node.tx.clone())
85 } else {
86 None
87 }
88 })
89 }
90
91 fn untrusted_utxos(&self, confirmed_height: Option<BlockHeight>) -> Vec<OutPoint> {
93 let w = self.borrow();
94 let mut ret = Vec::new();
95 for utxo in w.list_unspent() {
96 if let Some(h) = utxo.chain_position.confirmation_height_upper_bound() {
98 if let Some(min) = confirmed_height {
99 if h <= min {
100 continue;
101 }
102 } else {
103 continue;
104 }
105 }
106
107 let txid = utxo.outpoint.txid;
112 if let Some(tx) = w.get_tx(txid) {
113 if tx.tx_node.tx.input.iter().all(|i| w.get_tx(i.previous_output.txid).is_some()) {
114 continue;
115 }
116 }
117
118 ret.push(utxo.outpoint);
119 }
120 ret
121 }
122
123 fn set_checkpoint(&mut self, height: u32, hash: BlockHash) {
127 let checkpoint = BlockId { height, hash };
128 let wallet = self.borrow_mut();
129 wallet.apply_update(bdk_wallet::Update {
130 chain: Some(wallet.latest_checkpoint().insert(checkpoint)),
131 ..Default::default()
132 }).expect("should work, might fail if tip is genesis");
133 }
134
135 fn make_signed_p2a_cpfp(
136 &mut self,
137 tx: &Transaction,
138 fees: MakeCpfpFees,
139 ) -> Result<Transaction, CpfpInternalError> {
140 let wallet = self.borrow_mut();
141 let (outpoint, txout) = tx.fee_anchor()
142 .ok_or_else(|| CpfpInternalError::NoFeeAnchor(tx.compute_txid()))?;
143
144 let p2a_weight = tx.weight();
147 let extra_fee_needed = p2a_weight * fees.effective();
148
149 let change_addr = wallet.reveal_next_address(KEYCHAIN);
151
152 let mut spend_weight = Weight::ZERO;
155 let mut fee_needed = extra_fee_needed;
156 for i in 0..100 {
157 let mut b = wallet.build_tx();
158 b.only_witness_utxo();
159 b.exclude_unconfirmed();
160 b.version(3); b.add_fee_anchor_spend(outpoint, txout);
162 b.drain_to(change_addr.address.script_pubkey());
163 b.fee_absolute(fee_needed);
164
165 let mut psbt = b.finish().map_err(|e| match e {
167 CreateTxError::CoinSelection(e) => CpfpInternalError::InsufficientConfirmedFunds(e),
168 _ => CpfpInternalError::Create(e),
169 })?;
170 let opts = SignOptions {
171 trust_witness_utxo: true,
172 ..Default::default()
173 };
174 let finalized = wallet.sign(&mut psbt, opts)
175 .map_err(|e| CpfpInternalError::Signer(e))?;
176 if !finalized {
177 return Err(CpfpInternalError::FinalizeError("finalization failed".into()));
178 }
179 let tx = psbt.extract_tx()
180 .map_err(|e| CpfpInternalError::Extract(e))?;
181 let anchor_weight = FEE_ANCHOR_SPEND_WEIGHT.to_wu();
182 assert!(tx.input.iter().any(|i| i.witness.size() as u64 == anchor_weight),
183 "Missing anchor spend, tx is {}", serialize_hex(&tx),
184 );
185
186 let tx_weight = tx.weight();
188 let total_weight = tx_weight + p2a_weight;
189 if tx_weight != spend_weight {
190 wallet.cancel_tx(&tx);
193 spend_weight = tx_weight;
194 fee_needed = match fees {
195 MakeCpfpFees::Effective(fr) => total_weight * fr,
196 MakeCpfpFees::Rbf { min_effective_fee_rate, current_package_fee } => {
197 let min_tx_relay_fee = FeeRate::from_sat_per_vb(1).unwrap();
201 let min_package_fee = current_package_fee +
202 p2a_weight * min_tx_relay_fee +
203 tx_weight * min_tx_relay_fee;
204
205 let desired_fee = total_weight * min_effective_fee_rate;
210 if desired_fee < min_package_fee {
211 debug!("Using a minimum fee of {} instead of the desired fee of {} for RBF",
212 min_package_fee, desired_fee,
213 );
214 min_package_fee
215 } else {
216 trace!("Attempting to use the desired fee of {} for CPFP RBF",
217 desired_fee,
218 );
219 desired_fee
220 }
221 }
222 }
223 } else {
224 debug!("Created P2A CPFP with weight {} and fee {} in {} iterations",
225 total_weight, fee_needed, i,
226 );
227 return Ok(tx);
228 }
229 }
230 Err(CpfpInternalError::General("Reached max iterations".into()))
231 }
232}
233
234impl WalletExt for Wallet {}