From 32ae95f463f62c42c6d6aec62c1832a30298fce4 Mon Sep 17 00:00:00 2001 From: Cesar Alvarez Vallero <46329881+csralvall@users.noreply.github.com> Date: Mon, 13 Jun 2022 10:49:31 -0300 Subject: [PATCH] Move change calculus to coin_select The former way to compute and create change was inside `create_tx`, just after performing coin selection. It blocked the opportunity to have an "ensemble" algorithm to decide between multiple coin selection algorithms based on a metric, like Waste. Now, change isn't created inside `coin_select` but the change amount and the possibility to create change is decided inside the `coin_select` method. In this way, change is associated with the coin selection algorithm that generated it, and a method to decide between them can be implemented. --- CHANGELOG.md | 3 +- src/wallet/coin_selection.rs | 202 ++++++++++++++++++++++++++++++++--- src/wallet/mod.rs | 69 ++++++------ 3 files changed, 228 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcda4371e..c0b2959f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] - - Add `descriptor::checksum::get_checksum_bytes` method. +- Add `Excess` enum to handle remaining amount after coin selection. +- Move change creation from `Wallet::create_tx` to `CoinSelectionAlgorithm::coin_select`. ## [v0.20.0] - [v0.19.0] diff --git a/src/wallet/coin_selection.rs b/src/wallet/coin_selection.rs index 7b4a48334..ca0fc47d8 100644 --- a/src/wallet/coin_selection.rs +++ b/src/wallet/coin_selection.rs @@ -29,6 +29,7 @@ //! # use bdk::wallet::{self, coin_selection::*}; //! # use bdk::database::Database; //! # use bdk::*; +//! # use bdk::wallet::coin_selection::decide_change; //! # const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4 + 1) * 4; //! #[derive(Debug)] //! struct AlwaysSpendEverything; @@ -42,6 +43,7 @@ //! fee_rate: FeeRate, //! amount_needed: u64, //! fee_amount: u64, +//! drain_script: &Script, //! ) -> Result { //! let mut selected_amount = 0; //! let mut additional_weight = 0; @@ -59,16 +61,21 @@ //! .collect::>(); //! let additional_fees = fee_rate.fee_wu(additional_weight); //! let amount_needed_with_fees = (fee_amount + additional_fees) + amount_needed; -//! if amount_needed_with_fees > selected_amount { +//! if selected_amount < amount_needed_with_fees { //! return Err(bdk::Error::InsufficientFunds { //! needed: amount_needed_with_fees, //! available: selected_amount, //! }); //! } //! +//! let remaining_amount = selected_amount - amount_needed_with_fees; +//! +//! let excess = decide_change(remaining_amount, fee_rate, drain_script); +//! //! Ok(CoinSelectionResult { //! selected: all_utxos_selected, //! fee_amount: fee_amount + additional_fees, +//! excess, //! }) //! } //! } @@ -89,9 +96,13 @@ //! ``` use crate::types::FeeRate; +use crate::wallet::utils::IsDust; use crate::{database::Database, WeightedUtxo}; use crate::{error::Error, Utxo}; +use bitcoin::consensus::encode::serialize; +use bitcoin::Script; + use rand::seq::SliceRandom; #[cfg(not(test))] use rand::thread_rng; @@ -111,6 +122,27 @@ pub type DefaultCoinSelectionAlgorithm = LargestFirstCoinSelection; // make the // prev_txid (32 bytes) + prev_vout (4 bytes) + sequence (4 bytes) + script_len (1 bytes) pub(crate) const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4 + 1) * 4; +#[derive(Debug)] +/// Remaining amount after performing coin selection +pub enum Excess { + /// It's not possible to create spendable output from excess using the current drain output + NoChange { + /// Threshold to consider amount as dust for this particular change script_pubkey + dust_threshold: u64, + /// Exceeding amount of current selection over outgoing value and fee costs + remaining_amount: u64, + /// The calculated fee for the drain TxOut with the selected script_pubkey + change_fee: u64, + }, + /// It's possible to create spendable output from excess using the current drain output + Change { + /// Effective amount available to create change after deducting the change output fee + amount: u64, + /// The deducted change output fee + fee: u64, + }, +} + /// Result of a successful coin selection #[derive(Debug)] pub struct CoinSelectionResult { @@ -118,6 +150,8 @@ pub struct CoinSelectionResult { pub selected: Vec, /// Total fee amount in satoshi pub fee_amount: u64, + /// Remaining amount after deducing fees and outgoing outputs + pub excess: Excess, } impl CoinSelectionResult { @@ -157,6 +191,8 @@ pub trait CoinSelectionAlgorithm: std::fmt::Debug { /// - `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 + /// - `drain_script`: the script to use in case of change + #[allow(clippy::too_many_arguments)] fn coin_select( &self, database: &D, @@ -165,6 +201,7 @@ pub trait CoinSelectionAlgorithm: std::fmt::Debug { fee_rate: FeeRate, amount_needed: u64, fee_amount: u64, + drain_script: &Script, ) -> Result; } @@ -184,6 +221,7 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection { fee_rate: FeeRate, amount_needed: u64, fee_amount: u64, + drain_script: &Script, ) -> Result { log::debug!( "amount_needed = `{}`, fee_amount = `{}`, fee_rate = `{:?}`", @@ -202,7 +240,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, amount_needed, fee_amount, drain_script) } } @@ -222,6 +260,7 @@ impl CoinSelectionAlgorithm for OldestFirstCoinSelection { fee_rate: FeeRate, amount_needed: u64, fee_amount: u64, + drain_script: &Script, ) -> Result { // query db and create a blockheight lookup table let blockheights = optional_utxos @@ -261,7 +300,33 @@ 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, amount_needed, fee_amount, drain_script) + } +} + +/// Decide if change can be created +/// +/// - `remaining_amount`: the amount in which the selected coins exceed the target amount +/// - `fee_rate`: required fee rate for the current selection +/// - `drain_script`: script to consider change creation +pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Script) -> Excess { + // drain_output_len = size(len(script_pubkey)) + len(script_pubkey) + size(output_value) + let drain_output_len = serialize(drain_script).len() + 8usize; + let change_fee = fee_rate.fee_vb(drain_output_len); + let drain_val = remaining_amount.saturating_sub(change_fee); + + if drain_val.is_dust(drain_script) { + let dust_threshold = drain_script.dust_value().as_sat(); + Excess::NoChange { + dust_threshold, + change_fee, + remaining_amount, + } + } else { + Excess::Change { + amount: drain_val, + fee: change_fee, + } } } @@ -270,6 +335,7 @@ fn select_sorted_utxos( fee_rate: FeeRate, amount_needed: u64, mut fee_amount: u64, + drain_script: &Script, ) -> Result { let mut selected_amount = 0; let selected = utxos @@ -296,6 +362,7 @@ fn select_sorted_utxos( .collect::>(); let amount_needed_with_fees = amount_needed + fee_amount; + if selected_amount < amount_needed_with_fees { return Err(Error::InsufficientFunds { needed: amount_needed_with_fees, @@ -303,9 +370,14 @@ fn select_sorted_utxos( }); } + let remaining_amount = selected_amount - amount_needed_with_fees; + + let excess = decide_change(remaining_amount, fee_rate, drain_script); + Ok(CoinSelectionResult { selected, fee_amount, + excess, }) } @@ -366,6 +438,7 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { fee_rate: FeeRate, amount_needed: u64, fee_amount: u64, + drain_script: &Script, ) -> Result { // Mapping every (UTXO, usize) to an output group let required_utxos: Vec = required_utxos @@ -408,10 +481,18 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { .expect("Bitcoin amount to fit into i64"); if curr_value > actual_target { + // remaining_amount can't be negative as that would mean the + // selection wasn't successful + // actual_target = amount_needed + (fee_amount - vin_fees) + let remaining_amount = (curr_value - actual_target) as u64; + + let excess = decide_change(remaining_amount, fee_rate, drain_script); + return Ok(BranchAndBoundCoinSelection::calculate_cs_result( vec![], required_utxos, fee_amount, + excess, )); } @@ -424,6 +505,8 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { actual_target, fee_amount, cost_of_change, + drain_script, + fee_rate, ) .unwrap_or_else(|_| { self.single_random_draw( @@ -432,6 +515,8 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { curr_value, actual_target, fee_amount, + drain_script, + fee_rate, ) })) } @@ -450,6 +535,8 @@ impl BranchAndBoundCoinSelection { actual_target: i64, fee_amount: u64, cost_of_change: f32, + drain_script: &Script, + fee_rate: FeeRate, ) -> Result { // current_selection[i] will contain true if we are using optional_utxos[i], // false otherwise. Note that current_selection.len() could be less than @@ -541,15 +628,26 @@ impl BranchAndBoundCoinSelection { .into_iter() .zip(best_selection) .filter_map(|(optional, is_in_best)| if is_in_best { Some(optional) } else { None }) - .collect(); + .collect::>(); + + let selected_amount = best_selection_value.unwrap(); + + // remaining_amount can't be negative as that would mean the + // selection wasn't successful + // actual_target = amount_needed + (fee_amount - vin_fees) + let remaining_amount = (selected_amount - actual_target) as u64; + + let excess = decide_change(remaining_amount, fee_rate, drain_script); Ok(BranchAndBoundCoinSelection::calculate_cs_result( selected_utxos, required_utxos, fee_amount, + excess, )) } + #[allow(clippy::too_many_arguments)] fn single_random_draw( &self, required_utxos: Vec, @@ -557,6 +655,8 @@ impl BranchAndBoundCoinSelection { curr_value: i64, actual_target: i64, fee_amount: u64, + drain_script: &Script, + fee_rate: FeeRate, ) -> CoinSelectionResult { #[cfg(not(test))] optional_utxos.shuffle(&mut thread_rng()); @@ -567,25 +667,39 @@ impl BranchAndBoundCoinSelection { optional_utxos.shuffle(&mut rng); } - let selected_utxos = optional_utxos - .into_iter() - .scan(curr_value, |curr_value, utxo| { - if *curr_value >= actual_target { - None + let selected_utxos = optional_utxos.into_iter().fold( + (curr_value, vec![]), + |(mut amount, mut utxos), utxo| { + if amount >= actual_target { + (amount, utxos) } else { - *curr_value += utxo.effective_value; - Some(utxo) + amount += utxo.effective_value; + utxos.push(utxo); + (amount, utxos) } - }) - .collect::>(); + }, + ); + + // remaining_amount can't be negative as that would mean the + // selection wasn't successful + // actual_target = amount_needed + (fee_amount - vin_fees) + let remaining_amount = (selected_utxos.0 - actual_target) as u64; - BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos, required_utxos, fee_amount) + let excess = decide_change(remaining_amount, fee_rate, drain_script); + + BranchAndBoundCoinSelection::calculate_cs_result( + selected_utxos.1, + required_utxos, + fee_amount, + excess, + ) } fn calculate_cs_result( mut selected_utxos: Vec, mut required_utxos: Vec, mut fee_amount: u64, + excess: Excess, ) -> CoinSelectionResult { selected_utxos.append(&mut required_utxos); fee_amount += selected_utxos.iter().map(|u| u.fee).sum::(); @@ -597,6 +711,7 @@ impl BranchAndBoundCoinSelection { CoinSelectionResult { selected, fee_amount, + excess, } } } @@ -758,6 +873,7 @@ mod test { fn test_largest_first_coin_selection_success() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let drain_script = Script::default(); let result = LargestFirstCoinSelection::default() .coin_select( @@ -767,6 +883,7 @@ mod test { FeeRate::from_sat_per_vb(1.0), 250_000, FEE_AMOUNT, + &drain_script, ) .unwrap(); @@ -779,6 +896,7 @@ mod test { fn test_largest_first_coin_selection_use_all() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let drain_script = Script::default(); let result = LargestFirstCoinSelection::default() .coin_select( @@ -788,6 +906,7 @@ mod test { FeeRate::from_sat_per_vb(1.0), 20_000, FEE_AMOUNT, + &drain_script, ) .unwrap(); @@ -800,6 +919,7 @@ mod test { fn test_largest_first_coin_selection_use_only_necessary() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let drain_script = Script::default(); let result = LargestFirstCoinSelection::default() .coin_select( @@ -809,6 +929,7 @@ mod test { FeeRate::from_sat_per_vb(1.0), 20_000, FEE_AMOUNT, + &drain_script, ) .unwrap(); @@ -822,6 +943,7 @@ mod test { fn test_largest_first_coin_selection_insufficient_funds() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let drain_script = Script::default(); LargestFirstCoinSelection::default() .coin_select( @@ -831,6 +953,7 @@ mod test { FeeRate::from_sat_per_vb(1.0), 500_000, FEE_AMOUNT, + &drain_script, ) .unwrap(); } @@ -840,6 +963,7 @@ mod test { fn test_largest_first_coin_selection_insufficient_funds_high_fees() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let drain_script = Script::default(); LargestFirstCoinSelection::default() .coin_select( @@ -849,6 +973,7 @@ mod test { FeeRate::from_sat_per_vb(1000.0), 250_000, FEE_AMOUNT, + &drain_script, ) .unwrap(); } @@ -857,6 +982,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 drain_script = Script::default(); let result = OldestFirstCoinSelection::default() .coin_select( @@ -866,6 +992,7 @@ mod test { FeeRate::from_sat_per_vb(1.0), 180_000, FEE_AMOUNT, + &drain_script, ) .unwrap(); @@ -880,6 +1007,7 @@ mod test { let utxo1 = utxo(120_000, 1); let utxo2 = utxo(80_000, 2); let utxo3 = utxo(300_000, 3); + let drain_script = Script::default(); let mut database = MemoryDatabase::default(); @@ -922,6 +1050,7 @@ mod test { FeeRate::from_sat_per_vb(1.0), 180_000, FEE_AMOUNT, + &drain_script, ) .unwrap(); @@ -934,6 +1063,7 @@ mod 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 drain_script = Script::default(); let result = OldestFirstCoinSelection::default() .coin_select( @@ -943,6 +1073,7 @@ mod test { FeeRate::from_sat_per_vb(1.0), 20_000, FEE_AMOUNT, + &drain_script, ) .unwrap(); @@ -955,6 +1086,7 @@ mod 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 drain_script = Script::default(); let result = OldestFirstCoinSelection::default() .coin_select( @@ -964,6 +1096,7 @@ mod test { FeeRate::from_sat_per_vb(1.0), 20_000, FEE_AMOUNT, + &drain_script, ) .unwrap(); @@ -977,6 +1110,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 drain_script = Script::default(); OldestFirstCoinSelection::default() .coin_select( @@ -986,6 +1120,7 @@ mod test { FeeRate::from_sat_per_vb(1.0), 600_000, FEE_AMOUNT, + &drain_script, ) .unwrap(); } @@ -998,6 +1133,7 @@ mod test { let amount_needed: u64 = utxos.iter().map(|wu| wu.utxo.txout().value).sum::() - (FEE_AMOUNT + 50); + let drain_script = Script::default(); OldestFirstCoinSelection::default() .coin_select( @@ -1007,6 +1143,7 @@ mod test { FeeRate::from_sat_per_vb(1000.0), amount_needed, FEE_AMOUNT, + &drain_script, ) .unwrap(); } @@ -1018,6 +1155,7 @@ mod test { let utxos = generate_same_value_utxos(100_000, 20); let database = MemoryDatabase::default(); + let drain_script = Script::default(); let result = BranchAndBoundCoinSelection::default() .coin_select( @@ -1027,6 +1165,7 @@ mod test { FeeRate::from_sat_per_vb(1.0), 250_000, FEE_AMOUNT, + &drain_script, ) .unwrap(); @@ -1039,6 +1178,7 @@ mod test { fn test_bnb_coin_selection_required_are_enough() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let drain_script = Script::default(); let result = BranchAndBoundCoinSelection::default() .coin_select( @@ -1048,6 +1188,7 @@ mod test { FeeRate::from_sat_per_vb(1.0), 20_000, FEE_AMOUNT, + &drain_script, ) .unwrap(); @@ -1060,6 +1201,7 @@ mod test { fn test_bnb_coin_selection_optional_are_enough() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let drain_script = Script::default(); let result = BranchAndBoundCoinSelection::default() .coin_select( @@ -1069,6 +1211,7 @@ mod test { FeeRate::from_sat_per_vb(1.0), 299756, FEE_AMOUNT, + &drain_script, ) .unwrap(); @@ -1091,6 +1234,7 @@ mod test { assert_eq!(amount, 100_000); let amount: u64 = optional.iter().map(|u| u.utxo.txout().value).sum(); assert!(amount > 150_000); + let drain_script = Script::default(); let result = BranchAndBoundCoinSelection::default() .coin_select( @@ -1100,6 +1244,7 @@ mod test { FeeRate::from_sat_per_vb(1.0), 150_000, FEE_AMOUNT, + &drain_script, ) .unwrap(); @@ -1113,6 +1258,7 @@ mod test { fn test_bnb_coin_selection_insufficient_funds() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let drain_script = Script::default(); BranchAndBoundCoinSelection::default() .coin_select( @@ -1122,6 +1268,7 @@ mod test { FeeRate::from_sat_per_vb(1.0), 500_000, FEE_AMOUNT, + &drain_script, ) .unwrap(); } @@ -1131,6 +1278,7 @@ mod test { fn test_bnb_coin_selection_insufficient_funds_high_fees() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let drain_script = Script::default(); BranchAndBoundCoinSelection::default() .coin_select( @@ -1140,6 +1288,7 @@ mod test { FeeRate::from_sat_per_vb(1000.0), 250_000, FEE_AMOUNT, + &drain_script, ) .unwrap(); } @@ -1148,6 +1297,7 @@ mod test { fn test_bnb_coin_selection_check_fee_rate() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); + let drain_script = Script::default(); let result = BranchAndBoundCoinSelection::new(0) .coin_select( @@ -1157,6 +1307,7 @@ mod test { FeeRate::from_sat_per_vb(1.0), 99932, // first utxo's effective value 0, + &drain_script, ) .unwrap(); @@ -1176,6 +1327,7 @@ mod test { for _i in 0..200 { let mut optional_utxos = generate_random_utxos(&mut rng, 16); let target_amount = sum_random_utxos(&mut rng, &mut optional_utxos); + let drain_script = Script::default(); let result = BranchAndBoundCoinSelection::new(0) .coin_select( &database, @@ -1184,6 +1336,7 @@ mod test { FeeRate::from_sat_per_vb(0.0), target_amount, 0, + &drain_script, ) .unwrap(); assert_eq!(result.selected_amount(), target_amount); @@ -1203,6 +1356,9 @@ mod test { let size_of_change = 31; let cost_of_change = size_of_change as f32 * fee_rate.as_sat_vb(); + + let drain_script = Script::default(); + BranchAndBoundCoinSelection::new(size_of_change) .bnb( vec![], @@ -1212,6 +1368,8 @@ mod test { 20_000, FEE_AMOUNT, cost_of_change, + &drain_script, + fee_rate, ) .unwrap(); } @@ -1230,6 +1388,8 @@ mod test { let size_of_change = 31; let cost_of_change = size_of_change as f32 * fee_rate.as_sat_vb(); + let drain_script = Script::default(); + BranchAndBoundCoinSelection::new(size_of_change) .bnb( vec![], @@ -1239,6 +1399,8 @@ mod test { 20_000, FEE_AMOUNT, cost_of_change, + &drain_script, + fee_rate, ) .unwrap(); } @@ -1263,6 +1425,8 @@ mod test { // cost_of_change + 5. let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change.ceil() as i64 + 5; + let drain_script = Script::default(); + let result = BranchAndBoundCoinSelection::new(size_of_change) .bnb( vec![], @@ -1272,6 +1436,8 @@ mod test { target_amount, FEE_AMOUNT, cost_of_change, + &drain_script, + fee_rate, ) .unwrap(); assert_eq!(result.selected_amount(), 100_000); @@ -1300,6 +1466,8 @@ mod test { let target_amount = optional_utxos[3].effective_value + optional_utxos[23].effective_value; + let drain_script = Script::default(); + let result = BranchAndBoundCoinSelection::new(0) .bnb( vec![], @@ -1309,6 +1477,8 @@ mod test { target_amount, 0, 0.0, + &drain_script, + fee_rate, ) .unwrap(); assert_eq!(result.selected_amount(), target_amount as u64); @@ -1328,12 +1498,16 @@ mod test { .map(|u| OutputGroup::new(u, fee_rate)) .collect(); + let drain_script = Script::default(); + let result = BranchAndBoundCoinSelection::default().single_random_draw( vec![], utxos, 0, target_amount as i64, FEE_AMOUNT, + &drain_script, + fee_rate, ); assert!(result.selected_amount() > target_amount); diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 66366e9e4..4ee3835d9 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -72,6 +72,7 @@ use crate::psbt::PsbtUtils; use crate::signer::SignerError; use crate::testutils; use crate::types::*; +use crate::wallet::coin_selection::Excess::{Change, NoChange}; const CACHE_ADDR_BATCH_SIZE: u32 = 100; const COINBASE_MATURITY: u32 = 100; @@ -777,6 +778,15 @@ where current_height, )?; + // get drain script + let drain_script = match params.drain_to { + Some(ref drain_recipient) => drain_recipient.clone(), + None => self + .get_internal_address(AddressIndex::New)? + .address + .script_pubkey(), + }; + let coin_selection = coin_selection.coin_select( self.database.borrow().deref(), required_utxos, @@ -784,8 +794,10 @@ where fee_rate, outgoing, fee_amount, + &drain_script, )?; let mut fee_amount = coin_selection.fee_amount; + let excess = &coin_selection.excess; tx.input = coin_selection .selected @@ -798,26 +810,6 @@ where }) .collect(); - // prepare the drain output - let mut drain_output = { - let script_pubkey = match params.drain_to { - Some(ref drain_recipient) => drain_recipient.clone(), - None => self - .get_internal_address(AddressIndex::New)? - .address - .script_pubkey(), - }; - - TxOut { - script_pubkey, - value: 0, - } - }; - - fee_amount += fee_rate.fee_vb(serialize(&drain_output).len()); - - let drain_val = (coin_selection.selected_amount() - outgoing).saturating_sub(fee_amount); - if tx.output.is_empty() { // Uh oh, our transaction has no outputs. // We allow this when: @@ -827,10 +819,15 @@ where // Otherwise, we don't know who we should send the funds to, and how much // we should send! if params.drain_to.is_some() && (params.drain_wallet || !params.utxos.is_empty()) { - if drain_val.is_dust(&drain_output.script_pubkey) { + if let NoChange { + dust_threshold, + remaining_amount, + change_fee, + } = excess + { return Err(Error::InsufficientFunds { - needed: drain_output.script_pubkey.dust_value().as_sat(), - available: drain_val, + needed: *dust_threshold, + available: remaining_amount.saturating_sub(*change_fee), }); } } else { @@ -838,15 +835,25 @@ where } } - if drain_val.is_dust(&drain_output.script_pubkey) { - fee_amount += drain_val; - } else { - drain_output.value = drain_val; - if self.is_mine(&drain_output.script_pubkey)? { - received += drain_val; + match excess { + NoChange { + remaining_amount, .. + } => fee_amount += remaining_amount, + Change { amount, fee } => { + if self.is_mine(&drain_script)? { + received += amount; + } + fee_amount += fee; + + // create drain output + let drain_output = TxOut { + value: *amount, + script_pubkey: drain_script, + }; + + tx.output.push(drain_output); } - tx.output.push(drain_output); - } + }; // sort input/outputs according to the chosen algorithm params.ordering.sort_tx(&mut tx);