diff --git a/src/app/message.rs b/src/app/message.rs index 0835ed0a..4e15af59 100644 --- a/src/app/message.rs +++ b/src/app/message.rs @@ -67,6 +67,15 @@ pub enum Message { AddWatchtower, LoadDaemonConfig(DaemonConfig), DaemonConfigLoaded(Result<(), Error>), + CPFP(CPFPMessage), +} + +// [ZEE] Addition for CPFP +#[derive(Debug, Clone)] +pub enum CPFPMessage { + CPFP(String), + ConfirmCPFP, + CPFPed(Result<(), RevaultDError>), } #[derive(Debug, Clone)] diff --git a/src/app/state/cmd.rs b/src/app/state/cmd.rs index 3a786a81..48a9b736 100644 --- a/src/app/state/cmd.rs +++ b/src/app/state/cmd.rs @@ -89,6 +89,14 @@ pub async fn broadcast_spend_tx( revaultd.broadcast_spend_tx(&txid, with_priority) } +pub async fn cpfp( + revaultd: Arc, + txids: Vec, + fee_rate: f64, +) -> Result<(), RevaultDError> { + revaultd.cpfp(&txids, fee_rate) +} + pub async fn emergency(revaultd: Arc) -> Result<(), RevaultDError> { revaultd.emergency() } diff --git a/src/app/state/spend_transaction.rs b/src/app/state/spend_transaction.rs index 0445b37c..1936560b 100644 --- a/src/app/state/spend_transaction.rs +++ b/src/app/state/spend_transaction.rs @@ -9,15 +9,15 @@ use crate::{ app::{ context::Context, error::Error, - message::{Message, SpendTxMessage}, + message::{CPFPMessage, Message, SpendTxMessage}, state::{ - cmd::{broadcast_spend_tx, delete_spend_tx, list_vaults, update_spend_tx}, + cmd::{broadcast_spend_tx, cpfp, delete_spend_tx, list_vaults, update_spend_tx}, sign::{Signer, SpendTransactionTarget}, State, }, view::spend_transaction::{ spend_tx_confirmed, spend_tx_deprecated, spend_tx_processing, - SpendTransactionBroadcastView, SpendTransactionDeleteView, + SpendTransactionBroadcastView, SpendTransactionCPFPView, SpendTransactionDeleteView, SpendTransactionListItemView, SpendTransactionSharePsbtView, SpendTransactionSignView, SpendTransactionView, }, @@ -156,6 +156,102 @@ pub enum SpendTransactionAction { }, } +#[derive(Debug)] +pub struct SpendTransactionCPFP { + tx: model::SpendTx, + view: SpendTransactionCPFPView, + processing: bool, + success: bool, + warning: Option, + feerate: form::Value, +} +impl SpendTransactionCPFP { + pub fn new(tx: model::SpendTx) -> Self { + Self { + tx, + view: SpendTransactionCPFPView::new(), + processing: false, + success: false, + warning: None, + feerate: form::Value::default(), + } + } + + fn feerate(&self) -> Result { + if self.feerate.value.is_empty() { + return Err(Error::Unexpected("Amount should be non-zero".to_string())); + } + + let feerate: f64 = self + .feerate + .value + .to_string() + .parse() + .unwrap_or_else(|_str| 0.0); + + if feerate == 0.0 { + return Err(Error::Unexpected("Invalid feerate".to_string())); + } + + Ok(feerate) + } + + fn valid(&self) -> bool { + !self.feerate.value.is_empty() && self.feerate.valid + } + + fn update( + &mut self, + ctx: &Context, + psbt: &mut Psbt, + message: CPFPMessage, + ) -> Command { + match message { + CPFPMessage::CPFP(feerate) => { + self.feerate.value = feerate; + if let Ok(parsed_feerate) = self.feerate() { + self.feerate.valid = true; + } else { + self.feerate.valid = false; + } + } + CPFPMessage::ConfirmCPFP => { + if self.feerate.valid { + self.processing = true; + let fee_rate: f64 = self.feerate().unwrap(); + return Command::perform( + cpfp( + ctx.revaultd.clone(), + [psbt.global.unsigned_tx.txid()].to_vec(), + fee_rate, + ), + CPFPMessage::CPFPed, + ); + } + } + CPFPMessage::CPFPed(res) => { + self.processing = false; + match res { + Ok(()) => self.success = true, + Err(e) => self.warning = Error::from(e).into(), + }; + } + _ => {} + }; + Command::none() + } + + fn view(&mut self) -> Element { + self.view.view( + self.processing, + self.success, + &self.tx, + &self.feerate, + self.warning.as_ref(), + ) + } +} + impl SpendTransactionAction { fn new( managers_threshold: usize, @@ -311,6 +407,7 @@ impl SpendTransactionAction { *with_priority = priority; } } + // [ZEE] handling the Broadcast request. SpendTxMessage::Broadcast => { if let Self::Broadcast { processing, diff --git a/src/app/view/spend_transaction.rs b/src/app/view/spend_transaction.rs index 4f91e4e4..3a10879e 100644 --- a/src/app/view/spend_transaction.rs +++ b/src/app/view/spend_transaction.rs @@ -1,8 +1,8 @@ use bitcoin::{util::psbt::PartiallySignedTransaction as Psbt, Amount}; use iced::{ - alignment::Horizontal, scrollable, tooltip, Alignment, Checkbox, Column, Container, Element, - Length, Row, Tooltip, + alignment::Horizontal, scrollable, text_input, tooltip, Alignment, Checkbox, Column, Container, + Element, Length, Row, Tooltip, }; use revaultd::revault_tx::transactions::RevaultTransaction; @@ -20,7 +20,7 @@ use crate::{ app::{ context::Context, error::Error, - message::{Message, SpendTxMessage}, + message::{CPFPMessage, Message, SpendTxMessage}, view::{manager::spend_tx_with_feerate_view, warning::warn}, }, daemon::model, @@ -541,6 +541,7 @@ impl SpendTransactionBroadcastView { ) .spacing(5), ) + // [ZEE] The button for broadcasting the transaction. .push( button::important( &mut self.confirm_button, @@ -691,3 +692,89 @@ impl SpendTransactionListItemView { .into() } } + +#[derive(Debug)] +pub struct SpendTransactionCPFPView { + fee_input: text_input::State, + confirm_button: iced::button::State, +} + +impl SpendTransactionCPFPView { + pub fn new() -> Self { + Self { + fee_input: text_input::State::new(), + confirm_button: iced::button::State::new(), + } + } + + pub fn view( + &mut self, + processing: bool, + success: bool, + tx: &model::SpendTx, + feerate: &form::Value, + warning: Option<&Error>, + ) -> Element { + let mut col_action = Column::new(); + if let Some(error) = warning { + col_action = col_action.push(card::alert_warning(Container::new( + Text::new(&error.to_string()).small(), + ))); + } + + if processing { + col_action = col_action.push(button::important( + &mut self.confirm_button, + button::button_content(None, "CPFPing"), + )); + } else if success { + col_action = col_action.push( + card::success(Text::new("Transaction has been CPFPed")) + .padding(20) + .width(Length::Fill) + .align_x(Horizontal::Center), + ); + } else { + col_action = col_action + .push(Text::new("Transaction has not been mined")) + .push(match tx.status { + // Only `Broadcasted` can be CPFPed. + model::ListSpendStatus::Broadcasted => Row::new() + .push(Text::new("CPFP amount in sat/vbyte:").bold()) + .push( + form::Form::new(&mut self.fee_input, "CPFP", feerate, |msg| { + Message::CPFP(CPFPMessage::CPFP(msg)) + }) + .warning("Feerate must be a number.") + .size(20) + .padding(10) + .render(), + ), + _ => Row::new().push(Text::new("Transaction can't be CPFPed.")), + }) + // [ZEE] The button for broadcasting the transaction. + .push(match tx.status { + model::ListSpendStatus::Broadcasted => button::important( + &mut self.confirm_button, + button::button_content(None, "CPFP"), + ) + .width(Length::Units(200)) + .on_press(Message::CPFP(CPFPMessage::ConfirmCPFP)), + + _ => button::transparent( + &mut self.confirm_button, + button::button_content(None, "Can't CPFP"), + ) + .width(Length::Units(200)), + }); + } + + card::white(Container::new( + col_action.align_items(Alignment::Center).spacing(20), + )) + .width(Length::Fill) + .align_x(Horizontal::Center) + .padding(20) + .into() + } +} diff --git a/src/daemon/client/mod.rs b/src/daemon/client/mod.rs index a0c5f0eb..48e48b92 100644 --- a/src/daemon/client/mod.rs +++ b/src/daemon/client/mod.rs @@ -221,6 +221,12 @@ impl Daemon for RevaultD { )?; Ok(resp.events) } + + fn cpfp(&self, txids: &[Txid], feerate: f64) -> Result<(), RevaultDError> { + let _res: serde_json::value::Value = + self.call("cpfp", Some(vec![json!(txids), json!(feerate)]))?; + Ok(()) + } } #[derive(Debug, Clone, Deserialize, Serialize)] diff --git a/src/daemon/embedded.rs b/src/daemon/embedded.rs index 02d5f2b8..db8f19d6 100644 --- a/src/daemon/embedded.rs +++ b/src/daemon/embedded.rs @@ -319,4 +319,16 @@ impl Daemon for EmbeddedDaemon { .get_history(start, end, limit, kind) .map_err(|e| e.into()) } + fn cpfp(&self, txids: &[Txid], feerate: f64) -> Result<(), RevaultDError> { + Ok(()) + // Here we implement the call to the backend. + // self.handle + // .as_ref() + // .ok_or(RevaultDError::NoAnswer)? + // .lock() + // .unwrap() + // .control + // .cpfp(txids, feerate) + // .map_err(|e| e.into()) + } } diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index da8c5bd8..b0de6aec 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -114,4 +114,5 @@ pub trait Daemon: Debug { end: u32, limit: u64, ) -> Result, RevaultDError>; + fn cpfp(&self, txids: &[Txid], feerate: f64) -> Result<(), RevaultDError>; }