diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index 610e1f61..0fe97cf6 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -80,6 +80,7 @@ impl App { menu::Menu::Spend => SpendPanel::new(self.config.clone(), &self.cache.spend_txs).into(), menu::Menu::CreateSpendTx => CreateSpendPanel::new( self.config.clone(), + self.daemon.config().main_descriptor.clone(), &self.cache.coins, self.daemon.config().main_descriptor.timelock_value(), self.cache.blockheight as u32, diff --git a/gui/src/app/state/spend/mod.rs b/gui/src/app/state/spend/mod.rs index 9d10d729..cbbe990f 100644 --- a/gui/src/app/state/spend/mod.rs +++ b/gui/src/app/state/spend/mod.rs @@ -4,6 +4,8 @@ use std::sync::Arc; use iced::{Command, Element}; +use liana::descriptors::MultipathDescriptor; + use super::{redirect, State}; use crate::{ app::{cache::Cache, config::Config, error::Error, menu::Menu, message::Message, view}, @@ -104,13 +106,20 @@ pub struct CreateSpendPanel { } impl CreateSpendPanel { - pub fn new(config: Config, coins: &[Coin], timelock: u32, blockheight: u32) -> Self { + pub fn new( + config: Config, + descriptor: MultipathDescriptor, + coins: &[Coin], + timelock: u32, + blockheight: u32, + ) -> Self { Self { draft: step::TransactionDraft::default(), current: 0, steps: vec![ - Box::new(step::ChooseRecipients::default()), + Box::new(step::ChooseRecipients::new(coins)), Box::new(step::ChooseCoins::new( + descriptor, coins.to_vec(), timelock, blockheight, diff --git a/gui/src/app/state/spend/step.rs b/gui/src/app/state/spend/step.rs index 890ec0ad..ba5e7fb9 100644 --- a/gui/src/app/state/spend/step.rs +++ b/gui/src/app/state/spend/step.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use iced::{Command, Element}; use liana::{ - config::Config as DaemonConfig, + descriptors::MultipathDescriptor, miniscript::bitcoin::{ self, util::psbt::Psbt, Address, Amount, Denomination, Network, OutPoint, }, @@ -21,6 +21,9 @@ use crate::{ ui::component::form, }; +/// See: https://github.com/wizardsardine/liana/blob/master/src/commands/mod.rs#L32 +const DUST_OUTPUT_SATS: u64 = 5_000; + #[derive(Default, Clone)] pub struct TransactionDraft { inputs: Vec, @@ -42,22 +45,30 @@ pub trait Step { } pub struct ChooseRecipients { + balance_available: Amount, recipients: Vec, is_valid: bool, is_duplicate: bool, } -impl std::default::Default for ChooseRecipients { - fn default() -> Self { +impl ChooseRecipients { + pub fn new(coins: &[Coin]) -> Self { Self { + balance_available: coins + .iter() + .filter_map(|coin| { + if coin.spend_info.is_none() { + Some(coin.amount) + } else { + None + } + }) + .sum(), recipients: vec![Recipient::default()], is_valid: false, is_duplicate: false, } } -} - -impl ChooseRecipients { fn check_valid(&mut self) { self.is_valid = !self.recipients.is_empty(); self.is_duplicate = false; @@ -117,6 +128,7 @@ impl Step for ChooseRecipients { fn view<'a>(&'a self, _cache: &'a Cache) -> Element<'a, view::Message> { view::spend::step::choose_recipients_view( + &self.balance_available, self.recipients .iter() .enumerate() @@ -153,6 +165,10 @@ impl Recipient { return Err(Error::Unexpected("Amount should be non-zero".to_string())); } + if amount.to_sat() < DUST_OUTPUT_SATS { + return Err(Error::Unexpected("Amount should be non-zero".to_string())); + } + if let Ok(address) = Address::from_str(&self.address.value) { if amount <= address.script_pubkey().dust_value() { return Err(Error::Unexpected( @@ -207,8 +223,8 @@ impl Recipient { } } -#[derive(Default)] pub struct ChooseCoins { + descriptor: MultipathDescriptor, timelock: u32, coins: Vec<(Coin, bool)>, recipients: Vec<(Address, Amount)>, @@ -220,7 +236,12 @@ pub struct ChooseCoins { } impl ChooseCoins { - pub fn new(coins: Vec, timelock: u32, blockheight: u32) -> Self { + pub fn new( + descriptor: MultipathDescriptor, + coins: Vec, + timelock: u32, + blockheight: u32, + ) -> Self { let mut coins: Vec<(Coin, bool)> = coins .into_iter() .filter_map(|c| { @@ -243,6 +264,7 @@ impl ChooseCoins { } }); Self { + descriptor, timelock, coins, recipients: Vec::new(), @@ -253,7 +275,7 @@ impl ChooseCoins { } } - fn amount_left_to_select(&mut self, cfg: &DaemonConfig) { + fn amount_left_to_select(&mut self) { // We need the feerate in order to compute the required amount of BTC to // select. Return early if we don't to not do unnecessary computation. let feerate = match self.feerate.value.parse::() { @@ -293,7 +315,7 @@ impl ChooseCoins { }; // nValue size + scriptPubKey CompactSize + OP_0 + PUSH32 + const CHANGE_TXO_SIZE: usize = 8 + 1 + 1 + 1 + 32; - let satisfaction_vsize = cfg.main_descriptor.max_sat_weight() / 4; + let satisfaction_vsize = self.descriptor.max_sat_weight() / 4; let transaction_size = tx_template.vsize() + satisfaction_vsize * tx_template.input.len() + CHANGE_TXO_SIZE; @@ -317,6 +339,7 @@ impl Step for ChooseCoins { .iter() .map(|(k, v)| (k.clone(), Amount::from_sat(*v))) .collect(); + self.amount_left_to_select(); } fn apply(&self, draft: &mut TransactionDraft) { @@ -342,7 +365,7 @@ impl Step for ChooseCoins { if s.parse::().is_ok() { self.feerate.value = s; self.feerate.valid = true; - self.amount_left_to_select(daemon.config()); + self.amount_left_to_select(); } else if s.is_empty() { self.feerate.value = "".to_string(); self.feerate.valid = true; @@ -384,7 +407,7 @@ impl Step for ChooseCoins { Message::View(view::Message::CreateSpend(view::CreateSpendMessage::SelectCoin(i))) => { if let Some(coin) = self.coins.get_mut(i) { coin.1 = !coin.1; - self.amount_left_to_select(daemon.config()); + self.amount_left_to_select(); } } _ => {} diff --git a/gui/src/app/view/spend/step.rs b/gui/src/app/view/spend/step.rs index 93051760..ffc0b81c 100644 --- a/gui/src/app/view/spend/step.rs +++ b/gui/src/app/view/spend/step.rs @@ -23,12 +23,13 @@ use crate::{ }, }; -pub fn choose_recipients_view( - recipients: Vec>, +pub fn choose_recipients_view<'a>( + balance_available: &'a Amount, + recipients: Vec>, total_amount: Amount, is_valid: bool, duplicate: bool, -) -> Element { +) -> Element<'a, Message> { modal( false, None, @@ -53,15 +54,21 @@ pub fn choose_recipients_view( .spacing(20) .align_items(Alignment::Center) .push( - Container::new(text(format!("{}", total_amount)).bold()) - .width(Length::Fill), + Container::new( + Row::new() + .align_items(Alignment::Center) + .spacing(5) + .push(text(format!("{}", total_amount)).bold()) + .push(text(format!("/ {}", balance_available))), + ) + .width(Length::Fill), ) .push_maybe(if duplicate { Some(text("Two recipient addresses are the same").style(color::WARNING)) } else { None }) - .push(if is_valid { + .push(if is_valid && total_amount < *balance_available { button::primary(None, "Next") .on_press(Message::Next) .width(Length::Units(100)) @@ -93,11 +100,11 @@ pub fn recipient_view<'a>( form::Form::new("Amount", amount, move |msg| { CreateSpendMessage::RecipientEdited(index, "amount", msg) }) - .warning("Please enter correct amount") + .warning("Please enter correct amount (> 5000 sats)") .size(20) .padding(10), ) - .width(Length::Units(250)), + .width(Length::Units(300)), ) .spacing(5) .push(