Skip to content

Commit

Permalink
Merge #852: gui: enable use of RBF on unconfirmed transactions
Browse files Browse the repository at this point in the history
71fd9c4 gui: enable use of RBF on pending transactions (jp1ac4)
ce50dd8 gui: optionally filter spend transactions by txids (jp1ac4)
4846a0b gui: update liana dependency (jp1ac4)

Pull request description:

  This is to resolve #43.

  When viewing an unconfirmed transaction, a user can now either bump its fee or cancel it. This will generate a new PSBT that the user can jump to in order to sign and broadcast it.

  I haven't added any comparison between the previous and replacement inputs as suggested in #43 (comment). I think that would be a bigger change and might be better as a follow-up.

  I decided not to add "Unconfirmed" on the transaction screen as suggested in #43 (comment) as I thought it might be better as a separate PR.

  I haven't yet added the RBF buttons to the home screen, but that could also be done as a follow-up :)

  In a separate commit, I pass `None` to `create_spend` following #821.

ACKs for top commit:
  darosior:
    tested-but-not-review ACK 71fd9c4
  edouardparis:
    ACK 71fd9c4

Tree-SHA512: c3ddbb85ad008e9e450b79ba77816ad9065f1eec675913f20463c4271ff017d5cb9ff0a0fca9ed919c97b3f6bb2b806344dc4ff062f5393ad5ac85c8c039ab83
  • Loading branch information
darosior committed Dec 8, 2023
2 parents 6151c57 + 71fd9c4 commit 3211045
Show file tree
Hide file tree
Showing 11 changed files with 313 additions and 16 deletions.
7 changes: 4 additions & 3 deletions gui/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion gui/src/app/menu.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use liana::miniscript::bitcoin::OutPoint;
use liana::miniscript::bitcoin::{OutPoint, Txid};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Menu {
Home,
Expand All @@ -10,4 +10,5 @@ pub enum Menu {
CreateSpendTx,
Recovery,
RefreshCoins(Vec<OutPoint>),
PsbtPreSelected(Txid),
}
16 changes: 16 additions & 0 deletions gui/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,22 @@ impl App {
}
menu::Menu::Transactions => TransactionsPanel::new().into(),
menu::Menu::PSBTs => PsbtsPanel::new(self.wallet.clone(), &self.cache.spend_txs).into(),
menu::Menu::PsbtPreSelected(txid) => {
// Get preselected spend from DB in case it's not yet in the cache.
// We only need this single spend as we will go straight to its view and not show the PSBTs list.
// In case of any error loading the spend or if it doesn't exist, fall back to using the cache
// and load PSBTs list in usual way.
match self
.daemon
.list_spend_transactions(Some(&[*txid]))
.map(|txs| txs.first().cloned())
{
Ok(Some(spend_tx)) => {
PsbtsPanel::new_preselected(self.wallet.clone(), spend_tx).into()
}
_ => PsbtsPanel::new(self.wallet.clone(), &self.cache.spend_txs).into(),
}
}
menu::Menu::CreateSpendTx => CreateSpendPanel::new(
self.wallet.clone(),
&self.cache.coins,
Expand Down
14 changes: 13 additions & 1 deletion gui/src/app/state/psbts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ impl PsbtsPanel {
import_tx: None,
}
}

pub fn new_preselected(wallet: Arc<Wallet>, spend_tx: SpendTx) -> Self {
let psbt_state = psbt::PsbtState::new(wallet.clone(), spend_tx.clone(), true);

Self {
wallet,
spend_txs: vec![spend_tx],
warning: None,
selected_tx: Some(psbt_state),
import_tx: None,
}
}
}

