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