diff --git a/gui/src/app/state/spend/detail.rs b/gui/src/app/state/spend/detail.rs index 78be7014..fca9596c 100644 --- a/gui/src/app/state/spend/detail.rs +++ b/gui/src/app/state/spend/detail.rs @@ -1,7 +1,10 @@ use std::sync::Arc; use iced::{Command, Element}; -use liana::miniscript::bitcoin::util::{bip32::Fingerprint, psbt::Psbt}; +use liana::miniscript::bitcoin::{ + consensus, + util::{bip32::Fingerprint, psbt::Psbt}, +}; use crate::{ app::{ @@ -12,7 +15,7 @@ use crate::{ Daemon, }, hw::{list_hardware_wallets, HardwareWallet}, - ui::component::modal, + ui::component::{form, modal}, }; trait Action { @@ -79,6 +82,12 @@ impl SpendTxState { self.action = Some(Box::new(action)); return cmd; } + view::SpendTxMessage::EditPsbt => { + let action = UpdateAction::new(self.tx.psbt.to_string()); + let cmd = action.load(daemon); + self.action = Some(Box::new(action)); + return cmd; + } view::SpendTxMessage::Broadcast => { self.action = Some(Box::new(BroadcastAction::default())); } @@ -349,3 +358,110 @@ async fn sign_psbt( hw.sign_tx(&mut psbt).await.map_err(Error::from)?; Ok((psbt, fingerprint)) } + +pub struct UpdateAction { + psbt: String, + updated: form::Value, + processing: bool, + error: Option, + success: bool, +} + +impl UpdateAction { + pub fn new(psbt: String) -> Self { + Self { + psbt, + updated: form::Value::default(), + processing: false, + error: None, + success: false, + } + } +} + +impl Action for UpdateAction { + fn view(&self) -> Element { + if self.success { + view::spend::detail::update_spend_success_view() + } else { + view::spend::detail::update_spend_view( + self.psbt.clone(), + &self.updated, + self.error.as_ref(), + self.processing, + ) + } + } + + fn update( + &mut self, + daemon: Arc, + _cache: &Cache, + message: Message, + tx: &mut SpendTx, + ) -> Command { + match message { + Message::Updated(res) => { + self.processing = false; + match res { + Ok(()) => { + self.success = true; + self.error = None; + let psbt = consensus::encode::deserialize::( + &base64::decode(&self.updated.value).unwrap(), + ) + .expect("Already checked"); + for (i, input) in tx.psbt.inputs.iter_mut().enumerate() { + if tx + .psbt + .unsigned_tx + .input + .get(i) + .map(|tx_in| tx_in.previous_output) + != psbt + .unsigned_tx + .input + .get(i) + .map(|tx_in| tx_in.previous_output) + { + continue; + } + if let Some(updated_input) = psbt.inputs.get(i) { + input + .partial_sigs + .extend(updated_input.partial_sigs.clone().into_iter()); + } + } + } + Err(e) => self.error = e.into(), + } + } + Message::View(view::Message::ImportSpend(view::ImportSpendMessage::PsbtEdited(s))) => { + self.updated.value = s; + if let Some(psbt) = base64::decode(&self.updated.value) + .ok() + .and_then(|bytes| consensus::encode::deserialize::(&bytes).ok()) + { + self.updated.valid = tx.psbt.unsigned_tx.txid() == psbt.unsigned_tx.txid(); + } + } + Message::View(view::Message::ImportSpend(view::ImportSpendMessage::Confirm)) => { + if self.updated.valid { + self.processing = true; + self.error = None; + let updated: Psbt = consensus::encode::deserialize( + &base64::decode(&self.updated.value).expect("Already checked"), + ) + .unwrap(); + return Command::perform( + async move { daemon.update_spend_tx(&updated).map_err(|e| e.into()) }, + Message::Updated, + ); + } + } + _ => {} + } + + Command::none() + } +} diff --git a/gui/src/app/view/message.rs b/gui/src/app/view/message.rs index c074d568..ceb85482 100644 --- a/gui/src/app/view/message.rs +++ b/gui/src/app/view/message.rs @@ -41,6 +41,8 @@ pub enum SpendTxMessage { Confirm, Cancel, SelectHardwareWallet(usize), + EditPsbt, + PsbtEdited(String), Next, } diff --git a/gui/src/app/view/spend/detail.rs b/gui/src/app/view/spend/detail.rs index c7966a9c..7457dc5c 100644 --- a/gui/src/app/view/spend/detail.rs +++ b/gui/src/app/view/spend/detail.rs @@ -1,5 +1,5 @@ use iced::{ - widget::{Button, Column, Container, Row, Scrollable}, + widget::{Button, Column, Container, Row, Scrollable, Space}, Alignment, Element, Length, }; @@ -13,10 +13,11 @@ use crate::{ daemon::model::{Coin, SpendStatus, SpendTx}, hw::HardwareWallet, ui::{ + color, component::{ badge, button, card, collapse::Collapse, - container, separation, + container, form, separation, text::{text, Text}, }, icon, @@ -266,18 +267,35 @@ fn spend_overview_view<'a>(tx: &SpendTx) -> Element<'a, Message> { .push(separation().width(Length::Fill)) .push( Column::new() + .spacing(10) .push( Row::new() .push(text("Tx ID:").bold().width(Length::Fill)) - .push(text(format!("{}", tx.psbt.unsigned_tx.txid())).small()) + .push(text(tx.psbt.unsigned_tx.txid().to_string()).small()) + .push( + Button::new(icon::clipboard_icon()) + .on_press(Message::Clipboard( + tx.psbt.unsigned_tx.txid().to_string(), + )) + .style(button::Style::TransparentBorder.into()), + ) .align_items(Alignment::Center), ) .push( Row::new() - .push(text("Psbt:").bold().width(Length::Fill)) + .align_items(Alignment::Center) + .push(text("PSBT:").bold().width(Length::Fill)) .push( - button::transparent(Some(icon::clipboard_icon()), "Copy") - .on_press(Message::Clipboard(tx.psbt.to_string())), + Row::new() + .spacing(5) + .push( + button::border(Some(icon::clipboard_icon()), "Copy") + .on_press(Message::Clipboard(tx.psbt.to_string())), + ) + .push( + button::border(Some(icon::import_icon()), "Update") + .on_press(Message::Spend(SpendTxMessage::EditPsbt)), + ), ) .align_items(Alignment::Center), ), @@ -539,7 +557,7 @@ pub fn sign_action<'a>( Column::new() .push( Column::new() - .spacing(20) + .spacing(15) .width(Length::Fill) .push("Please connect a hardware wallet") .push(button::border(None, "Refresh").on_press(Message::Reload)) @@ -547,9 +565,72 @@ pub fn sign_action<'a>( ) .width(Length::Fill) }) + .spacing(20) .width(Length::Fill) .align_items(Alignment::Center), ) .width(Length::Units(500)) .into() } + +pub fn update_spend_view<'a>( + psbt: String, + updated: &form::Value, + error: Option<&Error>, + processing: bool, +) -> Element<'a, Message> { + Column::new() + .push(warn(error)) + .push(card::simple( + Column::new() + .spacing(20) + .push( + Row::new() + .push(text("PSBT:").bold().width(Length::Fill)) + .push( + button::border(Some(icon::clipboard_icon()), "Copy") + .on_press(Message::Clipboard(psbt)), + ) + .align_items(Alignment::Center), + ) + .push(separation().width(Length::Fill)) + .push( + Column::new() + .spacing(10) + .push(text("Insert updated PSBT:").bold()) + .push( + form::Form::new("PSBT", updated, move |msg| { + Message::ImportSpend(ImportSpendMessage::PsbtEdited(msg)) + }) + .warning("Please enter the correct base64 encoded PSBT") + .size(20) + .padding(10), + ) + .push(Row::new().push(Space::with_width(Length::Fill)).push( + if updated.valid && !updated.value.is_empty() && !processing { + button::primary(None, "Update") + .on_press(Message::ImportSpend(ImportSpendMessage::Confirm)) + } else if processing { + button::primary(None, "Processing...") + } else { + button::primary(None, "Update") + }, + )), + ), + )) + .max_width(400) + .into() +} + +pub fn update_spend_success_view<'a>() -> Element<'a, Message> { + Column::new() + .push( + card::simple(Container::new( + text("Spend transaction is updated").style(color::SUCCESS), + )) + .padding(50), + ) + .width(Length::Units(400)) + .align_items(Alignment::Center) + .into() +}