impl State for PsbtsPanel {
Expand Down Expand Up @@ -120,7 +132,7 @@ impl State for PsbtsPanel {
fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
let daemon = daemon.clone();
Command::perform(
async move { daemon.list_spend_transactions().map_err(|e| e.into()) },
async move { daemon.list_spend_transactions(None).map_err(|e| e.into()) },
Message::SpendTxs,
)
}
Expand Down
135 changes: 131 additions & 4 deletions gui/src/app/state/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ use std::{
};

use iced::Command;
use liana_ui::widget::*;
use liana::miniscript::bitcoin::Txid;
use liana_ui::{
component::{form, modal::Modal},
widget::*,
};

use crate::app::{
cache::Cache,
Expand All @@ -27,6 +31,7 @@ pub struct TransactionsPanel {
labels_edited: LabelsEdited,
selected_tx: Option<usize>,
warning: Option<Error>,
create_rbf_modal: Option<CreateRbfModal>,
}

impl TransactionsPanel {
Expand All @@ -37,6 +42,7 @@ impl TransactionsPanel {
pending_txs: Vec::new(),
labels_edited: LabelsEdited::default(),
warning: None,
create_rbf_modal: None,
}
}
}
Expand All @@ -49,12 +55,21 @@ impl State for TransactionsPanel {
} else {
&self.txs[i - self.pending_txs.len()]
};
view::transactions::tx_view(
let content = view::transactions::tx_view(
cache,
tx,
self.labels_edited.cache(),
self.warning.as_ref(),
)
);
if let Some(modal) = &self.create_rbf_modal {
Modal::new(content, modal.view())
.on_blur(Some(view::Message::CreateRbf(
view::CreateRbfMessage::Cancel,
)))
.into()
} else {
content
}
} else {
view::transactions::transactions_view(
cache,
Expand Down Expand Up @@ -100,6 +115,24 @@ impl State for TransactionsPanel {
Message::View(view::Message::Select(i)) => {
self.selected_tx = Some(i);
}
Message::View(view::Message::CreateRbf(view::CreateRbfMessage::Cancel)) => {
self.create_rbf_modal = None;
}
Message::View(view::Message::CreateRbf(view::CreateRbfMessage::New(is_cancel))) => {
if let Some(idx) = self.selected_tx {
if let Some(tx) = self.pending_txs.get(idx) {
if let Some(fee_amount) = tx.fee_amount {
let prev_feerate_vb = fee_amount
.to_sat()
.checked_div(tx.tx.vsize().try_into().unwrap())
.unwrap();
let modal =
CreateRbfModal::new(tx.tx.txid(), is_cancel, prev_feerate_vb);
self.create_rbf_modal = Some(modal);
}
}
}
}
Message::View(view::Message::Label(_, _)) | Message::LabelsUpdated(_) => {
match self.labels_edited.update(
daemon,
Expand Down Expand Up @@ -154,7 +187,11 @@ impl State for TransactionsPanel {
);
}
}
_ => {}
_ => {
if let Some(modal) = &mut self.create_rbf_modal {
return modal.update(daemon, _cache, message);
}
}
};
Command::none()
}
Expand Down Expand Up @@ -200,3 +237,93 @@ impl From<TransactionsPanel> for Box<dyn State> {
Box::new(s)
}
}

pub struct CreateRbfModal {
/// Transaction to replace.
txid: Txid,
/// Whether to cancel or bump fee.
is_cancel: bool,
/// Min feerate required for RBF.
min_feerate_vb: u64,
/// Feerate form value.
feerate_val: form::Value<String>,
/// Parsed feerate.
feerate_vb: Option<u64>,
warning: Option<Error>,
/// Replacement transaction ID.
replacement_txid: Option<Txid>,
}

impl CreateRbfModal {
fn new(txid: Txid, is_cancel: bool, prev_feerate_vb: u64) -> Self {
let min_feerate_vb = prev_feerate_vb.checked_add(1).unwrap();
Self {
txid,
is_cancel,
min_feerate_vb,
feerate_val: form::Value {
valid: true,
value: min_feerate_vb.to_string(),
},
// For cancel, we let `rbfpsbt` set the feerate.
feerate_vb: if is_cancel {
None
} else {
Some(min_feerate_vb)
},
warning: None,
replacement_txid: None,
}
}

fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
_cache: &Cache,
message: Message,
) -> Command<Message> {
match message {
Message::View(view::Message::CreateRbf(view::CreateRbfMessage::FeerateEdited(s))) => {
self.warning = None;
if let Ok(value) = s.parse::<u64>() {
self.feerate_val.value = s;
self.feerate_val.valid = value >= self.min_feerate_vb;
if self.feerate_val.valid {
self.feerate_vb = Some(value);
}
} else {
self.feerate_val.valid = false;
}
if !self.feerate_val.valid {
self.feerate_vb = None;
}
}
Message::View(view::Message::CreateRbf(view::CreateRbfMessage::Confirm)) => {
self.warning = None;

let psbt = match daemon.rbf_psbt(&self.txid, self.is_cancel, self.feerate_vb) {
Ok(res) => res.psbt,
Err(e) => {
self.warning = Some(e.into());
return Command::none();
}
};
if let Err(e) = daemon.update_spend_tx(&psbt) {
self.warning = Some(e.into());
return Command::none();
}
self.replacement_txid = Some(psbt.unsigned_tx.txid());
}
_ => {}
}
Command::none()
}
fn view(&self) -> Element<view::Message> {
view::transactions::create_rbf_modal(
self.is_cancel,
&self.feerate_val,
self.replacement_txid,
self.warning.as_ref(),
)
}
}
9 changes: 9 additions & 0 deletions gui/src/app/view/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub enum Message {
Next,
Previous,
SelectHardwareWallet(usize),
CreateRbf(CreateRbfMessage),
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -77,3 +78,11 @@ pub enum SettingsEditMessage {
Cancel,
Confirm,
}

#[derive(Debug, Clone)]
pub enum CreateRbfMessage {
New(bool),
FeerateEdited(String),
Cancel,
Confirm,
}
Loading

0 comments on commit 3211045

Please sign in to comment.