use std::borrow::BorrowMut;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use bdk_wallet::{AddressInfo, TxBuilder, Wallet};
use bdk_wallet::chain::{BlockId, CanonicalizationParams, ChainPosition, ConfirmationBlockTime};
use bdk_wallet::coin_selection::InsufficientFunds;
use bdk_wallet::error::CreateTxError;
use bitcoin::consensus::encode::serialize_hex;
use bitcoin::{Amount, BlockHash, FeeRate, OutPoint, Transaction, TxOut, Txid, Weight, Witness};
use bitcoin::psbt::{ExtractTxError, Input};
use log::{debug, trace};
use crate::TransactionExt;
use crate::cpfp::MakeCpfpFees;
use crate::fee::FEE_ANCHOR_SPEND_WEIGHT;
#[derive(Debug, Clone)]
pub struct LocalTransaction {
pub tx: Arc<Transaction>,
pub chain_position: ChainPosition<ConfirmationBlockTime>,
pub is_trusted: bool,
}
pub struct TrustedUtxo<'a> {
pub outpoint: OutPoint,
pub txout: &'a TxOut,
pub chain_position: &'a ChainPosition<ConfirmationBlockTime>,
pub is_trusted: bool,
}
pub struct TrustedCanonicalization {
txs: HashMap<Txid, LocalTransaction>,
unspent: Vec<OutPoint>,
}
impl TrustedCanonicalization {
pub fn from_wallet(w: &Wallet, min_confs: u32) -> Self {
let tip = w.latest_checkpoint().height();
let chain = w.local_chain();
let chain_tip = w.latest_checkpoint().block_id();
let mut txs: HashMap<Txid, LocalTransaction> = HashMap::new();
let mut spent: HashSet<OutPoint> = HashSet::new();
for ctx in w.tx_graph().list_ordered_canonical_txs(
chain, chain_tip, CanonicalizationParams::default(),
) {
let txid = ctx.tx_node.txid;
let tx = ctx.tx_node.tx.clone();
let chain_position = ctx.chain_position.clone();
for input in tx.input.iter() {
spent.insert(input.previous_output);
}
let nb_confs = match chain_position.confirmation_height_upper_bound() {
Some(h) => tip.saturating_sub(h) + 1,
None => 0,
};
let is_trusted = nb_confs >= min_confs || tx.input.iter().all(|input| {
let prev = input.previous_output;
let Some(prev_entry) = txs.get(&prev.txid) else { return false };
let Some(prev_out) = prev_entry.tx.output.get(prev.vout as usize) else { return false };
w.is_mine(prev_out.script_pubkey.clone()) && prev_entry.is_trusted
});
txs.insert(txid, LocalTransaction { tx, chain_position, is_trusted });
}
let unspent = w.spk_index().outpoints().iter()
.map(|(_, op)| *op)
.filter(|op| !spent.contains(op))
.filter(|op| txs.contains_key(&op.txid))
.collect();
Self { txs, unspent }
}
pub fn is_trusted(&self, txid: Txid) -> bool {
self.txs.get(&txid).map(|e| e.is_trusted).unwrap_or(false)
}
pub fn list_unspent(&self) -> impl Iterator<Item = TrustedUtxo<'_>> + '_ {
self.unspent.iter().map(move |op| {
let lt = &self.txs[&op.txid];
TrustedUtxo {
outpoint: *op,
txout: <.tx.output[op.vout as usize],
chain_position: <.chain_position,
is_trusted: lt.is_trusted,
}
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct TrustedBalance {
pub trusted: Amount,
pub untrusted: Amount,
}
impl TrustedBalance {
pub fn total(&self) -> Amount {
self.trusted + self.untrusted
}
}
pub const KEYCHAIN: bdk_wallet::KeychainKind = bdk_wallet::KeychainKind::External;
pub trait TxBuilderExt<'a, A>: BorrowMut<TxBuilder<'a, A>> {
fn add_fee_anchor_spend(&mut self, anchor: OutPoint, output: &TxOut)
where
A: bdk_wallet::coin_selection::CoinSelectionAlgorithm,
{
let psbt_in = Input {
witness_utxo: Some(output.clone()),
final_script_witness: Some(Witness::new()),
..Default::default()
};
self.borrow_mut().add_foreign_utxo(anchor, psbt_in, FEE_ANCHOR_SPEND_WEIGHT)
.expect("adding foreign utxo");
}
}
impl<'a, A> TxBuilderExt<'a, A> for TxBuilder<'a, A> {}
#[derive(Debug, thiserror::Error)]
pub enum CpfpInternalError {
#[error("{0}")]
General(String),
#[error("Unable to construct transaction: {0}")]
Create(CreateTxError),
#[error("Unable to extract the final transaction after signing the PSBT: {0}")]
Extract(ExtractTxError),
#[error("Failed to determine the weight/fee when creating a P2A CPFP")]
Fee(),
#[error("Unable to finalize CPFP transaction: {0}")]
FinalizeError(String),
#[error("You need more confirmations on your on-chain funds: {0}")]
InsufficientConfirmedFunds(InsufficientFunds),
#[error("Transaction has no fee anchor: {0}")]
NoFeeAnchor(Txid),
#[allow(deprecated)]
#[error("Unable to sign transaction: {0}")]
Signer(bdk_wallet::signer::SignerError),
}
pub trait WalletExt: BorrowMut<Wallet> {
fn peek_next_address(&self) -> AddressInfo {
self.borrow().peek_address(KEYCHAIN, self.borrow().next_derivation_index(KEYCHAIN))
}
fn unconfirmed_txids(&self) -> impl Iterator<Item = Txid> {
self.borrow().transactions().filter_map(|tx| {
if tx.chain_position.is_unconfirmed() {
Some(tx.tx_node.txid)
} else {
None
}
})
}
fn unconfirmed_txs(&self) -> impl Iterator<Item = Arc<Transaction>> {
self.borrow().transactions().filter_map(|tx| {
if tx.chain_position.is_unconfirmed() {
Some(tx.tx_node.tx.clone())
} else {
None
}
})
}
fn trusted_balance(&self, min_confs: u32) -> TrustedBalance {
let canon = TrustedCanonicalization::from_wallet(self.borrow(), min_confs);
let mut trusted = Amount::ZERO;
let mut untrusted = Amount::ZERO;
for utxo in canon.list_unspent() {
if utxo.is_trusted {
trusted += utxo.txout.value;
} else {
untrusted += utxo.txout.value;
}
}
TrustedBalance { trusted, untrusted }
}
fn untrusted_utxos(&self, min_confs: u32) -> Vec<OutPoint> {
TrustedCanonicalization::from_wallet(self.borrow(), min_confs)
.list_unspent()
.filter(|u| !u.is_trusted)
.map(|u| u.outpoint)
.collect()
}
fn is_fully_owned_tx(&self, txid: Txid) -> bool {
let wallet = self.borrow();
let graph = wallet.tx_graph();
match graph.get_tx(txid) {
Some(tx) => {
tx.input.iter().all(|input| {
let prev = input.previous_output;
graph.get_tx(prev.txid)
.and_then(|prev_tx| prev_tx.output.get(prev.vout as usize).cloned())
.map(|out| wallet.is_mine(out.script_pubkey))
.unwrap_or(false)
})
}, None => false
}
}
fn set_checkpoint(&mut self, height: u32, hash: BlockHash) {
let checkpoint = BlockId { height, hash };
let wallet = self.borrow_mut();
wallet.apply_update(bdk_wallet::Update {
chain: Some(wallet.latest_checkpoint().insert(checkpoint)),
..Default::default()
}).expect("should work, might fail if tip is genesis");
}
fn mark_output_keys_unused(&mut self, tx: &Transaction) {
let wallet = self.borrow_mut();
for txout in &tx.output {
if let Some((keychain, index)) = wallet.spk_index().index_of_spk(txout.script_pubkey.clone()) {
wallet.unmark_used(*keychain, *index);
}
}
}
fn make_signed_p2a_cpfp(
&mut self,
tx: &Transaction,
fees: MakeCpfpFees,
) -> Result<Transaction, CpfpInternalError> {
let wallet = self.borrow_mut();
let (fee_anchor_point, fee_anchor_txout) = tx.fee_anchor()
.ok_or_else(|| CpfpInternalError::NoFeeAnchor(tx.compute_txid()))?;
let parent_weight = tx.weight();
let extra_fee_needed = parent_weight * fees.effective();
let change_addr = wallet.next_unused_address(KEYCHAIN);
let dust_limit = change_addr.address.script_pubkey().minimal_non_dust();
let mut final_child_weight = Weight::ZERO;
let mut fee_needed = extra_fee_needed;
for i in 0..100 {
if fee_needed < fee_anchor_txout.value {
if fee_anchor_txout.value - fee_needed < dust_limit {
fee_needed = fee_anchor_txout.value + Amount::ONE_SAT;
}
}
let mut b = wallet.build_tx();
b.only_witness_utxo();
b.exclude_unconfirmed();
b.version(3); b.add_fee_anchor_spend(fee_anchor_point, fee_anchor_txout);
b.drain_to(change_addr.address.script_pubkey());
b.fee_absolute(fee_needed);
let mut psbt = b.finish().map_err(|e| match e {
CreateTxError::CoinSelection(e) => CpfpInternalError::InsufficientConfirmedFunds(e),
_ => CpfpInternalError::Create(e),
})?;
#[allow(deprecated)]
let opts = bdk_wallet::SignOptions {
trust_witness_utxo: true,
..Default::default()
};
let finalized = wallet.sign(&mut psbt, opts)
.map_err(|e| CpfpInternalError::Signer(e))?;
if !finalized {
return Err(CpfpInternalError::FinalizeError("finalization failed".into()));
}
let tx = psbt.extract_tx()
.map_err(|e| CpfpInternalError::Extract(e))?;
assert!(tx.input.iter().any(|i| i.previous_output == fee_anchor_point),
"Missing anchor spend, tx is {}", serialize_hex(&tx),
);
let tx_weight = tx.weight();
let total_weight = tx_weight + parent_weight;
if tx_weight != final_child_weight {
wallet.mark_output_keys_unused(&tx);
final_child_weight = tx_weight;
fee_needed = match fees {
MakeCpfpFees::Effective(fr) => total_weight * fr,
MakeCpfpFees::Rbf { min_effective_fee_rate, current_package_fee } => {
let min_tx_relay_fee = FeeRate::from_sat_per_vb(1).unwrap();
let min_package_fee = current_package_fee +
parent_weight * min_tx_relay_fee +
tx_weight * min_tx_relay_fee;
let desired_fee = total_weight * min_effective_fee_rate;
if desired_fee < min_package_fee {
debug!("Using a minimum fee of {} instead of the desired fee of {} for RBF",
min_package_fee, desired_fee,
);
min_package_fee
} else {
trace!("Attempting to use the desired fee of {} for CPFP RBF",
desired_fee,
);
desired_fee
}
}
}
} else {
debug!("Created P2A CPFP with weight {} and fee {} in {} iterations",
total_weight, fee_needed, i,
);
return Ok(tx);
}
}
Err(CpfpInternalError::General("Reached max iterations".into()))
}
}
impl WalletExt for Wallet {}