Merge #1503: Introduce lighter struct Payment in home panel

79057a0cd9ea8010e91f9d7ea035a85c1b06bdb1 Do not include self transfer in home events (edouardparis)
790beaa6700b514f6f94b38acb356d4938ab87f5 Use payment model in the home (edouardparis)
b1abce5a244b2fb101bb4faeabadab4ec877ac3d Separate Labelled from LabelsLoader (edouardparis)
0d8f88839eb01d3fce0b71bea955b288ef1c27cb change event selection in home to load model (edouardparis)

Pull request description:

  Some users may have txs with a lot of inputs/outputs. We wrongly iter over them in the view which is the origin of performance issues.

ACKs for top commit:
  jp1ac4:
    Tested ACK 79057a0cd9ea8010e91f9d7ea035a85c1b06bdb1.

Tree-SHA512: 2c66703e35c1e67447cfd50d72e5dcd79ac7e1469e513d082001ec5ec94ca88c6a0a5e65f87e6ceb213e21b43e9bfa2005ca1c63fee4d230d2cd2d1374fd3212
This commit is contained in:
edouardparis 2024-12-19 14:29:04 +01:00
commit 7367abcb36
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
11 changed files with 258 additions and 106 deletions

View File

@ -40,6 +40,9 @@ pub enum Message {
HardwareWallets(HardwareWalletMessage),
HistoryTransactionsExtension(Result<Vec<HistoryTransaction>, Error>),
HistoryTransactions(Result<Vec<HistoryTransaction>, Error>),
Payments(Result<Vec<Payment>, Error>),
PaymentsExtension(Result<Vec<Payment>, Error>),
Payment(Result<(HistoryTransaction, usize), Error>),
LabelsUpdated(Result<HashMap<String, Option<String>>, Error>),
BroadcastModal(Result<HashSet<Txid>, Error>),
RbfModal(Box<HistoryTransaction>, bool, Result<HashSet<Txid>, Error>),

View File

@ -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) => {

View File

@ -5,7 +5,7 @@ use std::{collections::HashMap, iter::IntoIterator, sync::Arc};
use crate::{
app::{error::Error, message::Message, view},
daemon::{
model::{LabelItem, Labelled},
model::{LabelItem, LabelsLoader},
Daemon,
},
};
@ -19,7 +19,7 @@ impl LabelsEdited {
pub fn cache(&self) -> &HashMap<String, form::Value<String>> {
&self.0
}
pub fn update<'a, T: IntoIterator<Item = &'a mut dyn Labelled>>(
pub fn update<'a, T: IntoIterator<Item = &'a mut dyn LabelsLoader>>(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
message: Message,

View File

@ -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,10 +128,10 @@ pub struct Home {
unconfirmed_balance: Amount,
remaining_sequence: Option<u32>,
expiring_coins: Vec<OutPoint>,
events: Vec<HistoryTransaction>,
events: Vec<Payment>,
is_last_page: bool,
processing: bool,
selected_event: Option<(usize, usize)>,
selected_event: Option<(HistoryTransaction, usize)>,
labels_edited: LabelsEdited,
warning: Option<Error>,
}
@ -167,11 +168,11 @@ impl Home {
impl State for Home {
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> {
if let Some((i, output_index)) = self.selected_event {
if let Some((tx, output_index)) = &self.selected_event {
view::home::payment_view(
cache,
&self.events[i],
output_index,
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);
}
@ -266,11 +267,35 @@ impl State for Home {
return self.reload(daemon, self.wallet.clone());
}
}
Message::Payment(res) => match res {
Ok(event) => {
self.selected_event = Some(event);
}
Err(e) => {
self.warning = Some(e);
}
},
Message::View(view::Message::SelectPayment(outpoint)) => {
return Command::perform(
async move {
let tx = daemon.get_history_txs(&[outpoint.txid]).await?.remove(0);
Ok((tx, outpoint.vout as usize))
},
Message::Payment,
);
}
Message::View(view::Message::Label(_, _)) | Message::LabelsUpdated(_) => {
match self.labels_edited.update(
daemon,
message,
self.events.iter_mut().map(|tx| tx as &mut dyn Labelled),
self.events
.iter_mut()
.map(|tx| tx as &mut dyn LabelsLoader)
.chain(
self.selected_event
.iter_mut()
.map(|(tx, _)| tx as &mut dyn LabelsLoader),
),
) {
Ok(cmd) => {
return cmd;
@ -286,9 +311,7 @@ impl State for Home {
Message::View(view::Message::Close) => {
self.selected_event = None;
}
Message::View(view::Message::SelectSub(i, j)) => {
self.selected_event = Some((i, j));
}
Message::View(view::Message::Next) => {
if let Some(last) = self.events.last() {
let daemon = daemon.clone();
@ -296,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
@ -321,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,
);
}
}
@ -359,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 {

View File

@ -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;

View File

@ -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) => {

View File

@ -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;

View File

@ -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<u32>,
expiring_coins: &[bitcoin::OutPoint],
events: &'a [HistoryTransaction],
events: &'a [Payment],
is_last_page: bool,
processing: bool,
sync_status: &SyncStatus,
@ -147,16 +147,13 @@ 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| {
if event.kind != PaymentKind::SendToSelf {
col.push(event_list_view(event))
} else {
col
}
}))
.push_maybe(if !is_last_page && !events.is_empty() {
Some(
Container::new(
@ -189,62 +186,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::<Utc>::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::<Utc>::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>(

View File

@ -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<String>, LabelMessage),
Settings(SettingsMessage),
CreateSpend(CreateSpendMessage),

View File

@ -323,6 +323,37 @@ pub trait Daemon: Debug {
load_labels(self, &mut txs).await?;
Ok(txs)
}
async fn list_pending_payments(&self) -> Result<Vec<model::Payment>, 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<Vec<model::Payment>, 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,
@ -333,7 +364,7 @@ pub trait Daemon: Debug {
}
}
async fn load_labels<T: model::Labelled, D: Daemon + ?Sized>(
async fn load_labels<T: model::Labelled + model::LabelsLoader, D: Daemon + ?Sized>(
daemon: &D,
targets: &mut Vec<T>,
) -> Result<(), DaemonError> {

View File

@ -408,6 +408,99 @@ impl HistoryTransaction {
}
}
#[derive(Debug, Clone)]
pub struct Payment {
pub label: Option<String>,
pub address: Option<String>,
pub address_label: Option<String>,
pub amount: Amount,
pub outpoint: OutPoint,
pub time: Option<chrono::DateTime<chrono::Utc>>,
pub kind: PaymentKind,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PaymentKind {
Outgoing,
Incoming,
SendToSelf,
}
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<String, Option<String>>) {
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<Payment> {
let time = history_tx
.time
.map(|t| chrono::DateTime::<chrono::Utc>::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_send_to_self() {
PaymentKind::SendToSelf
} else if history_tx.is_external() {
PaymentKind::Incoming
} else {
PaymentKind::Outgoing
},
});
array
})
}
#[derive(Debug, Clone)]
pub enum TransactionKind {
IncomingSinglePayment(OutPoint),
@ -447,6 +540,16 @@ impl Labelled for HistoryTransaction {
pub trait Labelled {
fn labelled(&self) -> Vec<LabelItem>;
fn labels(&mut self) -> &mut HashMap<String, String>;
}
pub trait LabelsLoader {
fn load_labels(&mut self, new_labels: &HashMap<String, Option<String>>);
}
impl<T: ?Sized> LabelsLoader for T
where
T: Labelled,
{
fn load_labels(&mut self, new_labels: &HashMap<String, Option<String>>) {
let items = self.labelled();
let labels = self.labels();