Attach outpoint and txid labels for single payment transaction

This commit is contained in:
edouard 2023-10-20 18:16:35 +02:00
parent bdac902307
commit 07fbc0c1b3
11 changed files with 201 additions and 75 deletions

View File

@ -26,34 +26,45 @@ impl LabelsEdited {
targets: T,
) -> Result<Command<Message>, 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::<LabelItem, String>::new();
let mut updated_labels_str = HashMap::<String, String>::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<LabelItem> {
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!()
}
}

View File

@ -184,10 +184,7 @@ impl Action for SaveAction {
let psbt = tx.psbt.clone();
let mut labels = HashMap::<LabelItem, String>::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 {

View File

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

View File

@ -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,
)

View File

@ -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()

View File

@ -10,7 +10,7 @@ use liana_ui::{
use crate::app::view;
pub fn label_editable(
labelled: String,
labelled: Vec<String>,
label: Option<&String>,
size: u16,
) -> Element<'_, view::Message> {
@ -50,7 +50,7 @@ pub fn label_editable(
}
pub fn label_editing(
labelled: String,
labelled: Vec<String>,
label: &form::Value<String>,
size: u16,
) -> Element<view::Message> {

View File

@ -9,7 +9,7 @@ pub enum Message {
Close,
Select(usize),
SelectSub(usize, usize),
Label(String, LabelMessage),
Label(Vec<String>, LabelMessage),
Settings(SettingsMessage),
CreateSpend(CreateSpendMessage),
ImportSpend(ImportSpendMessage),

View File

@ -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,
)

View File

@ -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,
)

View File

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

View File

@ -193,6 +193,7 @@ pub struct HistoryTransaction {
pub fee_amount: Option<Amount>,
pub height: Option<i32>,
pub time: Option<u32>,
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<OutPoint> = 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<OutPoint> {
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<OutPoint>),
SendToSelf,
OutgoingSinglePayment(OutPoint),
OutgoingPaymentBatch(Vec<OutPoint>),
}
impl Labelled for HistoryTransaction {
fn labels(&mut self) -> &mut HashMap<String, String> {
&mut self.labels