From a38c1739b6d6bdc5944e2300f1cdf9121babf7d1 Mon Sep 17 00:00:00 2001 From: jp1ac4 <121959000+jp1ac4@users.noreply.github.com> Date: Thu, 18 Jan 2024 11:45:53 +0000 Subject: [PATCH] spend: return additional fee paid for ancestors --- src/spend.rs | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/spend.rs b/src/spend.rs index 4f2ee7b6..001085e6 100644 --- a/src/spend.rs +++ b/src/spend.rs @@ -1,6 +1,10 @@ use crate::{bitcoin::MempoolEntry, descriptors}; -use std::{collections::BTreeMap, convert::TryInto, fmt}; +use std::{ + collections::{BTreeMap, HashMap}, + convert::TryInto, + fmt, +}; pub use bdk_coin_select::InsufficientFunds; use bdk_coin_select::{ @@ -210,6 +214,8 @@ pub struct CoinSelectionRes { /// Maximum change amount possible with the selection irrespective of any change /// policy. pub max_change_amount: bitcoin::Amount, + /// Fee added to pay for ancestors at the target feerate. + pub fee_for_ancestors: bitcoin::Amount, } /// Metric based on [`LowestFee`] that aims to minimize transaction fees @@ -324,6 +330,8 @@ fn select_coins_for_spend( let witness_factor: u32 = WITNESS_SCALE_FACTOR .try_into() .expect("scale factor must fit in u32"); + // This will be used to store any extra weight added to candidates. + let mut added_weights = HashMap::::with_capacity(candidate_coins.len()); let candidates: Vec = candidate_coins .iter() .map(|cand| Candidate { @@ -350,6 +358,9 @@ fn select_coins_for_spend( .expect("weight difference must fit in u32") }) .unwrap_or(0); + // Store the extra weight for this candidate for use later on. + // At the same time, make sure there are no duplicate outpoints. + assert!(added_weights.insert(cand.outpoint, extra).is_none()); max_input_weight .checked_add(extra) .expect("effective weight must fit in u32") @@ -393,9 +404,10 @@ fn select_coins_for_spend( // Finally, run the coin selection algorithm. We use an opportunistic BnB and if it couldn't // find any solution we fall back to selecting coins by descending value. + let feerate = FeeRate::from_sat_per_vb(feerate_vb); let target = Target { value: out_value_nochange, - feerate: FeeRate::from_sat_per_vb(feerate_vb), + feerate, min_fee, }; let lowest_fee = LowestFee { @@ -459,14 +471,29 @@ fn select_coins_for_spend( .try_into() .expect("value is non-negative"), ); + let mut total_added_weight: u32 = 0; + let selected = selector + .selected_indices() + .iter() + .map(|i| candidate_coins[*i]) + .inspect(|cand| { + total_added_weight = total_added_weight + .checked_add( + *added_weights + .get(&cand.outpoint) + .expect("contains added weight for all candidates"), + ) + .expect("should fit in u32") + }) + .collect(); + // Calculate added fee based on the feerate in sats/wu, which is the feerate used for coin selection. + let fee_for_ancestors = + bitcoin::Amount::from_sat(((total_added_weight as f32) * feerate.spwu()).ceil() as u64); Ok(CoinSelectionRes { - selected: selector - .selected_indices() - .iter() - .map(|i| candidate_coins[*i]) - .collect(), + selected, change_amount, max_change_amount, + fee_for_ancestors, }) } @@ -646,6 +673,7 @@ pub fn create_spend( selected, change_amount, max_change_amount, + .. } = { // At this point the transaction still has no input and no change output, as expected // by the coins selection helper function.