use std::collections::{HashMap, HashSet};
use bitcoin::transaction::{predict_weight, InputWeightPrediction};
use bitcoin::{
Address, Amount, FeeRate, Sequence, Transaction, TxIn, TxOut, Weight, Witness, ScriptBuf,
sighash,
};
use bitcoin::secp256k1::{Secp256k1, SecretKey};
use ark::Vtxo;
use ark::vtxo::Full;
use ark::vtxo::policy::signing::VtxoSigner;
use bitcoin_ext::TxStatus;
use ark::VtxoId;
use crate::Wallet;
use crate::exit::bdk::should_rbf;
use crate::exit::{Exit, ExitError, ExitState, ExitTxStatus};
use crate::onchain::MakeCpfpFees;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExitFeeEstimate {
pub exit_broadcast_fee: Amount,
pub claim_fee: Amount,
pub fee_rate: FeeRate,
pub txs_to_broadcast: usize,
pub fundable: bool,
}
impl ExitFeeEstimate {
pub fn total(&self) -> Amount {
self.exit_broadcast_fee + self.claim_fee
}
}
impl Exit {
pub async fn estimate_emergency_exit_fee(
&self,
vtxos: &[VtxoId],
wallet: &Wallet,
fee_rate: Option<FeeRate>,
destination: Option<Address>,
) -> anyhow::Result<ExitFeeEstimate, ExitError> {
let (broadcast_fee_rate, claim_fee_rate) = match fee_rate {
Some(fr) => (fr, fr),
None => (
self.default_exit_fee_rate().await,
wallet.chain().fee_rates().await.regular,
),
};
let mut full_vtxos = HashMap::with_capacity(vtxos.len());
let mut seen = HashSet::new();
let mut unconfirmed_parents = Vec::new();
let mut pending_status = Vec::new();
{
let guard = self.inner.read().await;
for &vtxo_id in vtxos {
if full_vtxos.contains_key(&vtxo_id) {
continue;
}
let vtxo = wallet.inner.db.get_full_vtxo(vtxo_id).await
.map_err(|e| ExitError::InvalidWalletState { error: e.to_string() })?
.ok_or(ExitError::UnknownVtxo { vtxo: vtxo_id })?;
if let Err(error) = vtxo.check_standard() {
return Err(ExitError::NonStandardVtxo { vtxo: vtxo_id, error }.into());
}
match guard.exit_vtxos.iter().find(|ev| ev.id() == vtxo_id).map(|ev| ev.state()) {
Some(ExitState::Claimed(_)) => {
return Err(ExitError::VtxoAlreadyExited { vtxo: vtxo_id });
},
Some(ExitState::VtxoAlreadySpent(_)) => {
return Err(ExitError::VtxoAlreadySpent { vtxo: vtxo_id });
},
Some(ExitState::Processing(s)) => {
for exit_tx in &s.transactions {
let fees = match &exit_tx.status {
ExitTxStatus::Confirmed { .. } => continue,
ExitTxStatus::AwaitingConfirmation { .. } => {
match guard.tx_manager.get_child_status(exit_tx.txid).await {
Ok(Some(c)) => match c.fee_info {
Some(fi) if should_rbf(broadcast_fee_rate, fi.fee_rate) => {
MakeCpfpFees::Rbf {
min_effective_fee_rate: broadcast_fee_rate,
current_package_fee: fi.total_fee,
}
},
_ => continue,
},
_ => continue,
}
},
ExitTxStatus::VerifyInputs |
ExitTxStatus::AwaitingCpfpBroadcast |
ExitTxStatus::AwaitingInputConfirmation { .. } => {
MakeCpfpFees::Effective(broadcast_fee_rate)
},
};
if !seen.insert(exit_tx.txid) {
continue;
}
let package = guard.tx_manager.get_package(exit_tx.txid)?;
let tx = package.read().await.exit.tx.clone();
unconfirmed_parents.push((tx, fees));
}
},
Some(ExitState::AwaitingDelta(_)) |
Some(ExitState::Claimable(_)) |
Some(ExitState::ClaimInProgress(_)) => {},
Some(ExitState::Start(_)) | Some(ExitState::Canceled(_)) | None => {
for item in vtxo.transactions() {
pending_status.push(item.tx);
}
},
}
full_vtxos.insert(vtxo_id, vtxo);
}
}
for tx in pending_status {
let txid = tx.compute_txid();
if seen.contains(&txid) {
continue;
}
let mut guard = self.inner.write().await;
let status = guard.tx_manager.tx_status(txid).await
.map_err(|e| ExitError::TransactionRetrievalFailure { txid, error: e.to_string() })?;
let rbf = match status {
TxStatus::NotFound => {
MakeCpfpFees::Effective(broadcast_fee_rate)
},
TxStatus::Mempool => {
match guard.tx_manager.get_child_status(txid).await {
Ok(Some(c)) => c.fee_info.map(|f| MakeCpfpFees::Rbf {
min_effective_fee_rate: broadcast_fee_rate,
current_package_fee: f.total_fee,
}),
_ => None,
}.unwrap_or(MakeCpfpFees::Effective(broadcast_fee_rate))
},
TxStatus::Confirmed(_) => {
continue;
},
};
seen.insert(txid);
unconfirmed_parents.push((tx, rbf));
}
let txs_to_broadcast = unconfirmed_parents.len();
let (children, fundable) = match wallet.onchain() {
Some(onchain) => {
let walk = onchain.read().await
.estimate_p2a_cpfp_walk(&unconfirmed_parents)
.map_err(|e| ExitError::InternalError { error: e.to_string() })?;
let fundable = walk.shortfall.is_none();
(walk.children, fundable)
},
None => (vec![], false)
};
let mut exit_broadcast_fee = children.iter().map(|(_, fee)| *fee).sum::<Amount>();
for (parent, _) in unconfirmed_parents.iter().skip(children.len()) {
exit_broadcast_fee += broadcast_fee_rate * (parent.weight() + canonical_cpfp_child_weight());
}
let vtxos = full_vtxos.into_iter().map(|(_, vtxo)| vtxo).collect::<Vec<_>>();
let claim_fee = self.estimate_claim_fee(&vtxos, wallet, claim_fee_rate, destination).await?;
Ok(ExitFeeEstimate {
exit_broadcast_fee,
claim_fee,
fee_rate: broadcast_fee_rate,
txs_to_broadcast,
fundable,
})
}
async fn estimate_claim_fee(
&self,
vtxos: &[Vtxo<Full>],
wallet: &Wallet,
fee_rate: FeeRate,
destination: Option<Address>,
) -> anyhow::Result<Amount, ExitError> {
if vtxos.is_empty() {
return Ok(Amount::ZERO);
}
let address = match destination {
Some(a) => a,
None => placeholder_p2tr_address(wallet).await?,
};
let tip = wallet.chain().tip().await
.map_err(|e| ExitError::TipRetrievalFailure { error: e.to_string() })?;
let locktime = bitcoin::absolute::LockTime::from_height(tip)
.map_err(|e| ExitError::InvalidLocktime { tip, error: e.to_string() })?;
let mut output_amount = Amount::ZERO;
let mut tx_ins = Vec::with_capacity(vtxos.len());
for vtxo in vtxos {
let clause = wallet.find_signable_clause(vtxo).await
.ok_or(ExitError::ClaimMissingSignableClause { vtxo: vtxo.id() })?;
output_amount += vtxo.amount();
tx_ins.push(TxIn {
previous_output: vtxo.point(),
script_sig: ScriptBuf::default(),
sequence: clause.sequence().unwrap_or(Sequence::ZERO),
witness: Witness::new(),
});
}
let mut tx = Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: locktime,
input: tx_ins,
output: vec![TxOut { script_pubkey: address.script_pubkey(), value: output_amount }],
};
let prevouts = vtxos.iter().map(|v| v.txout()).collect::<Vec<_>>();
let prevouts = sighash::Prevouts::All(&prevouts);
let mut witnesses = Vec::with_capacity(vtxos.len());
{
let mut shc = sighash::SighashCache::new(&tx);
for (i, vtxo) in vtxos.iter().enumerate() {
let witness = wallet.sign_input(vtxo, i, &mut shc, &prevouts).await
.map_err(|e| ExitError::ClaimSigningError { error: e.to_string() })?;
witnesses.push(witness);
}
}
for (input, witness) in tx.input.iter_mut().zip(witnesses) {
input.witness = witness;
}
Ok(fee_rate * tx.weight())
}
}
fn canonical_cpfp_child_weight() -> Weight {
const P2TR_SPK_LEN: usize = 34;
predict_weight(
[
InputWeightPrediction::new(0, [0usize; 0]),
InputWeightPrediction::P2TR_KEY_DEFAULT_SIGHASH,
],
[P2TR_SPK_LEN],
)
}
async fn placeholder_p2tr_address(wallet: &Wallet) -> anyhow::Result<Address, ExitError> {
let network = wallet.network().await
.map_err(|e| ExitError::InternalError { error: e.to_string() })?;
let secp = Secp256k1::new();
let sk = SecretKey::from_slice(&[1u8; 32]).expect("valid secret key");
let (xonly, _) = sk.public_key(&secp).x_only_public_key();
Ok(Address::p2tr(&secp, xonly, None, network))
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn canonical_child_weight_is_plausible() {
let w = canonical_cpfp_child_weight();
assert!(w > Weight::from_vb_unchecked(90), "child weight too small: {}", w);
assert!(w < Weight::from_vb_unchecked(200), "child weight too large: {}", w);
}
}