From fae32df08656bf550f6a035dad6a3f4b56a9b6fc Mon Sep 17 00:00:00 2001 From: jp1ac4 <121959000+jp1ac4@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:53:49 +0000 Subject: [PATCH 1/2] gui: pass change_address param to createspend --- gui/src/app/state/spend/step.rs | 2 +- gui/src/daemon/client/mod.rs | 18 ++++++++++-------- gui/src/daemon/embedded.rs | 3 ++- gui/src/daemon/mod.rs | 1 + 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/gui/src/app/state/spend/step.rs b/gui/src/app/state/spend/step.rs index d5dd1dad..38b621bd 100644 --- a/gui/src/app/state/spend/step.rs +++ b/gui/src/app/state/spend/step.rs @@ -381,7 +381,7 @@ impl Step for DefineSpend { return Command::perform( async move { daemon - .create_spend_tx(&inputs, &outputs, feerate_vb) + .create_spend_tx(&inputs, &outputs, feerate_vb, None) .map_err(|e| e.into()) .and_then(|res| match res { CreateSpendResult::Success { psbt, .. } => Ok(psbt), diff --git a/gui/src/daemon/client/mod.rs b/gui/src/daemon/client/mod.rs index 1f6a4ac8..7ec4c0c1 100644 --- a/gui/src/daemon/client/mod.rs +++ b/gui/src/daemon/client/mod.rs @@ -86,15 +86,17 @@ impl Daemon for Lianad { coins_outpoints: &[OutPoint], destinations: &HashMap, u64>, feerate_vb: u64, + change_address: Option>, ) -> Result { - self.call( - "createspend", - Some(vec![ - json!(destinations), - json!(coins_outpoints), - json!(feerate_vb), - ]), - ) + let mut input = vec![ + json!(destinations), + json!(coins_outpoints), + json!(feerate_vb), + ]; + if let Some(change_address) = change_address { + input.push(json!(change_address)); + } + self.call("createspend", Some(input)) } fn rbf_psbt( diff --git a/gui/src/daemon/embedded.rs b/gui/src/daemon/embedded.rs index 4b170844..2978c181 100644 --- a/gui/src/daemon/embedded.rs +++ b/gui/src/daemon/embedded.rs @@ -87,9 +87,10 @@ impl Daemon for EmbeddedDaemon { coins_outpoints: &[OutPoint], destinations: &HashMap, u64>, feerate_vb: u64, + change_address: Option>, ) -> Result { self.control()? - .create_spend(destinations, coins_outpoints, feerate_vb, None) + .create_spend(destinations, coins_outpoints, feerate_vb, change_address) .map_err(|e| DaemonError::Unexpected(e.to_string())) } diff --git a/gui/src/daemon/mod.rs b/gui/src/daemon/mod.rs index 72ddf1b2..1964c981 100644 --- a/gui/src/daemon/mod.rs +++ b/gui/src/daemon/mod.rs @@ -62,6 +62,7 @@ pub trait Daemon: Debug { coins_outpoints: &[OutPoint], destinations: &HashMap, u64>, feerate_vb: u64, + change_address: Option>, ) -> Result; fn rbf_psbt( &self, From cb5073c83a54b3e4d430a7b30ce590c8385f7f7c Mon Sep 17 00:00:00 2001 From: jp1ac4 <121959000+jp1ac4@users.noreply.github.com> Date: Mon, 29 Jan 2024 17:06:14 +0000 Subject: [PATCH 2/2] gui(spend): use create_spend_tx for missing amount --- gui/src/app/state/spend/step.rs | 115 +++++++++++--------------------- 1 file changed, 38 insertions(+), 77 deletions(-) diff --git a/gui/src/app/state/spend/step.rs b/gui/src/app/state/spend/step.rs index 38b621bd..13fe8c23 100644 --- a/gui/src/app/state/spend/step.rs +++ b/gui/src/app/state/spend/step.rs @@ -6,12 +6,9 @@ use iced::{Command, Subscription}; use liana::{ descriptors::LianaDescriptor, miniscript::bitcoin::{ - self, address, psbt::Psbt, secp256k1, Address, Amount, Denomination, Network, OutPoint, - }, - spend::{ - create_spend, CandidateCoin, SpendCreationError, SpendOutputAddress, SpendTxFees, TxGetter, - MAX_FEERATE, + address, psbt::Psbt, secp256k1, Address, Amount, Denomination, Network, OutPoint, }, + spend::{SpendCreationError, MAX_FEERATE}, }; use liana_ui::{component::form, widget::Element}; @@ -192,86 +189,60 @@ impl DefineSpend { return; } - let destinations: Vec<(SpendOutputAddress, Amount)> = self + let destinations: HashMap, u64> = self .recipients .iter() .map(|recipient| { ( - SpendOutputAddress { - addr: Address::from_str(&recipient.address.value) - .expect("Checked before") - .assume_checked(), - info: None, - }, - Amount::from_sat(recipient.amount().expect("Checked before")), + Address::from_str(&recipient.address.value).expect("Checked before"), + recipient.amount().expect("Checked before"), ) }) .collect(); - let coins: Vec = if self.is_user_coin_selection { - self.coins + let outpoints = if self.is_user_coin_selection { + let outpoints: Vec<_> = self + .coins .iter() - .filter_map(|(c, selected)| { - if *selected { - Some(CandidateCoin { - amount: c.amount, - outpoint: c.outpoint, - deriv_index: c.derivation_index, - is_change: c.is_change, - sequence: None, - must_select: *selected, - }) - } else { - None - } - }) - .collect() + .filter_map( + |(c, selected)| { + if *selected { + Some(c.outpoint) + } else { + None + } + }, + ) + .collect(); + if outpoints.is_empty() { + // If the user has deselected all coins, simply set the amount left to select as the + // total destination value. Note this doesn't take account of the fee, but passing + // an empty list to `create_spend_tx` would use auto-selection and so we settle for + // this approximation. + self.amount_left_to_select = Some(Amount::from_sat(destinations.values().sum())); + return; + } + outpoints } else { - // For automated coin selection, only confirmed coins are considered - self.coins - .iter() - .filter_map(|(c, _)| { - if c.block_height.is_some() { - Some(CandidateCoin { - amount: c.amount, - outpoint: c.outpoint, - deriv_index: c.derivation_index, - is_change: c.is_change, - sequence: None, - must_select: false, - }) - } else { - None - } - }) - .collect() + Vec::new() // pass empty list for auto-selection }; + // Use a fixed change address so that we don't increment the change index. let dummy_address = self .descriptor .change_descriptor() .derive(0.into(), &self.curve) - .address(self.network); + .address(self.network) + .as_unchecked() + .clone(); let feerate_vb = self.feerate.value.parse::().expect("Checked before"); - // Create a spend with empty inputs in order to use auto-selection. - match create_spend( - &self.descriptor, - &self.curve, - &mut DaemonTxGetter(&daemon), - &destinations, - &coins, - SpendTxFees::Regular(feerate_vb), - SpendOutputAddress { - addr: dummy_address, - info: None, - }, - ) { - Ok(spend) => { + + match daemon.create_spend_tx(&outpoints, &destinations, feerate_vb, Some(dummy_address)) { + Ok(CreateSpendResult::Success { psbt, .. }) => { self.warning = None; if !self.is_user_coin_selection { - let selected_coins: Vec = spend - .psbt + let selected_coins: Vec = psbt .unsigned_tx .input .iter() @@ -290,8 +261,8 @@ impl DefineSpend { // User can then either: // - modify recipient amounts and/or feerate and let coin selection run again, or // - select coins manually. - Err(SpendCreationError::CoinSelection(amount)) => { - self.amount_left_to_select = Some(Amount::from_sat(amount.missing)); + Ok(CreateSpendResult::InsufficientFunds { missing }) => { + self.amount_left_to_select = Some(Amount::from_sat(missing)); } Err(e) => { self.warning = Some(e.into()); @@ -300,16 +271,6 @@ impl DefineSpend { } } -pub struct DaemonTxGetter<'a>(&'a Arc); -impl<'a> TxGetter for DaemonTxGetter<'a> { - fn get_tx(&mut self, txid: &bitcoin::Txid) -> Option { - self.0 - .list_txs(&[*txid]) - .ok() - .and_then(|mut txs| txs.transactions.pop().map(|tx| tx.tx)) - } -} - impl Step for DefineSpend { fn update( &mut self,