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 ab50e82a..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,6 +96,37 @@ 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.feerate.valid && !self.feerate.value.is_empty(); self.is_duplicate = false; 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 5e97e8bb..80932b1d 100644 --- a/gui/src/app/view/spend/mod.rs +++ b/gui/src/app/view/spend/mod.rs @@ -117,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), ) 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}') }