diff --git a/gui/src/app/state/mod.rs b/gui/src/app/state/mod.rs index 0bb43a10..49d9e1dc 100644 --- a/gui/src/app/state/mod.rs +++ b/gui/src/app/state/mod.rs @@ -47,6 +47,7 @@ pub trait State { pub struct Home { wallet: Arc, balance: Amount, + unconfirmed_balance: Amount, recovery_warning: Option<(Amount, usize)>, recovery_alert: Option<(Amount, usize)>, pending_events: Vec, @@ -57,21 +58,22 @@ pub struct Home { impl Home { pub fn new(wallet: Arc, coins: &[Coin]) -> Self { + let (balance, unconfirmed_balance) = coins.iter().fold( + (Amount::from_sat(0), Amount::from_sat(0)), + |(balance, unconfirmed_balance), coin| { + if coin.spend_info.is_some() { + (balance, unconfirmed_balance) + } else if coin.block_height.is_some() { + (balance + coin.amount, unconfirmed_balance) + } else { + (balance, unconfirmed_balance + coin.amount) + } + }, + ); Self { wallet, - balance: Amount::from_sat( - coins - .iter() - .map(|coin| { - // If the coin is not spent and is its transaction is confirmed - if coin.spend_info.is_none() && coin.block_height.is_some() { - coin.amount.to_sat() - } else { - 0 - } - }) - .sum(), - ), + balance, + unconfirmed_balance, recovery_alert: None, recovery_warning: None, selected_event: None, @@ -103,6 +105,7 @@ impl State for Home { None, view::home::home_view( &self.balance, + &self.unconfirmed_balance, self.recovery_warning.as_ref(), self.recovery_alert.as_ref(), &self.pending_events, @@ -123,19 +126,25 @@ impl State for Home { Ok(coins) => { self.warning = None; self.balance = Amount::from_sat(0); + self.unconfirmed_balance = Amount::from_sat(0); let mut recovery_warning = (Amount::from_sat(0), 0); let mut recovery_alert = (Amount::from_sat(0), 0); for coin in coins { - if coin.spend_info.is_none() && coin.block_height.is_some() { - self.balance += coin.amount; - let timelock = self.wallet.main_descriptor.first_timelock_value(); - let seq = remaining_sequence(&coin, cache.blockheight as u32, timelock); - if seq == 0 { - recovery_alert.0 += coin.amount; - recovery_alert.1 += 1; - } else if seq < timelock as u32 * 10 / 100 { - recovery_warning.0 += coin.amount; - recovery_warning.1 += 1; + if coin.spend_info.is_none() { + if coin.block_height.is_some() { + self.balance += coin.amount; + let timelock = self.wallet.main_descriptor.first_timelock_value(); + let seq = + remaining_sequence(&coin, cache.blockheight as u32, timelock); + if seq == 0 { + recovery_alert.0 += coin.amount; + recovery_alert.1 += 1; + } else if seq < timelock as u32 * 10 / 100 { + recovery_warning.0 += coin.amount; + recovery_warning.1 += 1; + } + } else { + self.unconfirmed_balance += coin.amount; } } } diff --git a/gui/src/app/view/coins.rs b/gui/src/app/view/coins.rs index 790bea90..8ac14b16 100644 --- a/gui/src/app/view/coins.rs +++ b/gui/src/app/view/coins.rs @@ -62,15 +62,12 @@ fn coin_list_view( .push(badge::coin()) .push(if coin.spend_info.is_some() { badge::spent() + } else if coin.block_height.is_none() { + badge::unconfirmed() } else { let seq = remaining_sequence(coin, blockheight, timelock); coin_sequence_label(seq, timelock as u32) }) - .push_maybe(if coin.block_height.is_none() { - Some(badge::unconfirmed()) - } else { - None - }) .spacing(10) .align_items(Alignment::Center) .width(Length::Fill), @@ -208,31 +205,28 @@ pub fn coin_sequence_label<'a, T: 'a>(seq: u32, timelock: u32) -> Container<'a, /// returns y,m,d,h,m pub fn expire_message(sequence: u32) -> String { let mut n_minutes = sequence * 10; - let n_years = n_minutes / 525960; - n_minutes -= n_years * 525960; - let n_months = n_minutes / 43830; - n_minutes -= n_months * 43830; - let n_days = n_minutes / 1440; - n_minutes -= n_days * 1440; - let n_hours = n_minutes / 60; - n_minutes -= n_hours * 60; + if n_minutes <= 1440 { + "Expires today".to_string() + } else if n_minutes <= 2 * 1440 { + "Expires in ≈ 2 days".to_string() + } else { + let n_years = n_minutes / 525960; + n_minutes -= n_years * 525960; + let n_months = n_minutes / 43830; + n_minutes -= n_months * 43830; + let n_days = n_minutes / 1440; - let units: Vec = [ - (n_years, "year"), - (n_months, "month"), - (n_days, "day"), - (n_hours, "hour"), - (n_minutes, "minute"), - ] - .iter() - .filter_map(|(n, u)| { - if *n != 0 { - Some(format!("{} {}{}", n, u, if *n > 1 { "s" } else { "" })) - } else { - None - } - }) - .collect(); + let units: Vec = [(n_years, "year"), (n_months, "month"), (n_days, "day")] + .iter() + .filter_map(|(n, u)| { + if *n != 0 { + Some(format!("{} {}{}", n, u, if *n > 1 { "s" } else { "" })) + } else { + None + } + }) + .collect(); - format!("Expires in {}", units.join(",")) + format!("Expires in {}", units.join(",")) + } } diff --git a/gui/src/app/view/home.rs b/gui/src/app/view/home.rs index 90fbf4e7..a85b704e 100644 --- a/gui/src/app/view/home.rs +++ b/gui/src/app/view/home.rs @@ -17,6 +17,7 @@ pub const HISTORY_EVENT_PAGE_SIZE: u64 = 20; pub fn home_view<'a>( balance: &'a bitcoin::Amount, + unconfirmed_balance: &'a bitcoin::Amount, recovery_warning: Option<&(bitcoin::Amount, usize)>, recovery_alert: Option<&(bitcoin::Amount, usize)>, pending_events: &[HistoryTransaction], @@ -24,7 +25,21 @@ pub fn home_view<'a>( ) -> Element<'a, Message> { Column::new() .push(h3("Balance")) - .push(amount_with_size(balance, H1_SIZE)) + .push( + Column::new() + .push(amount_with_size(balance, H1_SIZE)) + .push_maybe(if unconfirmed_balance.to_sat() != 0 { + Some( + Row::new() + .spacing(10) + .push(text("+").size(H3_SIZE).style(color::GREY_3)) + .push(unconfirmed_amount_with_size(unconfirmed_balance, H3_SIZE)) + .push(text("unconfirmed").size(H3_SIZE).style(color::GREY_3)), + ) + } else { + None + }), + ) .push_maybe(recovery_warning.map(|(a, c)| { Row::new() .spacing(15) diff --git a/gui/src/app/view/spend/step.rs b/gui/src/app/view/spend/step.rs index df606985..8d87565c 100644 --- a/gui/src/app/view/spend/step.rs +++ b/gui/src/app/view/spend/step.rs @@ -209,15 +209,12 @@ fn coin_list_view<'a>( .push(badge::coin()) .push(if coin.spend_info.is_some() { badge::spent() + } else if coin.block_height.is_none() { + badge::unconfirmed() } else { let seq = remaining_sequence(coin, blockheight, timelock); coins::coin_sequence_label(seq, timelock as u32) }) - .push_maybe(if coin.block_height.is_none() { - Some(badge::unconfirmed()) - } else { - None - }) .spacing(10) .align_items(Alignment::Center) .width(Length::Fill), diff --git a/gui/ui/src/component/amount.rs b/gui/ui/src/component/amount.rs index 03971ebe..1c780e8d 100644 --- a/gui/ui/src/component/amount.rs +++ b/gui/ui/src/component/amount.rs @@ -12,9 +12,9 @@ pub fn amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a, T> { assert!(sats.len() >= 9); let row = Row::new() .spacing(spacing) - .push(split_digits(sats[0..sats.len() - 6].to_string(), size).into()) + .push(split_digits(sats[0..sats.len() - 6].to_string(), size, true).into()) .push(if a.to_sat() < 1_000_000 { - split_digits(sats[sats.len() - 6..sats.len() - 3].to_string(), size).into() + split_digits(sats[sats.len() - 6..sats.len() - 3].to_string(), size, true).into() } else { Row::new() .push( @@ -25,7 +25,7 @@ pub fn amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a, T> { .into() }) .push(if a.to_sat() < 1000 { - split_digits(sats[sats.len() - 3..sats.len()].to_string(), size).into() + split_digits(sats[sats.len() - 3..sats.len()].to_string(), size, true).into() } else { Row::new() .push( @@ -44,7 +44,42 @@ pub fn amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a, T> { .align_items(iced::Alignment::Center) } -fn split_digits<'a, T: 'a>(mut s: String, size: u16) -> impl Into> { +pub fn unconfirmed_amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a, T> { + let spacing = if size > P1_SIZE { 10 } else { 5 }; + let sats = format!("{:.8}", a.to_btc()); + assert!(sats.len() >= 9); + let row = Row::new() + .spacing(spacing) + .push(split_digits(sats[0..sats.len() - 6].to_string(), size, false).into()) + .push(if a.to_sat() < 1_000_000 { + split_digits( + sats[sats.len() - 6..sats.len() - 3].to_string(), + size, + false, + ) + .into() + } else { + Row::new() + .push(text(sats[sats.len() - 6..sats.len() - 3].to_string()).size(size)) + .into() + }) + .push(if a.to_sat() < 1000 { + split_digits(sats[sats.len() - 3..sats.len()].to_string(), size, false).into() + } else { + Row::new() + .push(text(sats[sats.len() - 3..sats.len()].to_string()).size(size)) + .into() + }); + + Row::with_children(vec![ + row.into(), + text("BTC").size(size).style(color::GREY_3).into(), + ]) + .spacing(spacing) + .align_items(iced::Alignment::Center) +} + +fn split_digits<'a, T: 'a>(mut s: String, size: u16, bold: bool) -> impl Into> { let prefixes = vec!["0.00", "0.0", "0.", "000", "00", "0"]; for prefix in prefixes { if s.starts_with(prefix) { @@ -53,10 +88,16 @@ fn split_digits<'a, T: 'a>(mut s: String, size: u16) -> impl Into .push(text(s).size(size).style(color::GREY_3)) .push_maybe(if right.is_empty() { None - } else { + } else if bold { Some(text(right).bold().size(size)) + } else { + Some(text(right).size(size)) }); } } - Row::new().push(text(s).bold().size(size)) + if bold { + Row::new().push(text(s).bold().size(size)) + } else { + Row::new().push(text(s).size(size)) + } }