From 07fbc0c1b3f3b55ebd7e684e2cf15032703c54db Mon Sep 17 00:00:00 2001 From: edouard Date: Fri, 20 Oct 2023 18:16:35 +0200 Subject: [PATCH] Attach outpoint and txid labels for single payment transaction --- gui/src/app/state/label.rs | 61 ++++++++++++++---------- gui/src/app/state/psbt.rs | 5 +- gui/src/app/state/spend/step.rs | 2 +- gui/src/app/view/coins.rs | 24 ++++++++-- gui/src/app/view/home.rs | 54 ++++++++++++++++------ gui/src/app/view/label.rs | 4 +- gui/src/app/view/message.rs | 2 +- gui/src/app/view/psbt.rs | 12 ++--- gui/src/app/view/receive.rs | 8 +++- gui/src/app/view/transactions.rs | 25 +++++++--- gui/src/daemon/model.rs | 79 ++++++++++++++++++++++++++++---- 11 files changed, 201 insertions(+), 75 deletions(-) diff --git a/gui/src/app/state/label.rs b/gui/src/app/state/label.rs index a8d435f0..b593b32a 100644 --- a/gui/src/app/state/label.rs +++ b/gui/src/app/state/label.rs @@ -26,34 +26,45 @@ impl LabelsEdited { targets: T, ) -> Result, Error> { match message { - Message::View(view::Message::Label(labelled, msg)) => match msg { + Message::View(view::Message::Label(items, msg)) => match msg { view::LabelMessage::Edited(value) => { let valid = value.len() <= 100; - if let Some(label) = self.0.get_mut(&labelled) { - label.valid = valid; - label.value = value; - } else { - self.0.insert(labelled, form::Value { valid, value }); + for item in items { + if let Some(label) = self.0.get_mut(&item) { + label.valid = valid; + label.value = value.clone(); + } else { + self.0.insert( + item, + form::Value { + valid, + value: value.clone(), + }, + ); + } } } view::LabelMessage::Cancel => { - self.0.remove(&labelled); + for item in items { + self.0.remove(&item); + } } view::LabelMessage::Confirm => { - if let Some(label) = self.0.get(&labelled).cloned() { - return Ok(Command::perform( - async move { - if let Some(item) = label_item_from_str(&labelled) { - daemon.update_labels(&HashMap::from([( - item, - label.value.clone(), - )]))?; - } - Ok(HashMap::from([(labelled, label.value)])) - }, - Message::LabelsUpdated, - )); + let mut updated_labels = HashMap::::new(); + let mut updated_labels_str = HashMap::::new(); + for item in items { + if let Some(label) = self.0.get(&item).cloned() { + updated_labels.insert(label_item_from_str(&item), label.value.clone()); + updated_labels_str.insert(item, label.value); + } } + return Ok(Command::perform( + async move { + daemon.update_labels(&updated_labels)?; + Ok(updated_labels_str) + }, + Message::LabelsUpdated, + )); } }, Message::LabelsUpdated(res) => match res { @@ -75,14 +86,14 @@ impl LabelsEdited { } } -pub fn label_item_from_str(s: &str) -> Option { +pub fn label_item_from_str(s: &str) -> LabelItem { if let Ok(addr) = bitcoin::Address::from_str(s) { - Some(LabelItem::Address(addr.assume_checked())) + LabelItem::Address(addr.assume_checked()) } else if let Ok(txid) = bitcoin::Txid::from_str(s) { - Some(LabelItem::Txid(txid)) + LabelItem::Txid(txid) } else if let Ok(outpoint) = bitcoin::OutPoint::from_str(s) { - Some(LabelItem::OutPoint(outpoint)) + LabelItem::OutPoint(outpoint) } else { - None + unreachable!() } } diff --git a/gui/src/app/state/psbt.rs b/gui/src/app/state/psbt.rs index 6d27bc71..6a3f2b51 100644 --- a/gui/src/app/state/psbt.rs +++ b/gui/src/app/state/psbt.rs @@ -184,10 +184,7 @@ impl Action for SaveAction { let psbt = tx.psbt.clone(); let mut labels = HashMap::::new(); for (item, label) in tx.labels() { - labels.insert( - label_item_from_str(item).expect("Must be a LabelItem"), - label.clone(), - ); + labels.insert(label_item_from_str(item), label.clone()); } return Command::perform( async move { diff --git a/gui/src/app/state/spend/step.rs b/gui/src/app/state/spend/step.rs index 77775286..99e611f6 100644 --- a/gui/src/app/state/spend/step.rs +++ b/gui/src/app/state/spend/step.rs @@ -533,7 +533,7 @@ impl Step for SaveSpend { if !recipient.label.value.is_empty() { let label = recipient.label.value.clone(); tx.labels - .insert(tx.psbt.unsigned_tx.txid().to_string(), label.clone()); + .insert(tx.psbt.unsigned_tx.txid().to_string(), label); } } diff --git a/gui/src/app/view/coins.rs b/gui/src/app/view/coins.rs index d8a32f6c..c96da27c 100644 --- a/gui/src/app/view/coins.rs +++ b/gui/src/app/view/coins.rs @@ -75,7 +75,25 @@ fn coin_list_view<'a>( .push(badge::coin()) .push(if !collapsed { if let Some(label) = labels.get(&outpoint) { - Container::new(p1_bold(label)).width(Length::Fill) + if !label.is_empty() { + Container::new(p1_bold(label)).width(Length::Fill) + } else if let Some(label) = labels.get(&txid) { + Container::new( + Row::new() + .spacing(5) + .push( + // It it not possible to know if a coin is a + // change coin or not so for now, From is + // enough + p1_bold("From").style(color::GREY_3), + ) + .push(p1_bold(label)), + ) + .width(Length::Fill) + } else { + Container::new(Space::with_width(Length::Fill)) + .width(Length::Fill) + } } else if let Some(label) = labels.get(&txid) { Container::new( Row::new() @@ -124,10 +142,10 @@ fn coin_list_view<'a>( .spacing(5) .push( Container::new(if let Some(label) = labels_editing.get(&outpoint) { - label::label_editing(outpoint.clone(), label, P1_SIZE) + label::label_editing(vec![outpoint.clone()], label, P1_SIZE) } else { label::label_editable( - outpoint.clone(), + vec![outpoint.clone()], labels.get(&outpoint), P1_SIZE, ) diff --git a/gui/src/app/view/home.rs b/gui/src/app/view/home.rs index 652268b3..cbb44cd5 100644 --- a/gui/src/app/view/home.rs +++ b/gui/src/app/view/home.rs @@ -19,7 +19,7 @@ use crate::{ menu::Menu, view::{coins, dashboard, label, message::Message}, }, - daemon::model::HistoryTransaction, + daemon::model::{HistoryTransaction, TransactionKind}, }; pub const HISTORY_EVENT_PAGE_SIZE: u64 = 20; @@ -103,7 +103,7 @@ pub fn home_view<'a>( .push(pending_events.iter().enumerate().fold( Column::new().spacing(10), |col, (i, event)| { - if !event.is_self_send() { + if !event.is_send_to_self() { col.push(event_list_view(i, event)) } else { col @@ -113,7 +113,7 @@ pub fn home_view<'a>( .push(events.iter().enumerate().fold( Column::new().spacing(10), |col, (i, event)| { - if !event.is_self_send() { + if !event.is_send_to_self() { col.push(event_list_view(i + pending_events.len(), event)) } else { col @@ -223,17 +223,33 @@ pub fn payment_view<'a>( cache, warning, Column::new() - .push(if tx.is_self_send() { - Container::new(h3("Payment")).width(Length::Fill) - } else if tx.is_external() { - Container::new(h3("Incoming payment")).width(Length::Fill) - } else { - Container::new(h3("Outgoing payment")).width(Length::Fill) + .push(match tx.kind { + TransactionKind::OutgoingSinglePayment(_) + | TransactionKind::OutgoingPaymentBatch(_) => { + Container::new(h3("Outgoing payment")).width(Length::Fill) + } + TransactionKind::IncomingSinglePayment(_) + | TransactionKind::IncomingPaymentBatch(_) => { + Container::new(h3("Incoming payment")).width(Length::Fill) + } + _ => Container::new(h3("Payment")).width(Length::Fill), }) - .push(if let Some(label) = labels_editing.get(&outpoint) { - label::label_editing(outpoint.clone(), label, H3_SIZE) + .push(if tx.is_single_payment().is_some() { + // if the payment is a payment of a single payment transaction then + // the label of the transaction is attached to the label of the payment outpoint + if let Some(label) = labels_editing.get(&outpoint) { + label::label_editing(vec![outpoint.clone(), txid.clone()], label, H3_SIZE) + } else { + label::label_editable( + vec![outpoint.clone(), txid.clone()], + tx.labels.get(&outpoint), + H3_SIZE, + ) + } + } else if let Some(label) = labels_editing.get(&outpoint) { + label::label_editing(vec![outpoint.clone()], label, H3_SIZE) } else { - label::label_editable(outpoint.clone(), tx.labels.get(&outpoint), H1_SIZE) + label::label_editable(vec![outpoint.clone()], tx.labels.get(&outpoint), H3_SIZE) }) .push(Container::new(amount_with_size( &Amount::from_sat(tx.tx.output[output_index].value), @@ -241,10 +257,18 @@ pub fn payment_view<'a>( ))) .push(Space::with_height(H3_SIZE)) .push(Container::new(h3("Transaction")).width(Length::Fill)) - .push(if let Some(label) = labels_editing.get(&txid) { - label::label_editing(txid.clone(), label, H3_SIZE) + .push_maybe(if tx.is_batch() { + if let Some(label) = labels_editing.get(&txid) { + Some(label::label_editing(vec![txid.clone()], label, H3_SIZE)) + } else { + Some(label::label_editable( + vec![txid.clone()], + tx.labels.get(&txid), + H3_SIZE, + )) + } } else { - label::label_editable(txid.clone(), tx.labels.get(&txid), H3_SIZE) + None }) .push_maybe(tx.fee_amount.map(|fee_amount| { Row::new() diff --git a/gui/src/app/view/label.rs b/gui/src/app/view/label.rs index c7d2dd71..5079d690 100644 --- a/gui/src/app/view/label.rs +++ b/gui/src/app/view/label.rs @@ -10,7 +10,7 @@ use liana_ui::{ use crate::app::view; pub fn label_editable( - labelled: String, + labelled: Vec, label: Option<&String>, size: u16, ) -> Element<'_, view::Message> { @@ -50,7 +50,7 @@ pub fn label_editable( } pub fn label_editing( - labelled: String, + labelled: Vec, label: &form::Value, size: u16, ) -> Element { diff --git a/gui/src/app/view/message.rs b/gui/src/app/view/message.rs index 3d634eca..d8d3033d 100644 --- a/gui/src/app/view/message.rs +++ b/gui/src/app/view/message.rs @@ -9,7 +9,7 @@ pub enum Message { Close, Select(usize), SelectSub(usize, usize), - Label(String, LabelMessage), + Label(Vec, LabelMessage), Settings(SettingsMessage), CreateSpend(CreateSpendMessage), ImportSpend(ImportSpendMessage), diff --git a/gui/src/app/view/psbt.rs b/gui/src/app/view/psbt.rs index 0189617b..42aadaf5 100644 --- a/gui/src/app/view/psbt.rs +++ b/gui/src/app/view/psbt.rs @@ -199,9 +199,9 @@ pub fn spend_header<'a>( Column::new() .spacing(20) .push(if let Some(label) = labels_editing.get(&txid) { - label::label_editing(txid.clone(), label, H3_SIZE) + label::label_editing(vec![txid.clone()], label, H3_SIZE) } else { - label::label_editable(txid.clone(), tx.labels.get(&txid), H1_SIZE) + label::label_editable(vec![txid.clone()], tx.labels.get(&txid), H1_SIZE) }) .push( Column::new() @@ -738,10 +738,10 @@ fn input_view<'a>( .align_items(Alignment::Center) .push( Container::new(if let Some(label) = labels_editing.get(&outpoint) { - label::label_editing(outpoint.clone(), label, text::P1_SIZE) + label::label_editing(vec![outpoint.clone()], label, text::P1_SIZE) } else { label::label_editable( - outpoint.clone(), + vec![outpoint.clone()], labels.get(&outpoint), text::P1_SIZE, ) @@ -825,10 +825,10 @@ fn payment_view<'a>( .align_items(Alignment::Center) .push( Container::new(if let Some(label) = labels_editing.get(&outpoint) { - label::label_editing(outpoint.clone(), label, text::P1_SIZE) + label::label_editing(vec![outpoint.clone()], label, text::P1_SIZE) } else { label::label_editable( - outpoint.clone(), + vec![outpoint.clone()], labels.get(&outpoint), text::P1_SIZE, ) diff --git a/gui/src/app/view/receive.rs b/gui/src/app/view/receive.rs index fcc5ae69..7b7a2c97 100644 --- a/gui/src/app/view/receive.rs +++ b/gui/src/app/view/receive.rs @@ -52,10 +52,14 @@ pub fn receive<'a>( card::simple( Column::new() .push(if let Some(label) = labels_editing.get(&addr) { - label::label_editing(addr.clone(), label, text::P1_SIZE) + label::label_editing( + vec![addr.clone()], + label, + text::P1_SIZE, + ) } else { label::label_editable( - addr.clone(), + vec![addr.clone()], labels.get(&addr), text::P1_SIZE, ) diff --git a/gui/src/app/view/transactions.rs b/gui/src/app/view/transactions.rs index 838afe44..ca2de2ef 100644 --- a/gui/src/app/view/transactions.rs +++ b/gui/src/app/view/transactions.rs @@ -92,7 +92,7 @@ fn tx_list_view(i: usize, tx: &HistoryTransaction) -> Element<'_, Message> { Row::new() .push(if tx.is_external() { badge::receive() - } else if tx.is_self_send() { + } else if tx.is_send_to_self() { badge::cycle() } else { badge::spend() @@ -165,22 +165,35 @@ pub fn tx_view<'a>( cache, warning, Column::new() - .push(if tx.is_self_send() { + .push(if tx.is_send_to_self() { Container::new(h3("Transaction")).width(Length::Fill) } else if tx.is_external() { Container::new(h3("Incoming transaction")).width(Length::Fill) } else { Container::new(h3("Outgoing transaction")).width(Length::Fill) }) - .push(if let Some(label) = labels_editing.get(&txid) { - label::label_editing(txid.clone(), label, H3_SIZE) + .push(if let Some(outpoint) = tx.is_single_payment() { + // if the payment is a payment of a single payment transaction then + // the label of the transaction is attached to the label of the payment outpoint + let outpoint = outpoint.to_string(); + if let Some(label) = labels_editing.get(&outpoint) { + label::label_editing(vec![outpoint.clone(), txid.clone()], label, H3_SIZE) + } else { + label::label_editable( + vec![outpoint.clone(), txid.clone()], + tx.labels.get(&outpoint), + H3_SIZE, + ) + } + } else if let Some(label) = labels_editing.get(&txid) { + label::label_editing(vec![txid.clone()], label, H3_SIZE) } else { - label::label_editable(txid.clone(), tx.labels.get(&txid), H1_SIZE) + label::label_editable(vec![txid.clone()], tx.labels.get(&txid), H1_SIZE) }) .push( Column::new().spacing(20).push( Column::new() - .push(if tx.is_self_send() { + .push(if tx.is_send_to_self() { Container::new(h1("Self-transfer")) } else if tx.is_external() { Container::new(amount_with_size(&tx.incoming_amount, H1_SIZE)) diff --git a/gui/src/daemon/model.rs b/gui/src/daemon/model.rs index ae5c36e7..b37485a4 100644 --- a/gui/src/daemon/model.rs +++ b/gui/src/daemon/model.rs @@ -193,6 +193,7 @@ pub struct HistoryTransaction { pub fee_amount: Option, pub height: Option, pub time: Option, + pub kind: TransactionKind, } impl HistoryTransaction { @@ -228,6 +229,47 @@ impl HistoryTransaction { Self { labels: HashMap::new(), + kind: if coins.is_empty() { + if change_indexes.len() == 1 { + TransactionKind::IncomingSinglePayment(OutPoint { + txid: tx.txid(), + vout: change_indexes[0] as u32, + }) + } else { + TransactionKind::IncomingPaymentBatch( + change_indexes + .iter() + .map(|i| OutPoint { + txid: tx.txid(), + vout: *i as u32, + }) + .collect(), + ) + } + } else if outgoing_amount == Amount::from_sat(0) { + TransactionKind::SendToSelf + } else { + let outpoints: Vec = tx + .output + .iter() + .enumerate() + .filter_map(|(i, _)| { + if !change_indexes.contains(&i) { + Some(OutPoint { + txid: tx.txid(), + vout: i as u32, + }) + } else { + None + } + }) + .collect(); + if outpoints.len() == 1 { + TransactionKind::OutgoingSinglePayment(outpoints[0]) + } else { + TransactionKind::OutgoingPaymentBatch(outpoints) + } + }, tx, coins, change_indexes, @@ -241,24 +283,41 @@ impl HistoryTransaction { } pub fn is_external(&self) -> bool { - self.coins.is_empty() + matches!( + self.kind, + TransactionKind::IncomingSinglePayment(_) | TransactionKind::IncomingPaymentBatch(_) + ) } - pub fn is_self_send(&self) -> bool { - !self.coins.is_empty() && self.outgoing_amount == Amount::from_sat(0) + pub fn is_send_to_self(&self) -> bool { + matches!(self.kind, TransactionKind::SendToSelf) + } + + pub fn is_single_payment(&self) -> Option { + match self.kind { + TransactionKind::IncomingSinglePayment(outpoint) => Some(outpoint), + TransactionKind::OutgoingSinglePayment(outpoint) => Some(outpoint), + _ => None, + } } pub fn is_batch(&self) -> bool { - self.tx - .output - .iter() - .enumerate() - .filter(|(i, _)| !self.change_indexes.contains(i)) - .count() - > 1 + matches!( + self.kind, + TransactionKind::IncomingPaymentBatch(_) | TransactionKind::OutgoingPaymentBatch(_) + ) } } +#[derive(Debug, Clone)] +pub enum TransactionKind { + IncomingSinglePayment(OutPoint), + IncomingPaymentBatch(Vec), + SendToSelf, + OutgoingSinglePayment(OutPoint), + OutgoingPaymentBatch(Vec), +} + impl Labelled for HistoryTransaction { fn labels(&mut self) -> &mut HashMap { &mut self.labels