gui(psbt): check for conflicting txs before broadcast

This commit is contained in:
jp1ac4 2024-02-01 16:25:17 +00:00
parent beabf08cbf
commit 881d9a74a2
No known key found for this signature in database
GPG Key ID: A7ACD32423568D7B
3 changed files with 111 additions and 7 deletions

View File

@ -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<Vec<HistoryTransaction>, Error>),
PendingTransactions(Result<Vec<HistoryTransaction>, Error>),
LabelsUpdated(Result<HashMap<String, Option<String>>, Error>),
BroadcastModal(Result<HashSet<Txid>, Error>),
}

View File

@ -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<Error>,
/// IDs of any directly conflicting transactions.
conflicting_txids: HashSet<Txid>,
}
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()

View File

@ -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<Txid>,
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()
}
}