From bdac902307c25ee02093f369aca9258edacc4a4e Mon Sep 17 00:00:00 2001 From: edouard Date: Fri, 20 Oct 2023 16:44:43 +0200 Subject: [PATCH 1/8] Change coin change labelling --- gui/src/app/state/coins.rs | 12 +++-- gui/src/app/state/spend/step.rs | 18 -------- gui/src/app/view/coins.rs | 33 +++++++++++++- gui/src/app/view/home.rs | 2 +- gui/src/app/view/psbt.rs | 78 ++++++--------------------------- 5 files changed, 53 insertions(+), 90 deletions(-) diff --git a/gui/src/app/state/coins.rs b/gui/src/app/state/coins.rs index 23490db5..3474d37d 100644 --- a/gui/src/app/state/coins.rs +++ b/gui/src/app/state/coins.rs @@ -29,10 +29,13 @@ pub struct Coins { impl Labelled for Coins { fn labelled(&self) -> Vec { - self.list - .iter() - .map(|a| LabelItem::OutPoint(a.outpoint)) - .collect() + let mut items = Vec::new(); + for coin in &self.list { + items.push(LabelItem::OutPoint(coin.outpoint)); + items.push(LabelItem::Txid(coin.outpoint.txid)); + items.push(LabelItem::Address(coin.address.clone())); + } + items } fn labels(&mut self) -> &mut HashMap { &mut self.labels @@ -169,6 +172,7 @@ impl State for CoinsPanel { let mut targets = HashSet::::new(); for coin in coins { targets.insert(LabelItem::OutPoint(coin.outpoint)); + targets.insert(LabelItem::Txid(coin.outpoint.txid)); targets.insert(LabelItem::Address(coin.address)); } daemon2.get_labels(&targets).map_err(|e| e.into()) diff --git a/gui/src/app/state/spend/step.rs b/gui/src/app/state/spend/step.rs index b43f8092..77775286 100644 --- a/gui/src/app/state/spend/step.rs +++ b/gui/src/app/state/spend/step.rs @@ -528,30 +528,12 @@ impl Step for SaveSpend { if let Some(label) = &draft.batch_label { tx.labels .insert(tx.psbt.unsigned_tx.txid().to_string(), label.clone()); - for (i, output) in tx.psbt.unsigned_tx.output.iter().enumerate() { - let address_str = Address::from_script(&output.script_pubkey, tx.network) - .unwrap() - .to_string(); - if tx.change_indexes.contains(&i) && tx.labels.contains_key(&address_str) { - tx.labels - .insert(address_str, format!("Change of {}", label.clone())); - } - } } } else if let Some(recipient) = draft.recipients.first() { if !recipient.label.value.is_empty() { let label = recipient.label.value.clone(); tx.labels .insert(tx.psbt.unsigned_tx.txid().to_string(), label.clone()); - for (i, output) in tx.psbt.unsigned_tx.output.iter().enumerate() { - let address_str = Address::from_script(&output.script_pubkey, tx.network) - .unwrap() - .to_string(); - if tx.change_indexes.contains(&i) && tx.labels.contains_key(&address_str) { - tx.labels - .insert(address_str, format!("Change of {}", label.clone())); - } - } } } diff --git a/gui/src/app/view/coins.rs b/gui/src/app/view/coins.rs index 1bab2fec..d8a32f6c 100644 --- a/gui/src/app/view/coins.rs +++ b/gui/src/app/view/coins.rs @@ -64,6 +64,7 @@ fn coin_list_view<'a>( ) -> Container<'a, Message> { let outpoint = coin.outpoint.to_string(); let address = coin.address.to_string(); + let txid = coin.outpoint.txid.to_string(); Container::new( Column::new() .push( @@ -75,6 +76,19 @@ fn coin_list_view<'a>( .push(if !collapsed { if let Some(label) = labels.get(&outpoint) { 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) @@ -141,6 +155,21 @@ fn coin_list_view<'a>( }) .push( Column::new() + .push( + Row::new() + .align_items(Alignment::Center) + .push( + p2_regular("Address label:") + .bold() + .style(color::GREY_2), + ) + .push(if let Some(label) = labels.get(&address) { + p2_regular(label).style(color::GREY_2) + } else { + p2_regular("No label").style(color::GREY_2) + }) + .spacing(5), + ) .push( Row::new() .align_items(Alignment::Center) @@ -166,11 +195,11 @@ fn coin_list_view<'a>( Row::new() .align_items(Alignment::Center) .push( - p2_regular("Address label:") + p2_regular("Deposit transaction label:") .bold() .style(color::GREY_2), ) - .push(if let Some(label) = labels.get(&address) { + .push(if let Some(label) = labels.get(&txid) { p2_regular(label).style(color::GREY_2) } else { p2_regular("No label").style(color::GREY_2) diff --git a/gui/src/app/view/home.rs b/gui/src/app/view/home.rs index 131a9f8e..652268b3 100644 --- a/gui/src/app/view/home.rs +++ b/gui/src/app/view/home.rs @@ -237,7 +237,7 @@ pub fn payment_view<'a>( }) .push(Container::new(amount_with_size( &Amount::from_sat(tx.tx.output[output_index].value), - H1_SIZE, + H3_SIZE, ))) .push(Space::with_height(H3_SIZE)) .push(Container::new(h3("Transaction")).width(Length::Fill)) diff --git a/gui/src/app/view/psbt.rs b/gui/src/app/view/psbt.rs index b0bad4ce..0189617b 100644 --- a/gui/src/app/view/psbt.rs +++ b/gui/src/app/view/psbt.rs @@ -707,15 +707,8 @@ pub fn inputs_and_outputs_view<'a>( .filter(|(i, _)| change_indexes.as_ref().unwrap().contains(i)) .fold( Column::new().padding(20), - |col: Column<'a, Message>, (i, output)| { - col.spacing(10).push(change_view( - i, - tx.txid(), - output, - network, - labels, - labels_editing, - )) + |col: Column<'a, Message>, (_, output)| { + col.spacing(10).push(change_view(output, network)) }, ) .into() @@ -881,77 +874,32 @@ fn payment_view<'a>( .into() } -fn change_view<'a>( - i: usize, - txid: Txid, - output: &'a TxOut, - network: Network, - labels: &'a HashMap, - labels_editing: &'a HashMap>, -) -> Element<'a, Message> { +fn change_view(output: &TxOut, network: Network) -> Element { let addr = Address::from_script(&output.script_pubkey, network) .unwrap() .to_string(); - let outpoint = OutPoint { - txid, - vout: i as u32, - } - .to_string(); - Column::new() + Row::new() .width(Length::Fill) .spacing(5) .push( Row::new() - .spacing(5) .align_items(Alignment::Center) - .push( - Container::new(if let Some(label) = labels_editing.get(&outpoint) { - label::label_editing(outpoint.clone(), label, text::P1_SIZE) - } else { - label::label_editable( - outpoint.clone(), - labels.get(&outpoint), - text::P1_SIZE, - ) - }) - .width(Length::Fill), - ) - .push(amount(&Amount::from_sat(output.value))), - ) - .push( - Column::new() + .width(Length::Fill) .push( Row::new() .align_items(Alignment::Center) .width(Length::Fill) + .spacing(5) + .push(p1_bold("Address:").style(color::GREY_3)) + .push(p2_regular(addr.clone()).style(color::GREY_3)) .push( - Row::new() - .align_items(Alignment::Center) - .width(Length::Fill) - .spacing(5) - .push(p1_bold("Address:").style(color::GREY_3)) - .push(p2_regular(addr.clone()).style(color::GREY_3)) - .push( - Button::new(icon::clipboard_icon().style(color::GREY_3)) - .on_press(Message::Clipboard(addr.clone())) - .style(theme::Button::TransparentBorder), - ), + Button::new(icon::clipboard_icon().style(color::GREY_3)) + .on_press(Message::Clipboard(addr)) + .style(theme::Button::TransparentBorder), ), - ) - .push_maybe(labels.get(&addr).map(|label| { - Row::new() - .align_items(Alignment::Center) - .width(Length::Fill) - .push( - Row::new() - .align_items(Alignment::Center) - .width(Length::Fill) - .spacing(5) - .push(p1_bold("Address label:").style(color::GREY_3)) - .push(p2_regular(label).style(color::GREY_3)), - ) - })), + ), ) + .push(amount(&Amount::from_sat(output.value))) .into() } From 07fbc0c1b3f3b55ebd7e684e2cf15032703c54db Mon Sep 17 00:00:00 2001 From: edouard Date: Fri, 20 Oct 2023 18:16:35 +0200 Subject: [PATCH 2/8] 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 From 1aae2a52da94b6a3791a7a5b058e9e6a23483089 Mon Sep 17 00:00:00 2001 From: edouard Date: Fri, 20 Oct 2023 18:29:45 +0200 Subject: [PATCH 3/8] Add From: txid label for coin selection view --- gui/src/app/state/spend/mod.rs | 1 + gui/src/app/view/spend/mod.rs | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/gui/src/app/state/spend/mod.rs b/gui/src/app/state/spend/mod.rs index 31a585a5..3cea189d 100644 --- a/gui/src/app/state/spend/mod.rs +++ b/gui/src/app/state/spend/mod.rs @@ -126,6 +126,7 @@ impl State for CreateSpendPanel { let mut targets = HashSet::::new(); for coin in coins { targets.insert(LabelItem::OutPoint(coin.outpoint)); + targets.insert(LabelItem::Txid(coin.outpoint.txid)); } daemon2.get_labels(&targets).map_err(|e| e.into()) }, diff --git a/gui/src/app/view/spend/mod.rs b/gui/src/app/view/spend/mod.rs index 8c34ad5e..cb9b6552 100644 --- a/gui/src/app/view/spend/mod.rs +++ b/gui/src/app/view/spend/mod.rs @@ -361,6 +361,19 @@ fn coin_list_view<'a>( .push( if let Some(label) = coins_labels.get(&coin.outpoint.to_string()) { Container::new(p1_bold(label)).width(Length::Fill) + } else if let Some(label) = coins_labels.get(&coin.outpoint.txid.to_string()) { + 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(p1_bold("")).width(Length::Fill) }, From 0873cdb944e0bc4976b0f9dc6208fe68e322f330 Mon Sep 17 00:00:00 2001 From: edouard Date: Mon, 23 Oct 2023 12:09:22 +0200 Subject: [PATCH 4/8] disable confirm button if label len > 100 --- gui/src/app/view/label.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gui/src/app/view/label.rs b/gui/src/app/view/label.rs index 5079d690..c37b7e5d 100644 --- a/gui/src/app/view/label.rs +++ b/gui/src/app/view/label.rs @@ -60,7 +60,11 @@ pub fn label_editing( .warning("Invalid label length, cannot be superior to 100") .size(size) .padding(10), - button::primary(None, "Save").on_press(view::message::LabelMessage::Confirm), + if label.valid { + button::primary(None, "Save").on_press(view::message::LabelMessage::Confirm) + } else { + button::primary(None, "Save") + }, button::primary(None, "Cancel").on_press(view::message::LabelMessage::Cancel) ) .spacing(5) From 28aaca919aa7a1afc63a659780c6af063578a5ef Mon Sep 17 00:00:00 2001 From: edouard Date: Mon, 23 Oct 2023 15:18:30 +0200 Subject: [PATCH 5/8] gui: update liana:master --- gui/Cargo.lock | 2 +- gui/src/app/message.rs | 2 +- gui/src/app/state/label.rs | 14 ++++++++++---- gui/src/app/state/psbt.rs | 6 ++++-- gui/src/daemon/client/mod.rs | 4 ++-- gui/src/daemon/embedded.rs | 2 +- gui/src/daemon/mod.rs | 11 +++++++++-- gui/src/daemon/model.rs | 10 +++++++--- 8 files changed, 35 insertions(+), 16 deletions(-) diff --git a/gui/Cargo.lock b/gui/Cargo.lock index 118698a5..e9d6e8c7 100644 --- a/gui/Cargo.lock +++ b/gui/Cargo.lock @@ -2113,7 +2113,7 @@ dependencies = [ [[package]] name = "liana" version = "2.0.0" -source = "git+https://github.com/wizardsardine/liana?branch=master#4f2ff1abc0e2700d563f3077eb71c22ef9573e5c" +source = "git+https://github.com/wizardsardine/liana?branch=master#6869d8554a6214d8e73614adf3c9334c1c6188d4" dependencies = [ "backtrace", "bip39", diff --git a/gui/src/app/message.rs b/gui/src/app/message.rs index 44bbd27e..ad35271f 100644 --- a/gui/src/app/message.rs +++ b/gui/src/app/message.rs @@ -35,5 +35,5 @@ pub enum Message { ConnectedHardwareWallets(Vec), HistoryTransactions(Result, Error>), PendingTransactions(Result, Error>), - LabelsUpdated(Result, Error>), + LabelsUpdated(Result>, Error>), } diff --git a/gui/src/app/state/label.rs b/gui/src/app/state/label.rs index b593b32a..faf4da46 100644 --- a/gui/src/app/state/label.rs +++ b/gui/src/app/state/label.rs @@ -50,12 +50,18 @@ impl LabelsEdited { } } view::LabelMessage::Confirm => { - let mut updated_labels = HashMap::::new(); - let mut updated_labels_str = HashMap::::new(); + 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); + let item_str = label_item_from_str(&item); + if label.value.is_empty() { + updated_labels.insert(item_str, None); + updated_labels_str.insert(item, None); + } else { + updated_labels.insert(item_str, Some(label.value.clone())); + updated_labels_str.insert(item, Some(label.value)); + } } } return Ok(Command::perform( diff --git a/gui/src/app/state/psbt.rs b/gui/src/app/state/psbt.rs index 6a3f2b51..841ff011 100644 --- a/gui/src/app/state/psbt.rs +++ b/gui/src/app/state/psbt.rs @@ -182,9 +182,11 @@ impl Action for SaveAction { Message::View(view::Message::Spend(view::SpendTxMessage::Confirm)) => { let daemon = daemon.clone(); let psbt = tx.psbt.clone(); - let mut labels = HashMap::::new(); + let mut labels = HashMap::>::new(); for (item, label) in tx.labels() { - labels.insert(label_item_from_str(item), label.clone()); + if !label.is_empty() { + labels.insert(label_item_from_str(item), Some(label.clone())); + } } return Command::perform( async move { diff --git a/gui/src/daemon/client/mod.rs b/gui/src/daemon/client/mod.rs index 185dfb2a..32268a7e 100644 --- a/gui/src/daemon/client/mod.rs +++ b/gui/src/daemon/client/mod.rs @@ -157,8 +157,8 @@ impl Daemon for Lianad { Ok(res.labels) } - fn update_labels(&self, items: &HashMap) -> Result<(), DaemonError> { - let labels: HashMap = + fn update_labels(&self, items: &HashMap>) -> Result<(), DaemonError> { + let labels: HashMap> = HashMap::from_iter(items.iter().map(|(a, l)| (a.to_string(), l.clone()))); let _res: serde_json::value::Value = self.call("updatelabels", Some(vec![labels]))?; Ok(()) diff --git a/gui/src/daemon/embedded.rs b/gui/src/daemon/embedded.rs index 5103932c..f7c64e6b 100644 --- a/gui/src/daemon/embedded.rs +++ b/gui/src/daemon/embedded.rs @@ -135,7 +135,7 @@ impl Daemon for EmbeddedDaemon { Ok(self.handle.control.get_labels(items).labels) } - fn update_labels(&self, items: &HashMap) -> Result<(), DaemonError> { + fn update_labels(&self, items: &HashMap>) -> Result<(), DaemonError> { self.handle.control.update_labels(items); Ok(()) } diff --git a/gui/src/daemon/mod.rs b/gui/src/daemon/mod.rs index 6d5b1743..1b680e30 100644 --- a/gui/src/daemon/mod.rs +++ b/gui/src/daemon/mod.rs @@ -5,6 +5,7 @@ pub mod model; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::io::ErrorKind; +use std::iter::FromIterator; use liana::{ commands::LabelItem, @@ -80,7 +81,8 @@ pub trait Daemon: Debug { &self, labels: &HashSet, ) -> Result, DaemonError>; - fn update_labels(&self, labels: &HashMap) -> Result<(), DaemonError>; + fn update_labels(&self, labels: &HashMap>) + -> Result<(), DaemonError>; fn list_spend_transactions(&self) -> Result, DaemonError> { let info = self.get_info()?; @@ -228,7 +230,12 @@ fn load_labels( items.insert(item); } } - let labels = daemon.get_labels(&items)?; + let labels = HashMap::from_iter( + daemon + .get_labels(&items)? + .into_iter() + .map(|(k, v)| (k, Some(v))), + ); for target in targets { target.load_labels(&labels); } diff --git a/gui/src/daemon/model.rs b/gui/src/daemon/model.rs index b37485a4..0afbffc4 100644 --- a/gui/src/daemon/model.rs +++ b/gui/src/daemon/model.rs @@ -346,13 +346,17 @@ impl Labelled for HistoryTransaction { pub trait Labelled { fn labelled(&self) -> Vec; fn labels(&mut self) -> &mut HashMap; - fn load_labels(&mut self, new_labels: &HashMap) { + fn load_labels(&mut self, new_labels: &HashMap>) { let items = self.labelled(); let labels = self.labels(); for item in items { let item_str = item.to_string(); - if let Some(l) = new_labels.get(&item_str) { - labels.insert(item_str, l.to_string()); + if let Some(label) = new_labels.get(&item_str) { + if let Some(l) = label { + labels.insert(item_str, l.to_string()); + } else { + labels.remove(&item_str); + } } } } From c170f7b25ba704beaa4e37a99302ccb25e0baa77 Mon Sep 17 00:00:00 2001 From: edouard Date: Mon, 23 Oct 2023 16:45:09 +0200 Subject: [PATCH 6/8] fix psbt labelling for single payment --- gui/src/app/view/psbt.rs | 17 +++++++++--- gui/src/app/view/psbts.rs | 4 +-- gui/src/daemon/model.rs | 54 ++++++++++++++++++++++++++++++--------- 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/gui/src/app/view/psbt.rs b/gui/src/app/view/psbt.rs index 42aadaf5..4a4f4e6b 100644 --- a/gui/src/app/view/psbt.rs +++ b/gui/src/app/view/psbt.rs @@ -198,14 +198,25 @@ pub fn spend_header<'a>( let txid = tx.psbt.unsigned_tx.txid().to_string(); Column::new() .spacing(20) - .push(if let Some(label) = labels_editing.get(&txid) { + .push(if let Some(outpoint) = tx.is_single_payment() { + 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(vec![txid.clone()], tx.labels.get(&txid), H1_SIZE) + label::label_editable(vec![txid.clone()], tx.labels.get(&txid), H3_SIZE) }) .push( Column::new() - .push(if tx.is_self_send() { + .push(if tx.is_send_to_self() { Container::new(h1("Self-transfer")) } else { Container::new(amount_with_size(&tx.spend_amount, H1_SIZE)) diff --git a/gui/src/app/view/psbts.rs b/gui/src/app/view/psbts.rs index 1cff99c9..a386cc7a 100644 --- a/gui/src/app/view/psbts.rs +++ b/gui/src/app/view/psbts.rs @@ -96,7 +96,7 @@ fn spend_tx_list_view(i: usize, tx: &SpendTx) -> Element<'_, Message> { Row::new() .push( Row::new() - .push(if tx.is_self_send() { + .push(if tx.is_send_to_self() { badge::cycle() } else { badge::spend() @@ -147,7 +147,7 @@ fn spend_tx_list_view(i: usize, tx: &SpendTx) -> Element<'_, Message> { .push( Column::new() .align_items(Alignment::End) - .push(if !tx.is_self_send() { + .push(if !tx.is_send_to_self() { Container::new(amount(&tx.spend_amount)) } else { Container::new(p1_regular("Self-transfer")) diff --git a/gui/src/daemon/model.rs b/gui/src/daemon/model.rs index 0afbffc4..005ab851 100644 --- a/gui/src/daemon/model.rs +++ b/gui/src/daemon/model.rs @@ -41,6 +41,7 @@ pub struct SpendTx { pub status: SpendStatus, pub sigs: PartialSpendInfo, pub updated_at: Option, + pub kind: TransactionKind, } #[derive(PartialOrd, Ord, Debug, Clone, PartialEq, Eq)] @@ -92,6 +93,31 @@ impl SpendTx { Self { labels: HashMap::new(), + kind: if spend_amount == Amount::from_sat(0) { + TransactionKind::SendToSelf + } else { + let outpoints: Vec = psbt + .unsigned_tx + .output + .iter() + .enumerate() + .filter_map(|(i, _)| { + if !change_indexes.contains(&i) { + Some(OutPoint { + txid: psbt.unsigned_tx.txid(), + vout: i as u32, + }) + } else { + None + } + }) + .collect(); + if outpoints.len() == 1 { + TransactionKind::OutgoingSinglePayment(outpoints[0]) + } else { + TransactionKind::OutgoingPaymentBatch(outpoints) + } + }, updated_at, coins, psbt, @@ -132,10 +158,6 @@ impl SpendTx { signers } - pub fn is_self_send(&self) -> bool { - !self.coins.is_empty() && self.spend_amount == Amount::from_sat(0) - } - /// Feerate obtained if all transaction inputs have the maximum satisfaction size. pub fn min_feerate_vb(&self) -> u64 { // This assumes all inputs are internal (have same max satisfaction size). @@ -144,15 +166,23 @@ impl SpendTx { self.fee_amount.to_sat() / max_tx_vbytes as u64 } + 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.psbt - .unsigned_tx - .output - .iter() - .enumerate() - .filter(|(i, _)| !self.change_indexes.contains(i)) - .count() - > 1 + matches!( + self.kind, + TransactionKind::IncomingPaymentBatch(_) | TransactionKind::OutgoingPaymentBatch(_) + ) } } From 8e806404c4e12f6d67cef320a4c5485723a0f993 Mon Sep 17 00:00:00 2001 From: edouard Date: Mon, 23 Oct 2023 18:21:10 +0200 Subject: [PATCH 7/8] change labels font weight for lists --- gui/src/app/view/coins.rs | 10 +++++----- gui/src/app/view/home.rs | 6 ++++-- gui/src/app/view/psbts.rs | 2 +- gui/src/app/view/spend/mod.rs | 8 ++++---- gui/src/app/view/transactions.rs | 4 +++- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/gui/src/app/view/coins.rs b/gui/src/app/view/coins.rs index c96da27c..c4ef4209 100644 --- a/gui/src/app/view/coins.rs +++ b/gui/src/app/view/coins.rs @@ -76,7 +76,7 @@ fn coin_list_view<'a>( .push(if !collapsed { if let Some(label) = labels.get(&outpoint) { if !label.is_empty() { - Container::new(p1_bold(label)).width(Length::Fill) + Container::new(p1_regular(label)).width(Length::Fill) } else if let Some(label) = labels.get(&txid) { Container::new( Row::new() @@ -85,9 +85,9 @@ fn coin_list_view<'a>( // 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), + p1_regular("From").style(color::GREY_3), ) - .push(p1_bold(label)), + .push(p1_regular(label)), ) .width(Length::Fill) } else { @@ -102,9 +102,9 @@ fn coin_list_view<'a>( // 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), + p1_regular("From").style(color::GREY_3), ) - .push(p1_bold(label)), + .push(p1_regular(label)), ) .width(Length::Fill) } else { diff --git a/gui/src/app/view/home.rs b/gui/src/app/view/home.rs index cbb44cd5..507da705 100644 --- a/gui/src/app/view/home.rs +++ b/gui/src/app/view/home.rs @@ -157,7 +157,7 @@ fn event_list_view(i: usize, event: &HistoryTransaction) -> Column<'_, Message> } .to_string(), ) { - Some(p1_bold(label)) + Some(p1_regular(label)) } else { event .labels @@ -166,7 +166,9 @@ fn event_list_view(i: usize, event: &HistoryTransaction) -> Column<'_, Message> .unwrap() .to_string(), ) - .map(|label| p1_bold(format!("address label: {}", label)).style(color::GREY_3)) + .map(|label| { + p1_regular(format!("address label: {}", label)).style(color::GREY_3) + }) }; if event.is_external() { if !event.change_indexes.contains(&output_index) { diff --git a/gui/src/app/view/psbts.rs b/gui/src/app/view/psbts.rs index a386cc7a..6e25a2bc 100644 --- a/gui/src/app/view/psbts.rs +++ b/gui/src/app/view/psbts.rs @@ -127,7 +127,7 @@ fn spend_tx_list_view(i: usize, tx: &SpendTx) -> Element<'_, Message> { .push_maybe( tx.labels .get(&tx.psbt.unsigned_tx.txid().to_string()) - .map(p1_bold), + .map(p1_regular), ) .spacing(10) .align_items(Alignment::Center) diff --git a/gui/src/app/view/spend/mod.rs b/gui/src/app/view/spend/mod.rs index cb9b6552..10ba1e65 100644 --- a/gui/src/app/view/spend/mod.rs +++ b/gui/src/app/view/spend/mod.rs @@ -360,7 +360,7 @@ fn coin_list_view<'a>( })) .push( if let Some(label) = coins_labels.get(&coin.outpoint.to_string()) { - Container::new(p1_bold(label)).width(Length::Fill) + Container::new(p1_regular(label)).width(Length::Fill) } else if let Some(label) = coins_labels.get(&coin.outpoint.txid.to_string()) { Container::new( Row::new() @@ -369,13 +369,13 @@ fn coin_list_view<'a>( // 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), + p1_regular("From").style(color::GREY_3), ) - .push(p1_bold(label)), + .push(p1_regular(label)), ) .width(Length::Fill) } else { - Container::new(p1_bold("")).width(Length::Fill) + Container::new(p1_regular("")).width(Length::Fill) }, ) .push(if coin.spend_info.is_some() { diff --git a/gui/src/app/view/transactions.rs b/gui/src/app/view/transactions.rs index ca2de2ef..4a0cff87 100644 --- a/gui/src/app/view/transactions.rs +++ b/gui/src/app/view/transactions.rs @@ -99,7 +99,9 @@ fn tx_list_view(i: usize, tx: &HistoryTransaction) -> Element<'_, Message> { }) .push( Column::new() - .push_maybe(tx.labels.get(&tx.tx.txid().to_string()).map(p1_bold)) + .push_maybe( + tx.labels.get(&tx.tx.txid().to_string()).map(p1_regular), + ) .push_maybe(tx.time.map(|t| { Container::new( text(format!( From b88781e62828cc6fc56e49f3ad1ebdeb39788573 Mon Sep 17 00:00:00 2001 From: edouard Date: Mon, 23 Oct 2023 18:28:53 +0200 Subject: [PATCH 8/8] remove font bold for labels --- gui/src/app/view/label.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gui/src/app/view/label.rs b/gui/src/app/view/label.rs index c37b7e5d..aac02bf5 100644 --- a/gui/src/app/view/label.rs +++ b/gui/src/app/view/label.rs @@ -3,7 +3,7 @@ use iced::{widget::row, Alignment}; use liana_ui::{ color, component::{button, form}, - font, icon, + icon, widget::*, }; @@ -18,7 +18,7 @@ pub fn label_editable( if !label.is_empty() { return Container::new( row!( - iced::widget::Text::new(label).size(size).font(font::BOLD), + iced::widget::Text::new(label).size(size), button::primary(Some(icon::pencil_icon()), "Edit").on_press( view::Message::Label( labelled, @@ -36,7 +36,6 @@ pub fn label_editable( row!( iced::widget::Text::new("Add Label") .size(size) - .font(font::BOLD) .style(color::GREY_3), button::primary(Some(icon::pencil_icon()), "Edit").on_press(view::Message::Label( labelled,