mod models;
mod vtxo;
pub mod bdk;
pub(crate) mod progress;
pub(crate) mod transaction_manager;
pub use self::models::{
ExitCpfpRequest, ExitTransactionPackage, FeeInfo, RbfRequirement, TransactionInfo,
ChildTransactionInfo, ExitError, ExitState, ExitTx, ExitTxStatus, ExitTxOrigin, ExitStartState,
ExitProcessingState, ExitAwaitingDeltaState, ExitClaimableState, ExitClaimInProgressState,
ExitClaimedState, ExitVtxoAlreadySpentState, ExitCanceledState, ExitStateKind,
ExitProgressStatus, ExitTransactionStatus,
};
pub use self::vtxo::ExitVtxo;
use std::borrow::Borrow;
use std::cmp;
use std::collections::HashMap;
use std::sync::Arc;
use anyhow::Context;
use bitcoin::{
Address, Amount, FeeRate, Psbt, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness, sighash
};
use bitcoin::consensus::Params;
use log::{error, info, trace, warn};
use ark::{Vtxo, VtxoId};
use ark::vtxo::Bare;
use ark::vtxo::policy::signing::VtxoSigner;
use bitcoin_ext::{BlockHeight, P2TR_DUST, TxStatus};
use crate::Wallet;
use crate::chain::ChainSource;
use crate::exit::transaction_manager::ExitTransactionManager;
use crate::movement::{MovementDestination, MovementStatus, PaymentMethod};
use crate::movement::manager::MovementManager;
use crate::movement::update::MovementUpdate;
use crate::persist::BarkPersister;
use crate::persist::models::StoredExit;
use crate::psbtext::PsbtInputExt;
use crate::subsystem::{ExitMovement, Subsystem};
use crate::vtxo::VtxoStateKind;
pub(crate) struct ExitInner {
tx_manager: ExitTransactionManager,
persister: Arc<dyn BarkPersister>,
chain_source: Arc<ChainSource>,
movement_manager: Arc<MovementManager>,
exit_vtxos: Vec<ExitVtxo>,
}
impl ExitInner {
async fn start_exit_for_vtxos(
&mut self,
vtxos: &[impl Borrow<Vtxo<Bare>>],
skip_standardness_checks: bool,
) -> anyhow::Result<()> {
if vtxos.is_empty() {
return Ok(());
}
let tip = self.chain_source.tip().await?;
let params = Params::new(self.chain_source.network());
for vtxo in vtxos {
let vtxo = vtxo.borrow();
let vtxo_id = vtxo.id();
if self.exit_vtxos.iter().any(|ev| ev.id() == vtxo_id) {
warn!("VTXO {} is already in the exit process", vtxo_id);
continue;
}
if !skip_standardness_checks {
if vtxo.amount() < P2TR_DUST {
return Err(ExitError::DustLimit {
vtxo: vtxo.amount(),
dust: P2TR_DUST,
}.into());
}
let full_vtxo = self.persister.get_full_vtxo(vtxo_id).await?
.ok_or_else(|| ExitError::InvalidWalletState {
error: format!("missing genesis for VTXO {vtxo_id}"),
})?;
if let Err(error) = full_vtxo.check_standard() {
return Err(ExitError::NonStandardVtxo { vtxo: vtxo_id, error }.into());
}
}
let balance = -vtxo.amount().to_signed()?;
let script_pubkey = vtxo.output_script_pubkey();
let payment_method = match Address::from_script(&script_pubkey, ¶ms) {
Ok(addr) => PaymentMethod::Bitcoin(addr.into_unchecked()),
Err(e) => {
warn!("Unable to convert script pubkey to address: {:#}", e);
PaymentMethod::OutputScript(script_pubkey)
}
};
let movement_id = self.movement_manager.new_movement_with_update(
Subsystem::EXIT,
ExitMovement::Exit.to_string(),
MovementUpdate::new()
.intended_and_effective_balance(balance)
.consumed_vtxo(vtxo_id)
.sent_to([MovementDestination::new(payment_method, vtxo.amount())]),
).await.context("Failed to register exit movement")?;
trace!("Starting exit for VTXO: {}", vtxo_id);
let exit = ExitVtxo::new(vtxo, tip, Some(movement_id));
self.persister.store_exit_vtxo_entry(&StoredExit::new(&exit)).await?;
self.exit_vtxos.push(exit);
trace!("Exit for VTXO started successfully: {}", vtxo_id);
}
Ok(())
}
async fn refresh_tx_state(&mut self) -> anyhow::Result<()> {
let mut exit_vtxos = std::mem::take(&mut self.exit_vtxos);
for exit in &mut exit_vtxos {
if !exit.is_initialized() {
match exit.initialize(&mut self.tx_manager, &*self.persister).await {
Ok(()) => continue,
Err(e) => {
error!("Error initializing exit for VTXO {}: {:#}", exit.id(), e);
}
}
}
}
self.exit_vtxos = exit_vtxos;
self.tx_manager.sync().await?;
Ok(())
}
async fn sign_exit_claim_inputs(
&self,
psbt: &mut Psbt,
wallet: &Wallet,
) -> anyhow::Result<()> {
let prevouts = psbt.inputs.iter()
.map(|i| i.witness_utxo.clone().unwrap())
.collect::<Vec<_>>();
let prevouts = sighash::Prevouts::All(&prevouts);
let mut shc = sighash::SighashCache::new(&psbt.unsigned_tx);
let claimable = self.exit_vtxos.iter()
.filter(|ev| ev.is_claimable())
.map(|e| (e.id(), e))
.collect::<HashMap<_, _>>();
for (i, input) in psbt.inputs.iter_mut().enumerate() {
let vtxo = input.get_exit_claim_input();
if let Some(vtxo) = vtxo {
let exit_vtxo = claimable.get(&vtxo.id()).context("vtxo is not claimable yet")?;
let witness = wallet.sign_input(&vtxo, i, &mut shc, &prevouts).await
.map_err(|e| ExitError::ClaimSigningError { error: e.to_string() })?;
input.final_script_witness = Some(witness);
let _ = exit_vtxo;
}
}
Ok(())
}
async fn exit_status(
&self,
entry: StoredExit,
include_history: bool,
include_transactions: bool,
) -> anyhow::Result<ExitTransactionStatus> {
let mut transactions = Vec::new();
if include_transactions {
let vtxo = self.persister.get_full_vtxo(entry.vtxo_id).await
.context("failed to retrieve VTXO for exit")?
.with_context(|| format!("failed to retrieve VTXO for exit {}", entry.vtxo_id))?;
for tx in vtxo.transactions() {
let txid = tx.tx.compute_txid();
if let Some(package) = self.tx_manager.try_get_package(txid) {
transactions.push(package.read().await.clone());
continue;
}
let child = self.persister.get_exit_child_tx(txid).await
.context("failed to retrieve child tx for exit")?;
transactions.push(ExitTransactionPackage {
exit: TransactionInfo {
txid,
tx: tx.tx,
},
child: child.map(|(tx, origin)| ChildTransactionInfo {
origin,
info: TransactionInfo {
txid: tx.compute_txid(),
tx,
},
fee_info: None,
}),
});
}
}
Ok(ExitTransactionStatus {
vtxo_id: entry.vtxo_id,
state: entry.state,
history: if include_history { Some(entry.history) } else { None },
transactions,
})
}
async fn exit_statuses(
&self,
entries: Vec<StoredExit>,
include_history: bool,
include_transactions: bool,
) -> anyhow::Result<Vec<ExitTransactionStatus>> {
let mut statuses = Vec::with_capacity(entries.len());
for entry in entries {
statuses.push(self.exit_status(entry, include_history, include_transactions).await?);
}
Ok(statuses)
}
}
pub struct Exit {
inner: Arc<tokio::sync::RwLock<ExitInner>>,
}
impl Exit {
pub(crate) async fn new(
persister: Arc<dyn BarkPersister>,
chain_source: Arc<ChainSource>,
movement_manager: Arc<MovementManager>,
) -> anyhow::Result<Exit> {
let tx_manager = ExitTransactionManager::new(persister.clone(), chain_source.clone())?;
let inner = ExitInner {
exit_vtxos: Vec::new(),
tx_manager,
persister,
chain_source,
movement_manager,
};
Ok(Exit { inner: Arc::new(tokio::sync::RwLock::new(inner)) })
}
pub(crate) async fn load(&self) -> anyhow::Result<()> {
let mut guard = self.inner.write().await;
let inner = &mut *guard;
let exit_vtxo_entries = inner.persister
.get_exit_vtxo_entries_with_states(ExitStateKind::LIVE_STATES).await?;
inner.exit_vtxos.reserve(exit_vtxo_entries.len());
for entry in exit_vtxo_entries {
if let Some(vtxo) = inner.persister.get_wallet_vtxo(entry.vtxo_id).await? {
let mut exit = ExitVtxo::from_entry(entry, &vtxo);
exit.initialize(&mut inner.tx_manager, &*inner.persister).await?;
inner.exit_vtxos.push(exit);
} else {
error!("VTXO {} is marked for exit but it's missing from the database", entry.vtxo_id);
}
}
Ok(())
}
pub async fn get_exit_status(
&self,
vtxo_id: VtxoId,
include_history: bool,
include_transactions: bool,
) -> anyhow::Result<Option<ExitTransactionStatus>> {
let guard = self.inner.read().await;
match guard.persister.get_exit_vtxo_entry(&vtxo_id).await? {
None => Ok(None),
Some(entry) => {
Ok(Some(guard.exit_status(entry, include_history, include_transactions).await?))
},
}
}
pub async fn get_exit_vtxo(&self, vtxo_id: VtxoId) -> Option<ExitVtxo> {
let guard = self.inner.read().await;
guard.exit_vtxos.iter().find(|ev| ev.id() == vtxo_id).cloned()
}
pub async fn get_exit_vtxo_ids(&self) -> Vec<VtxoId> {
let guard = self.inner.read().await;
guard.exit_vtxos.iter().map(|ev| ev.id()).collect()
}
pub async fn get_exit_vtxos(&self) -> Vec<ExitVtxo> {
let guard = self.inner.read().await;
guard.exit_vtxos.clone()
}
pub async fn list_all(
&self,
include_history: bool,
include_transactions: bool,
) -> anyhow::Result<Vec<ExitTransactionStatus>> {
let guard = self.inner.read().await;
let entries = guard.persister.get_exit_vtxo_entries().await?;
guard.exit_statuses(entries, include_history, include_transactions).await
}
pub async fn list_live(
&self,
include_history: bool,
include_transactions: bool,
) -> anyhow::Result<Vec<ExitTransactionStatus>> {
let guard = self.inner.read().await;
let entries = guard.persister
.get_exit_vtxo_entries_with_states(ExitStateKind::LIVE_STATES).await?;
guard.exit_statuses(entries, include_history, include_transactions).await
}
pub async fn list_finished(
&self,
include_history: bool,
include_transactions: bool,
) -> anyhow::Result<Vec<ExitTransactionStatus>> {
let guard = self.inner.read().await;
let entries = guard.persister
.get_exit_vtxo_entries_with_states(ExitStateKind::FINISHED_STATES).await?;
guard.exit_statuses(entries, include_history, include_transactions).await
}
pub async fn is_exiting(&self, vtxo_id: VtxoId) -> bool {
let guard = self.inner.read().await;
let state = guard.exit_vtxos.iter().find(|ev| ev.id() == vtxo_id).map(|ev| ev.state());
match state {
Some(ExitState::Start(_)) => true,
Some(ExitState::Processing(_)) => true,
Some(ExitState::AwaitingDelta(_)) => true,
Some(ExitState::Claimable(_)) => true,
Some(ExitState::ClaimInProgress(_)) => true,
Some(ExitState::Claimed(_)) => true,
Some(ExitState::VtxoAlreadySpent(_)) => false,
Some(ExitState::Canceled(_)) => false,
None => false,
}
}
pub async fn has_pending_exits(&self) -> bool {
let guard = self.inner.read().await;
guard.exit_vtxos.iter().any(|ev| ev.state().is_pending())
}
pub fn try_pending_total(&self) -> Option<Amount> {
self.inner.try_read().ok().map(|guard| {
guard.exit_vtxos.iter()
.filter(|ev| matches!(
ev.state(),
ExitState::AwaitingDelta(_)
| ExitState::Claimable(_)
| ExitState::ClaimInProgress(_),
))
.map(|ev| ev.amount())
.sum()
})
}
pub async fn all_claimable_at_height(&self) -> Option<BlockHeight> {
let guard = self.inner.read().await;
let mut highest_claimable_height = None;
for exit in &guard.exit_vtxos {
match exit.state().claimable_height() {
Some(h) => highest_claimable_height = cmp::max(highest_claimable_height, Some(h)),
None => continue,
}
}
highest_claimable_height
}
pub async fn start_exit_for_entire_wallet(&self) -> anyhow::Result<()> {
let mut guard = self.inner.write().await;
let all_vtxos = guard.persister.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?
.into_iter();
let total_vtxos = all_vtxos.len();
let mut eligible = Vec::with_capacity(total_vtxos);
for v in all_vtxos {
match guard.persister.get_full_vtxo(v.id()).await {
Ok(Some(full)) => match full.check_standard() {
Ok(()) => eligible.push(v.vtxo),
Err(e) => warn!("Skipping non-standard VTXO {}: {:#}", v.id(), e),
},
Ok(None) => error!("Failed to retrieve full VTXO: {}", v.id()),
Err(e) => error!("Failed to retrieve full VTXO {}: {:#}", v.id(), e),
}
}
let ineligible = total_vtxos - eligible.len();
if eligible.is_empty() && ineligible > 0 {
warn!(
"Exit not started: all {} VTXOs are non-standard. To exit and consolidate you \
should try refreshing your VTXOs first",
ineligible,
);
return Ok(());
}
guard.start_exit_for_vtxos(&eligible, false).await
}
pub async fn start_exit_for_vtxos(
&self,
vtxos: &[impl Borrow<Vtxo<Bare>>],
) -> anyhow::Result<()> {
let mut guard = self.inner.write().await;
guard.start_exit_for_vtxos(vtxos, false).await
}
pub async fn start_exit_for_vtxos_including_non_standard(
&self,
vtxos: &[impl Borrow<Vtxo<Bare>>],
) -> anyhow::Result<()> {
let mut guard = self.inner.write().await;
guard.start_exit_for_vtxos(vtxos, true).await
}
pub async fn cancel_exit(&self, vtxo_id: VtxoId) -> anyhow::Result<(), ExitError> {
let mut guard = self.inner.write().await;
let inner = &mut *guard;
let idx = match inner.exit_vtxos.iter().position(|ev| ev.id() == vtxo_id) {
Some(idx) => idx,
None => {
let entry = inner.persister.get_exit_vtxo_entry(&vtxo_id).await
.map_err(|e| ExitError::InternalError { error: e.to_string() })?;
return match entry.map(|e| e.state.kind()) {
Some(ExitStateKind::Canceled) => Ok(()),
Some(kind) => Err(ExitError::CannotCancelExit { vtxo: vtxo_id, state: kind }),
None => Err(ExitError::NotExiting { vtxo: vtxo_id }),
};
},
};
if !inner.exit_vtxos[idx].state().is_cancelable() {
return Err(ExitError::CannotCancelExit {
vtxo: vtxo_id,
state: inner.exit_vtxos[idx].state().kind(),
});
}
let leaf_txid = inner.exit_vtxos[idx].get_vtxo(&*inner.persister).await?.point().txid;
match inner.tx_manager.sync_exit_tx(leaf_txid).await? {
TxStatus::NotFound => {},
TxStatus::Mempool | TxStatus::Confirmed(_) => {
return Err(ExitError::ExitTxAlreadyBroadcast { vtxo: vtxo_id, txid: leaf_txid });
},
}
let tip = inner.chain_source.tip().await
.map_err(|e| ExitError::TipRetrievalFailure { error: e.to_string() })?;
inner.exit_vtxos[idx].cancel(tip, &*inner.persister).await?;
if let Some(txids) = inner.exit_vtxos[idx].txids() {
inner.tx_manager.untrack_vtxo_exits(&txids).await;
}
if let Some(movement_id) = inner.exit_vtxos[idx].movement_id() {
if let Err(e) = inner.movement_manager
.finish_movement(movement_id, MovementStatus::Canceled).await
{
error!("Failed to finalize exit movement {} as Canceled: {:#}", movement_id, e);
}
}
inner.exit_vtxos.swap_remove(idx);
info!("Canceled unilateral exit for VTXO {}", vtxo_id);
Ok(())
}
pub(crate) async fn dangerous_clear_exit(&self) -> anyhow::Result<()> {
let mut guard = self.inner.write().await;
for exit in &guard.exit_vtxos {
guard.persister.remove_exit_vtxo_entry(&exit.id()).await?;
}
guard.exit_vtxos.clear();
Ok(())
}
pub async fn progress_exits(
&self,
wallet: &Wallet,
) -> anyhow::Result<Option<Vec<ExitProgressStatus>>> {
let mut guard = self.inner.write().await;
guard.refresh_tx_state().await?;
let mut exit_vtxos = std::mem::take(&mut guard.exit_vtxos);
let mut exit_statuses = Vec::with_capacity(exit_vtxos.len());
for ev in exit_vtxos.iter_mut() {
if !ev.is_initialized() {
warn!("Skipping progress of uninitialized unilateral exit {}", ev.id());
continue;
}
info!("Progressing exit for VTXO {}", ev.id());
let pre_state = ev.state().clone();
let error = match ev.progress(
wallet,
&mut guard.tx_manager,
true,
).await {
Ok(_) => None,
Err(e) => {
match &e {
ExitError::InsufficientConfirmedFunds { .. } => {
warn!("Can't progress exit for VTXO {} at this time: {}", ev.id(), e);
},
_ => {
error!("Error progressing exit for VTXO {}: {}", ev.id(), e);
}
}
Some(e)
}
};
let state_changed = ev.state() != &pre_state;
Self::reconcile_vtxo_and_movement(
wallet, &guard.movement_manager, ev, state_changed,
).await;
if !matches!(ev.state(), ExitState::Claimed(..)) {
exit_statuses.push(ExitProgressStatus {
vtxo_id: ev.id(),
state: ev.state().clone(),
error,
});
}
}
guard.exit_vtxos = exit_vtxos;
Ok(Some(exit_statuses))
}
async fn reconcile_vtxo_and_movement(
wallet: &Wallet,
movements: &MovementManager,
ev: &ExitVtxo,
state_changed: bool,
) {
if ev.state().warrants_exited_vtxo() {
if let Err(e) = wallet.mark_vtxos_as_exited([ev.id()]).await {
error!("Failed to mark VTXO {} as Exited: {:#}", ev.id(), e);
}
}
if !state_changed {
return;
}
let Some(movement_id) = ev.movement_id() else { return };
let new_status = match ev.state() {
ExitState::Claimed(_) => MovementStatus::Successful,
ExitState::VtxoAlreadySpent(_) => MovementStatus::Canceled,
_ => return,
};
if let Err(e) = movements.finish_movement(movement_id, new_status).await {
error!(
"Failed to finalize exit movement {} as {:?}: {:#}",
movement_id, new_status, e,
);
}
}
pub async fn sync(
&self,
wallet: &Wallet,
) -> anyhow::Result<()> {
let mut guard = self.inner.write().await;
guard.refresh_tx_state().await?;
let mut exit_vtxos = std::mem::take(&mut guard.exit_vtxos);
for exit in &mut exit_vtxos {
if !exit.is_initialized() {
warn!("Skipping progress of uninitialized unilateral exit {}", exit.id());
continue;
}
let pre_state = exit.state().clone();
if let Err(e) = exit.progress(
wallet, &mut guard.tx_manager, true,
).await {
error!("Error syncing exit for VTXO {}: {}", exit.id(), e);
}
let state_changed = exit.state() != &pre_state;
Self::reconcile_vtxo_and_movement(
wallet, &guard.movement_manager, exit, state_changed,
).await;
}
guard.exit_vtxos = exit_vtxos;
Ok(())
}
pub async fn exits_needing_cpfp(&self) -> Vec<ExitCpfpRequest> {
let guard = self.inner.read().await;
let mut requests = Vec::new();
for ev in &guard.exit_vtxos {
let ExitState::Processing(s) = ev.state() else { continue };
for tx in &s.transactions {
let rbf_requirement = match &tx.status {
ExitTxStatus::AwaitingCpfpBroadcast => None,
ExitTxStatus::AwaitingConfirmation {..} => {
match guard.tx_manager.get_child_status(tx.txid).await {
Ok(Some(c)) => match c.fee_info {
Some(fi) => Some(RbfRequirement {
min_fee_rate: fi.fee_rate,
current_package_fee: fi.total_fee,
}),
None => continue,
},
_ => continue,
}
},
_ => continue,
};
let package = match guard.tx_manager.get_package(tx.txid) {
Ok(p) => p,
Err(_) => continue,
};
let exit_tx = package.read().await.exit.tx.clone();
requests.push(ExitCpfpRequest {
vtxo_id: ev.id(),
exit_tx,
rbf_requirement,
});
}
}
requests
}
pub async fn provide_cpfp_tx(
&self,
wallet: &Wallet,
exit_txid: Txid,
child_tx: Transaction,
) -> anyhow::Result<(), ExitError> {
let origin = ExitTxOrigin::Wallet { confirmed_in: None };
let mut guard = self.inner.write().await;
let inner = &mut *guard;
inner.tx_manager.set_wallet_child_tx(exit_txid, child_tx, origin).await?;
let package = inner.tx_manager.get_package(exit_txid)?;
let pkg_guard = package.read().await;
match inner.tx_manager.broadcast_package(&*pkg_guard).await {
Ok(_) => {},
Err(ExitError::ExitPackageBroadcastFailure { ref error, .. })
if error.is_mempool_conflict() =>
{
warn!("CPFP broadcast conflict for {}: {} — another CPFP may already be in mempool", exit_txid, error);
},
Err(e) => return Err(e),
}
drop(pkg_guard);
for ev in inner.exit_vtxos.iter_mut() {
let ExitState::Processing(s) = ev.state() else { continue };
let has_tx = s.transactions.iter().any(|tx| tx.txid == exit_txid);
if has_tx {
if let Err(e) = ev.progress(wallet, &mut inner.tx_manager, false).await {
warn!("Failed to progress exit for {} after CPFP: {}", exit_txid, e);
}
break;
}
}
Ok(())
}
pub async fn list_claimable(&self) -> Vec<ExitVtxo> {
let guard = self.inner.read().await;
guard.exit_vtxos.iter().filter(|ev| ev.is_claimable()).cloned().collect()
}
pub async fn sign_exit_claim_inputs(&self, psbt: &mut Psbt, wallet: &Wallet) -> anyhow::Result<()> {
let guard = self.inner.read().await;
guard.sign_exit_claim_inputs(psbt, wallet).await
}
pub async fn drain_exits(
&self,
inputs: &[impl Borrow<ExitVtxo>],
wallet: &Wallet,
address: Address,
fee_rate_override: Option<FeeRate>,
) -> anyhow::Result<Psbt, ExitError> {
let guard = self.inner.read().await;
let tip = guard.chain_source.tip().await
.map_err(|e| ExitError::TipRetrievalFailure { error: e.to_string() })?;
if inputs.is_empty() {
return Err(ExitError::ClaimMissingInputs);
}
let mut vtxos = HashMap::with_capacity(inputs.len());
for input in inputs {
let i = input.borrow();
let vtxo = i.get_full_vtxo(&*guard.persister).await?;
vtxos.insert(i.id(), vtxo);
}
let mut tx = {
let mut output_amount = Amount::ZERO;
let mut tx_ins = Vec::with_capacity(inputs.len());
for input in inputs {
let input = input.borrow();
let vtxo = &vtxos[&input.id()];
if !matches!(input.state(), ExitState::Claimable(..)) {
return Err(ExitError::VtxoNotClaimable { vtxo: input.id() });
}
output_amount += vtxo.amount();
let clause = wallet.find_signable_clause(vtxo).await
.ok_or(ExitError::ClaimMissingSignableClause { vtxo: vtxo.id() })?;
tx_ins.push(TxIn {
previous_output: vtxo.point(),
script_sig: ScriptBuf::default(),
sequence: clause.sequence().unwrap_or(Sequence::ZERO),
witness: Witness::new(),
});
}
let locktime = bitcoin::absolute::LockTime::from_height(tip)
.map_err(|e| ExitError::InvalidLocktime { tip, error: e.to_string() })?;
Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: locktime,
input: tx_ins,
output: vec![
TxOut {
script_pubkey: address.script_pubkey(),
value: output_amount,
},
],
}
};
let create_psbt = |tx: Transaction| async {
let mut psbt = Psbt::from_unsigned_tx(tx)
.map_err(|e| ExitError::InternalError {
error: format!("Failed to create exit claim PSBT: {}", e),
})?;
psbt.inputs.iter_mut().zip(inputs).for_each(|(i, e)| {
let v = &vtxos[&e.borrow().id()];
i.set_exit_claim_input(v);
i.witness_utxo = Some(v.txout())
});
guard.sign_exit_claim_inputs(&mut psbt, wallet).await
.map_err(|e| ExitError::ClaimSigningError { error: e.to_string() })?;
Ok(psbt)
};
let fee_amount = {
let fee_rate = fee_rate_override
.unwrap_or(guard.chain_source.fee_rates().await.regular);
fee_rate * create_psbt(tx.clone()).await?
.extract_tx()
.map_err(|e| ExitError::InternalError {
error: format!("Failed to get tx from signed exit claim PSBT: {}", e),
})?
.weight()
};
let needed = fee_amount + P2TR_DUST;
if needed > tx.output[0].value {
return Err(ExitError::ClaimFeeExceedsOutput {
needed, output: tx.output[0].value,
});
}
tx.output[0].value -= fee_amount;
create_psbt(tx).await
}
}