diff --git a/gui/src/app/state/spend/mod.rs b/gui/src/app/state/spend/mod.rs index f37f3a37..983d044f 100644 --- a/gui/src/app/state/spend/mod.rs +++ b/gui/src/app/state/spend/mod.rs @@ -87,12 +87,13 @@ pub struct CreateSpendPanel { } impl CreateSpendPanel { - pub fn new(_coins: &[Coin]) -> Self { + pub fn new(coins: &[Coin]) -> Self { Self { draft: step::TransactionDraft::default(), current: 0, steps: vec![ Box::new(step::ChooseRecipients::default()), + Box::new(step::ChooseCoins::new(coins.to_vec())), Box::new(step::ChooseFeerate::default()), ], } @@ -115,6 +116,10 @@ impl State for CreateSpendPanel { } if matches!(message, Message::View(view::Message::Next)) { + if let Some(step) = self.steps.get(self.current) { + step.apply(&mut self.draft); + } + if self.steps.get(self.current + 1).is_some() { self.current += 1; } @@ -127,7 +132,7 @@ impl State for CreateSpendPanel { } if let Some(step) = self.steps.get_mut(self.current) { - return step.update(daemon, cache, message); + return step.update(daemon, cache, &self.draft, message); } Command::none() diff --git a/gui/src/app/state/spend/step.rs b/gui/src/app/state/spend/step.rs index d3fb7cb5..26bee254 100644 --- a/gui/src/app/state/spend/step.rs +++ b/gui/src/app/state/spend/step.rs @@ -26,6 +26,7 @@ pub trait Step { &mut self, daemon: Arc, cache: &Cache, + draft: &TransactionDraft, message: Message, ) -> Command; @@ -49,6 +50,7 @@ impl Step for ChooseRecipients { &mut self, _daemon: Arc, _cache: &Cache, + _draft: &TransactionDraft, message: Message, ) -> Command { match message { @@ -173,6 +175,7 @@ impl Step for ChooseFeerate { &mut self, _daemon: Arc, _cache: &Cache, + _draft: &TransactionDraft, message: Message, ) -> Command { if let Message::View(view::Message::CreateSpend(view::CreateSpendMessage::FeerateEdited( @@ -204,3 +207,67 @@ impl Step for ChooseFeerate { ) } } + +#[derive(Default)] +pub struct ChooseCoins { + coins: Vec<(Coin, bool)>, + /// draft output amount must be superior to total input amount. + is_valid: bool, + total_needed: Option, +} + +impl ChooseCoins { + pub fn new(coins: Vec) -> Self { + Self { + coins: coins.into_iter().map(|c| (c, false)).collect(), + is_valid: false, + total_needed: None, + } + } +} + +impl Step for ChooseCoins { + fn update( + &mut self, + _daemon: Arc, + _cache: &Cache, + draft: &TransactionDraft, + message: Message, + ) -> Command { + if let Message::View(view::Message::CreateSpend(view::CreateSpendMessage::SelectCoin(i))) = + message + { + if let Some(coin) = self.coins.get_mut(i) { + coin.1 = !coin.1; + } + + let total_needed = draft + .outputs + .values() + .fold(Amount::from_sat(0), |acc, a| acc + *a); + + self.is_valid = self + .coins + .iter() + .filter_map(|(coin, selected)| if *selected { Some(coin.amount) } else { None }) + .sum::() + > total_needed; + + self.total_needed = Some(total_needed); + } + + Command::none() + } + + fn apply(&self, draft: &mut TransactionDraft) { + draft.inputs = self + .coins + .iter() + .filter_map(|(coin, selected)| if *selected { Some(coin.outpoint) } else { None }) + .collect(); + } + + fn view<'a>(&'a self, _cache: &'a Cache) -> Element<'a, view::Message> { + view::spend::step::choose_coins_view(&self.coins, self.total_needed.as_ref(), self.is_valid) + } +} diff --git a/gui/src/app/view/message.rs b/gui/src/app/view/message.rs index c4a7f0a4..bf5768d2 100644 --- a/gui/src/app/view/message.rs +++ b/gui/src/app/view/message.rs @@ -17,7 +17,7 @@ pub enum Message { pub enum CreateSpendMessage { AddRecipient, DeleteRecipient(usize), - SelectInput(usize), + SelectCoin(usize), RecipientEdited(usize, &'static str, String), FeerateEdited(String), Generate, diff --git a/gui/src/app/view/spend/step.rs b/gui/src/app/view/spend/step.rs index 7b426e90..376211e3 100644 --- a/gui/src/app/view/spend/step.rs +++ b/gui/src/app/view/spend/step.rs @@ -3,11 +3,14 @@ use iced::{ Alignment, Length, }; +use minisafe::miniscript::bitcoin::Amount; + use crate::{ app::view::{message::*, modal}, + daemon::model::Coin, ui::{ component::{ - button, form, + badge, button, card, form, text::{text, Text}, }, icon, @@ -117,3 +120,75 @@ pub fn choose_feerate_view<'a>( .align_items(Alignment::Center), ) } + +pub fn choose_coins_view<'a>( + coins: &[(Coin, bool)], + total_needed: Option<&Amount>, + is_valid: bool, +) -> Element<'a, Message> { + modal( + true, + None, + column() + .push(text("Choose coins").bold().size(50)) + .push( + column().spacing(10).push( + coins + .iter() + .enumerate() + .fold(column().spacing(10), |col, (i, (coin, selected))| { + col.push(coin_list_view(i, coin, *selected)) + }), + ), + ) + .push_maybe(if is_valid { + Some(container( + button::primary(None, "Next") + .on_press(Message::Next) + .width(Length::Units(100)), + )) + } else if total_needed.is_some() { + Some(container(card::warning(&format!( + "Total amount must be superior to {}", + total_needed.unwrap().to_btc(), + )))) + } else { + None + }) + .spacing(20) + .align_items(Alignment::Center), + ) +} + +fn coin_list_view<'a>(i: usize, coin: &Coin, selected: bool) -> Element<'a, Message> { + container( + iced::pure::button( + row() + .push( + row() + .push(if selected { + icon::square_check_icon() + } else { + icon::square_icon() + }) + .push(badge::coin()) + .push(text(&format!("block: {}", coin.block_height.unwrap_or(0))).small()) + .spacing(10) + .align_items(Alignment::Center) + .width(Length::Fill), + ) + .push( + text(&format!("{} BTC", coin.amount.to_btc())) + .bold() + .width(Length::Shrink), + ) + .align_items(Alignment::Center) + .spacing(20), + ) + .padding(10) + .on_press(Message::CreateSpend(CreateSpendMessage::SelectCoin(i))) + .style(button::Style::TransparentBorder), + ) + .style(card::SimpleCardStyle) + .into() +} diff --git a/gui/src/ui/component/card.rs b/gui/src/ui/component/card.rs index dccc26ee..672cd83a 100644 --- a/gui/src/ui/component/card.rs +++ b/gui/src/ui/component/card.rs @@ -17,6 +17,32 @@ impl widget::container::StyleSheet for SimpleCardStyle { } } +/// display an error card with the message and the error in a tooltip. +pub fn warning<'a, T: 'a>(message: &str) -> widget::Container<'a, T> { + container( + row() + .spacing(20) + .align_items(iced::Alignment::Center) + .push(icon::warning_octagon_icon().color(color::WARNING)) + .push(text(message).color(color::WARNING)), + ) + .padding(15) + .style(WarningCardStyle) +} + +pub struct WarningCardStyle; +impl widget::container::StyleSheet for WarningCardStyle { + fn style(&self) -> widget::container::Style { + widget::container::Style { + border_radius: 10.0, + border_color: color::WARNING, + border_width: 1.5, + background: color::FOREGROUND.into(), + ..widget::container::Style::default() + } + } +} + /// display an error card with the message and the error in a tooltip. pub fn error<'a, T: 'a>(message: &str, error: &str) -> widget::Container<'a, T> { container(