From 6bff5a2f968f9a3f2b029e0222fdae019c5e6df7 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Mon, 20 Nov 2023 15:10:14 +0100 Subject: [PATCH] handle psbt missing input coins As coins deposited by an unconfirmed transactions can be removed from the database, GUI should not rely on them to calculate fees, but instead use the psbt input witness_utxo field. When importing an external psbt, gui checks now that the witness utxo is present. --- gui/src/app/state/psbts.rs | 9 ++- gui/src/app/view/psbt.rs | 55 ++++++++++------- gui/src/app/view/psbts.rs | 8 ++- gui/src/daemon/model.rs | 123 ++++++++++++++++++++++--------------- 4 files changed, 120 insertions(+), 75 deletions(-) diff --git a/gui/src/app/state/psbts.rs b/gui/src/app/state/psbts.rs index 3023ba34b..2917ad1ad 100644 --- a/gui/src/app/state/psbts.rs +++ b/gui/src/app/state/psbts.rs @@ -181,7 +181,14 @@ impl ImportPsbtModal { self.imported.valid = base64::decode(&self.imported.value) .ok() .and_then(|bytes| Psbt::deserialize(&bytes).ok()) - .is_some(); + .and_then(|psbt| { + if !psbt.inputs.iter().any(|input| input.witness_utxo.is_none()) { + Some(true) + } else { + None + } + }) + .is_some() } Message::View(view::Message::ImportSpend(view::ImportSpendMessage::Confirm)) => { if self.imported.valid { diff --git a/gui/src/app/view/psbt.rs b/gui/src/app/view/psbt.rs index a1ace3f00..0a9b4fc6e 100644 --- a/gui/src/app/view/psbt.rs +++ b/gui/src/app/view/psbt.rs @@ -521,7 +521,7 @@ pub fn path_view<'a>( } pub fn inputs_and_outputs_view<'a>( - coins: &'a [Coin], + coins: &'a HashMap, tx: &'a Transaction, network: Network, change_indexes: Option>, @@ -572,12 +572,17 @@ pub fn inputs_and_outputs_view<'a>( .style(theme::Button::TransparentBorder) }, move || { - coins + tx.input .iter() .fold( Column::new().spacing(10).padding(20), - |col: Column<'a, Message>, coin| { - col.push(input_view(coin, labels, labels_editing)) + |col: Column<'a, Message>, input| { + col.push(input_view( + &input.previous_output, + coins.get(&input.previous_output), + labels, + labels_editing, + )) }, ) .into() @@ -729,12 +734,12 @@ pub fn inputs_and_outputs_view<'a>( } fn input_view<'a>( - coin: &'a Coin, + outpoint: &'a OutPoint, + coin: Option<&'a Coin>, labels: &'a HashMap, labels_editing: &'a HashMap>, ) -> Element<'a, Message> { - let outpoint = coin.outpoint.to_string(); - let addr = coin.address.to_string(); + let outpoint = outpoint.to_string(); Column::new() .width(Length::Fill) .push( @@ -753,7 +758,7 @@ fn input_view<'a>( }) .width(Length::Fill), ) - .push(amount(&coin.amount)), + .push_maybe(coin.map(|c| amount(&c.amount))), ) .push( Column::new() @@ -765,11 +770,12 @@ fn input_view<'a>( .push(p2_regular(outpoint.clone()).style(color::GREY_3)) .push( Button::new(icon::clipboard_icon().style(color::GREY_3)) - .on_press(Message::Clipboard(coin.outpoint.to_string())) + .on_press(Message::Clipboard(outpoint.clone())) .style(theme::Button::TransparentBorder), ), ) - .push( + .push_maybe(coin.map(|c| { + let addr = c.address.to_string(); Row::new() .align_items(Alignment::Center) .width(Length::Fill) @@ -785,21 +791,26 @@ fn input_view<'a>( .on_press(Message::Clipboard(addr.clone())) .style(theme::Button::TransparentBorder), ), - ), - ) - .push_maybe(labels.get(&addr).map(|label| { - Row::new() - .align_items(Alignment::Center) - .width(Length::Fill) - .push( + ) + })) + .push_maybe( + coin.map(|c| { + labels.get(&c.address.to_string()).map(|label| { 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( + 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)), + ) + }) + }) + .flatten(), + ), ) .spacing(5) .into() diff --git a/gui/src/app/view/psbts.rs b/gui/src/app/view/psbts.rs index 6e25a2bcd..c95db487e 100644 --- a/gui/src/app/view/psbts.rs +++ b/gui/src/app/view/psbts.rs @@ -2,7 +2,7 @@ use iced::{widget::Space, Alignment, Length}; use liana_ui::{ color, - component::{amount::*, badge, button, card, form, text::*}, + component::{amount::*, badge, button, card, form, text::*, tooltip::tooltip}, icon, theme, util::Collection, widget::*, @@ -34,6 +34,12 @@ pub fn import_psbt_view<'a>( .size(20) .padding(10), ) + .push( + Row::new() + .spacing(5) + .push(p2_regular("Psbt must follow Liana standards").small()) + .push(tooltip("All inputs witness utxos must be present")), + ) .push(Row::new().push(Space::with_width(Length::Fill)).push( if imported.valid && !imported.value.is_empty() && !processing { button::primary(None, "Import") diff --git a/gui/src/daemon/model.rs b/gui/src/daemon/model.rs index b62c06f23..783ab3cfd 100644 --- a/gui/src/daemon/model.rs +++ b/gui/src/daemon/model.rs @@ -32,7 +32,7 @@ pub fn remaining_sequence(coin: &Coin, blockheight: u32, timelock: u16) -> u32 { #[derive(Debug, Clone)] pub struct SpendTx { pub network: Network, - pub coins: Vec, + pub coins: HashMap, pub labels: HashMap, pub psbt: Psbt, pub change_indexes: Vec, @@ -78,9 +78,15 @@ impl SpendTx { ); let mut inputs_amount = Amount::from_sat(0); + for input in &psbt.inputs { + if let Some(utxo) = &input.witness_utxo { + inputs_amount += Amount::from_sat(utxo.value); + } + } + let mut status = SpendStatus::Pending; - for coin in &coins { - inputs_amount += coin.amount; + let mut coins_map = HashMap::::new(); + for coin in coins { if let Some(info) = coin.spend_info { if info.txid == psbt.unsigned_tx.txid() { if info.height.is_some() { @@ -92,7 +98,14 @@ impl SpendTx { status = SpendStatus::Deprecated } } + coins_map.insert(coin.outpoint, coin); + } + + // One input coin is missing, the psbt is deprecated for now. + if coins_map.len() != psbt.inputs.len() { + status = SpendStatus::Deprecated } + let sigs = desc .partial_spend_info(&psbt) .expect("PSBT must be generated by Liana"); @@ -125,11 +138,15 @@ impl SpendTx { } }, updated_at, - coins, + coins: coins_map, psbt, change_indexes, spend_amount, - fee_amount: inputs_amount - spend_amount - change_amount, + fee_amount: if inputs_amount > spend_amount + change_amount { + inputs_amount - spend_amount + change_amount + } else { + Amount::from_sat(0) + }, max_sat_vbytes, status, sigs, @@ -200,7 +217,7 @@ impl Labelled for SpendTx { let mut items = Vec::new(); let txid = self.psbt.unsigned_tx.txid(); items.push(LabelItem::Txid(txid)); - for coin in &self.coins { + for coin in self.coins.values() { items.push(LabelItem::Address(coin.address.clone())); items.push(LabelItem::OutPoint(coin.outpoint)); } @@ -221,7 +238,7 @@ impl Labelled for SpendTx { pub struct HistoryTransaction { pub network: Network, pub labels: HashMap, - pub coins: Vec, + pub coins: HashMap, pub change_indexes: Vec, pub tx: Transaction, pub outgoing_amount: Amount, @@ -252,9 +269,53 @@ impl HistoryTransaction { }, ); + let 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) + } + }; + let mut inputs_amount = Amount::from_sat(0); - for coin in &coins { + let mut coins_map = HashMap::::new(); + for coin in coins { inputs_amount += coin.amount; + coins_map.insert(coin.outpoint, coin); } let fee_amount = if inputs_amount > outgoing_amount + incoming_amount { @@ -265,49 +326,9 @@ 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) - } - }, + kind, tx, - coins, + coins: coins_map, change_indexes, outgoing_amount, incoming_amount, @@ -362,7 +383,7 @@ impl Labelled for HistoryTransaction { let mut items = Vec::new(); let txid = self.tx.txid(); items.push(LabelItem::Txid(txid)); - for coin in &self.coins { + for coin in self.coins.values() { items.push(LabelItem::Address(coin.address.clone())); items.push(LabelItem::OutPoint(coin.outpoint)); }