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//!    - Periodically call [Exit::progress_exits] to advance the exit process. This will create or
35//!      update transactions, adjust fees for existing transactions, and refresh the status of each
36//!      unilateral exit until it has been confirmed and subsequentially spent onchain.
37//!    - [Exit::sync_exit] can be used to re-sync state with the blockchain and mempool without
38//!      taking progress actions.
39//! 3) Inspect status
40//!    - Use [Exit::get_exit_status] for detailed per-VTXO status (optionally including
41//!      history and transactions).
42//!    - Use [Exit::get_exit_vtxos] or [Exit::list_claimable] to browse tracked exits and locate
43//!      those that are fully confirmed onchain.
44//! 4) Claim the exited funds (optional)
45//!    - Once your transaction is confirmed onchain the funds are fully yours. However, recovery
46//!      from seed is not supported. By claiming your VTXO you move them to your onchain wallet.
47//!    - Once claimable, construct a PSBT to drain them with [Exit::drain_exits].
48//!    - Alternatively, you can use [Exit::sign_exit_claim_inputs] to sign the inputs of a given
49//!      PSBT if any are the outputs of a claimable unilateral exit.
50//!
51//! Fees rates
52//! - Suitable fee rates will be calculated based on the current network conditions, however, if you
53//!   wish to override this, you can do so by providing your own [FeeRate] in [Exit::progress_exits]
54//!   and [Exit::drain_exits]
55//!
56//! Error handling and persistence
57//! - The coordinator surfaces operational errors via [anyhow::Result] and domain-specific errors
58//!   via [ExitError] where appropriate. Persistent state is kept via the configured persister and
59//!   refreshed against the current chain view provided by the chain source client.
60//!
61//! Minimal example (high-level):
62//! ```no_run
63//! # use std::sync::Arc;
64//! # use std::str::FromStr;
65//! # use std::path::PathBuf;
66//! #
67//! # use bitcoin::Network;
68//! # use tokio::fs;
69//! #
70//! # use bark::{Config, Wallet, SqliteClient};
71//! # use bark::onchain::OnchainWallet;
72//! #
73//! # async fn get_wallets() -> (Wallet, OnchainWallet) {
74//! #   let datadir = PathBuf::from("./bark");
75//! #   let config = Config::network_default(bitcoin::Network::Bitcoin);
76//! #   let db = Arc::new(SqliteClient::open(datadir.join("db.sqlite")).unwrap());
77//! #   let mnemonic_str = fs::read_to_string(datadir.join("mnemonic")).await.unwrap();
78//! #   let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str).unwrap();
79//! #   let bark_wallet = Wallet::open(&mnemonic, db.clone(), config).await.unwrap();
80//! #   let seed = mnemonic.to_seed("");
81//! #   let onchain_wallet = OnchainWallet::load_or_create(Network::Regtest, seed, db).unwrap();
82//! #   (bark_wallet, onchain_wallet)
83//! # }
84//! #
85//! # #[tokio::main]
86//! # async fn main() -> anyhow::Result<()> {
87//! let (mut bark_wallet, mut onchain_wallet) = get_wallets().await;
88//!
89//! // Mark all VTXOs for exit.
90//! bark_wallet.exit.get_mut().start_exit_for_entire_wallet(&onchain_wallet).await?;
91//!
92//! // Transactions will be broadcast and require confirmations so keep periodically calling this.
93//! bark_wallet.exit.get_mut().progress_exits(&mut onchain_wallet, None).await?;
94//!
95//! // Once all VTXOs are claimable, construct a PSBT to drain them.
96//! let drain_to = bitcoin::Address::from_str("bc1p...")?.assume_checked();
97//! let exit = bark_wallet.exit.read().await;
98//! let drain_psbt = exit.drain_exits(
99//!   &exit.list_claimable(),
100//!   &bark_wallet,
101//!   drain_to,
102//!   None,
103//! ).await?;
104//!
105//! // Next you should broadcast the PSBT, once it's confirmed the unilateral exit is complete.
106//! // broadcast_psbt(drain_psbt).await?;
107//! #   Ok(())
108//! # }
109//! ```
110
111pub mod models;
112
113pub(crate) mod progress;
114pub(crate) mod transaction_manager;
115
116pub use vtxo::ExitVtxo;
117
118mod vtxo;
119
120use std::borrow::Borrow;
121use std::cmp;
122use std::collections::{HashMap, HashSet};
123use std::sync::Arc;
124
125use anyhow::Context;
126use bitcoin::{
127	sighash, Address, Amount, FeeRate, Psbt, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness,
128};
129use bitcoin::consensus::Params;
130use log::{error, info, warn};
131
132use ark::{Vtxo, VtxoId, SECP};
133use bitcoin_ext::{BlockHeight, P2TR_DUST};
134
135use crate::Wallet;
136use crate::exit::models::{ExitError, ExitProgressStatus, ExitState, ExitTransactionStatus};
137use crate::exit::transaction_manager::ExitTransactionManager;
138use crate::movement::{MovementDestination, MovementStatus};
139use crate::movement::manager::MovementManager;
140use crate::movement::update::MovementUpdate;
141use crate::onchain::{ChainSource, ExitUnilaterally};
142use crate::persist::BarkPersister;
143use crate::persist::models::StoredExit;
144use crate::psbtext::PsbtInputExt;
145use crate::subsystem::{BarkSubsystem, ExitMovement, SubsystemId};
146use crate::vtxo::state::{VtxoState, UNSPENT_STATES};
147
148/// Handles the process of ongoing VTXO exits.
149pub struct Exit {
150	tx_manager: ExitTransactionManager,
151	persister: Arc<dyn BarkPersister>,
152	chain_source: Arc<ChainSource>,
153	movement_manager: Arc<MovementManager>,
154
155	subsystem_id: SubsystemId,
156	vtxos_to_exit: HashSet<VtxoId>,
157	exit_vtxos: Vec<ExitVtxo>,
158}
159
160impl Exit {
161	pub (crate) async fn new(
162		persister: Arc<dyn BarkPersister>,
163		chain_source: Arc<ChainSource>,
164		movement_manager: Arc<MovementManager>,
165	) -> anyhow::Result<Exit> {
166		let tx_manager = ExitTransactionManager::new(persister.clone(), chain_source.clone())?;
167
168		// Gather the database entries for our exit and convert them into ExitVtxo structs
169		let exit_vtxo_entries = persister.get_exit_vtxo_entries()?;
170
171		let subsystem_id = movement_manager.register_subsystem(
172			BarkSubsystem::Exit.as_str().into(),
173		).await?;
174		Ok(Exit {
175			vtxos_to_exit: HashSet::new(),
176			exit_vtxos: Vec::with_capacity(exit_vtxo_entries.len()),
177			subsystem_id,
178			tx_manager,
179			persister,
180			chain_source,
181			movement_manager,
182		})
183	}
184
185	pub (crate) async fn load(
186		&mut self,
187		onchain: &dyn ExitUnilaterally,
188	) -> anyhow::Result<()> {
189		let exit_vtxo_entries = self.persister.get_exit_vtxo_entries()?;
190		for entry in exit_vtxo_entries {
191			if let Some(vtxo) = self.persister.get_wallet_vtxo(entry.vtxo_id)? {
192				let txids = self.tx_manager.track_vtxo_exits(&vtxo.vtxo, onchain).await?;
193				self.exit_vtxos.push(ExitVtxo::from_parts(vtxo.vtxo, txids, entry.state, entry.history));
194			} else {
195				error!("VTXO {} is marked for exit but it's missing from the database", entry.vtxo_id);
196			}
197		}
198
199		Ok(())
200	}
201
202	/// Returns the unilateral exit status for a given VTXO, if any.
203	///
204	/// - vtxo_id: The ID of the VTXO to check.
205	/// - include_history: Whether to include the full state machine history of the exit
206	/// - include_transactions: Whether to include the full set of transactions related to the exit.
207	/// Errors if status retrieval fails.
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 transactions = if include_transactions {
218					let mut vec = Vec::with_capacity(exit.txids().len());
219					for txid in exit.txids() {
220						vec.push(self.tx_manager.get_package(*txid)?.read().await.clone());
221					}
222					vec
223				} else {
224					vec![]
225				};
226				Ok(Some(ExitTransactionStatus {
227					vtxo_id: exit.id(),
228					state: exit.state().clone(),
229					history: if include_history { Some(exit.history().clone()) } else { None },
230					transactions,
231				}))
232			},
233		}
234	}
235
236	/// Returns a reference to the tracked [ExitVtxo] if it exists.
237	pub fn get_exit_vtxo(&self, vtxo_id: VtxoId) -> Option<&ExitVtxo> {
238		self.exit_vtxos.iter().find(|ev| ev.id() == vtxo_id)
239	}
240
241	/// Returns all known unilateral exits in this wallet.
242	pub fn get_exit_vtxos(&self) -> &Vec<ExitVtxo> {
243		&self.exit_vtxos
244	}
245
246	/// True if there are any unilateral exits which have been started but are not yet claimable.
247	pub fn has_pending_exits(&self) -> bool {
248		self.exit_vtxos.iter().any(|ev| ev.state().is_pending())
249	}
250
251	/// Returns the total amount of all VTXOs requiring more txs to be confirmed
252	pub fn pending_total(&self) -> Amount {
253		self.exit_vtxos
254			.iter()
255			.filter_map(|ev| {
256				if ev.state().is_pending() {
257					Some(ev.vtxo().spec().amount)
258				} else {
259					None
260				}
261			}).sum()
262	}
263
264	/// Returns the earliest block height at which all tracked exits will be claimable
265	pub async fn all_claimable_at_height(&self) -> Option<BlockHeight> {
266		let mut highest_claimable_height = None;
267		for exit in &self.exit_vtxos {
268			if matches!(exit.state(), ExitState::Claimed(..)) {
269				continue;
270			}
271			match exit.state().claimable_height() {
272				Some(h) => highest_claimable_height = cmp::max(highest_claimable_height, Some(h)),
273				None => return None,
274			}
275		}
276		highest_claimable_height
277	}
278
279	/// Starts the unilateral exit process for the entire wallet (all eligible VTXOs).
280	///
281	/// It does not block until completion, you must use [Exit::progress_exits] to advance each exit.
282	///
283	/// It's recommended to sync the wallet, by using something like [Wallet::maintenance] being
284	/// doing this.
285	pub async fn start_exit_for_entire_wallet(
286		&mut self,
287		onchain: &dyn ExitUnilaterally,
288	) -> anyhow::Result<()> {
289		let vtxos: Vec<Vtxo> = self.persister.get_vtxos_by_state(&UNSPENT_STATES)?.into_iter()
290			.map(|v| v.vtxo).collect();
291		self.start_exit_for_vtxos(&vtxos, onchain).await?;
292
293		Ok(())
294	}
295
296	/// Starts the unilateral exit process for the given VTXOs.
297	///
298	/// It does not block until completion, you must use [Exit::progress_exits] to advance each exit.
299	///
300	/// It's recommended to sync the wallet, by using something like [Wallet::maintenance] being
301	/// doing this.
302	pub async fn start_exit_for_vtxos<V: Borrow<Vtxo>>(
303		&mut self,
304		vtxos: &[V],
305		onchain: &dyn ExitUnilaterally,
306	) -> anyhow::Result<()> {
307		self.mark_vtxos_for_exit(&vtxos).await?;
308		self.start_vtxo_exits(onchain).await?;
309		Ok(())
310	}
311
312	/// Lists the IDs of VTXOs marked for unilateral exit.
313	pub fn list_vtxos_to_exit(&self) -> Vec<VtxoId> {
314		self.vtxos_to_exit.iter().cloned().collect()
315	}
316
317	/// Mark a vtxo for unilateral exit.
318	///
319	/// This is a lower level primitive used as a buffer to mark vtxos for exit without having to
320	/// provide an onchain wallet. The actual exit process is started with [Exit::start_vtxo_exits].
321	pub async fn mark_vtxos_for_exit<'a>(
322		&mut self,
323		vtxos: &[impl Borrow<Vtxo>],
324	) -> anyhow::Result<()> {
325		for vtxo in vtxos {
326			let vtxo = vtxo.borrow();
327			let vtxo_id = vtxo.id();
328			if self.exit_vtxos.iter().any(|ev| ev.id() == vtxo_id) {
329				warn!("VTXO {} is already in the exit process", vtxo_id);
330				continue;
331			}
332			self.vtxos_to_exit.insert(vtxo.id());
333
334			// Register the movement now so users can be aware of where their funds have gone.
335			self.persister.update_vtxo_state_checked(vtxo_id, VtxoState::Spent, &UNSPENT_STATES)?;
336			let params = Params::new(self.chain_source.network());
337			let balance = -vtxo.amount().to_signed()?;
338			let destination = MovementDestination::new(
339				Address::from_script(&vtxo.output_script_pubkey(), &params)?.to_string(),
340				vtxo.amount(),
341			);
342
343			// A big reason for creating a finished movement is that we currently don't support
344			// cancelling exits. When we do, we can leave this in pending until it's either finished
345			// or canceled by the user.
346			self.movement_manager.new_finished_movement(
347				self.subsystem_id,
348				ExitMovement::Exit.to_string(),
349				MovementStatus::Finished,
350				MovementUpdate::new()
351					.intended_and_effective_balance(balance)
352					.consumed_vtxo(vtxo.id())
353					.sent_to([destination]),
354			).await.context("Failed to register exit movement")?;
355		}
356		Ok(())
357	}
358
359	/// Starts the unilateral exit process for any VTXOs marked for exit.
360	///
361	/// This is a lower level primitive to be used in conjunction with [Exit::mark_vtxos_for_exit].
362	pub async fn start_vtxo_exits(&mut self, onchain: &dyn ExitUnilaterally) -> anyhow::Result<()> {
363		if self.vtxos_to_exit.is_empty() {
364			return Ok(());
365		}
366
367		let tip = self.chain_source.tip().await?;
368
369		let cloned = self.vtxos_to_exit.iter().cloned().collect::<Vec<_>>();
370		for vtxo_id in cloned {
371			let vtxo = match self.persister.get_wallet_vtxo(vtxo_id)? {
372				Some(vtxo) => vtxo.vtxo,
373				None => {
374					error!("Could not find vtxo to exit {}", vtxo_id);
375					continue;
376				}
377			};
378
379			if self.exit_vtxos.iter().any(|ev| ev.id() == vtxo.id()) {
380				warn!("VTXO {} is already in the exit process", vtxo.id());
381				continue;
382			} else {
383				// The idea is to convert all our vtxos into an exit process structure
384				// that we then store in the database, and we can gradually proceed on.
385				let txids = self.tx_manager.track_vtxo_exits(&vtxo, &*onchain).await?;
386				let exit = ExitVtxo::new(vtxo.clone(), txids, tip);
387				self.persister.store_exit_vtxo_entry(&StoredExit::new(&exit))?;
388				self.exit_vtxos.push(exit);
389			}
390
391			self.vtxos_to_exit.remove(&vtxo_id);
392		}
393
394		Ok(())
395	}
396
397	/// Reset exit to an empty state. Should be called when dropping VTXOs
398	///
399	/// Note: _This method is **dangerous** and can lead to funds loss. Be cautious._
400	pub (crate) fn clear_exit(&mut self) -> anyhow::Result<()> {
401		for exit in &self.exit_vtxos {
402			self.persister.remove_exit_vtxo_entry(&exit.id())?;
403		}
404		self.exit_vtxos.clear();
405		Ok(())
406	}
407
408	/// Returns a list of per-VTXO progress statuses if any changes occurred, or None if there was nothing to do.
409	///
410	/// Iterates over each registered VTXO and attempts to progress their unilateral exit
411	///
412	/// ### Arguments
413	///
414	/// - `onchain` is used to build the CPFP transaction package we use to broadcast
415	///   the unilateral exit transaction
416	/// - `fee_rate_override` sets the desired fee-rate in sats/kvB to use broadcasting exit
417	///   transactions. Note that due to rules imposed by the network with regard to RBF fee bumping,
418	///   replaced transactions may have a higher fee rate than you specify here.
419	///
420	/// ### Return
421	///
422	/// The exit status of each VTXO being exited which has also not yet been spent
423	pub async fn progress_exits(
424		&mut self,
425		onchain: &mut dyn ExitUnilaterally,
426		fee_rate_override: Option<FeeRate>,
427	) -> anyhow::Result<Option<Vec<ExitProgressStatus>>> {
428		self.tx_manager.sync().await?;
429		let mut exit_statuses = Vec::with_capacity(self.exit_vtxos.len());
430		for ev in self.exit_vtxos.iter_mut() {
431			info!("Progressing exit for VTXO {}", ev.id());
432			let error = match ev.progress(
433				&self.chain_source,
434				&mut self.tx_manager,
435				&*self.persister,
436				onchain,
437				fee_rate_override,
438			).await {
439				Ok(_) => None,
440				Err(e) => {
441					match &e {
442						ExitError::InsufficientConfirmedFunds { .. } => {
443							warn!("Can't progress exit for VTXO {} at this time: {}", ev.id(), e);
444						},
445						_ => {
446							error!("Error progressing exit for VTXO {}: {}", ev.id(), e);
447						}
448					}
449					Some(e)
450				}
451			};
452			if !matches!(ev.state(), ExitState::Claimed(..)) {
453				exit_statuses.push(ExitProgressStatus {
454					vtxo_id: ev.id(),
455					state: ev.state().clone(),
456					error,
457				});
458			}
459		}
460		Ok(Some(exit_statuses))
461	}
462
463	/// For use when syncing. This progresses any unilateral exit in a state that needs updating
464	/// such as a when claimable exit may have been spent onchain.
465	pub async fn sync_exit(
466		&mut self,
467		onchain: &mut dyn ExitUnilaterally,
468	) -> anyhow::Result<()> {
469		self.tx_manager.sync().await?;
470		self.start_vtxo_exits(onchain).await?;
471		for exit in &mut self.exit_vtxos {
472			// If the exit is waiting for new blocks, we should trigger an update
473			if exit.state().requires_network_update() {
474				if let Err(e) = exit.progress(
475					&self.chain_source, &mut self.tx_manager, &*self.persister, onchain, None,
476				).await {
477					error!("Error syncing exit for VTXO {}: {}", exit.id(), e);
478				}
479			}
480		}
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 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(|v| (v.vtxo().id(), v))
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 exited yet")?;
515
516				let keypair = wallet.get_vtxo_key(&vtxo)?;
517
518				input.maybe_sign_exit_claim_input(
519					&SECP,
520					&mut shc,
521					&prevouts,
522					i,
523					&keypair
524				)?;
525
526				spent.push(exit_vtxo);
527			}
528		}
529
530		Ok(())
531	}
532
533	/// Builds a PSBT that drains the provided claimable unilateral exits to the given address.
534	///
535	/// - `inputs`: Claimable unilateral exits.
536	/// - `wallet`: The bark wallet containing the keys needed to spend the unilateral exits.
537	/// - `address`: Destination address for the claim.
538	/// - `fee_rate_override`: Optional fee rate to use.
539	///
540	/// Returns a PSBT ready to be broadcast.
541	pub async fn drain_exits<'a>(
542		&self,
543		inputs: &[impl Borrow<ExitVtxo>],
544		wallet: &Wallet,
545		address: Address,
546		fee_rate_override: Option<FeeRate>,
547	) -> anyhow::Result<Psbt, ExitError> {
548		if inputs.is_empty() {
549			return Err(ExitError::ClaimMissingInputs);
550		}
551		let mut tx = {
552			let mut output_amount = Amount::ZERO;
553			let mut tx_ins = Vec::with_capacity(inputs.len());
554			for input in inputs {
555				let input = input.borrow();
556				if !matches!(input.state(), ExitState::Claimable(..)) {
557					return Err(ExitError::VtxoNotClaimable { vtxo: input.id() });
558				}
559				output_amount += input.vtxo().amount();
560				tx_ins.push(TxIn {
561					previous_output: input.vtxo().point(),
562					script_sig: ScriptBuf::default(),
563					sequence: Sequence::from_height(input.vtxo().exit_delta()),
564					witness: Witness::new(),
565				});
566			}
567			Transaction {
568				version: bitcoin::transaction::Version(3),
569				lock_time: bitcoin::absolute::LockTime::ZERO,
570				input: tx_ins,
571				output: vec![
572					TxOut {
573						script_pubkey: address.script_pubkey(),
574						value: output_amount,
575					},
576				],
577			}
578		};
579
580		// Create a PSBT to determine the weight of the transaction so we can deduct a tx fee
581		let create_psbt = |tx: Transaction| {
582			let mut psbt = Psbt::from_unsigned_tx(tx)
583				.map_err(|e| ExitError::InternalError {
584					error: format!("Failed to create exit claim PSBT: {}", e),
585				})?;
586			psbt.inputs.iter_mut().zip(inputs).for_each(|(i, v)| {
587				i.set_exit_claim_input(&v.borrow().vtxo());
588				i.witness_utxo = Some(v.borrow().vtxo().txout())
589			});
590			self.sign_exit_claim_inputs(&mut psbt, wallet)
591				.map_err(|e| ExitError::ClaimSigningError { error: e.to_string() })?;
592			Ok(psbt)
593		};
594		let fee_amount = {
595			let fee_rate = fee_rate_override
596				.unwrap_or(self.chain_source.fee_rates().await.regular);
597			fee_rate * create_psbt(tx.clone())?
598				.extract_tx()
599				.map_err(|e| ExitError::InternalError {
600					error: format!("Failed to get tx from signed exit claim PSBT: {}", e),
601				})?
602				.weight()
603		};
604
605		// We adjust the drain output to cover the fee
606		let needed = fee_amount + P2TR_DUST;
607		if needed > tx.output[0].value {
608			return Err(ExitError::ClaimFeeExceedsOutput {
609				needed, output: tx.output[0].value,
610			});
611		}
612		tx.output[0].value -= fee_amount;
613
614		// Now create the final signed PSBT
615		create_psbt(tx)
616	}
617}