From dc817dee3be64bba5e8bee7a0ec646bb90e643d4 Mon Sep 17 00:00:00 2001 From: jp1ac4 <121959000+jp1ac4@users.noreply.github.com> Date: Wed, 31 Jan 2024 17:31:15 +0000 Subject: [PATCH] gui(transactions): check for direct descendants before rbf --- gui/src/app/message.rs | 1 + gui/src/app/state/transactions.rs | 50 +++++++++++++++++++++++--- gui/src/app/view/transactions.rs | 59 +++++++++++++++++++++++++++++-- 3 files changed, 104 insertions(+), 6 deletions(-) diff --git a/gui/src/app/message.rs b/gui/src/app/message.rs index d7495752..d8758e5b 100644 --- a/gui/src/app/message.rs +++ b/gui/src/app/message.rs @@ -43,4 +43,5 @@ pub enum Message { PendingTransactions(Result, Error>), LabelsUpdated(Result>, Error>), BroadcastModal(Result, Error>), + RbfModal(HistoryTransaction, bool, u64, Result, Error>), } diff --git a/gui/src/app/state/transactions.rs b/gui/src/app/state/transactions.rs index 17faa458..7dd66394 100644 --- a/gui/src/app/state/transactions.rs +++ b/gui/src/app/state/transactions.rs @@ -1,5 +1,5 @@ use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, convert::TryInto, sync::Arc, time::{SystemTime, UNIX_EPOCH}, @@ -112,6 +112,16 @@ impl State for TransactionsPanel { } } }, + Message::RbfModal(tx, is_cancel, prev_feerate_vb, res) => match res { + Ok(descendant_txids) => { + let modal = + CreateRbfModal::new(tx, is_cancel, prev_feerate_vb, descendant_txids); + self.create_rbf_modal = Some(modal); + } + Err(e) => { + self.warning = e.into(); + } + }, Message::View(view::Message::Close) => { self.selected_tx = None; } @@ -129,8 +139,30 @@ impl State for TransactionsPanel { .to_sat() .checked_div(tx.tx.vsize().try_into().unwrap()) .unwrap(); - let modal = CreateRbfModal::new(tx.clone(), is_cancel, prev_feerate_vb); - self.create_rbf_modal = Some(modal); + let tx = tx.clone(); + let txid = tx.tx.txid(); + return Command::perform( + async move { + daemon + // TODO: filter for spending coins when this is possible: + // https://github.com/wizardsardine/liana/issues/677 + .list_coins() + .map(|res| { + res.coins + .iter() + .filter_map(|c| { + if c.outpoint.txid == txid { + c.spend_info.map(|info| info.txid) + } else { + None + } + }) + .collect() + }) + .map_err(|e| e.into()) + }, + move |res| Message::RbfModal(tx, is_cancel, prev_feerate_vb, res), + ); } } } @@ -247,6 +279,9 @@ pub struct CreateRbfModal { is_cancel: bool, /// Min feerate required for RBF. min_feerate_vb: u64, + /// IDs of any transactions from this wallet that are direct descendants of + /// the transaction to be replaced. + descendant_txids: HashSet, /// Feerate form value. feerate_val: form::Value, /// Parsed feerate. @@ -259,12 +294,18 @@ pub struct CreateRbfModal { } impl CreateRbfModal { - fn new(tx: model::HistoryTransaction, is_cancel: bool, prev_feerate_vb: u64) -> Self { + fn new( + tx: model::HistoryTransaction, + is_cancel: bool, + prev_feerate_vb: u64, + descendant_txids: HashSet, + ) -> Self { let min_feerate_vb = prev_feerate_vb.checked_add(1).unwrap(); Self { tx, is_cancel, min_feerate_vb, + descendant_txids, feerate_val: form::Value { valid: true, value: min_feerate_vb.to_string(), @@ -329,6 +370,7 @@ impl CreateRbfModal { content, view::transactions::create_rbf_modal( self.is_cancel, + &self.descendant_txids, &self.feerate_val, self.replacement_txid, self.warning.as_ref(), diff --git a/gui/src/app/view/transactions.rs b/gui/src/app/view/transactions.rs index 77ab9955..d69f926b 100644 --- a/gui/src/app/view/transactions.rs +++ b/gui/src/app/view/transactions.rs @@ -1,5 +1,5 @@ use chrono::NaiveDateTime; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use iced::{alignment, widget::tooltip, Alignment, Length}; @@ -157,8 +157,13 @@ fn tx_list_view(i: usize, tx: &HistoryTransaction) -> Element<'_, Message> { .into() } +/// Return the modal view for a new RBF transaction. +/// +/// `descendant_txids` contains the IDs of any transactions from this wallet that are +/// direct descendants of the transaction to be replaced. pub fn create_rbf_modal<'a>( is_cancel: bool, + descendant_txids: &HashSet, feerate: &form::Value, replacement_txid: Option, warning: Option<&'a Error>, @@ -183,6 +188,56 @@ pub fn create_rbf_modal<'a>( .spacing(10) .push(Container::new(h4_bold("Transaction replacement")).width(Length::Fill)) .push(Row::new().push(text(help_text))) + .push_maybe(if descendant_txids.is_empty() { + None + } else { + Some( + descendant_txids.iter().fold( + Column::new() + .spacing(5) + .push(Row::new().spacing(10).push(icon::warning_icon()).push(text( + if descendant_txids.len() > 1 { + "WARNING: Replacing this transaction \ + will invalidate some later payments." + } else { + "WARNING: Replacing this transaction \ + will invalidate a later payment." + }, + ))) + .push(Row::new().padding([0, 30]).push(text( + if descendant_txids.len() > 1 { + "The following transactions are \ + spending one or more outputs \ + from the transaction to be replaced \ + and will be dropped when the replacement \ + is broadcast, along with any other \ + transactions that depend on them:" + } else { + "The following transaction is \ + spending one or more outputs \ + from the transaction to be replaced \ + and will be dropped when the replacement \ + is broadcast, 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_maybe(if !is_cancel { Some( Row::new() @@ -225,7 +280,7 @@ pub fn create_rbf_modal<'a>( ) })), ) - .width(Length::Fixed(600.0)) + .width(Length::Fixed(800.0)) .into() }