1mod models;
117mod vtxo;
118mod estimate;
119pub mod bdk;
120pub(crate) mod progress;
121pub(crate) mod transaction_manager;
122
123pub use self::models::{
124 ExitCpfpRequest, ExitTransactionPackage, FeeInfo, RbfRequirement, TransactionInfo,
125 ChildTransactionInfo, ExitError, ExitState, ExitTx, ExitTxStatus, ExitTxOrigin, ExitStartState,
126 ExitProcessingState, ExitAwaitingDeltaState, ExitClaimableState, ExitClaimInProgressState,
127 ExitClaimedState, ExitVtxoAlreadySpentState, ExitCanceledState, ExitStateKind,
128 ExitProgressStatus, ExitTransactionStatus,
129};
130pub use self::vtxo::ExitVtxo;
131pub use self::estimate::ExitFeeEstimate;
132
133use std::borrow::Borrow;
134use std::cmp;
135use std::collections::HashMap;
136use std::sync::Arc;
137
138use anyhow::Context;
139use bitcoin::{
140 Address, Amount, FeeRate, Psbt, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness, sighash
141};
142use bitcoin::consensus::Params;
143use log::{error, info, trace, warn};
144
145use ark::{Vtxo, VtxoId};
146use ark::vtxo::Bare;
147use ark::vtxo::policy::signing::VtxoSigner;
148use bitcoin_ext::{BlockHeight, P2TR_DUST, TxStatus};
149
150use crate::Wallet;
151use crate::chain::ChainSource;
152use crate::exit::transaction_manager::ExitTransactionManager;
153use crate::movement::{MovementDestination, MovementStatus, PaymentMethod};
154use crate::movement::manager::MovementManager;
155use crate::movement::update::MovementUpdate;
156
157use crate::persist::BarkPersister;
158use crate::persist::models::StoredExit;
159use crate::psbtext::PsbtInputExt;
160use crate::subsystem::{ExitMovement, Subsystem};
161use crate::vtxo::VtxoStateKind;
162
163pub(crate) struct ExitInner {
165 tx_manager: ExitTransactionManager,
166 persister: Arc<dyn BarkPersister>,
167 chain_source: Arc<ChainSource>,
168 movement_manager: Arc<MovementManager>,
169
170 exit_vtxos: Vec<ExitVtxo>,
171}
172
173impl ExitInner {
174 async fn start_exit_for_vtxos(
177 &mut self,
178 vtxos: &[impl Borrow<Vtxo<Bare>>],
179 skip_standardness_checks: bool,
180 ) -> anyhow::Result<()> {
181 if vtxos.is_empty() {
182 return Ok(());
183 }
184 let tip = self.chain_source.tip().await?;
185 let params = Params::new(self.chain_source.network());
186 for vtxo in vtxos {
187 let vtxo = vtxo.borrow();
188 let vtxo_id = vtxo.id();
189 if self.exit_vtxos.iter().any(|ev| ev.id() == vtxo_id) {
190 warn!("VTXO {} is already in the exit process", vtxo_id);
191 continue;
192 }
193
194 if !skip_standardness_checks {
195 if vtxo.amount() < P2TR_DUST {
197 return Err(ExitError::DustLimit {
198 vtxo: vtxo_id,
199 amount: vtxo.amount(),
200 dust: P2TR_DUST,
201 }.into());
202 }
203
204 let full_vtxo = self.persister.get_full_vtxo(vtxo_id).await?
212 .ok_or_else(|| ExitError::InvalidWalletState {
213 error: format!("missing genesis for VTXO {vtxo_id}"),
214 })?;
215 if let Err(error) = full_vtxo.check_standard() {
216 return Err(ExitError::NonStandardVtxo { vtxo: vtxo_id, error }.into());
217 }
218 }
219
220 let balance = -vtxo.amount().to_signed()?;
225 let script_pubkey = vtxo.output_script_pubkey();
226 let payment_method = match Address::from_script(&script_pubkey, ¶ms) {
227 Ok(addr) => PaymentMethod::Bitcoin(addr.into_unchecked()),
228 Err(e) => {
229 warn!("Unable to convert script pubkey to address: {:#}", e);
230 PaymentMethod::OutputScript(script_pubkey)
231 }
232 };
233
234 let movement_id = self.movement_manager.new_movement_with_update(
235 Subsystem::EXIT,
236 ExitMovement::Exit.to_string(),
237 MovementUpdate::new()
238 .intended_and_effective_balance(balance)
239 .consumed_vtxo(vtxo_id)
240 .sent_to([MovementDestination::new(payment_method, vtxo.amount())]),
241 ).await.context("Failed to register exit movement")?;
242
243 trace!("Starting exit for VTXO: {}", vtxo_id);
246 let exit = ExitVtxo::new(vtxo, tip, Some(movement_id));
247 self.persister.store_exit_vtxo_entry(&StoredExit::new(&exit)).await?;
248 self.exit_vtxos.push(exit);
249 trace!("Exit for VTXO started successfully: {}", vtxo_id);
250 }
251 Ok(())
252 }
253
254 async fn refresh_tx_state(&mut self) -> anyhow::Result<()> {
256 let mut exit_vtxos = std::mem::take(&mut self.exit_vtxos);
257 for exit in &mut exit_vtxos {
258 if !exit.is_initialized() {
259 match exit.initialize(&mut self.tx_manager, &*self.persister).await {
260 Ok(()) => continue,
261 Err(e) => {
262 error!("Error initializing exit for VTXO {}: {:#}", exit.id(), e);
263 }
264 }
265 }
266 }
267 self.exit_vtxos = exit_vtxos;
268 self.tx_manager.sync().await?;
269 Ok(())
270 }
271
272 async fn sign_exit_claim_inputs(
275 &self,
276 psbt: &mut Psbt,
277 wallet: &Wallet,
278 ) -> anyhow::Result<()> {
279 let prevouts = psbt.inputs.iter()
280 .map(|i| i.witness_utxo.clone().unwrap())
281 .collect::<Vec<_>>();
282
283 let prevouts = sighash::Prevouts::All(&prevouts);
284 let mut shc = sighash::SighashCache::new(&psbt.unsigned_tx);
285
286 let claimable = self.exit_vtxos.iter()
287 .filter(|ev| ev.is_claimable())
288 .map(|e| (e.id(), e))
289 .collect::<HashMap<_, _>>();
290
291 for (i, input) in psbt.inputs.iter_mut().enumerate() {
292 let vtxo = input.get_exit_claim_input();
293
294 if let Some(vtxo) = vtxo {
295 let exit_vtxo = claimable.get(&vtxo.id()).context("vtxo is not claimable yet")?;
296
297 let witness = wallet.sign_input(&vtxo, i, &mut shc, &prevouts).await
298 .map_err(|e| ExitError::ClaimSigningError { error: e.to_string() })?;
299
300 input.final_script_witness = Some(witness);
301 let _ = exit_vtxo;
302 }
303 }
304
305 Ok(())
306 }
307
308 async fn exit_status(
311 &self,
312 entry: StoredExit,
313 include_history: bool,
314 include_transactions: bool,
315 ) -> anyhow::Result<ExitTransactionStatus> {
316 let mut transactions = Vec::new();
317 if include_transactions {
318 let vtxo = self.persister.get_full_vtxo(entry.vtxo_id).await
319 .context("failed to retrieve VTXO for exit")?
320 .with_context(|| format!("failed to retrieve VTXO for exit {}", entry.vtxo_id))?;
321 for tx in vtxo.transactions() {
322 let txid = tx.tx.compute_txid();
323 if let Some(package) = self.tx_manager.try_get_package(txid) {
324 transactions.push(package.read().await.clone());
325 continue;
326 }
327 let child = self.persister.get_exit_child_tx(txid).await
328 .context("failed to retrieve child tx for exit")?;
329 transactions.push(ExitTransactionPackage {
330 exit: TransactionInfo {
331 txid,
332 tx: tx.tx,
333 },
334 child: child.map(|(tx, origin)| ChildTransactionInfo {
335 origin,
336 info: TransactionInfo {
337 txid: tx.compute_txid(),
338 tx,
339 },
340 fee_info: None,
341 }),
342 });
343 }
344 }
345 Ok(ExitTransactionStatus {
346 vtxo_id: entry.vtxo_id,
347 state: entry.state,
348 history: if include_history { Some(entry.history) } else { None },
349 transactions,
350 })
351 }
352
353 async fn exit_statuses(
355 &self,
356 entries: Vec<StoredExit>,
357 include_history: bool,
358 include_transactions: bool,
359 ) -> anyhow::Result<Vec<ExitTransactionStatus>> {
360 let mut statuses = Vec::with_capacity(entries.len());
361 for entry in entries {
362 statuses.push(self.exit_status(entry, include_history, include_transactions).await?);
363 }
364 Ok(statuses)
365 }
366}
367
368pub struct Exit {
371 inner: Arc<tokio::sync::RwLock<ExitInner>>,
372}
373
374impl Exit {
375 pub(crate) async fn new(
376 persister: Arc<dyn BarkPersister>,
377 chain_source: Arc<ChainSource>,
378 movement_manager: Arc<MovementManager>,
379 ) -> anyhow::Result<Exit> {
380 let tx_manager = ExitTransactionManager::new(persister.clone(), chain_source.clone())?;
381 let inner = ExitInner {
382 exit_vtxos: Vec::new(),
383 tx_manager,
384 persister,
385 chain_source,
386 movement_manager,
387 };
388 Ok(Exit { inner: Arc::new(tokio::sync::RwLock::new(inner)) })
389 }
390
391 pub(crate) async fn load(&self) -> anyhow::Result<()> {
392 let mut guard = self.inner.write().await;
393 let inner = &mut *guard;
394
395 let exit_vtxo_entries = inner.persister
397 .get_exit_vtxo_entries_with_states(ExitStateKind::LIVE_STATES).await?;
398 inner.exit_vtxos.reserve(exit_vtxo_entries.len());
399
400 for entry in exit_vtxo_entries {
401 if let Some(vtxo) = inner.persister.get_wallet_vtxo(entry.vtxo_id).await? {
402 let mut exit = ExitVtxo::from_entry(entry, &vtxo);
403 exit.initialize(&mut inner.tx_manager, &*inner.persister).await?;
404 inner.exit_vtxos.push(exit);
405 } else {
406 error!("VTXO {} is marked for exit but it's missing from the database", entry.vtxo_id);
407 }
408 }
409 Ok(())
410 }
411
412 pub async fn default_exit_fee_rate(&self) -> FeeRate {
416 self.inner.read().await.chain_source.fee_rates().await.fast
417 }
418
419 pub async fn get_exit_status(
426 &self,
427 vtxo_id: VtxoId,
428 include_history: bool,
429 include_transactions: bool,
430 ) -> anyhow::Result<Option<ExitTransactionStatus>> {
431 let guard = self.inner.read().await;
432 match guard.persister.get_exit_vtxo_entry(&vtxo_id).await? {
433 None => Ok(None),
434 Some(entry) => {
435 Ok(Some(guard.exit_status(entry, include_history, include_transactions).await?))
436 },
437 }
438 }
439
440 pub async fn get_exit_vtxo(&self, vtxo_id: VtxoId) -> Option<ExitVtxo> {
442 let guard = self.inner.read().await;
443 guard.exit_vtxos.iter().find(|ev| ev.id() == vtxo_id).cloned()
444 }
445
446 pub async fn get_exit_vtxo_ids(&self) -> Vec<VtxoId> {
448 let guard = self.inner.read().await;
449 guard.exit_vtxos.iter().map(|ev| ev.id()).collect()
450 }
451
452 pub async fn get_exit_vtxos(&self) -> Vec<ExitVtxo> {
454 let guard = self.inner.read().await;
455 guard.exit_vtxos.clone()
456 }
457
458 pub async fn list_all(
460 &self,
461 include_history: bool,
462 include_transactions: bool,
463 ) -> anyhow::Result<Vec<ExitTransactionStatus>> {
464 let guard = self.inner.read().await;
465 let entries = guard.persister.get_exit_vtxo_entries().await?;
466 guard.exit_statuses(entries, include_history, include_transactions).await
467 }
468
469 pub async fn list_live(
471 &self,
472 include_history: bool,
473 include_transactions: bool,
474 ) -> anyhow::Result<Vec<ExitTransactionStatus>> {
475 let guard = self.inner.read().await;
476 let entries = guard.persister
477 .get_exit_vtxo_entries_with_states(ExitStateKind::LIVE_STATES).await?;
478 guard.exit_statuses(entries, include_history, include_transactions).await
479 }
480
481 pub async fn list_finished(
484 &self,
485 include_history: bool,
486 include_transactions: bool,
487 ) -> anyhow::Result<Vec<ExitTransactionStatus>> {
488 let guard = self.inner.read().await;
489 let entries = guard.persister
490 .get_exit_vtxo_entries_with_states(ExitStateKind::FINISHED_STATES).await?;
491 guard.exit_statuses(entries, include_history, include_transactions).await
492 }
493
494 pub async fn is_exiting(&self, vtxo_id: VtxoId) -> bool {
496 let guard = self.inner.read().await;
497 let state = guard.exit_vtxos.iter().find(|ev| ev.id() == vtxo_id).map(|ev| ev.state());
498 match state {
499 Some(ExitState::Start(_)) => true,
500 Some(ExitState::Processing(_)) => true,
501 Some(ExitState::AwaitingDelta(_)) => true,
502 Some(ExitState::Claimable(_)) => true,
503 Some(ExitState::ClaimInProgress(_)) => true,
504 Some(ExitState::Claimed(_)) => true,
505 Some(ExitState::VtxoAlreadySpent(_)) => false,
506 Some(ExitState::Canceled(_)) => false,
507 None => false,
508 }
509 }
510
511 pub async fn has_pending_exits(&self) -> bool {
513 let guard = self.inner.read().await;
514 guard.exit_vtxos.iter().any(|ev| ev.state().is_pending())
515 }
516
517 pub fn try_pending_total(&self) -> Option<Amount> {
523 self.inner.try_read().ok().map(|guard| {
524 guard.exit_vtxos.iter()
525 .filter(|ev| matches!(
526 ev.state(),
527 ExitState::AwaitingDelta(_)
528 | ExitState::Claimable(_)
529 | ExitState::ClaimInProgress(_),
530 ))
531 .map(|ev| ev.amount())
532 .sum()
533 })
534 }
535
536 pub async fn all_claimable_at_height(&self) -> Option<BlockHeight> {
538 let guard = self.inner.read().await;
539 let mut highest_claimable_height = None;
540 for exit in &guard.exit_vtxos {
541 match exit.state().claimable_height() {
542 Some(h) => highest_claimable_height = cmp::max(highest_claimable_height, Some(h)),
543 None => continue,
544 }
545 }
546 highest_claimable_height
547 }
548
549 pub async fn start_exit_for_entire_wallet(&self) -> anyhow::Result<()> {
556 let mut guard = self.inner.write().await;
557 let all_vtxos = guard.persister.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?
558 .into_iter();
559
560 let total_vtxos = all_vtxos.len();
562 let mut eligible = Vec::with_capacity(total_vtxos);
563 for v in all_vtxos {
564 match guard.persister.get_full_vtxo(v.id()).await {
566 Ok(Some(full)) => match full.check_standard() {
567 Ok(()) => eligible.push(v.vtxo),
568 Err(e) => warn!("Skipping non-standard VTXO {}: {:#}", v.id(), e),
569 },
570 Ok(None) => error!("Failed to retrieve full VTXO: {}", v.id()),
571 Err(e) => error!("Failed to retrieve full VTXO {}: {:#}", v.id(), e),
572 }
573 }
574
575 let ineligible = total_vtxos - eligible.len();
577 if eligible.is_empty() && ineligible > 0 {
578 warn!(
579 "Exit not started: all {} VTXOs are non-standard. To exit and consolidate you \
580 should try refreshing your VTXOs first",
581 ineligible,
582 );
583 return Ok(());
584 }
585
586 guard.start_exit_for_vtxos(&eligible, false).await
587 }
588
589 pub async fn start_exit_for_vtxos(
596 &self,
597 vtxos: &[impl Borrow<Vtxo<Bare>>],
598 ) -> anyhow::Result<()> {
599 let mut guard = self.inner.write().await;
600 guard.start_exit_for_vtxos(vtxos, false).await
601 }
602
603 pub async fn start_exit_for_vtxos_including_non_standard(
608 &self,
609 vtxos: &[impl Borrow<Vtxo<Bare>>],
610 ) -> anyhow::Result<()> {
611 let mut guard = self.inner.write().await;
612 guard.start_exit_for_vtxos(vtxos, true).await
613 }
614
615 pub async fn cancel_exit(&self, vtxo_id: VtxoId) -> anyhow::Result<(), ExitError> {
630 let mut guard = self.inner.write().await;
631 let inner = &mut *guard;
632
633 let idx = match inner.exit_vtxos.iter().position(|ev| ev.id() == vtxo_id) {
634 Some(idx) => idx,
635 None => {
636 let entry = inner.persister.get_exit_vtxo_entry(&vtxo_id).await
638 .map_err(|e| ExitError::InternalError { error: e.to_string() })?;
639 return match entry.map(|e| e.state.kind()) {
640 Some(ExitStateKind::Canceled) => Ok(()),
641 Some(kind) => Err(ExitError::CannotCancelExit { vtxo: vtxo_id, state: kind }),
642 None => Err(ExitError::NotExiting { vtxo: vtxo_id }),
643 };
644 },
645 };
646
647 if !inner.exit_vtxos[idx].state().is_cancelable() {
648 return Err(ExitError::CannotCancelExit {
649 vtxo: vtxo_id,
650 state: inner.exit_vtxos[idx].state().kind(),
651 });
652 }
653
654 let leaf_txid = inner.exit_vtxos[idx].get_vtxo(&*inner.persister).await?.point().txid;
656 match inner.tx_manager.sync_exit_tx(leaf_txid).await? {
657 TxStatus::NotFound => {},
658 TxStatus::Mempool | TxStatus::Confirmed(_) => {
659 return Err(ExitError::ExitTxAlreadyBroadcast { vtxo: vtxo_id, txid: leaf_txid });
660 },
661 }
662
663 let tip = inner.chain_source.tip().await
664 .map_err(|e| ExitError::TipRetrievalFailure { error: e.to_string() })?;
665
666 inner.exit_vtxos[idx].cancel(tip, &*inner.persister).await?;
668
669 if let Some(txids) = inner.exit_vtxos[idx].txids() {
671 inner.tx_manager.untrack_vtxo_exits(&txids).await;
672 }
673
674 if let Some(movement_id) = inner.exit_vtxos[idx].movement_id() {
676 if let Err(e) = inner.movement_manager
677 .finish_movement(movement_id, MovementStatus::Canceled).await
678 {
679 error!("Failed to finalize exit movement {} as Canceled: {:#}", movement_id, e);
680 }
681 }
682
683 inner.exit_vtxos.swap_remove(idx);
685 info!("Canceled unilateral exit for VTXO {}", vtxo_id);
686 Ok(())
687 }
688
689 pub(crate) async fn dangerous_clear_exit(&self) -> anyhow::Result<()> {
693 let mut guard = self.inner.write().await;
694 for exit in &guard.exit_vtxos {
695 guard.persister.remove_exit_vtxo_entry(&exit.id()).await?;
696 }
697 guard.exit_vtxos.clear();
698 Ok(())
699 }
700
701 pub async fn progress_exits(
714 &self,
715 wallet: &Wallet,
716 ) -> anyhow::Result<Option<Vec<ExitProgressStatus>>> {
717 let mut guard = self.inner.write().await;
718 guard.refresh_tx_state().await?;
719 let mut exit_vtxos = std::mem::take(&mut guard.exit_vtxos);
720 let mut exit_statuses = Vec::with_capacity(exit_vtxos.len());
721
722 for ev in exit_vtxos.iter_mut() {
723 if !ev.is_initialized() {
724 warn!("Skipping progress of uninitialized unilateral exit {}", ev.id());
725 continue;
726 }
727
728 info!("Progressing exit for VTXO {}", ev.id());
729 let pre_state = ev.state().clone();
730 let error = match ev.progress(
731 wallet,
732 &mut guard.tx_manager,
733 true,
734 ).await {
735 Ok(_) => None,
736 Err(e) => {
737 match &e {
738 ExitError::InsufficientConfirmedFunds { .. } => {
739 warn!("Can't progress exit for VTXO {} at this time: {}", ev.id(), e);
740 },
741 _ => {
742 error!("Error progressing exit for VTXO {}: {}", ev.id(), e);
743 }
744 }
745 Some(e)
746 }
747 };
748
749 let state_changed = ev.state() != &pre_state;
750 Self::reconcile_vtxo_and_movement(
751 wallet, &guard.movement_manager, ev, state_changed,
752 ).await;
753
754 if !matches!(ev.state(), ExitState::Claimed(..)) {
755 exit_statuses.push(ExitProgressStatus {
756 vtxo_id: ev.id(),
757 state: ev.state().clone(),
758 error,
759 });
760 }
761 }
762
763 guard.exit_vtxos = exit_vtxos;
764 Ok(Some(exit_statuses))
765 }
766
767 async fn reconcile_vtxo_and_movement(
777 wallet: &Wallet,
778 movements: &MovementManager,
779 ev: &ExitVtxo,
780 state_changed: bool,
781 ) {
782 if ev.state().warrants_exited_vtxo() {
783 if let Err(e) = wallet.mark_vtxos_as_exited([ev.id()]).await {
784 error!("Failed to mark VTXO {} as Exited: {:#}", ev.id(), e);
785 }
786 }
787
788 if !state_changed {
789 return;
790 }
791 let Some(movement_id) = ev.movement_id() else { return };
792 let new_status = match ev.state() {
793 ExitState::Claimed(_) => MovementStatus::Successful,
794 ExitState::VtxoAlreadySpent(_) => MovementStatus::Canceled,
795 _ => return,
796 };
797 if let Err(e) = movements.finish_movement(movement_id, new_status).await {
798 error!(
799 "Failed to finalize exit movement {} as {:?}: {:#}",
800 movement_id, new_status, e,
801 );
802 }
803 }
804
805 pub async fn sync(
809 &self,
810 wallet: &Wallet,
811 ) -> anyhow::Result<()> {
812 let mut guard = self.inner.write().await;
813 guard.refresh_tx_state().await?;
814 let mut exit_vtxos = std::mem::take(&mut guard.exit_vtxos);
815 for exit in &mut exit_vtxos {
816 if !exit.is_initialized() {
817 warn!("Skipping progress of uninitialized unilateral exit {}", exit.id());
818 continue;
819 }
820
821 let pre_state = exit.state().clone();
822 if let Err(e) = exit.progress(
823 wallet, &mut guard.tx_manager, true,
824 ).await {
825 error!("Error syncing exit for VTXO {}: {}", exit.id(), e);
826 }
827 let state_changed = exit.state() != &pre_state;
828 Self::reconcile_vtxo_and_movement(
829 wallet, &guard.movement_manager, exit, state_changed,
830 ).await;
831 }
832 guard.exit_vtxos = exit_vtxos;
833 Ok(())
834 }
835
836 pub async fn exits_needing_cpfp(&self) -> Vec<ExitCpfpRequest> {
843 let guard = self.inner.read().await;
844 let mut requests = Vec::new();
845 for ev in &guard.exit_vtxos {
846 let ExitState::Processing(s) = ev.state() else { continue };
847 for tx in &s.transactions {
848 let rbf_requirement = match &tx.status {
849 ExitTxStatus::AwaitingCpfpBroadcast => None,
850 ExitTxStatus::AwaitingConfirmation {..} => {
851 match guard.tx_manager.get_child_status(tx.txid).await {
856 Ok(Some(c)) => match c.fee_info {
857 Some(fi) => Some(RbfRequirement {
858 min_fee_rate: fi.fee_rate,
859 current_package_fee: fi.total_fee,
860 }),
861 None => continue,
862 },
863 _ => continue,
864 }
865 },
866 _ => continue,
867 };
868 let package = match guard.tx_manager.get_package(tx.txid) {
869 Ok(p) => p,
870 Err(_) => continue,
871 };
872 let exit_tx = package.read().await.exit.tx.clone();
873 requests.push(ExitCpfpRequest {
874 vtxo_id: ev.id(),
875 exit_tx,
876 rbf_requirement,
877 });
878 }
879 }
880 requests
881 }
882
883 pub async fn provide_cpfp_tx(
896 &self,
897 wallet: &Wallet,
898 exit_txid: Txid,
899 child_tx: Transaction,
900 ) -> anyhow::Result<(), ExitError> {
901 let origin = ExitTxOrigin::Wallet { confirmed_in: None };
902 let mut guard = self.inner.write().await;
903 let inner = &mut *guard;
904
905 if !inner.tx_manager.broadcast_and_set_child(exit_txid, child_tx, origin).await? {
907 return Ok(());
908 }
909
910 for ev in inner.exit_vtxos.iter_mut() {
911 let ExitState::Processing(s) = ev.state() else { continue };
912 let has_tx = s.transactions.iter().any(|tx| tx.txid == exit_txid);
913 if has_tx {
914 if let Err(e) = ev.progress(wallet, &mut inner.tx_manager, false).await {
915 warn!("Failed to progress exit for {} after CPFP: {}", exit_txid, e);
916 }
917 break;
918 }
919 }
920
921 Ok(())
922 }
923
924 pub async fn list_claimable(&self) -> Vec<ExitVtxo> {
926 let guard = self.inner.read().await;
927 guard.exit_vtxos.iter().filter(|ev| ev.is_claimable()).cloned().collect()
928 }
929
930 pub async fn sign_exit_claim_inputs(&self, psbt: &mut Psbt, wallet: &Wallet) -> anyhow::Result<()> {
938 let guard = self.inner.read().await;
939 guard.sign_exit_claim_inputs(psbt, wallet).await
940 }
941
942 pub async fn drain_exits(
951 &self,
952 inputs: &[impl Borrow<ExitVtxo>],
953 wallet: &Wallet,
954 address: Address,
955 fee_rate_override: Option<FeeRate>,
956 ) -> anyhow::Result<Psbt, ExitError> {
957 let guard = self.inner.read().await;
958
959 let tip = guard.chain_source.tip().await
960 .map_err(|e| ExitError::TipRetrievalFailure { error: e.to_string() })?;
961
962 if inputs.is_empty() {
963 return Err(ExitError::ClaimMissingInputs);
964 }
965 let mut vtxos = HashMap::with_capacity(inputs.len());
966 for input in inputs {
967 let i = input.borrow();
968 let vtxo = i.get_full_vtxo(&*guard.persister).await?;
969 vtxos.insert(i.id(), vtxo);
970 }
971
972 let mut tx = {
973 let mut output_amount = Amount::ZERO;
974 let mut tx_ins = Vec::with_capacity(inputs.len());
975 for input in inputs {
976 let input = input.borrow();
977 let vtxo = &vtxos[&input.id()];
978 if !matches!(input.state(), ExitState::Claimable(..)) {
979 return Err(ExitError::VtxoNotClaimable { vtxo: input.id() });
980 }
981
982 output_amount += vtxo.amount();
983
984 let clause = wallet.find_signable_clause(vtxo).await
985 .ok_or(ExitError::ClaimMissingSignableClause { vtxo: vtxo.id() })?;
986
987 tx_ins.push(TxIn {
988 previous_output: vtxo.point(),
989 script_sig: ScriptBuf::default(),
990 sequence: clause.sequence().unwrap_or(Sequence::ZERO),
991 witness: Witness::new(),
992 });
993 }
994
995 let locktime = bitcoin::absolute::LockTime::from_height(tip)
996 .map_err(|e| ExitError::InvalidLocktime { tip, error: e.to_string() })?;
997
998 Transaction {
999 version: bitcoin::transaction::Version::TWO,
1000 lock_time: locktime,
1001 input: tx_ins,
1002 output: vec![
1003 TxOut {
1004 script_pubkey: address.script_pubkey(),
1005 value: output_amount,
1006 },
1007 ],
1008 }
1009 };
1010
1011 let create_psbt = |tx: Transaction| async {
1013 let mut psbt = Psbt::from_unsigned_tx(tx)
1014 .map_err(|e| ExitError::InternalError {
1015 error: format!("Failed to create exit claim PSBT: {}", e),
1016 })?;
1017 psbt.inputs.iter_mut().zip(inputs).for_each(|(i, e)| {
1018 let v = &vtxos[&e.borrow().id()];
1019 i.set_exit_claim_input(v);
1020 i.witness_utxo = Some(v.txout())
1021 });
1022 guard.sign_exit_claim_inputs(&mut psbt, wallet).await
1023 .map_err(|e| ExitError::ClaimSigningError { error: e.to_string() })?;
1024 Ok(psbt)
1025 };
1026 let fee_amount = {
1027 let fee_rate = fee_rate_override
1028 .unwrap_or(guard.chain_source.fee_rates().await.regular);
1029 fee_rate * create_psbt(tx.clone()).await?
1030 .extract_tx()
1031 .map_err(|e| ExitError::InternalError {
1032 error: format!("Failed to get tx from signed exit claim PSBT: {}", e),
1033 })?
1034 .weight()
1035 };
1036
1037 let needed = fee_amount + P2TR_DUST;
1039 if needed > tx.output[0].value {
1040 return Err(ExitError::ClaimFeeExceedsOutput {
1041 needed, output: tx.output[0].value,
1042 });
1043 }
1044 tx.output[0].value -= fee_amount;
1045
1046 create_psbt(tx).await
1048 }
1049}
1050