1use std::collections::{HashMap, HashSet};
15
16use bitcoin::transaction::{predict_weight, InputWeightPrediction};
17use bitcoin::{
18 Address, Amount, FeeRate, Sequence, Transaction, TxIn, TxOut, Weight, Witness, ScriptBuf,
19 sighash,
20};
21use bitcoin::secp256k1::{Secp256k1, SecretKey};
22
23use ark::Vtxo;
24use ark::vtxo::Full;
25use ark::vtxo::policy::signing::VtxoSigner;
26use bitcoin_ext::TxStatus;
27
28use ark::VtxoId;
29
30use crate::Wallet;
31use crate::exit::bdk::should_rbf;
32use crate::exit::{Exit, ExitError, ExitState, ExitTxStatus};
33use crate::onchain::MakeCpfpFees;
34
35#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct ExitFeeEstimate {
42 pub exit_broadcast_fee: Amount,
46 pub claim_fee: Amount,
49 pub fee_rate: FeeRate,
53 pub txs_to_broadcast: usize,
55 pub fundable: bool,
63}
64
65impl ExitFeeEstimate {
66 pub fn total(&self) -> Amount {
68 self.exit_broadcast_fee + self.claim_fee
69 }
70}
71
72impl Exit {
73 pub async fn estimate_emergency_exit_fee(
91 &self,
92 vtxos: &[VtxoId],
93 wallet: &Wallet,
94 fee_rate: Option<FeeRate>,
95 destination: Option<Address>,
96 ) -> anyhow::Result<ExitFeeEstimate, ExitError> {
97 let (broadcast_fee_rate, claim_fee_rate) = match fee_rate {
98 Some(fr) => (fr, fr),
99 None => (
100 self.default_exit_fee_rate().await,
101 wallet.chain().fee_rates().await.regular,
102 ),
103 };
104
105 let mut full_vtxos = HashMap::with_capacity(vtxos.len());
108 let mut seen = HashSet::new();
109 let mut unconfirmed_parents = Vec::new();
110 let mut pending_status = Vec::new();
111
112 {
113 let guard = self.inner.read().await;
114 for &vtxo_id in vtxos {
115 if full_vtxos.contains_key(&vtxo_id) {
116 continue;
117 }
118
119 let vtxo = wallet.inner.db.get_full_vtxo(vtxo_id).await
120 .map_err(|e| ExitError::InvalidWalletState { error: e.to_string() })?
121 .ok_or(ExitError::UnknownVtxo { vtxo: vtxo_id })?;
122
123 if let Err(error) = vtxo.check_standard() {
124 return Err(ExitError::NonStandardVtxo { vtxo: vtxo_id, error }.into());
125 }
126
127 match guard.exit_vtxos.iter().find(|ev| ev.id() == vtxo_id).map(|ev| ev.state()) {
128 Some(ExitState::Claimed(_)) => {
129 return Err(ExitError::VtxoAlreadyExited { vtxo: vtxo_id });
130 },
131 Some(ExitState::VtxoAlreadySpent(_)) => {
134 return Err(ExitError::VtxoAlreadySpent { vtxo: vtxo_id });
135 },
136 Some(ExitState::Processing(s)) => {
137 for exit_tx in &s.transactions {
144 let fees = match &exit_tx.status {
145 ExitTxStatus::Confirmed { .. } => continue,
146 ExitTxStatus::AwaitingConfirmation { .. } => {
147 match guard.tx_manager.get_child_status(exit_tx.txid).await {
148 Ok(Some(c)) => match c.fee_info {
149 Some(fi) if should_rbf(broadcast_fee_rate, fi.fee_rate) => {
151 MakeCpfpFees::Rbf {
152 min_effective_fee_rate: broadcast_fee_rate,
153 current_package_fee: fi.total_fee,
154 }
155 },
156 _ => continue,
157 },
158 _ => continue,
159 }
160 },
161 ExitTxStatus::VerifyInputs |
162 ExitTxStatus::AwaitingCpfpBroadcast |
163 ExitTxStatus::AwaitingInputConfirmation { .. } => {
164 MakeCpfpFees::Effective(broadcast_fee_rate)
165 },
166 };
167 if !seen.insert(exit_tx.txid) {
168 continue;
169 }
170 let package = guard.tx_manager.get_package(exit_tx.txid)?;
171 let tx = package.read().await.exit.tx.clone();
172 unconfirmed_parents.push((tx, fees));
173 }
174 },
175 Some(ExitState::AwaitingDelta(_)) |
177 Some(ExitState::Claimable(_)) |
178 Some(ExitState::ClaimInProgress(_)) => {},
179 Some(ExitState::Start(_)) | Some(ExitState::Canceled(_)) | None => {
182 for item in vtxo.transactions() {
183 pending_status.push(item.tx);
184 }
185 },
186 }
187
188 full_vtxos.insert(vtxo_id, vtxo);
189 }
190 }
191
192 for tx in pending_status {
195 let txid = tx.compute_txid();
196 if seen.contains(&txid) {
197 continue;
198 }
199 let mut guard = self.inner.write().await;
200 let status = guard.tx_manager.tx_status(txid).await
201 .map_err(|e| ExitError::TransactionRetrievalFailure { txid, error: e.to_string() })?;
202
203 let rbf = match status {
204 TxStatus::NotFound => {
205 MakeCpfpFees::Effective(broadcast_fee_rate)
206 },
207 TxStatus::Mempool => {
208 match guard.tx_manager.get_child_status(txid).await {
209 Ok(Some(c)) => c.fee_info.map(|f| MakeCpfpFees::Rbf {
210 min_effective_fee_rate: broadcast_fee_rate,
211 current_package_fee: f.total_fee,
212 }),
213 _ => None,
214 }.unwrap_or(MakeCpfpFees::Effective(broadcast_fee_rate))
215 },
216 TxStatus::Confirmed(_) => {
217 continue;
218 },
219 };
220
221 seen.insert(txid);
222 unconfirmed_parents.push((tx, rbf));
223 }
224
225 let txs_to_broadcast = unconfirmed_parents.len();
227 let (children, fundable) = match wallet.onchain() {
228 Some(onchain) => {
229 let walk = onchain.read().await
230 .estimate_p2a_cpfp_walk(&unconfirmed_parents)
231 .map_err(|e| ExitError::InternalError { error: e.to_string() })?;
232
233 let fundable = walk.shortfall.is_none();
237
238 (walk.children, fundable)
239 },
240 None => (vec![], false)
241 };
242
243 let mut exit_broadcast_fee = children.iter().map(|(_, fee)| *fee).sum::<Amount>();
244 for (parent, _) in unconfirmed_parents.iter().skip(children.len()) {
247 exit_broadcast_fee += broadcast_fee_rate * (parent.weight() + canonical_cpfp_child_weight());
248 }
249
250 let vtxos = full_vtxos.into_iter().map(|(_, vtxo)| vtxo).collect::<Vec<_>>();
252 let claim_fee = self.estimate_claim_fee(&vtxos, wallet, claim_fee_rate, destination).await?;
253
254 Ok(ExitFeeEstimate {
255 exit_broadcast_fee,
256 claim_fee,
257 fee_rate: broadcast_fee_rate,
258 txs_to_broadcast,
259 fundable,
260 })
261 }
262
263 async fn estimate_claim_fee(
268 &self,
269 vtxos: &[Vtxo<Full>],
270 wallet: &Wallet,
271 fee_rate: FeeRate,
272 destination: Option<Address>,
273 ) -> anyhow::Result<Amount, ExitError> {
274 if vtxos.is_empty() {
275 return Ok(Amount::ZERO);
276 }
277
278 let address = match destination {
279 Some(a) => a,
280 None => placeholder_p2tr_address(wallet).await?,
281 };
282
283 let tip = wallet.chain().tip().await
284 .map_err(|e| ExitError::TipRetrievalFailure { error: e.to_string() })?;
285 let locktime = bitcoin::absolute::LockTime::from_height(tip)
286 .map_err(|e| ExitError::InvalidLocktime { tip, error: e.to_string() })?;
287
288 let mut output_amount = Amount::ZERO;
289 let mut tx_ins = Vec::with_capacity(vtxos.len());
290 for vtxo in vtxos {
291 let clause = wallet.find_signable_clause(vtxo).await
292 .ok_or(ExitError::ClaimMissingSignableClause { vtxo: vtxo.id() })?;
293 output_amount += vtxo.amount();
294 tx_ins.push(TxIn {
295 previous_output: vtxo.point(),
296 script_sig: ScriptBuf::default(),
297 sequence: clause.sequence().unwrap_or(Sequence::ZERO),
298 witness: Witness::new(),
299 });
300 }
301
302 let mut tx = Transaction {
303 version: bitcoin::transaction::Version::TWO,
304 lock_time: locktime,
305 input: tx_ins,
306 output: vec![TxOut { script_pubkey: address.script_pubkey(), value: output_amount }],
307 };
308
309 let prevouts = vtxos.iter().map(|v| v.txout()).collect::<Vec<_>>();
312 let prevouts = sighash::Prevouts::All(&prevouts);
313 let mut witnesses = Vec::with_capacity(vtxos.len());
314 {
315 let mut shc = sighash::SighashCache::new(&tx);
316 for (i, vtxo) in vtxos.iter().enumerate() {
317 let witness = wallet.sign_input(vtxo, i, &mut shc, &prevouts).await
318 .map_err(|e| ExitError::ClaimSigningError { error: e.to_string() })?;
319 witnesses.push(witness);
320 }
321 }
322 for (input, witness) in tx.input.iter_mut().zip(witnesses) {
323 input.witness = witness;
324 }
325
326 Ok(fee_rate * tx.weight())
327 }
328}
329
330fn canonical_cpfp_child_weight() -> Weight {
333 const P2TR_SPK_LEN: usize = 34;
334 predict_weight(
335 [
336 InputWeightPrediction::new(0, [0usize; 0]),
338 InputWeightPrediction::P2TR_KEY_DEFAULT_SIGHASH,
339 ],
340 [P2TR_SPK_LEN],
341 )
342}
343
344async fn placeholder_p2tr_address(wallet: &Wallet) -> anyhow::Result<Address, ExitError> {
346 let network = wallet.network().await
347 .map_err(|e| ExitError::InternalError { error: e.to_string() })?;
348 let secp = Secp256k1::new();
349 let sk = SecretKey::from_slice(&[1u8; 32]).expect("valid secret key");
350 let (xonly, _) = sk.public_key(&secp).x_only_public_key();
351 Ok(Address::p2tr(&secp, xonly, None, network))
352}
353
354#[cfg(test)]
355mod test {
356 use super::*;
357
358 #[test]
359 fn canonical_child_weight_is_plausible() {
360 let w = canonical_cpfp_child_weight();
363 assert!(w > Weight::from_vb_unchecked(90), "child weight too small: {}", w);
364 assert!(w < Weight::from_vb_unchecked(200), "child weight too large: {}", w);
365 }
366}