gui(transactions): check for direct descendants before rbf

This commit is contained in:
jp1ac4 2024-01-31 17:31:15 +00:00
parent 881d9a74a2
commit dc817dee3b
No known key found for this signature in database
GPG Key ID: A7ACD32423568D7B
3 changed files with 104 additions and 6 deletions

View File

@ -43,4 +43,5 @@ pub enum Message {
PendingTransactions(Result<Vec<HistoryTransaction>, Error>),
LabelsUpdated(Result<HashMap<String, Option<String>>, Error>),
BroadcastModal(Result<HashSet<Txid>, Error>),
RbfModal(HistoryTransaction, bool, u64, Result<HashSet<Txid>, Error>),
}

View File

@ -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<Txid>,
/// Feerate form value.
feerate_val: form::Value<String>,
/// 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<Txid>,
) -> 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(),

View File

@ -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<Txid>,
feerate: &form::Value<String>,
replacement_txid: Option<Txid>,
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()
}