diff --git a/liana-gui/src/app/message.rs b/liana-gui/src/app/message.rs index 84b97755..33d6cde6 100644 --- a/liana-gui/src/app/message.rs +++ b/liana-gui/src/app/message.rs @@ -40,6 +40,8 @@ pub enum Message { HardwareWallets(HardwareWalletMessage), HistoryTransactionsExtension(Result, Error>), HistoryTransactions(Result, Error>), + Payments(Result, Error>), + PaymentsExtension(Result, Error>), Payment(Result<(HistoryTransaction, usize), Error>), LabelsUpdated(Result>, Error>), BroadcastModal(Result, Error>), diff --git a/liana-gui/src/app/state/coins.rs b/liana-gui/src/app/state/coins.rs index 054241d1..2cdb9b22 100644 --- a/liana-gui/src/app/state/coins.rs +++ b/liana-gui/src/app/state/coins.rs @@ -7,6 +7,7 @@ use iced::Command; use liana_ui::widget::Element; use lianad::commands::CoinStatus; +use crate::daemon::model::LabelsLoader; use crate::{ app::{ cache::Cache, @@ -132,7 +133,7 @@ impl State for CoinsPanel { match self.labels_edited.update( daemon, message, - std::iter::once(&mut self.coins).map(|a| a as &mut dyn Labelled), + std::iter::once(&mut self.coins).map(|a| a as &mut dyn LabelsLoader), ) { Ok(cmd) => return cmd, Err(e) => { diff --git a/liana-gui/src/app/state/label.rs b/liana-gui/src/app/state/label.rs index 72f58c5c..1519f3ab 100644 --- a/liana-gui/src/app/state/label.rs +++ b/liana-gui/src/app/state/label.rs @@ -5,7 +5,7 @@ use std::{collections::HashMap, iter::IntoIterator, sync::Arc}; use crate::{ app::{error::Error, message::Message, view}, daemon::{ - model::{LabelItem, Labelled, LabelsLoader}, + model::{LabelItem, LabelsLoader}, Daemon, }, }; @@ -19,7 +19,7 @@ impl LabelsEdited { pub fn cache(&self) -> &HashMap> { &self.0 } - pub fn update<'a, T: IntoIterator>( + pub fn update<'a, T: IntoIterator>( &mut self, daemon: Arc, message: Message, diff --git a/liana-gui/src/app/state/mod.rs b/liana-gui/src/app/state/mod.rs index 88e34f08..2168c89a 100644 --- a/liana-gui/src/app/state/mod.rs +++ b/liana-gui/src/app/state/mod.rs @@ -28,8 +28,9 @@ use super::{ pub const HISTORY_EVENT_PAGE_SIZE: u64 = 20; +use crate::daemon::model::LabelsLoader; use crate::daemon::{ - model::{remaining_sequence, Coin, HistoryTransaction, Labelled}, + model::{remaining_sequence, Coin, HistoryTransaction, Payment}, Daemon, }; pub use coins::CoinsPanel; @@ -127,7 +128,7 @@ pub struct Home { unconfirmed_balance: Amount, remaining_sequence: Option, expiring_coins: Vec, - events: Vec, + events: Vec, is_last_page: bool, processing: bool, selected_event: Option<(HistoryTransaction, usize)>, @@ -170,7 +171,7 @@ impl State for Home { if let Some((tx, output_index)) = &self.selected_event { view::home::payment_view( cache, - &tx, + tx, *output_index, self.labels_edited.cache(), self.warning.as_ref(), @@ -217,7 +218,7 @@ impl State for Home { ); } }, - Message::HistoryTransactions(res) => match res { + Message::Payments(res) => match res { Err(e) => self.warning = Some(e), Ok(events) => { self.warning = None; @@ -225,7 +226,7 @@ impl State for Home { self.is_last_page = (self.events.len() as u64) < HISTORY_EVENT_PAGE_SIZE; } }, - Message::HistoryTransactionsExtension(res) => match res { + Message::PaymentsExtension(res) => match res { Err(e) => self.warning = Some(e), Ok(events) => { self.processing = false; @@ -235,13 +236,13 @@ impl State for Home { if let Some(position) = self .events .iter() - .position(|event2| event2.txid == event.txid) + .position(|event2| event2.outpoint == event.outpoint) { let len = self.events.len(); for event in events { if !self.events[position..len] .iter() - .any(|event2| event2.txid == event.txid) + .any(|event2| event2.outpoint == event.outpoint) { self.events.push(event); } @@ -274,12 +275,11 @@ impl State for Home { self.warning = Some(e); } }, - Message::View(view::Message::SelectSub(i, j)) => { - let txid = self.events[i].txid; + Message::View(view::Message::SelectPayment(outpoint)) => { return Command::perform( async move { - let tx = daemon.get_history_txs(&[txid]).await?.remove(0); - Ok((tx, j)) + let tx = daemon.get_history_txs(&[outpoint.txid]).await?.remove(0); + Ok((tx, outpoint.vout as usize)) }, Message::Payment, ); @@ -290,11 +290,11 @@ impl State for Home { message, self.events .iter_mut() - .map(|tx| tx as &mut dyn Labelled) + .map(|tx| tx as &mut dyn LabelsLoader) .chain( self.selected_event .iter_mut() - .map(|(tx, _)| tx as &mut dyn Labelled), + .map(|(tx, _)| tx as &mut dyn LabelsLoader), ), ) { Ok(cmd) => { @@ -319,9 +319,10 @@ impl State for Home { self.processing = true; return Command::perform( async move { + let last_event_date = last_event_date.timestamp() as u32; let mut limit = HISTORY_EVENT_PAGE_SIZE; let mut events = daemon - .list_history_txs(0_u32, last_event_date, limit) + .list_confirmed_payments(0_u32, last_event_date, limit) .await?; // because gethistory cursor is inclusive and use blocktime @@ -344,12 +345,14 @@ impl State for Home { { // increments of the equivalent of one page more. limit += HISTORY_EVENT_PAGE_SIZE; - events = daemon.list_history_txs(0, last_event_date, limit).await?; + events = daemon + .list_confirmed_payments(0, last_event_date, limit) + .await?; } events.sort_by(|a, b| a.compare(b)); Ok(events) }, - Message::HistoryTransactionsExtension, + Message::PaymentsExtension, ); } } @@ -382,16 +385,16 @@ impl State for Home { Command::batch(vec![ Command::perform( async move { - let mut txs = daemon - .list_history_txs(0, now, HISTORY_EVENT_PAGE_SIZE) + let mut payments = daemon + .list_confirmed_payments(0, now, HISTORY_EVENT_PAGE_SIZE) .await?; - txs.sort_by(|a, b| a.compare(b)); + payments.sort_by(|a, b| a.compare(b)); - let mut pending_txs = daemon.list_pending_txs().await?; - pending_txs.extend(txs); - Ok(pending_txs) + let mut pending_payments = daemon.list_pending_payments().await?; + pending_payments.extend(payments); + Ok(pending_payments) }, - Message::HistoryTransactions, + Message::Payments, ), Command::perform( async move { diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index c133865c..bfa1ccf3 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -18,6 +18,7 @@ use liana_ui::{ widget::Element, }; +use crate::daemon::model::LabelsLoader; use crate::{ app::{ cache::Cache, @@ -193,7 +194,7 @@ impl PsbtState { match self.labels_edited.update( daemon, message, - std::iter::once(&mut self.tx).map(|tx| tx as &mut dyn Labelled), + std::iter::once(&mut self.tx).map(|tx| tx as &mut dyn LabelsLoader), ) { Ok(cmd) => { return cmd; diff --git a/liana-gui/src/app/state/receive.rs b/liana-gui/src/app/state/receive.rs index 17ef1780..1dc177ad 100644 --- a/liana-gui/src/app/state/receive.rs +++ b/liana-gui/src/app/state/receive.rs @@ -9,6 +9,7 @@ use liana::miniscript::bitcoin::{ }; use liana_ui::{component::modal, widget::*}; +use crate::daemon::model::LabelsLoader; use crate::{ app::{ cache::Cache, @@ -117,7 +118,7 @@ impl State for ReceivePanel { match self.labels_edited.update( daemon, message, - std::iter::once(&mut self.addresses).map(|a| a as &mut dyn Labelled), + std::iter::once(&mut self.addresses).map(|a| a as &mut dyn LabelsLoader), ) { Ok(cmd) => cmd, Err(e) => { diff --git a/liana-gui/src/app/state/transactions.rs b/liana-gui/src/app/state/transactions.rs index 8a7c334a..ed41c166 100644 --- a/liana-gui/src/app/state/transactions.rs +++ b/liana-gui/src/app/state/transactions.rs @@ -27,7 +27,7 @@ use crate::{ view, wallet::Wallet, }, - daemon::model, + daemon::model::{self, LabelsLoader}, }; use crate::daemon::{ @@ -192,11 +192,14 @@ impl State for TransactionsPanel { match self.labels_edited.update( daemon, message, - self.txs.iter_mut().map(|tx| tx as &mut dyn Labelled).chain( - self.selected_tx - .iter_mut() - .map(|tx| tx as &mut dyn Labelled), - ), + self.txs + .iter_mut() + .map(|tx| tx as &mut dyn LabelsLoader) + .chain( + self.selected_tx + .iter_mut() + .map(|tx| tx as &mut dyn LabelsLoader), + ), ) { Ok(cmd) => { return cmd; diff --git a/liana-gui/src/app/view/home.rs b/liana-gui/src/app/view/home.rs index c7f809d9..8300d14f 100644 --- a/liana-gui/src/app/view/home.rs +++ b/liana-gui/src/app/view/home.rs @@ -23,7 +23,7 @@ use crate::{ view::{coins, dashboard, label, message::Message}, wallet::SyncStatus, }, - daemon::model::{HistoryTransaction, TransactionKind}, + daemon::model::{HistoryTransaction, Payment, PaymentKind, TransactionKind}, }; #[allow(clippy::too_many_arguments)] @@ -32,7 +32,7 @@ pub fn home_view<'a>( unconfirmed_balance: &'a bitcoin::Amount, remaining_sequence: &Option, expiring_coins: &[bitcoin::OutPoint], - events: &'a [HistoryTransaction], + events: &'a [Payment], is_last_page: bool, processing: bool, sync_status: &SyncStatus, @@ -147,16 +147,9 @@ pub fn home_view<'a>( Column::new() .spacing(10) .push(h4_bold("Last payments")) - .push(events.iter().enumerate().fold( - Column::new().spacing(10), - |col, (i, event)| { - if !event.is_send_to_self() { - col.push(event_list_view(i, event)) - } else { - col - } - }, - )) + .push(events.iter().fold(Column::new().spacing(10), |col, event| { + col.push(event_list_view(event)) + })) .push_maybe(if !is_last_page && !events.is_empty() { Some( Container::new( @@ -189,62 +182,48 @@ pub fn home_view<'a>( .into() } -fn event_list_view(i: usize, event: &HistoryTransaction) -> Column<'_, Message> { - event.tx.output.iter().enumerate().fold( - Column::new().spacing(10), - |col, (output_index, output)| { - let label = if let Some(label) = event.labels.get( - &bitcoin::OutPoint { - txid: event.tx.txid(), - vout: output_index as u32, - } - .to_string(), - ) { - Some(p1_regular(label)) - } else if let Ok(addr) = - bitcoin::Address::from_script(&output.script_pubkey, event.network) - { - event.labels.get(&addr.to_string()).map(|label| { - p1_regular(format!("address label: {}", label)).style(color::GREY_3) - }) - } else { - None - }; - if event.is_external() { - if !event.change_indexes.contains(&output_index) { - col - } else if let Some(t) = event.time { - col.push(event::confirmed_incoming_event( - label, - DateTime::::from_timestamp(t as i64, 0).unwrap(), - &output.value, - Message::SelectSub(i, output_index), - )) - } else { - col.push(event::unconfirmed_incoming_event( - label, - &output.value, - Message::SelectSub(i, output_index), - )) - } - } else if event.change_indexes.contains(&output_index) { - col - } else if let Some(t) = event.time { - col.push(event::confirmed_outgoing_event( - label, - DateTime::::from_timestamp(t as i64, 0).unwrap(), - &output.value, - Message::SelectSub(i, output_index), - )) - } else { - col.push(event::unconfirmed_outgoing_event( - label, - &output.value, - Message::SelectSub(i, output_index), - )) - } - }, - ) +fn event_list_view(event: &Payment) -> Element<'_, Message> { + let label = if let Some(label) = &event.label { + Some(p1_regular(label)) + } else { + event + .address_label + .as_ref() + .map(|label| p1_regular(format!("address label: {}", label)).style(color::GREY_3)) + }; + if event.kind == PaymentKind::Incoming { + if let Some(t) = event.time { + event::confirmed_incoming_event( + label, + t, + &event.amount, + Message::SelectPayment(event.outpoint), + ) + .into() + } else { + event::unconfirmed_incoming_event( + label, + &event.amount, + Message::SelectPayment(event.outpoint), + ) + .into() + } + } else if let Some(t) = event.time { + event::confirmed_outgoing_event( + label, + t, + &event.amount, + Message::SelectPayment(event.outpoint), + ) + .into() + } else { + event::unconfirmed_outgoing_event( + label, + &event.amount, + Message::SelectPayment(event.outpoint), + ) + .into() + } } pub fn payment_view<'a>( diff --git a/liana-gui/src/app/view/message.rs b/liana-gui/src/app/view/message.rs index c726b5fc..2a3581f3 100644 --- a/liana-gui/src/app/view/message.rs +++ b/liana-gui/src/app/view/message.rs @@ -1,5 +1,5 @@ use crate::{app::menu::Menu, node::bitcoind::RpcAuthType}; -use liana::miniscript::bitcoin::bip32::Fingerprint; +use liana::miniscript::bitcoin::{bip32::Fingerprint, OutPoint}; #[derive(Debug, Clone)] pub enum Message { @@ -8,7 +8,7 @@ pub enum Message { Menu(Menu), Close, Select(usize), - SelectSub(usize, usize), + SelectPayment(OutPoint), Label(Vec, LabelMessage), Settings(SettingsMessage), CreateSpend(CreateSpendMessage), diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index b6ca2890..073a3a7a 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -323,6 +323,37 @@ pub trait Daemon: Debug { load_labels(self, &mut txs).await?; Ok(txs) } + + async fn list_pending_payments(&self) -> Result, DaemonError> { + let mut txs = self.list_pending_txs().await?; + txs.sort_by(|a, b| b.time.cmp(&a.time)); + let events = txs.into_iter().fold(Vec::new(), |mut array, tx| { + let mut events = model::payments_from_tx(tx); + array.append(&mut events); + array + }); + + Ok(events) + } + + /// returns a sorted list of payments. + async fn list_confirmed_payments( + &self, + start: u32, + end: u32, + limit: u64, + ) -> Result, DaemonError> { + let mut txs = self.list_history_txs(start, end, limit).await?; + txs.sort_by(|a, b| b.time.cmp(&a.time)); + let events = txs.into_iter().fold(Vec::new(), |mut array, tx| { + let mut events = model::payments_from_tx(tx); + array.append(&mut events); + array + }); + + Ok(events) + } + /// Implemented by LianaLite backend async fn update_wallet_metadata( &self, diff --git a/liana-gui/src/daemon/model.rs b/liana-gui/src/daemon/model.rs index b69069b6..4b0abbdf 100644 --- a/liana-gui/src/daemon/model.rs +++ b/liana-gui/src/daemon/model.rs @@ -408,6 +408,96 @@ impl HistoryTransaction { } } +#[derive(Debug, Clone)] +pub struct Payment { + pub label: Option, + pub address: Option, + pub address_label: Option, + pub amount: Amount, + pub outpoint: OutPoint, + pub time: Option>, + pub kind: PaymentKind, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum PaymentKind { + Outgoing, + Incoming, +} + +impl Payment { + pub fn compare(&self, other: &Self) -> Ordering { + match (&self.time, &other.time) { + // `None` values come first + (None, Some(_)) => Ordering::Less, + (Some(_), None) => Ordering::Greater, + // Both are `None`, so we consider them equal + (None, None) => self + .outpoint + .txid + .cmp(&other.outpoint.txid) + .then_with(|| self.outpoint.vout.cmp(&other.outpoint.vout)), + // Both are `Some`, compare by descending time, then by txid + (Some(time1), Some(time2)) => time2 + .cmp(time1) + .then_with(|| self.outpoint.txid.cmp(&other.outpoint.txid)) + .then_with(|| self.outpoint.vout.cmp(&other.outpoint.vout)), + } + } +} + +impl LabelsLoader for Payment { + fn load_labels(&mut self, new_labels: &HashMap>) { + if let Some(label) = self.address.as_ref().and_then(|addr| new_labels.get(addr)) { + self.address_label = label.clone(); + } + if let Some(label) = new_labels.get(&self.outpoint.to_string()) { + self.label = label.clone(); + } + } +} + +pub fn payments_from_tx(history_tx: HistoryTransaction) -> Vec { + let time = history_tx + .time + .map(|t| chrono::DateTime::::from_timestamp(t as i64, 0).unwrap()); + history_tx + .tx + .output + .iter() + .enumerate() + .fold(Vec::new(), |mut array, (output_index, output)| { + if history_tx.is_external() && !history_tx.change_indexes.contains(&output_index) { + return array; + } + let outpoint = OutPoint { + txid: history_tx.tx.txid(), + vout: output_index as u32, + }; + let label = history_tx.labels.get(&outpoint.to_string()).cloned(); + let address = Address::from_script(&output.script_pubkey, history_tx.network) + .ok() + .map(|addr| addr.to_string()); + let address_label = address + .as_ref() + .and_then(|addr| history_tx.labels.get(addr).cloned()); + array.push(Payment { + label, + address, + address_label, + outpoint, + time, + amount: output.value, + kind: if history_tx.is_external() { + PaymentKind::Incoming + } else { + PaymentKind::Outgoing + }, + }); + array + }) +} + #[derive(Debug, Clone)] pub enum TransactionKind { IncomingSinglePayment(OutPoint),