From 641718b19d5a95d207e1ec64f8f1f07b3d968db5 Mon Sep 17 00:00:00 2001 From: Cesar Alvarez Vallero <46329881+csralvall@users.noreply.github.com> Date: Sun, 10 Jul 2022 19:57:44 -0300 Subject: [PATCH] Consolidate `fee_amount` and `amount_needed` Before this commit `fee_amount` and `amount_needed` were passed as independent parameters. From the perspective of coin selection algorithms, they are always used jointly for the same purpose, to create a coin selection with a total effective value greater than it's summed values. This commit removes the abstraction that the use of the two parameter introduced by consolidating both into a single parameter, `target_amount`, who carries their values added up. Resolves: #641 --- CHANGELOG.md | 1 + src/wallet/coin_selection.rs | 209 ++++++++++++++++------------------- src/wallet/mod.rs | 6 +- 3 files changed, 97 insertions(+), 119 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4982ae27c..cc61c85d8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix hang when `ElectrumBlockchainConfig::stop_gap` is zero. - Set coin type in BIP44, BIP49, and BIP84 templates - Get block hash given a block height - A `get_block_hash` method is now defined on the `GetBlockHash` trait and implemented on every blockchain backend. This method expects a block height and returns the corresponding block hash. +- Consolidate params `fee_amount` and `amount_needed` in `target_amount` in `CoinSelectionAlgorithm::coin_select` signature. ## [v0.19.0] - [v0.18.0] diff --git a/src/wallet/coin_selection.rs b/src/wallet/coin_selection.rs index 7b4a48334a..c0b65b7e3a 100644 --- a/src/wallet/coin_selection.rs +++ b/src/wallet/coin_selection.rs @@ -40,8 +40,7 @@ //! required_utxos: Vec, //! optional_utxos: Vec, //! fee_rate: FeeRate, -//! amount_needed: u64, -//! fee_amount: u64, +//! target_amount: u64, //! ) -> Result { //! let mut selected_amount = 0; //! let mut additional_weight = 0; @@ -58,7 +57,7 @@ //! ) //! .collect::>(); //! let additional_fees = fee_rate.fee_wu(additional_weight); -//! let amount_needed_with_fees = (fee_amount + additional_fees) + amount_needed; +//! let amount_needed_with_fees = additional_fees + target_amount; //! if amount_needed_with_fees > selected_amount { //! return Err(bdk::Error::InsufficientFunds { //! needed: amount_needed_with_fees, @@ -68,7 +67,7 @@ //! //! Ok(CoinSelectionResult { //! selected: all_utxos_selected, -//! fee_amount: fee_amount + additional_fees, +//! fee_amount: additional_fees, //! }) //! } //! } @@ -116,7 +115,7 @@ pub(crate) const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4 + 1) * 4; pub struct CoinSelectionResult { /// List of outputs selected for use as inputs pub selected: Vec, - /// Total fee amount in satoshi + /// Total fee amount for the selected utxos in satoshis pub fee_amount: u64, } @@ -149,22 +148,20 @@ pub trait CoinSelectionAlgorithm: std::fmt::Debug { /// /// - `database`: a reference to the wallet's database that can be used to lookup additional /// details for a specific UTXO - /// - `required_utxos`: the utxos that must be spent regardless of `amount_needed` with their + /// - `required_utxos`: the utxos that must be spent regardless of `target_amount` with their /// weight cost - /// - `optional_utxos`: the remaining available utxos to satisfy `amount_needed` with their + /// - `optional_utxos`: the remaining available utxos to satisfy `target_amount` with their /// weight cost /// - `fee_rate`: fee rate to use - /// - `amount_needed`: the amount in satoshi to select - /// - `fee_amount`: the amount of fees in satoshi already accumulated from adding outputs and - /// the transaction's header + /// - `target_amount`: the outgoing amount in satoshis and the fees already + /// accumulated from added outputs and transaction’s header. fn coin_select( &self, database: &D, required_utxos: Vec, optional_utxos: Vec, fee_rate: FeeRate, - amount_needed: u64, - fee_amount: u64, + target_amount: u64, ) -> Result; } @@ -182,13 +179,11 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection { required_utxos: Vec, mut optional_utxos: Vec, fee_rate: FeeRate, - amount_needed: u64, - fee_amount: u64, + target_amount: u64, ) -> Result { log::debug!( - "amount_needed = `{}`, fee_amount = `{}`, fee_rate = `{:?}`", - amount_needed, - fee_amount, + "target_amount = `{}`, fee_rate = `{:?}`", + target_amount, fee_rate ); @@ -202,7 +197,7 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection { .chain(optional_utxos.into_iter().rev().map(|utxo| (false, utxo))) }; - select_sorted_utxos(utxos, fee_rate, amount_needed, fee_amount) + select_sorted_utxos(utxos, fee_rate, target_amount) } } @@ -220,8 +215,7 @@ impl CoinSelectionAlgorithm for OldestFirstCoinSelection { required_utxos: Vec, mut optional_utxos: Vec, fee_rate: FeeRate, - amount_needed: u64, - fee_amount: u64, + target_amount: u64, ) -> Result { // query db and create a blockheight lookup table let blockheights = optional_utxos @@ -261,22 +255,22 @@ impl CoinSelectionAlgorithm for OldestFirstCoinSelection { .chain(optional_utxos.into_iter().map(|utxo| (false, utxo))) }; - select_sorted_utxos(utxos, fee_rate, amount_needed, fee_amount) + select_sorted_utxos(utxos, fee_rate, target_amount) } } fn select_sorted_utxos( utxos: impl Iterator, fee_rate: FeeRate, - amount_needed: u64, - mut fee_amount: u64, + target_amount: u64, ) -> Result { let mut selected_amount = 0; + let mut fee_amount = 0; let selected = utxos .scan( (&mut selected_amount, &mut fee_amount), |(selected_amount, fee_amount), (must_use, weighted_utxo)| { - if must_use || **selected_amount < amount_needed + **fee_amount { + if must_use || **selected_amount < target_amount + **fee_amount { **fee_amount += fee_rate.fee_wu(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight); **selected_amount += weighted_utxo.utxo.txout().value; @@ -295,7 +289,7 @@ fn select_sorted_utxos( ) .collect::>(); - let amount_needed_with_fees = amount_needed + fee_amount; + let amount_needed_with_fees = target_amount + fee_amount; if selected_amount < amount_needed_with_fees { return Err(Error::InsufficientFunds { needed: amount_needed_with_fees, @@ -364,8 +358,7 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { required_utxos: Vec, optional_utxos: Vec, fee_rate: FeeRate, - amount_needed: u64, - fee_amount: u64, + target_amount: u64, ) -> Result { // Mapping every (UTXO, usize) to an output group let required_utxos: Vec = required_utxos @@ -387,7 +380,6 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { .iter() .fold(0, |acc, x| acc + x.effective_value); - let actual_target = fee_amount + amount_needed; let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_vb(); let expected = (curr_available_value + curr_value) @@ -396,22 +388,21 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { Error::Generic("Sum of UTXO spendable values does not fit into u64".to_string()) })?; - if expected < actual_target { + if expected < target_amount { return Err(Error::InsufficientFunds { - needed: actual_target, + needed: target_amount, available: expected, }); } - let actual_target = actual_target + let target_amount = target_amount .try_into() .expect("Bitcoin amount to fit into i64"); - if curr_value > actual_target { + if curr_value > target_amount { return Ok(BranchAndBoundCoinSelection::calculate_cs_result( vec![], required_utxos, - fee_amount, )); } @@ -421,18 +412,11 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { optional_utxos.clone(), curr_value, curr_available_value, - actual_target, - fee_amount, + target_amount, cost_of_change, ) .unwrap_or_else(|_| { - self.single_random_draw( - required_utxos, - optional_utxos, - curr_value, - actual_target, - fee_amount, - ) + self.single_random_draw(required_utxos, optional_utxos, curr_value, target_amount) })) } } @@ -447,8 +431,7 @@ impl BranchAndBoundCoinSelection { mut optional_utxos: Vec, mut curr_value: i64, mut curr_available_value: i64, - actual_target: i64, - fee_amount: u64, + target_amount: i64, cost_of_change: f32, ) -> Result { // current_selection[i] will contain true if we are using optional_utxos[i], @@ -472,11 +455,11 @@ impl BranchAndBoundCoinSelection { // Cannot possibly reach target with the amount remaining in the curr_available_value, // or the selected value is out of range. // Go back and try other branch - if curr_value + curr_available_value < actual_target - || curr_value > actual_target + cost_of_change as i64 + if curr_value + curr_available_value < target_amount + || curr_value > target_amount + cost_of_change as i64 { backtrack = true; - } else if curr_value >= actual_target { + } else if curr_value >= target_amount { // Selected value is within range, there's no point in going forward. Start // backtracking backtrack = true; @@ -489,7 +472,7 @@ impl BranchAndBoundCoinSelection { } // If we found a perfect match, break here - if curr_value == actual_target { + if curr_value == target_amount { break; } } @@ -546,7 +529,6 @@ impl BranchAndBoundCoinSelection { Ok(BranchAndBoundCoinSelection::calculate_cs_result( selected_utxos, required_utxos, - fee_amount, )) } @@ -555,8 +537,7 @@ impl BranchAndBoundCoinSelection { required_utxos: Vec, mut optional_utxos: Vec, curr_value: i64, - actual_target: i64, - fee_amount: u64, + target_amount: i64, ) -> CoinSelectionResult { #[cfg(not(test))] optional_utxos.shuffle(&mut thread_rng()); @@ -570,7 +551,7 @@ impl BranchAndBoundCoinSelection { let selected_utxos = optional_utxos .into_iter() .scan(curr_value, |curr_value, utxo| { - if *curr_value >= actual_target { + if *curr_value >= target_amount { None } else { *curr_value += utxo.effective_value; @@ -579,16 +560,15 @@ impl BranchAndBoundCoinSelection { }) .collect::>(); - BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos, required_utxos, fee_amount) + BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos, required_utxos) } fn calculate_cs_result( mut selected_utxos: Vec, mut required_utxos: Vec, - mut fee_amount: u64, ) -> CoinSelectionResult { selected_utxos.append(&mut required_utxos); - fee_amount += selected_utxos.iter().map(|u| u.fee).sum::(); + let fee_amount = selected_utxos.iter().map(|u| u.fee).sum::(); let selected = selected_utxos .into_iter() .map(|u| u.weighted_utxo.utxo) @@ -758,6 +738,7 @@ mod test { fn test_largest_first_coin_selection_success() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let target_amount = 250_000 + FEE_AMOUNT; let result = LargestFirstCoinSelection::default() .coin_select( @@ -765,20 +746,20 @@ mod test { utxos, vec![], FeeRate::from_sat_per_vb(1.0), - 250_000, - FEE_AMOUNT, + target_amount, ) .unwrap(); assert_eq!(result.selected.len(), 3); assert_eq!(result.selected_amount(), 300_010); - assert_eq!(result.fee_amount, 254) + assert_eq!(result.fee_amount, 204) } #[test] fn test_largest_first_coin_selection_use_all() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let target_amount = 20_000 + FEE_AMOUNT; let result = LargestFirstCoinSelection::default() .coin_select( @@ -786,20 +767,20 @@ mod test { utxos, vec![], FeeRate::from_sat_per_vb(1.0), - 20_000, - FEE_AMOUNT, + target_amount, ) .unwrap(); assert_eq!(result.selected.len(), 3); assert_eq!(result.selected_amount(), 300_010); - assert_eq!(result.fee_amount, 254); + assert_eq!(result.fee_amount, 204); } #[test] fn test_largest_first_coin_selection_use_only_necessary() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let target_amount = 20_000 + FEE_AMOUNT; let result = LargestFirstCoinSelection::default() .coin_select( @@ -807,14 +788,13 @@ mod test { vec![], utxos, FeeRate::from_sat_per_vb(1.0), - 20_000, - FEE_AMOUNT, + target_amount, ) .unwrap(); assert_eq!(result.selected.len(), 1); assert_eq!(result.selected_amount(), 200_000); - assert_eq!(result.fee_amount, 118); + assert_eq!(result.fee_amount, 68); } #[test] @@ -822,6 +802,7 @@ mod test { fn test_largest_first_coin_selection_insufficient_funds() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let target_amount = 500_000 + FEE_AMOUNT; LargestFirstCoinSelection::default() .coin_select( @@ -829,8 +810,7 @@ mod test { vec![], utxos, FeeRate::from_sat_per_vb(1.0), - 500_000, - FEE_AMOUNT, + target_amount, ) .unwrap(); } @@ -840,6 +820,7 @@ mod test { fn test_largest_first_coin_selection_insufficient_funds_high_fees() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let target_amount = 250_000 + FEE_AMOUNT; LargestFirstCoinSelection::default() .coin_select( @@ -847,8 +828,7 @@ mod test { vec![], utxos, FeeRate::from_sat_per_vb(1000.0), - 250_000, - FEE_AMOUNT, + target_amount, ) .unwrap(); } @@ -857,6 +837,7 @@ mod test { fn test_oldest_first_coin_selection_success() { let mut database = MemoryDatabase::default(); let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database); + let target_amount = 180_000 + FEE_AMOUNT; let result = OldestFirstCoinSelection::default() .coin_select( @@ -864,14 +845,13 @@ mod test { vec![], utxos, FeeRate::from_sat_per_vb(1.0), - 180_000, - FEE_AMOUNT, + target_amount, ) .unwrap(); assert_eq!(result.selected.len(), 2); assert_eq!(result.selected_amount(), 200_000); - assert_eq!(result.fee_amount, 186) + assert_eq!(result.fee_amount, 136) } #[test] @@ -914,26 +894,28 @@ mod test { database.set_tx(&utxo1_tx_details).unwrap(); database.set_tx(&utxo2_tx_details).unwrap(); + let target_amount = 180_000 + FEE_AMOUNT; + let result = OldestFirstCoinSelection::default() .coin_select( &database, vec![], vec![utxo3, utxo1, utxo2], FeeRate::from_sat_per_vb(1.0), - 180_000, - FEE_AMOUNT, + target_amount, ) .unwrap(); assert_eq!(result.selected.len(), 2); assert_eq!(result.selected_amount(), 200_000); - assert_eq!(result.fee_amount, 186) + assert_eq!(result.fee_amount, 136) } #[test] fn test_oldest_first_coin_selection_use_all() { let mut database = MemoryDatabase::default(); let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database); + let target_amount = 20_000 + FEE_AMOUNT; let result = OldestFirstCoinSelection::default() .coin_select( @@ -941,20 +923,20 @@ mod test { utxos, vec![], FeeRate::from_sat_per_vb(1.0), - 20_000, - FEE_AMOUNT, + target_amount, ) .unwrap(); assert_eq!(result.selected.len(), 3); assert_eq!(result.selected_amount(), 500_000); - assert_eq!(result.fee_amount, 254); + assert_eq!(result.fee_amount, 204); } #[test] fn test_oldest_first_coin_selection_use_only_necessary() { let mut database = MemoryDatabase::default(); let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database); + let target_amount = 20_000 + FEE_AMOUNT; let result = OldestFirstCoinSelection::default() .coin_select( @@ -962,14 +944,13 @@ mod test { vec![], utxos, FeeRate::from_sat_per_vb(1.0), - 20_000, - FEE_AMOUNT, + target_amount, ) .unwrap(); assert_eq!(result.selected.len(), 1); assert_eq!(result.selected_amount(), 120_000); - assert_eq!(result.fee_amount, 118); + assert_eq!(result.fee_amount, 68); } #[test] @@ -977,6 +958,7 @@ mod test { fn test_oldest_first_coin_selection_insufficient_funds() { let mut database = MemoryDatabase::default(); let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database); + let target_amount = 600_000 + FEE_AMOUNT; OldestFirstCoinSelection::default() .coin_select( @@ -984,8 +966,7 @@ mod test { vec![], utxos, FeeRate::from_sat_per_vb(1.0), - 600_000, - FEE_AMOUNT, + target_amount, ) .unwrap(); } @@ -996,8 +977,7 @@ mod test { let mut database = MemoryDatabase::default(); let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database); - let amount_needed: u64 = - utxos.iter().map(|wu| wu.utxo.txout().value).sum::() - (FEE_AMOUNT + 50); + let target_amount: u64 = utxos.iter().map(|wu| wu.utxo.txout().value).sum::() - 50; OldestFirstCoinSelection::default() .coin_select( @@ -1005,8 +985,7 @@ mod test { vec![], utxos, FeeRate::from_sat_per_vb(1000.0), - amount_needed, - FEE_AMOUNT, + target_amount, ) .unwrap(); } @@ -1019,26 +998,28 @@ mod test { let database = MemoryDatabase::default(); + let target_amount = 250_000 + FEE_AMOUNT; + let result = BranchAndBoundCoinSelection::default() .coin_select( &database, vec![], utxos, FeeRate::from_sat_per_vb(1.0), - 250_000, - FEE_AMOUNT, + target_amount, ) .unwrap(); assert_eq!(result.selected.len(), 3); assert_eq!(result.selected_amount(), 300_000); - assert_eq!(result.fee_amount, 254); + assert_eq!(result.fee_amount, 204); } #[test] fn test_bnb_coin_selection_required_are_enough() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let target_amount = 20_000 + FEE_AMOUNT; let result = BranchAndBoundCoinSelection::default() .coin_select( @@ -1046,20 +1027,20 @@ mod test { utxos.clone(), utxos, FeeRate::from_sat_per_vb(1.0), - 20_000, - FEE_AMOUNT, + target_amount, ) .unwrap(); assert_eq!(result.selected.len(), 3); assert_eq!(result.selected_amount(), 300_010); - assert_eq!(result.fee_amount, 254); + assert_eq!(result.fee_amount, 204); } #[test] fn test_bnb_coin_selection_optional_are_enough() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let target_amount = 299756 + FEE_AMOUNT; let result = BranchAndBoundCoinSelection::default() .coin_select( @@ -1067,14 +1048,13 @@ mod test { vec![], utxos, FeeRate::from_sat_per_vb(1.0), - 299756, - FEE_AMOUNT, + target_amount, ) .unwrap(); assert_eq!(result.selected.len(), 3); assert_eq!(result.selected_amount(), 300010); - assert_eq!(result.fee_amount, 254); + assert_eq!(result.fee_amount, 204); } #[test] @@ -1092,20 +1072,21 @@ mod test { let amount: u64 = optional.iter().map(|u| u.utxo.txout().value).sum(); assert!(amount > 150_000); + let target_amount = 150_000 + FEE_AMOUNT; + let result = BranchAndBoundCoinSelection::default() .coin_select( &database, required, optional, FeeRate::from_sat_per_vb(1.0), - 150_000, - FEE_AMOUNT, + target_amount, ) .unwrap(); assert_eq!(result.selected.len(), 3); assert_eq!(result.selected_amount(), 300_010); - assert!((result.fee_amount as f32 - 254.0).abs() < f32::EPSILON); + assert!((result.fee_amount as f32 - 204.0).abs() < f32::EPSILON); } #[test] @@ -1113,6 +1094,7 @@ mod test { fn test_bnb_coin_selection_insufficient_funds() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let target_amount = 500_000 + FEE_AMOUNT; BranchAndBoundCoinSelection::default() .coin_select( @@ -1120,8 +1102,7 @@ mod test { vec![], utxos, FeeRate::from_sat_per_vb(1.0), - 500_000, - FEE_AMOUNT, + target_amount, ) .unwrap(); } @@ -1131,6 +1112,7 @@ mod test { fn test_bnb_coin_selection_insufficient_funds_high_fees() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let target_amount = 250_000 + FEE_AMOUNT; BranchAndBoundCoinSelection::default() .coin_select( @@ -1138,8 +1120,7 @@ mod test { vec![], utxos, FeeRate::from_sat_per_vb(1000.0), - 250_000, - FEE_AMOUNT, + target_amount, ) .unwrap(); } @@ -1148,6 +1129,7 @@ mod test { fn test_bnb_coin_selection_check_fee_rate() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let target_amount = 99932; // first utxo's effective value let result = BranchAndBoundCoinSelection::new(0) .coin_select( @@ -1155,16 +1137,15 @@ mod test { vec![], utxos, FeeRate::from_sat_per_vb(1.0), - 99932, // first utxo's effective value - 0, + target_amount, ) .unwrap(); assert_eq!(result.selected.len(), 1); assert_eq!(result.selected_amount(), 100_000); let input_size = (TXIN_BASE_WEIGHT + P2WPKH_WITNESS_SIZE).vbytes(); - let epsilon = 0.5; - assert!((1.0 - (result.fee_amount as f32 / input_size as f32)).abs() < epsilon); + // the final fee rate should be exactly the same that the fee rate given + assert_eq!((result.fee_amount as f32 / input_size as f32), 1.0); } #[test] @@ -1183,7 +1164,6 @@ mod test { optional_utxos, FeeRate::from_sat_per_vb(0.0), target_amount, - 0, ) .unwrap(); assert_eq!(result.selected_amount(), target_amount); @@ -1203,14 +1183,14 @@ mod test { let size_of_change = 31; let cost_of_change = size_of_change as f32 * fee_rate.as_sat_vb(); + let target_amount = 20_000 + FEE_AMOUNT; BranchAndBoundCoinSelection::new(size_of_change) .bnb( vec![], utxos, 0, curr_available_value, - 20_000, - FEE_AMOUNT, + target_amount as i64, cost_of_change, ) .unwrap(); @@ -1229,6 +1209,7 @@ mod test { let size_of_change = 31; let cost_of_change = size_of_change as f32 * fee_rate.as_sat_vb(); + let target_amount = 20_000 + FEE_AMOUNT; BranchAndBoundCoinSelection::new(size_of_change) .bnb( @@ -1236,8 +1217,7 @@ mod test { utxos, 0, curr_available_value, - 20_000, - FEE_AMOUNT, + target_amount as i64, cost_of_change, ) .unwrap(); @@ -1270,12 +1250,11 @@ mod test { curr_value, curr_available_value, target_amount, - FEE_AMOUNT, cost_of_change, ) .unwrap(); assert_eq!(result.selected_amount(), 100_000); - assert_eq!(result.fee_amount, 186); + assert_eq!(result.fee_amount, 136); } // TODO: bnb() function should be optimized, and this test should be done with more utxos @@ -1307,7 +1286,6 @@ mod test { curr_value, curr_available_value, target_amount, - 0, 0.0, ) .unwrap(); @@ -1333,10 +1311,9 @@ mod test { utxos, 0, target_amount as i64, - FEE_AMOUNT, ); assert!(result.selected_amount() > target_amount); - assert_eq!(result.fee_amount, (50 + result.selected.len() * 68) as u64); + assert_eq!(result.fee_amount, (result.selected.len() * 68) as u64); } } diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index cab3b02029..8bf872dfe9 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -774,10 +774,10 @@ where required_utxos, optional_utxos, fee_rate, - outgoing, - fee_amount, + outgoing + fee_amount, )?; - let mut fee_amount = coin_selection.fee_amount; + + fee_amount += coin_selection.fee_amount; tx.input = coin_selection .selected