bark/exit/
mod.rs

1//! Unilateral exit management
2//!
3//! This module coordinates unilateral exits of VTXOs back to on-chain bitcoin without
4//! requiring any third-party cooperation. It tracks which VTXOs should be exited, prepares
5//! and signs the required transactions, and drives the process forward until the funds are
6//! confirmed and claimable.
7//!
8//! What this module provides
9//! - Discovery, tracking, and persistence of the exit state for VTXOs.
10//! - Initiation of exits for the entire wallet or a selected set of VTXOs.
11//! - Periodic progress of exits (broadcasting, fee-bumping, and state updates).
12//! - APIs to inspect the current exit status, history, and related transactions.
13//! - Construction and signing of a final claim (drain) transaction once exits become claimable.
14//!
15//! When to use this module
16//! - Whenever VTXOs must be unilaterally moved on-chain, e.g., during counterparty unavailability,
17//!   or when the counterparty turns malicious.
18//!
19//! When not to use this module
20//! - If the server is cooperative. You can always offboard or pay onchain in a way that is much
21//!   cheaper and faster.
22//!
23//! Core types
24//! - [Exit]: High-level coordinator for the exit workflow. It persists state and advances
25//!   unilateral exits until they are claimable.
26//! - [ExitVtxo]: A VTXO marked for, and progressing through, unilateral exit. Each instance exposes
27//!   its current state and related metadata.
28//!
29//! Typical lifecycle
30//! 1) Choose what to exit
31//!    - Mark individual VTXOs for exit with [Exit::start_exit_for_vtxos], or exit everything with
32//!      [Exit::start_exit_for_entire_wallet].
33//! 2) Drive progress
34//!    - Use either [Exit::sync] or [Exit::sync_no_progress] to update the state of tracked exits.
35//!    - Periodically call [Exit::progress_exits] to advance the exit process. This will create or
36//!      update transactions, adjust fees for existing transactions, and refresh the status of each
37//!      unilateral exit until it has been confirmed and subsequentially spent onchain.
38//! 3) Inspect status
39//!    - Use [Exit::get_exit_status] for detailed per-VTXO status (optionally including
40//!      history and transactions).
41//!    - Use [Exit::get_exit_vtxos] or [Exit::list_claimable] to browse tracked exits and locate
42//!      those that are fully confirmed onchain.
43//! 4) Claim the exited funds (optional)
44//!    - Once your transaction is confirmed onchain the funds are fully yours. However, recovery
45//!      from seed is not supported. By claiming your VTXO you move them to your onchain wallet.
46//!    - Once claimable, construct a PSBT to drain them with [Exit::drain_exits].
47//!    - Alternatively, you can use [Exit::sign_exit_claim_inputs] to sign the inputs of a given
48//!      PSBT if any are the outputs of a claimable unilateral exit.
49//!
50//! Fees rates
51//! - Suitable fee rates will be calculated based on the current network conditions, however, if you
52//!   wish to override this, you can do so by providing your own [FeeRate] in [Exit::progress_exits]
53//!   and [Exit::drain_exits]
54//!
55//! Error handling and persistence
56//! - The coordinator surfaces operational errors via [anyhow::Result] and domain-specific errors
57//!   via [ExitError] where appropriate. Persistent state is kept via the configured persister and
58//!   refreshed against the current chain view provided by the chain source client.
59//!
60//! Minimal example (high-level):
61//! ```no_run
62//! # use std::sync::Arc;
63//! # use std::str::FromStr;
64//! # use std::path::PathBuf;
65//! #
66//! # use bitcoin::Network;
67//! # use tokio::fs;
68//! #
69//! # use bark::{Config, Wallet, SqliteClient};
70//! # use bark::onchain::OnchainWallet;
71//! #
72//! # async fn get_wallets() -> (Wallet, OnchainWallet) {
73//! #   let datadir = PathBuf::from("./bark");
74//! #   let config = Config::network_default(bitcoin::Network::Bitcoin);
75//! #   let db = Arc::new(SqliteClient::open(datadir.join("db.sqlite")).unwrap());
76//! #   let mnemonic_str = fs::read_to_string(datadir.join("mnemonic")).await.unwrap();
77//! #   let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str).unwrap();
78//! #   let bark_wallet = Wallet::open(&mnemonic, db.clone(), config).await.unwrap();
79//! #   let seed = mnemonic.to_seed("");
80//! #   let onchain_wallet = OnchainWallet::load_or_create(Network::Regtest, seed, db).await.unwrap();
81//! #   (bark_wallet, onchain_wallet)
82//! # }
83//! #
84//! # #[tokio::main]
85//! # async fn main() -> anyhow::Result<()> {
86//! let (mut bark_wallet, mut onchain_wallet) = get_wallets().await;
87//!
88//! // Get lock on exit system
89//! let mut exit_lock = bark_wallet.exit.write().await;
90//!
91//! // Mark all VTXOs for exit.
92//! exit_lock.start_exit_for_entire_wallet().await?;
93//!
94//! // Transactions will be broadcast and require confirmations so keep periodically calling this.
95//! exit_lock.sync_no_progress(&onchain_wallet).await?;
96//! exit_lock.progress_exits(&bark_wallet, &mut onchain_wallet, None).await?;
97//!
98//! // Once all VTXOs are claimable, construct a PSBT to drain them.
99//! let drain_to = bitcoin::Address::from_str("bc1p...")?.assume_checked();
100//! let claimable_outputs = exit_lock.list_claimable();
101//! let drain_psbt = exit_lock.drain_exits(
102//!   &claimable_outputs,
103//!   &bark_wallet,
104//!   drain_to,
105//!   None,
106//! ).await?;
107//!
108//! // Next you should broadcast the PSBT, once it's confirmed the unilateral exit is complete.
109//! // broadcast_psbt(drain_psbt).await?;
110//! #   Ok(())
111//! # }
112//! ```
113
114mod models;
115mod vtxo;
116pub(crate) mod progress;
117pub(crate) mod transaction_manager;
118
119pub use self::models::{
120	ExitTransactionPackage, TransactionInfo, ChildTransactionInfo, ExitError, ExitState,
121	ExitTx, ExitTxStatus, ExitTxOrigin, ExitStartState, ExitProcessingState, ExitAwaitingDeltaState,
122	ExitClaimableState, ExitClaimInProgressState, ExitClaimedState, ExitProgressStatus,
123	ExitTransactionStatus,
124};
125pub use self::vtxo::ExitVtxo;
126
127use std::borrow::Borrow;
128use std::cmp;
129use std::collections::HashMap;
130use std::sync::Arc;
131
132use anyhow::Context;
133use bitcoin::{
134	Address, Amount, FeeRate, Psbt, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, sighash
135};
136use bitcoin::consensus::Params;
137use log::{error, info, trace, warn};
138
139use ark::{Vtxo, VtxoId};
140use ark::vtxo::policy::signing::VtxoSigner;
141use bitcoin_ext::{BlockHeight, P2TR_DUST};
142
143use crate::Wallet;
144use crate::chain::ChainSource;
145use crate::exit::transaction_manager::ExitTransactionManager;
146use crate::movement::{MovementDestination, MovementStatus, PaymentMethod};
147use crate::movement::manager::MovementManager;
148use crate::movement::update::MovementUpdate;
149use crate::onchain::ExitUnilaterally;
150use crate::persist::BarkPersister;
151use crate::persist::models::StoredExit;
152use crate::psbtext::PsbtInputExt;
153use crate::subsystem::{ExitMovement, Subsystem};
154use crate::vtxo::{VtxoState, VtxoStateKind};
155
156/// Handles the process of ongoing VTXO exits.
157pub struct Exit {
158	tx_manager: ExitTransactionManager,
159	persister: Arc<dyn BarkPersister>,
160	chain_source: Arc<ChainSource>,
161	movement_manager: Arc<MovementManager>,
162
163	exit_vtxos: Vec<ExitVtxo>,
164}
165
166impl Exit {
167	pub (crate) async fn new(
168		persister: Arc<dyn BarkPersister>,
169		chain_source: Arc<ChainSource>,
170		movement_manager: Arc<MovementManager>,
171	) -> anyhow::Result<Exit> {
172		let tx_manager = ExitTransactionManager::new(persister.clone(), chain_source.clone())?;
173
174		Ok(Exit {
175			exit_vtxos: Vec::new(),
176			tx_manager,
177			persister,
178			chain_source,
179			movement_manager,
180		})
181	}
182
183	pub (crate) async fn load(
184		&mut self,
185		onchain: &dyn ExitUnilaterally,
186	) -> anyhow::Result<()> {
187		let exit_vtxo_entries = self.persister.get_exit_vtxo_entries().await?;
188		self.exit_vtxos.reserve(exit_vtxo_entries.len());
189
190		for entry in exit_vtxo_entries {
191			if let Some(vtxo) = self.persister.get_wallet_vtxo(entry.vtxo_id).await? {
192				let mut exit = ExitVtxo::from_entry(entry, &vtxo);
193				exit.initialize(&mut self.tx_manager, &*self.persister, onchain).await?;
194				self.exit_vtxos.push(exit);
195			} else {
196				error!("VTXO {} is marked for exit but it's missing from the database", entry.vtxo_id);
197			}
198		}
199		Ok(())
200	}
201
202	/// Returns the unilateral exit status for a given VTXO, if any.
203	///
204	/// # Parameters
205	/// - vtxo_id: The ID of the VTXO to check.
206	/// - include_history: Whether to include the full state machine history of the exit
207	/// - include_transactions: Whether to include the full set of transactions related to the exit.
208	pub async fn get_exit_status(
209		&self,
210		vtxo_id: VtxoId,
211		include_history: bool,
212		include_transactions: bool,
213	) -> Result<Option<ExitTransactionStatus>, ExitError> {
214		match self.exit_vtxos.iter().find(|ev| ev.id() == vtxo_id) {
215			None => Ok(None),
216			Some(exit) => {
217				let mut txs = Vec::new();
218				if include_transactions {
219					if let Some(txids) = exit.txids() {
220						txs.reserve(txids.len());
221						for txid in txids {
222							txs.push(self.tx_manager.get_package(*txid)?.read().await.clone());
223						}
224					} else {
225						let exit_vtxo = exit.get_vtxo(&*self.persister).await?;
226						// Realistically, the only way an exit isn't initialized is if it has been
227						// marked for exit, and we haven't synced the exit system yet. On this basis
228						// we can just return the VTXO transactions since there shouldn't be any
229						// children.
230						for tx in exit_vtxo.vtxo.transactions() {
231							txs.push(ExitTransactionPackage {
232								exit: TransactionInfo {
233									txid: tx.tx.compute_txid(),
234									tx: tx.tx,
235								},
236								child: None,
237							})
238						}
239					}
240				}
241				Ok(Some(ExitTransactionStatus {
242					vtxo_id: exit.id(),
243					state: exit.state().clone(),
244					history: if include_history { Some(exit.history().clone()) } else { None },
245					transactions: txs,
246				}))
247			},
248		}
249	}
250
251	/// Returns a reference to the tracked [ExitVtxo] if it exists.
252	pub fn get_exit_vtxo(&self, vtxo_id: VtxoId) -> Option<&ExitVtxo> {
253		self.exit_vtxos.iter().find(|ev| ev.id() == vtxo_id)
254	}
255
256	/// Returns all known unilateral exits in this wallet.
257	pub fn get_exit_vtxos(&self) -> &Vec<ExitVtxo> {
258		&self.exit_vtxos
259	}
260
261	/// True if there are any unilateral exits which have been started but are not yet claimable.
262	pub fn has_pending_exits(&self) -> bool {
263		self.exit_vtxos.iter().any(|ev| ev.state().is_pending())
264	}
265
266	/// Returns the total amount of all VTXOs requiring more txs to be confirmed
267	pub fn pending_total(&self) -> Amount {
268		self.exit_vtxos
269			.iter()
270			.filter_map(|ev| {
271				if ev.state().is_pending() {
272					Some(ev.amount())
273				} else {
274					None
275				}
276			}).sum()
277	}
278
279	/// Returns the earliest block height at which all tracked exits will be claimable
280	pub async fn all_claimable_at_height(&self) -> Option<BlockHeight> {
281		let mut highest_claimable_height = None;
282		for exit in &self.exit_vtxos {
283			if matches!(exit.state(), ExitState::Claimed(..)) {
284				continue;
285			}
286			match exit.state().claimable_height() {
287				Some(h) => highest_claimable_height = cmp::max(highest_claimable_height, Some(h)),
288				None => return None,
289			}
290		}
291		highest_claimable_height
292	}
293
294	/// Starts the unilateral exit process for the entire wallet (all eligible VTXOs).
295	///
296	/// It does not block until completion, you must use [Exit::progress_exits] to advance each exit.
297	///
298	/// It's recommended to sync the wallet, by using something like [Wallet::maintenance] being
299	/// doing this.
300	pub async fn start_exit_for_entire_wallet(&mut self) -> anyhow::Result<()> {
301		let vtxos = self.persister.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?.into_iter()
302			.map(|v| v.vtxo)
303			.collect::<Vec<_>>();
304		self.start_exit_for_vtxos(&vtxos).await?;
305
306		Ok(())
307	}
308
309	/// Starts the unilateral exit process for the given VTXOs.
310	///
311	/// It does not block until completion, you must use [Exit::progress_exits] to advance each exit.
312	///
313	/// It's recommended to sync the wallet, by using something like [Wallet::maintenance] being
314	/// doing this.
315	pub async fn start_exit_for_vtxos<'a>(
316		&mut self,
317		vtxos: &[impl Borrow<Vtxo>],
318	) -> anyhow::Result<()> {
319		if vtxos.is_empty() {
320			return Ok(());
321		}
322		let tip = self.chain_source.tip().await?;
323		let params = Params::new(self.chain_source.network());
324		for vtxo in vtxos {
325			let vtxo = vtxo.borrow();
326			let vtxo_id = vtxo.id();
327			if self.exit_vtxos.iter().any(|ev| ev.id() == vtxo_id) {
328				warn!("VTXO {} is already in the exit process", vtxo_id);
329				continue;
330			}
331
332			// We avoid composing the TXID vector since that requires access to the onchain wallet,
333			// as such the ExitVtxo will be considered uninitialized.
334			trace!("Starting exit for VTXO: {}", vtxo_id);
335			let exit = ExitVtxo::new(vtxo, tip);
336			self.persister.store_exit_vtxo_entry(&StoredExit::new(&exit)).await?;
337			self.persister.update_vtxo_state_checked(
338				vtxo_id, VtxoState::Spent, &VtxoStateKind::UNSPENT_STATES,
339			).await?;
340			self.exit_vtxos.push(exit);
341			trace!("Exit for VTXO started successfully: {}", vtxo_id);
342
343			// Register the movement now so users can be aware of where their funds have gone.
344			let balance = -vtxo.amount().to_signed()?;
345			let script_pubkey = vtxo.output_script_pubkey();
346			let payment_method = match Address::from_script(&script_pubkey, &params) {
347				Ok(addr) => PaymentMethod::Bitcoin(addr.into_unchecked()),
348				Err(e) => {
349					warn!("Unable to convert script pubkey to address: {:#}", e);
350					PaymentMethod::OutputScript(script_pubkey)
351				}
352			};
353
354			// A big reason for creating a finished movement is that we currently don't support
355			// canceling exits. When we do, we can leave this in pending until it's either finished
356			// or canceled by the user.
357			self.movement_manager.new_finished_movement(
358				Subsystem::EXIT,
359				ExitMovement::Exit.to_string(),
360				MovementStatus::Successful,
361				MovementUpdate::new()
362					.intended_and_effective_balance(balance)
363					.consumed_vtxo(vtxo_id)
364					.sent_to([MovementDestination::new(payment_method, vtxo.amount())]),
365			).await.context("Failed to register exit movement")?;
366		}
367		Ok(())
368	}
369
370	/// Reset exit to an empty state. Should be called when dropping VTXOs
371	///
372	/// Note: _This method is **dangerous** and can lead to funds loss. Be cautious._
373	pub (crate) async fn dangerous_clear_exit(&mut self) -> anyhow::Result<()> {
374		for exit in &self.exit_vtxos {
375			self.persister.remove_exit_vtxo_entry(&exit.id()).await?;
376		}
377		self.exit_vtxos.clear();
378		Ok(())
379	}
380
381	/// Iterates over each registered VTXO and attempts to progress their unilateral exit. Note that
382	/// [Exit::sync] or [Exit::sync_no_progress] should be called before calling this method.
383	///
384	/// # Parameters
385	///
386	/// - `onchain` is used to build the CPFP transaction package we use to broadcast
387	///   the unilateral exit transaction
388	/// - `fee_rate_override` sets the desired fee-rate in sats/kvB to use broadcasting exit
389	///   transactions. Note that due to rules imposed by the network with regard to RBF fee bumping,
390	///   replaced transactions may have a higher fee rate than you specify here.
391	///
392	/// # Returns
393	///
394	/// The exit status of each VTXO being exited which has also not yet been spent
395	pub async fn progress_exits(
396		&mut self,
397		wallet: &Wallet,
398		onchain: &mut dyn ExitUnilaterally,
399		fee_rate_override: Option<FeeRate>,
400	) -> anyhow::Result<Option<Vec<ExitProgressStatus>>> {
401		let mut exit_statuses = Vec::with_capacity(self.exit_vtxos.len());
402		for ev in self.exit_vtxos.iter_mut() {
403			if !ev.is_initialized() {
404				warn!("Skipping progress of uninitialized unilateral exit {}", ev.id());
405				continue;
406			}
407
408			info!("Progressing exit for VTXO {}", ev.id());
409			let error = match ev.progress(
410				wallet,
411				&mut self.tx_manager,
412				onchain,
413				fee_rate_override,
414				true,
415			).await {
416				Ok(_) => None,
417				Err(e) => {
418					match &e {
419						ExitError::InsufficientConfirmedFunds { .. } => {
420							warn!("Can't progress exit for VTXO {} at this time: {}", ev.id(), e);
421						},
422						_ => {
423							error!("Error progressing exit for VTXO {}: {}", ev.id(), e);
424						}
425					}
426					Some(e)
427				}
428			};
429			if !matches!(ev.state(), ExitState::Claimed(..)) {
430				exit_statuses.push(ExitProgressStatus {
431					vtxo_id: ev.id(),
432					state: ev.state().clone(),
433					error,
434				});
435			}
436		}
437		Ok(Some(exit_statuses))
438	}
439
440	/// For use when syncing. Pending exits will be initialized, the network status of each
441	/// [ExitTransactionPackage] will be updated, and finally, any unilateral exits that are waiting
442	/// for network updates will be progressed.
443	pub async fn sync(
444		&mut self,
445		wallet: &Wallet,
446		onchain: &mut dyn ExitUnilaterally,
447	) -> anyhow::Result<()> {
448		self.sync_no_progress(onchain).await?;
449		for exit in &mut self.exit_vtxos {
450			// If the exit is waiting for new blocks, we should trigger an update
451			if exit.state().requires_network_update() {
452				if let Err(e) = exit.progress(
453					wallet, &mut self.tx_manager, onchain, None, false,
454				).await {
455					error!("Error syncing exit for VTXO {}: {}", exit.id(), e);
456				}
457			}
458		}
459		Ok(())
460	}
461
462	/// For use when syncing. Initializes pending exits and syncs any confirmed or broadcast child
463	/// transactions. This differs from [Exit::sync] in that it doesn't update the [ExitState]
464	/// of a unilateral exit. This must be done manually by calling [Exit::progress_exits]. This
465	/// permits the use of a read-only reference to the onchain wallet.
466	pub async fn sync_no_progress(
467		&mut self,
468		onchain: &dyn ExitUnilaterally,
469	) -> anyhow::Result<()> {
470		for exit in &mut self.exit_vtxos {
471			if !exit.is_initialized() {
472				match exit.initialize(&mut self.tx_manager, &*self.persister, onchain).await {
473					Ok(()) => continue,
474					Err(e) => {
475						error!("Error initializing exit for VTXO {}: {:#}", exit.id(), e);
476					}
477				}
478			}
479		}
480		self.tx_manager.sync().await?;
481		Ok(())
482	}
483
484	/// Lists all exits that are claimable
485	pub fn list_claimable(&self) -> Vec<&ExitVtxo> {
486		self.exit_vtxos.iter().filter(|ev| ev.is_claimable()).collect()
487	}
488
489	/// Sign any inputs of the PSBT that is an exit claim input
490	///
491	/// Can take the result PSBT of [`bdk_wallet::TxBuilder::finish`] on which
492	/// [`crate::onchain::TxBuilderExt::add_exit_claim_inputs`] has been used
493	///
494	/// Note: This doesn't mark the exit output as spent, it's up to the caller to
495	/// do that, or it will be done once the transaction is seen in the network
496	pub async fn sign_exit_claim_inputs(&self, psbt: &mut Psbt, wallet: &Wallet) -> anyhow::Result<()> {
497		let prevouts = psbt.inputs.iter()
498			.map(|i| i.witness_utxo.clone().unwrap())
499			.collect::<Vec<_>>();
500
501		let prevouts = sighash::Prevouts::All(&prevouts);
502		let mut shc = sighash::SighashCache::new(&psbt.unsigned_tx);
503
504		let claimable = self.list_claimable()
505			.into_iter()
506			.map(|e| (e.id(), e))
507			.collect::<HashMap<_, _>>();
508
509		let mut spent = Vec::new();
510		for (i, input) in psbt.inputs.iter_mut().enumerate() {
511			let vtxo = input.get_exit_claim_input();
512
513			if let Some(vtxo) = vtxo {
514				let exit_vtxo = *claimable.get(&vtxo.id()).context("vtxo is not claimable yet")?;
515
516				let witness = wallet.sign_input(&vtxo, i, &mut shc, &prevouts).await
517					.map_err(|e| ExitError::ClaimSigningError { error: e.to_string() })?;
518
519				input.final_script_witness = Some(witness);
520				spent.push(exit_vtxo);
521			}
522		}
523
524		Ok(())
525	}
526
527	/// Builds a PSBT that drains the provided claimable unilateral exits to the given address.
528	///
529	/// - `inputs`: Claimable unilateral exits.
530	/// - `wallet`: The bark wallet containing the keys needed to spend the unilateral exits.
531	/// - `address`: Destination address for the claim.
532	/// - `fee_rate_override`: Optional fee rate to use.
533	///
534	/// Returns a PSBT ready to be broadcast.
535	pub async fn drain_exits<'a>(
536		&self,
537		inputs: &[impl Borrow<ExitVtxo>],
538		wallet: &Wallet,
539		address: Address,
540		fee_rate_override: Option<FeeRate>,
541	) -> anyhow::Result<Psbt, ExitError> {
542		let tip = self.chain_source.tip().await
543			.map_err(|e| ExitError::TipRetrievalFailure { error: e.to_string() })?;
544
545		if inputs.is_empty() {
546			return Err(ExitError::ClaimMissingInputs);
547		}
548		let mut vtxos = HashMap::with_capacity(inputs.len());
549		for input in inputs {
550			let i = input.borrow();
551			let vtxo = i.get_vtxo(&*self.persister).await?;
552			vtxos.insert(i.id(), vtxo);
553		}
554
555		let mut tx = {
556			let mut output_amount = Amount::ZERO;
557			let mut tx_ins = Vec::with_capacity(inputs.len());
558			for input in inputs {
559				let input = input.borrow();
560				let vtxo = &vtxos[&input.id()];
561				if !matches!(input.state(), ExitState::Claimable(..)) {
562					return Err(ExitError::VtxoNotClaimable { vtxo: input.id() });
563				}
564
565				output_amount += vtxo.amount();
566
567				let clause = wallet.find_signable_clause(vtxo).await
568					.ok_or(ExitError::ClaimMissingSignableClause { vtxo: vtxo.id() })?;
569
570				tx_ins.push(TxIn {
571					previous_output: vtxo.point(),
572					script_sig: ScriptBuf::default(),
573					sequence: clause.sequence().unwrap_or(Sequence::ZERO),
574					witness: Witness::new(),
575				});
576			}
577
578			let locktime = bitcoin::absolute::LockTime::from_height(tip)
579				.map_err(|e| ExitError::InvalidLocktime { tip, error: e.to_string() })?;
580
581			Transaction {
582				version: bitcoin::transaction::Version(3),
583				lock_time: locktime,
584				input: tx_ins,
585				output: vec![
586					TxOut {
587						script_pubkey: address.script_pubkey(),
588						value: output_amount,
589					},
590				],
591			}
592		};
593
594		// Create a PSBT to determine the weight of the transaction so we can deduct a tx fee
595		let create_psbt = |tx: Transaction| async {
596			let mut psbt = Psbt::from_unsigned_tx(tx)
597				.map_err(|e| ExitError::InternalError {
598					error: format!("Failed to create exit claim PSBT: {}", e),
599				})?;
600			psbt.inputs.iter_mut().zip(inputs).for_each(|(i, e)| {
601				let v = &vtxos[&e.borrow().id()];
602				i.set_exit_claim_input(&v.vtxo);
603				i.witness_utxo = Some(v.vtxo.txout())
604			});
605			self.sign_exit_claim_inputs(&mut psbt, wallet).await
606				.map_err(|e| ExitError::ClaimSigningError { error: e.to_string() })?;
607			Ok(psbt)
608		};
609		let fee_amount = {
610			let fee_rate = fee_rate_override
611				.unwrap_or(self.chain_source.fee_rates().await.regular);
612			fee_rate * create_psbt(tx.clone()).await?
613				.extract_tx()
614				.map_err(|e| ExitError::InternalError {
615					error: format!("Failed to get tx from signed exit claim PSBT: {}", e),
616				})?
617				.weight()
618		};
619
620		// We adjust the drain output to cover the fee
621		let needed = fee_amount + P2TR_DUST;
622		if needed > tx.output[0].value {
623			return Err(ExitError::ClaimFeeExceedsOutput {
624				needed, output: tx.output[0].value,
625			});
626		}
627		tx.output[0].value -= fee_amount;
628
629		// Now create the final signed PSBT
630		create_psbt(tx).await
631	}
632}