pub(crate) mod states;
pub(crate) mod util;
use std::collections::HashSet;
use bitcoin::{Amount, FeeRate, Transaction, Txid};
use log::{debug, error, warn};
use ark::Vtxo;
use ark::vtxo::Full;
use bitcoin_ext::{BlockHeight, BlockRef, TxStatus};
use bitcoin_ext::cpfp::{CpfpError, MakeCpfpFees};
use crate::exit::models::{ExitError, ExitState, ExitTx, ExitTxOrigin, ExitTxStatus};
use crate::exit::transaction_manager::ExitTransactionManager;
use crate::onchain::ExitUnilaterally;
use crate::Wallet;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub(crate) trait ExitStateProgress {
async fn progress(
self,
ctx: &mut ProgressContext<'_>,
onchain: &mut dyn ExitUnilaterally,
) -> anyhow::Result<ExitState, ExitProgressError>;
}
pub(crate) enum ProgressStep {
Continue,
Done,
}
impl ProgressStep {
pub fn from_exit_state(state: &ExitState) -> ProgressStep {
match state {
ExitState::Start(_) => ProgressStep::Continue,
ExitState::Processing(s) => {
let should_continue = s.transactions.iter().any(|tx| {
match &tx.status {
ExitTxStatus::VerifyInputs => true,
ExitTxStatus::AwaitingInputConfirmation { .. } => false,
ExitTxStatus::NeedsSignedPackage => true,
ExitTxStatus::NeedsReplacementPackage { .. } => true,
ExitTxStatus::NeedsBroadcasting { .. } => true,
ExitTxStatus::BroadcastWithCpfp { .. } => false,
ExitTxStatus::Confirmed { .. } => false,
}
});
if should_continue {
ProgressStep::Continue
} else {
ProgressStep::Done
}
},
ExitState::AwaitingDelta(_) => ProgressStep::Done,
ExitState::Claimable(_) => ProgressStep::Done,
ExitState::ClaimInProgress(_) => ProgressStep::Done,
ExitState::Claimed(_) => ProgressStep::Done,
}
}
}
pub(crate) struct ExitProgressError {
pub state: Option<ExitState>,
pub error: ExitError,
}
impl From<ExitError> for ExitProgressError {
fn from(error: ExitError) -> Self {
Self {
state: None,
error,
}
}
}
pub(crate) struct ProgressContext<'a> {
pub vtxo: &'a Vtxo<Full>,
pub exit_txids: &'a Vec<Txid>,
pub wallet: &'a Wallet,
pub fee_rate: FeeRate,
pub tx_manager: &'a mut ExitTransactionManager,
}
impl<'a> ProgressContext<'a> {
pub async fn check_confirmed(&mut self, txid: Txid) -> bool {
matches!(self.tx_manager.tx_status(txid).await, Ok(TxStatus::Confirmed(_)))
}
pub async fn check_status_from_inputs(
&mut self, exit: &ExitTx, inputs: &HashSet<Txid>,
) -> anyhow::Result<ExitTxStatus, ExitError> {
let mut txids = HashSet::with_capacity(inputs.len());
for txid in inputs.iter() {
debug!("Checking if exit tx {} has the following confirmed input: {}",
exit.txid, txid,
);
if !self.check_confirmed(*txid).await {
debug!("Exit tx {} has unconfirmed input: {}", exit.txid, txid);
txids.insert(*txid);
}
}
if txids.is_empty() {
debug!("All inputs are confirmed for exit tx {}", exit.txid);
Ok(ExitTxStatus::NeedsSignedPackage)
} else {
debug!("Exit tx {} has {} unconfirmed inputs: {:?}", exit.txid, txids.len(), txids);
Ok(ExitTxStatus::AwaitingInputConfirmation { txids })
}
}
pub fn create_exit_cpfp_tx(
&mut self,
exit_tx: &Transaction,
onchain: &mut dyn ExitUnilaterally,
min_rbf_fees: Option<(FeeRate, Amount)>,
) -> anyhow::Result<Transaction, ExitError> {
let fees = if let Some((min_fee_rate, min_fee)) = min_rbf_fees {
MakeCpfpFees::Rbf {
min_effective_fee_rate: if min_fee_rate < self.fee_rate {
self.fee_rate
} else {
min_fee_rate
},
current_package_fee: min_fee,
}
} else {
MakeCpfpFees::Effective(self.fee_rate)
};
onchain.make_signed_p2a_cpfp(&exit_tx, fees)
.map_err(|e| match e {
CpfpError::NoFeeAnchor(_) => ExitError::InternalError { error: e.to_string() },
CpfpError::InsufficientConfirmedFunds { needed, available } => {
ExitError::InsufficientConfirmedFunds { needed, available }
},
e => ExitError::ExitPackageFinalizeFailure { error: e.to_string() },
})
}
pub async fn get_block_ref(&self, height: BlockHeight) -> anyhow::Result<BlockRef, ExitError> {
self.wallet.chain.block_ref(height).await
.map_err(|e| ExitError::BlockRetrievalFailure { height, error: e.to_string() })
}
pub async fn get_exit_child_status(
&mut self,
exit: &ExitTx,
child_txid: Txid,
) -> anyhow::Result<ExitTxStatus, ExitError> {
let current_child_txid = self.tx_manager.get_child_txid(exit.txid).await?;
if let Some(current) = current_child_txid {
if current != child_txid {
warn!("Exit CPFP tx {} for exit tx {} has been replaced by {}", child_txid, exit.txid, current);
}
debug!("Updating CPFP tx status {} for exit tx {}", current, exit.txid);
self.get_exit_tx_status(exit).await
} else {
error!("Exit CPFP tx {} for exit tx {} has disappeared", child_txid, exit.txid);
Ok(ExitTxStatus::NeedsSignedPackage)
}
}
pub async fn get_exit_tx_status(
&mut self,
exit: &ExitTx,
) -> anyhow::Result<ExitTxStatus, ExitError> {
if let Some(child) = self.tx_manager.get_child_status(exit.txid).await? {
match child.status {
TxStatus::NotFound => Ok(ExitTxStatus::NeedsBroadcasting {
child_txid: child.txid,
origin: child.origin,
}),
TxStatus::Mempool => {
match child.origin {
ExitTxOrigin::Wallet { .. } => {
Ok(ExitTxStatus::BroadcastWithCpfp {
child_txid: child.txid,
origin: child.origin,
})
},
ExitTxOrigin::Mempool { fee_rate, total_fee } => {
if fee_rate < self.fee_rate {
Ok(ExitTxStatus::NeedsReplacementPackage {
min_fee_rate: fee_rate,
min_fee: total_fee,
})
} else {
Ok(ExitTxStatus::BroadcastWithCpfp {
child_txid: child.txid,
origin: child.origin,
})
}
},
ExitTxOrigin::Block { .. } => Err(ExitError::InternalError {
error: format!("TxStatus was {:?} when origin is {}, this should never happen", child.status, child.origin),
})
}
},
TxStatus::Confirmed(b) => Ok(ExitTxStatus::Confirmed {
child_txid: child.txid,
block: b,
origin: child.origin,
})
}
} else {
Ok(ExitTxStatus::NeedsSignedPackage)
}
}
pub(crate) async fn get_unique_inputs(
&self,
exit_txid: Txid,
) -> Result<HashSet<Txid>, ExitError> {
let package = self.tx_manager.get_package(exit_txid)?;
let guard = package.read().await;
Ok(guard.exit.tx.input
.iter()
.map(|i| i.previous_output.txid)
.collect::<HashSet<_>>()
)
}
pub async fn tip_height(&self) -> anyhow::Result<u32, ExitError> {
self.wallet.chain.tip().await
.map_err(|e| ExitError::TipRetrievalFailure { error: e.to_string() })
}
}