From 881d9a74a29eef21aafd75f9851f588142e0091f Mon Sep 17 00:00:00 2001 From: jp1ac4 <121959000+jp1ac4@users.noreply.github.com> Date: Thu, 1 Feb 2024 16:25:17 +0000 Subject: [PATCH] gui(psbt): check for conflicting txs before broadcast --- gui/src/app/message.rs | 3 +- gui/src/app/state/psbt.rs | 45 +++++++++++++++++++++++-- gui/src/app/view/psbt.rs | 70 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 111 insertions(+), 7 deletions(-) diff --git a/gui/src/app/message.rs b/gui/src/app/message.rs index 0a0ae5d2..d7495752 100644 --- a/gui/src/app/message.rs +++ b/gui/src/app/message.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use liana::{ @@ -42,4 +42,5 @@ pub enum Message { HistoryTransactions(Result, Error>), PendingTransactions(Result, Error>), LabelsUpdated(Result>, Error>), + BroadcastModal(Result, Error>), } diff --git a/gui/src/app/state/psbt.rs b/gui/src/app/state/psbt.rs index 88072960..3d18db5a 100644 --- a/gui/src/app/state/psbt.rs +++ b/gui/src/app/state/psbt.rs @@ -8,7 +8,7 @@ use iced::Subscription; use iced::Command; use liana::{ descriptors::LianaPolicy, - miniscript::bitcoin::{bip32::Fingerprint, psbt::Psbt, Network}, + miniscript::bitcoin::{bip32::Fingerprint, psbt::Psbt, Network, Txid}, }; use liana_ui::component::toast; @@ -167,7 +167,29 @@ impl PsbtState { return cmd; } Message::View(view::Message::Spend(view::SpendTxMessage::Broadcast)) => { - self.action = Some(PsbtAction::Broadcast(BroadcastAction::default())); + let outpoints: HashSet<_> = self.tx.coins.keys().cloned().collect(); + return Command::perform( + async move { + daemon + // TODO: filter for the outpoints in `tx.coins` when this is possible: + // https://github.com/wizardsardine/liana/issues/677 + .list_coins() + .map(|res| { + res.coins + .iter() + .filter_map(|c| { + if outpoints.contains(&c.outpoint) { + c.spend_info.map(|info| info.txid) + } else { + None + } + }) + .collect() + }) + .map_err(|e| e.into()) + }, + Message::BroadcastModal, + ); } Message::View(view::Message::Spend(view::SpendTxMessage::Save)) => { self.action = Some(PsbtAction::Save(SaveAction::default())); @@ -194,6 +216,17 @@ impl PsbtState { .update(daemon.clone(), message, &mut self.tx); } } + Message::BroadcastModal(res) => match res { + Ok(conflicting_txids) => { + self.action = Some(PsbtAction::Broadcast(BroadcastAction { + conflicting_txids, + ..Default::default() + })); + } + Err(e) => { + self.warning = Some(e); + } + }, _ => { if let Some(action) = self.action.as_mut() { return action @@ -277,6 +310,8 @@ impl Action for SaveAction { pub struct BroadcastAction { broadcast: bool, error: Option, + /// IDs of any directly conflicting transactions. + conflicting_txids: HashSet, } impl Action for BroadcastAction { @@ -314,7 +349,11 @@ impl Action for BroadcastAction { fn view<'a>(&'a self, content: Element<'a, view::Message>) -> Element<'a, view::Message> { modal::Modal::new( content, - view::psbt::broadcast_action(self.error.as_ref(), self.broadcast), + view::psbt::broadcast_action( + &self.conflicting_txids, + self.error.as_ref(), + self.broadcast, + ), ) .on_blur(Some(view::Message::Spend(view::SpendTxMessage::Cancel))) .into() diff --git a/gui/src/app/view/psbt.rs b/gui/src/app/view/psbt.rs index 081f37fe..a7035cad 100644 --- a/gui/src/app/view/psbt.rs +++ b/gui/src/app/view/psbt.rs @@ -140,7 +140,15 @@ pub fn save_action<'a>(warning: Option<&Error>, saved: bool) -> Element<'a, Mess } } -pub fn broadcast_action<'a>(warning: Option<&Error>, saved: bool) -> Element<'a, Message> { +/// Return the modal view to broadcast a transaction. +/// +/// `conflicting_txids` contains the IDs of any directly conflicting transactions +/// of the transaction to be broadcast. +pub fn broadcast_action<'a>( + conflicting_txids: &HashSet, + warning: Option<&Error>, + saved: bool, +) -> Element<'a, Message> { if saved { card::simple(text("Transaction is broadcast")) .width(Length::Fixed(400.0)) @@ -151,7 +159,59 @@ pub fn broadcast_action<'a>(warning: Option<&Error>, saved: bool) -> Element<'a, Column::new() .spacing(10) .push_maybe(warning.map(|w| warn(Some(w)))) - .push(text("Broadcast the transaction")) + .push(Container::new(h4_bold("Broadcast the transaction")).width(Length::Fill)) + .push_maybe(if conflicting_txids.is_empty() { + None + } else { + Some( + conflicting_txids.iter().fold( + Column::new() + .spacing(5) + .push(Row::new().spacing(10).push(icon::warning_icon()).push(text( + if conflicting_txids.len() > 1 { + "WARNING: Broadcasting this transaction \ + will invalidate some pending payments." + } else { + "WARNING: Broadcasting this transaction \ + will invalidate a pending payment." + }, + ))) + .push(Row::new().padding([0, 30]).push(text( + if conflicting_txids.len() > 1 { + "The following transactions are \ + spending one or more inputs \ + from the transaction to be \ + broadcast and will be \ + dropped, along with any other \ + transactions that depend on them:" + } else { + "The following transaction is \ + spending one or more inputs \ + from the transaction to be \ + broadcast and will be \ + dropped, along with any other \ + transactions that depend on it:" + }, + ))), + |col, txid| { + col.push( + Row::new() + .padding([0, 30]) + .spacing(5) + .align_items(Alignment::Center) + .push(text(txid.to_string())) + .push( + Button::new( + icon::clipboard_icon().style(color::GREY_3), + ) + .on_press(Message::Clipboard(txid.to_string())) + .style(theme::Button::TransparentBorder), + ), + ) + }, + ), + ) + }) .push( Row::new().push(Column::new().width(Length::Fill)).push( button::primary(None, "Broadcast") @@ -159,7 +219,11 @@ pub fn broadcast_action<'a>(warning: Option<&Error>, saved: bool) -> Element<'a, ), ), ) - .width(Length::Fixed(400.0)) + .width(Length::Fixed(if conflicting_txids.is_empty() { + 400.0 + } else { + 800.0 + })) .into() } }