diff --git a/gui/Cargo.lock b/gui/Cargo.lock index 816617c4..27252cbb 100644 --- a/gui/Cargo.lock +++ b/gui/Cargo.lock @@ -1841,7 +1841,7 @@ dependencies = [ [[package]] name = "liana" version = "0.4.0" -source = "git+https://github.com/wizardsardine/liana?branch=0.4-lianad#81b81b2f789052cd8ef6b13964f9953f4fcbc0a4" +source = "git+https://github.com/wizardsardine/liana?branch=master#9a78b8cfffd5075dea3a041a9e661c27b14c57a2" dependencies = [ "backtrace", "base64", diff --git a/gui/Cargo.toml b/gui/Cargo.toml index 3f52c063..8e79db47 100644 --- a/gui/Cargo.toml +++ b/gui/Cargo.toml @@ -15,7 +15,7 @@ path = "src/main.rs" [dependencies] async-hwi = "0.0.6" -liana = { git = "https://github.com/wizardsardine/liana", branch = "0.4-lianad", default-features = false } +liana = { git = "https://github.com/wizardsardine/liana", branch = "master", default-features = false } liana_ui = { path = "ui" } backtrace = "0.3" base64 = "0.13" diff --git a/gui/src/app/menu.rs b/gui/src/app/menu.rs index 8d0f1857..8239f675 100644 --- a/gui/src/app/menu.rs +++ b/gui/src/app/menu.rs @@ -1,3 +1,4 @@ +use liana::miniscript::bitcoin::OutPoint; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Menu { Home, @@ -8,4 +9,5 @@ pub enum Menu { Coins, CreateSpendTx, Recovery, + RefreshCoins(Vec), } diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index d9553fa8..164f06fa 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -92,6 +92,13 @@ impl App { self.cache.blockheight as u32, ) .into(), + menu::Menu::RefreshCoins(preselected) => CreateSpendPanel::new_self_send( + self.wallet.clone(), + &self.cache.coins, + self.cache.blockheight as u32, + preselected, + ) + .into(), }; self.state.load(self.daemon.clone()) } diff --git a/gui/src/app/state/mod.rs b/gui/src/app/state/mod.rs index b35325f3..ff284362 100644 --- a/gui/src/app/state/mod.rs +++ b/gui/src/app/state/mod.rs @@ -11,7 +11,7 @@ use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use iced::{widget::qr_code, Command, Subscription}; -use liana::miniscript::bitcoin::{Address, Amount}; +use liana::miniscript::bitcoin::{Address, Amount, OutPoint}; use liana_ui::widget::*; use super::{cache::Cache, error::Error, menu::Menu, message::Message, view, wallet::Wallet}; @@ -50,7 +50,7 @@ pub struct Home { balance: Amount, unconfirmed_balance: Amount, remaining_sequence: Option, - number_of_expiring_coins: usize, + expiring_coins: Vec, pending_events: Vec, events: Vec, selected_event: Option<(usize, usize)>, @@ -76,7 +76,7 @@ impl Home { balance, unconfirmed_balance, remaining_sequence: None, - number_of_expiring_coins: 0, + expiring_coins: Vec::new(), selected_event: None, events: Vec::new(), pending_events: Vec::new(), @@ -103,7 +103,7 @@ impl State for Home { &self.balance, &self.unconfirmed_balance, &self.remaining_sequence, - self.number_of_expiring_coins, + &self.expiring_coins, &self.pending_events, &self.events, ), @@ -125,7 +125,7 @@ impl State for Home { self.balance = Amount::from_sat(0); self.unconfirmed_balance = Amount::from_sat(0); self.remaining_sequence = None; - self.number_of_expiring_coins = 0; + self.expiring_coins = Vec::new(); for coin in coins { if coin.spend_info.is_none() { if coin.block_height.is_some() { @@ -135,7 +135,7 @@ impl State for Home { remaining_sequence(&coin, cache.blockheight as u32, timelock); // number of block in a day if seq <= 144 { - self.number_of_expiring_coins += 1; + self.expiring_coins.push(coin.outpoint); } if let Some(last) = &mut self.remaining_sequence { if seq < *last { diff --git a/gui/src/app/state/spend/mod.rs b/gui/src/app/state/spend/mod.rs index ce4ccc6e..67587efd 100644 --- a/gui/src/app/state/spend/mod.rs +++ b/gui/src/app/state/spend/mod.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use iced::Command; +use liana::miniscript::bitcoin::OutPoint; use liana_ui::widget::Element; use super::{redirect, State}; @@ -25,12 +26,33 @@ impl CreateSpendPanel { draft: step::TransactionDraft::default(), current: 0, steps: vec![ - Box::new(step::DefineSpend::new( - descriptor, - coins.to_vec(), - timelock, - blockheight, - )), + Box::new( + step::DefineSpend::new(descriptor, coins, timelock) + .with_coins_sorted(blockheight), + ), + Box::new(step::SaveSpend::new(wallet)), + ], + } + } + + pub fn new_self_send( + wallet: Arc, + coins: &[Coin], + blockheight: u32, + preselected_coins: &[OutPoint], + ) -> Self { + let descriptor = wallet.main_descriptor.clone(); + let timelock = descriptor.first_timelock_value(); + Self { + draft: step::TransactionDraft::default(), + current: 0, + steps: vec![ + Box::new( + step::DefineSpend::new(descriptor, coins, timelock) + .with_preselected_coins(preselected_coins) + .with_coins_sorted(blockheight) + .self_send(), + ), Box::new(step::SaveSpend::new(wallet)), ], } diff --git a/gui/src/app/state/spend/step.rs b/gui/src/app/state/spend/step.rs index 1f58f805..2b8ec09b 100644 --- a/gui/src/app/state/spend/step.rs +++ b/gui/src/app/state/spend/step.rs @@ -60,12 +60,7 @@ pub struct DefineSpend { } impl DefineSpend { - pub fn new( - descriptor: LianaDescriptor, - coins: Vec, - timelock: u16, - blockheight: u32, - ) -> Self { + pub fn new(descriptor: LianaDescriptor, coins: &[Coin], timelock: u16) -> Self { let balance_available = coins .iter() .filter_map(|coin| { @@ -76,27 +71,17 @@ impl DefineSpend { } }) .sum(); - let mut coins: Vec<(Coin, bool)> = coins - .into_iter() + let coins: Vec<(Coin, bool)> = coins + .iter() .filter_map(|c| { if c.spend_info.is_none() { - Some((c, false)) + Some((*c, false)) } else { None } }) .collect(); - coins.sort_by(|(a, _), (b, _)| { - if remaining_sequence(a, blockheight, timelock) - == remaining_sequence(b, blockheight, timelock) - { - // bigger amount first - b.amount.cmp(&a.amount) - } else { - // smallest blockheight (remaining_sequence) first - a.block_height.cmp(&b.block_height) - } - }); + Self { balance_available, descriptor, @@ -111,9 +96,43 @@ impl DefineSpend { warning: None, } } + + pub fn with_preselected_coins(mut self, preselected_coins: &[OutPoint]) -> Self { + for (coin, selected) in &mut self.coins { + *selected = preselected_coins.contains(&coin.outpoint); + } + self + } + + pub fn with_coins_sorted(mut self, blockheight: u32) -> Self { + let timelock = self.timelock; + self.coins.sort_by(|(a, a_selected), (b, b_selected)| { + if *a_selected && !b_selected || !a_selected && *b_selected { + b_selected.cmp(a_selected) + } else if remaining_sequence(a, blockheight, timelock) + == remaining_sequence(b, blockheight, timelock) + { + // bigger amount first + b.amount.cmp(&a.amount) + } else { + // smallest blockheight (remaining_sequence) first + a.block_height.cmp(&b.block_height) + } + }); + self + } + + pub fn self_send(mut self) -> Self { + self.recipients = Vec::new(); + self + } + fn check_valid(&mut self) { - self.is_valid = !self.recipients.is_empty(); + self.is_valid = self.feerate.valid && !self.feerate.value.is_empty(); self.is_duplicate = false; + if !self.coins.iter().any(|(_, selected)| *selected) { + self.is_valid = false; + } for (i, recipient) in self.recipients.iter().enumerate() { if !recipient.valid() { self.is_valid = false; @@ -212,9 +231,9 @@ impl Step for DefineSpend { } view::CreateSpendMessage::FeerateEdited(s) => { - if s.parse::().is_ok() { + if let Ok(value) = s.parse::() { self.feerate.value = s; - self.feerate.valid = true; + self.feerate.valid = value != 0; self.amount_left_to_select(); } else if s.is_empty() { self.feerate.value = "".to_string(); diff --git a/gui/src/app/view/coins.rs b/gui/src/app/view/coins.rs index 6dfe6968..a1e31e33 100644 --- a/gui/src/app/view/coins.rs +++ b/gui/src/app/view/coins.rs @@ -1,8 +1,8 @@ -use iced::{Alignment, Length}; +use iced::{widget::Space, Alignment, Length}; use liana_ui::{ color, - component::{amount::*, badge, text::*}, + component::{amount::*, badge, button, text::*}, icon, image::*, theme, @@ -11,7 +11,7 @@ use liana_ui::{ }; use crate::{ - app::{cache::Cache, view::message::Message}, + app::{cache::Cache, menu::Menu, view::message::Message}, daemon::model::{remaining_sequence, Coin}, }; @@ -136,7 +136,7 @@ fn coin_list_view( .spacing(5) })), ) - .push_maybe(coin.spend_info.map(|info| { + .push(if let Some(info) = coin.spend_info { Column::new() .push( Row::new() @@ -159,7 +159,16 @@ fn coin_list_view( ) }) .spacing(5) - })), + } else { + Column::new().push( + Row::new().push(Space::with_width(Length::Fill)).push( + button::primary(Some(icon::arrow_repeat()), "Refresh coin") + .on_press(Message::Menu(Menu::RefreshCoins(vec![ + coin.outpoint, + ]))), + ), + ) + }), ) } else { None diff --git a/gui/src/app/view/home.rs b/gui/src/app/view/home.rs index 2802a3da..4a271294 100644 --- a/gui/src/app/view/home.rs +++ b/gui/src/app/view/home.rs @@ -5,7 +5,7 @@ use iced::{alignment, Alignment, Length}; use liana::miniscript::bitcoin; use liana_ui::{ color, - component::{amount::*, card, event, text::*}, + component::{amount::*, button, card, event, text::*}, icon, theme, util::Collection, widget::*, @@ -27,7 +27,7 @@ pub fn home_view<'a>( balance: &'a bitcoin::Amount, unconfirmed_balance: &'a bitcoin::Amount, remaining_sequence: &Option, - number_of_expiring_coins: usize, + expiring_coins: &Vec, pending_events: &[HistoryTransaction], events: &Vec, ) -> Element<'a, Message> { @@ -48,7 +48,7 @@ pub fn home_view<'a>( None }), ) - .push_maybe(if number_of_expiring_coins == 0 { + .push_maybe(if expiring_coins.is_empty() { remaining_sequence.map(|sequence| { Container::new( Row::new() @@ -75,13 +75,21 @@ pub fn home_view<'a>( } else { Some( Container::new( - Row::new().spacing(15).align_items(Alignment::Center).push( - h4_regular(format!( - "You have {} coins that are already or about to be expired", - number_of_expiring_coins - )) - .width(Length::Fill), - ), + Row::new() + .spacing(15) + .align_items(Alignment::Center) + .push( + h4_regular(format!( + "You have {} coins that are already or about to be expired", + expiring_coins.len(), + )) + .width(Length::Fill), + ) + .push( + button::primary(Some(icon::arrow_repeat()), "Refresh coins").on_press( + Message::Menu(Menu::RefreshCoins(expiring_coins.clone())), + ), + ), ) .padding(25) .style(theme::Card::Invalid), diff --git a/gui/src/app/view/mod.rs b/gui/src/app/view/mod.rs index d88ad951..699e0cb3 100644 --- a/gui/src/app/view/mod.rs +++ b/gui/src/app/view/mod.rs @@ -177,7 +177,7 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { .center_x(), ) .style(theme::Button::Menu(true)) - .on_press(Message::Reload) + .on_press(Message::Menu(Menu::CreateSpendTx)) .width(iced::Length::Fill), menu_green_bar() ) diff --git a/gui/src/app/view/spend/mod.rs b/gui/src/app/view/spend/mod.rs index 5cf664fe..80932b1d 100644 --- a/gui/src/app/view/spend/mod.rs +++ b/gui/src/app/view/spend/mod.rs @@ -93,12 +93,13 @@ pub fn create_spend_tx<'a>( feerate: &form::Value, error: Option<&Error>, ) -> Element<'a, Message> { + let is_self_send = recipients.is_empty(); dashboard( &Menu::CreateSpendTx, cache, error, Column::new() - .push(h3("Send")) + .push(h3(if is_self_send { "Self send" } else { "Send" })) .push( Column::new() .push(Column::with_children(recipients).spacing(10)) @@ -116,12 +117,16 @@ pub fn create_spend_tx<'a>( None }) .push(Space::with_width(Length::Fill)) - .push( - button::secondary(Some(icon::plus_icon()), "Add recipient") - .on_press(Message::CreateSpend( - CreateSpendMessage::AddRecipient, - )), - ), + .push_maybe(if is_self_send { + None + } else { + Some( + button::secondary(Some(icon::plus_icon()), "Add recipient") + .on_press(Message::CreateSpend( + CreateSpendMessage::AddRecipient, + )), + ) + }), ) .spacing(20), ) @@ -151,7 +156,26 @@ pub fn create_spend_tx<'a>( Row::new() .align_items(Alignment::Center) .push(p1_bold("Coins selection").width(Length::Fill)) - .push(Container::new(if let Some(amount_left) = amount_left { + .push(if is_self_send { + Row::new() + .spacing(5) + .push(amount_with_size( + &Amount::from_sat( + coins + .iter() + .filter_map(|(coin, selected)| { + if *selected { + Some(coin.amount.to_sat()) + } else { + None + } + }) + .sum(), + ), + P2_SIZE, + )) + .push(p2_regular("selected").style(color::GREY_3)) + } else if let Some(amount_left) = amount_left { Row::new() .spacing(5) .push(amount_with_size(amount_left, P2_SIZE)) @@ -159,7 +183,7 @@ pub fn create_spend_tx<'a>( } else { Row::new() .push(text("Feerate needs to be set.").style(color::GREY_3)) - })) + }) .width(Length::Fill), ) .push( @@ -193,8 +217,9 @@ pub fn create_spend_tx<'a>( ) .push( if is_valid - && total_amount < *balance_available - && Some(&Amount::from_sat(0)) == amount_left + && (is_self_send + || (total_amount < *balance_available + && Some(&Amount::from_sat(0)) == amount_left)) { button::primary(None, "Next") .on_press(Message::CreateSpend(CreateSpendMessage::Generate)) diff --git a/gui/ui/Cargo.toml b/gui/ui/Cargo.toml index cf064395..647eb492 100644 --- a/gui/ui/Cargo.toml +++ b/gui/ui/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -iced = { version = "0.7", features = ["svg", "image"] } +iced = { version = "0.7", default_features = false, features = ["svg", "image", "glow"] } iced_native = "0.8" iced_lazy = { version = "0.4"} bitcoin = "0.29" diff --git a/gui/ui/src/icon.rs b/gui/ui/src/icon.rs index b5590c7e..3337bb19 100644 --- a/gui/ui/src/icon.rs +++ b/gui/ui/src/icon.rs @@ -22,6 +22,10 @@ pub fn arrow_right() -> Text<'static> { icon('\u{F138}') } +pub fn arrow_repeat() -> Text<'static> { + icon('\u{F130}') +} + pub fn arrow_return_right() -> Text<'static> { icon('\u{F132}') }