From 64fcf3415481d516d516148aebf57a2a0ff40e42 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 8 Apr 2025 05:57:48 +0200 Subject: [PATCH 1/7] gui: rename Action => Modal for consistency --- liana-gui/src/app/state/psbt.rs | 100 +++++++++++++------------- liana-gui/src/app/state/spend/step.rs | 4 +- 2 files changed, 50 insertions(+), 54 deletions(-) diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index cc6f2a7e..7924b5be 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -35,7 +35,7 @@ use crate::{ hw::{HardwareWallet, HardwareWallets}, }; -pub trait Action { +pub trait Modal { fn load(&self, _daemon: Arc) -> Task { Task::none() } @@ -53,16 +53,16 @@ pub trait Action { fn view<'a>(&'a self, content: Element<'a, view::Message>) -> Element<'a, view::Message>; } -pub enum PsbtAction { - Save(SaveAction), - Sign(SignAction), - Update(UpdateAction), - Broadcast(BroadcastAction), - Delete(DeleteAction), +pub enum PsbtModal { + Save(SaveModal), + Sign(SignModal), + Update(UpdateModal), + Broadcast(BroadcastModal), + Delete(DeleteModal), } -impl<'a> AsRef for PsbtAction { - fn as_ref(&self) -> &(dyn Action + 'a) { +impl<'a> AsRef for PsbtModal { + fn as_ref(&self) -> &(dyn Modal + 'a) { match &self { Self::Save(a) => a, Self::Sign(a) => a, @@ -73,8 +73,8 @@ impl<'a> AsRef for PsbtAction { } } -impl<'a> AsMut for PsbtAction { - fn as_mut(&mut self) -> &mut (dyn Action + 'a) { +impl<'a> AsMut for PsbtModal { + fn as_mut(&mut self) -> &mut (dyn Modal + 'a) { match self { Self::Save(a) => a, Self::Sign(a) => a, @@ -92,7 +92,7 @@ pub struct PsbtState { pub saved: bool, pub warning: Option, pub labels_edited: LabelsEdited, - pub action: Option, + pub modal: Option, } impl PsbtState { @@ -102,27 +102,27 @@ impl PsbtState { wallet, labels_edited: LabelsEdited::default(), warning: None, - action: None, + modal: None, tx, saved, } } pub fn interrupt(&mut self) { - self.action = None; + self.modal = None; } pub fn subscription(&self) -> Subscription { - if let Some(action) = &self.action { - action.as_ref().subscription() + if let Some(modal) = &self.modal { + modal.as_ref().subscription() } else { Subscription::none() } } pub fn load(&self, daemon: Arc) -> Task { - if let Some(action) = &self.action { - action.as_ref().load(daemon) + if let Some(modal) = &self.modal { + modal.as_ref().load(daemon) } else { Task::none() } @@ -136,37 +136,37 @@ impl PsbtState { ) -> Task { match message { Message::View(view::Message::Spend(view::SpendTxMessage::Cancel)) => { - if let Some(PsbtAction::Sign(SignAction { display_modal, .. })) = &mut self.action { + if let Some(PsbtModal::Sign(SignModal { display_modal, .. })) = &mut self.modal { *display_modal = false; return Task::none(); } - self.action = None; + self.modal = None; } Message::View(view::Message::Spend(view::SpendTxMessage::Delete)) => { - self.action = Some(PsbtAction::Delete(DeleteAction::default())); + self.modal = Some(PsbtModal::Delete(DeleteModal::default())); } Message::View(view::Message::Spend(view::SpendTxMessage::Sign)) => { - if let Some(PsbtAction::Sign(SignAction { display_modal, .. })) = &mut self.action { + if let Some(PsbtModal::Sign(SignModal { display_modal, .. })) = &mut self.modal { *display_modal = true; return Task::none(); } - let action = SignAction::new( + let modal = SignModal::new( self.tx.signers(), self.wallet.clone(), cache.datadir_path.clone(), cache.network, self.saved, ); - let cmd = action.load(daemon); - self.action = Some(PsbtAction::Sign(action)); + let cmd = modal.load(daemon); + self.modal = Some(PsbtModal::Sign(modal)); return cmd; } Message::View(view::Message::Spend(view::SpendTxMessage::EditPsbt)) => { - let action = UpdateAction::new(self.wallet.clone(), self.tx.psbt.to_string()); - let cmd = action.load(daemon); - self.action = Some(PsbtAction::Update(action)); + let modal = UpdateModal::new(self.wallet.clone(), self.tx.psbt.to_string()); + let cmd = modal.load(daemon); + self.modal = Some(PsbtModal::Update(modal)); return cmd; } Message::View(view::Message::Spend(view::SpendTxMessage::Broadcast)) => { @@ -188,7 +188,7 @@ impl PsbtState { ); } Message::View(view::Message::Spend(view::SpendTxMessage::Save)) => { - self.action = Some(PsbtAction::Save(SaveAction::default())); + self.modal = Some(PsbtModal::Save(SaveModal::default())); } Message::View(view::Message::Label(_, _)) | Message::LabelsUpdated(_) => { match self.labels_edited.update( @@ -206,15 +206,13 @@ impl PsbtState { } Message::Updated(Ok(_)) => { self.saved = true; - if let Some(action) = self.action.as_mut() { - return action - .as_mut() - .update(daemon.clone(), message, &mut self.tx); + if let Some(modal) = self.modal.as_mut() { + return modal.as_mut().update(daemon.clone(), message, &mut self.tx); } } Message::BroadcastModal(res) => match res { Ok(conflicting_txids) => { - self.action = Some(PsbtAction::Broadcast(BroadcastAction { + self.modal = Some(PsbtModal::Broadcast(BroadcastModal { conflicting_txids, ..Default::default() })); @@ -224,10 +222,8 @@ impl PsbtState { } }, _ => { - if let Some(action) = self.action.as_mut() { - return action - .as_mut() - .update(daemon.clone(), message, &mut self.tx); + if let Some(modal) = self.modal.as_mut() { + return modal.as_mut().update(daemon.clone(), message, &mut self.tx); } } }; @@ -245,8 +241,8 @@ impl PsbtState { cache.network, self.warning.as_ref(), ); - if let Some(action) = &self.action { - action.as_ref().view(content) + if let Some(modal) = &self.modal { + modal.as_ref().view(content) } else { content } @@ -254,12 +250,12 @@ impl PsbtState { } #[derive(Default)] -pub struct SaveAction { +pub struct SaveModal { saved: bool, error: Option, } -impl Action for SaveAction { +impl Modal for SaveModal { fn update( &mut self, daemon: Arc, @@ -303,14 +299,14 @@ impl Action for SaveAction { } #[derive(Default)] -pub struct BroadcastAction { +pub struct BroadcastModal { broadcast: bool, error: Option, /// IDs of any directly conflicting transactions. conflicting_txids: HashSet, } -impl Action for BroadcastAction { +impl Modal for BroadcastModal { fn update( &mut self, daemon: Arc, @@ -358,12 +354,12 @@ impl Action for BroadcastAction { } #[derive(Default)] -pub struct DeleteAction { +pub struct DeleteModal { deleted: bool, error: Option, } -impl Action for DeleteAction { +impl Modal for DeleteModal { fn update( &mut self, daemon: Arc, @@ -403,7 +399,7 @@ impl Action for DeleteAction { } } -pub struct SignAction { +pub struct SignModal { wallet: Arc, hws: HardwareWallets, error: Option, @@ -413,7 +409,7 @@ pub struct SignAction { display_modal: bool, } -impl SignAction { +impl SignModal { pub fn new( signed: HashSet, wallet: Arc, @@ -433,7 +429,7 @@ impl SignAction { } } -impl Action for SignAction { +impl Modal for SignModal { fn subscription(&self) -> Subscription { self.hws.refresh().map(Message::HardwareWallets) } @@ -637,7 +633,7 @@ async fn sign_psbt( Ok(psbt) } -pub struct UpdateAction { +pub struct UpdateModal { wallet: Arc, psbt: String, updated: form::Value, @@ -646,7 +642,7 @@ pub struct UpdateAction { success: bool, } -impl UpdateAction { +impl UpdateModal { pub fn new(wallet: Arc, psbt: String) -> Self { Self { wallet, @@ -659,7 +655,7 @@ impl UpdateAction { } } -impl Action for UpdateAction { +impl Modal for UpdateModal { fn view<'a>(&'a self, content: Element<'a, view::Message>) -> Element<'a, view::Message> { modal::Modal::new( content, diff --git a/liana-gui/src/app/state/spend/step.rs b/liana-gui/src/app/state/spend/step.rs index 504966e4..8f0da61e 100644 --- a/liana-gui/src/app/state/spend/step.rs +++ b/liana-gui/src/app/state/spend/step.rs @@ -825,8 +825,8 @@ impl Step for SaveSpend { cache.network, psbt_state.warning.as_ref(), ); - if let Some(action) = &psbt_state.action { - action.as_ref().view(content) + if let Some(modal) = &psbt_state.modal { + modal.as_ref().view(content) } else { content } From 0796efae19e2bce521cd55c3f7bbc9f731bbad0e Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 8 Apr 2025 11:16:21 +0200 Subject: [PATCH 2/7] gui: integrate PSBT export instead copy to clipboard --- liana-gui/src/app/state/export.rs | 33 ++++++++++++++++++++++++++++++- liana-gui/src/app/state/psbt.rs | 25 +++++++++++++++++++++++ liana-gui/src/app/view/message.rs | 1 + liana-gui/src/app/view/psbt.rs | 6 +++--- 4 files changed, 61 insertions(+), 4 deletions(-) diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index 046d8c2f..269044bf 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -8,7 +8,10 @@ use liana_ui::{component::modal::Modal, widget::Element}; use tokio::task::JoinHandle; use crate::{ - app::view::{export::export_modal, Close}, + app::{ + self, + view::{export::export_modal, Close}, + }, daemon::Daemon, export::{self, get_path, ImportExportMessage, ImportExportState, ImportExportType, Progress}, }; @@ -23,6 +26,34 @@ pub struct ExportModal { import_export_type: ImportExportType, } +impl app::state::psbt::Modal for ExportModal { + fn subscription(&self) -> Subscription { + self.subscription() + .map(|s| s.map(|m| app::Message::Export(ImportExportMessage::Progress(m)))) + .unwrap_or(Subscription::none()) + } + + fn update( + &mut self, + _daemon: Arc, + message: app::Message, + _tx: &mut crate::daemon::model::SpendTx, + ) -> Task { + if let app::Message::Export(m) = message { + self.update(m) + } else { + Task::none() + } + } + + fn view<'a>( + &'a self, + content: Element<'a, app::view::Message>, + ) -> Element<'a, app::view::Message> { + self.view(content) + } +} + impl ExportModal { #[allow(clippy::new_without_default)] pub fn new( diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index 7924b5be..48a6c637 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -19,6 +19,7 @@ use liana_ui::{ }; use crate::daemon::model::LabelsLoader; +use crate::export::{ImportExportMessage, ImportExportType}; use crate::{ app::{ cache::Cache, @@ -35,6 +36,8 @@ use crate::{ hw::{HardwareWallet, HardwareWallets}, }; +use super::export::ExportModal; + pub trait Modal { fn load(&self, _daemon: Arc) -> Task { Task::none() @@ -59,6 +62,7 @@ pub enum PsbtModal { Update(UpdateModal), Broadcast(BroadcastModal), Delete(DeleteModal), + Export(ExportModal), } impl<'a> AsRef for PsbtModal { @@ -69,6 +73,7 @@ impl<'a> AsRef for PsbtModal { Self::Update(a) => a, Self::Broadcast(a) => a, Self::Delete(a) => a, + Self::Export(a) => a, } } } @@ -81,6 +86,7 @@ impl<'a> AsMut for PsbtModal { Self::Update(a) => a, Self::Broadcast(a) => a, Self::Delete(a) => a, + Self::Export(a) => a, } } } @@ -135,6 +141,25 @@ impl PsbtState { message: Message, ) -> Task { match message { + Message::View(view::Message::ExportPsbt) => { + if self.modal.is_none() { + let psbt_str = self.tx.psbt.to_string(); + let modal = ExportModal::new(None, ImportExportType::ExportPsbt(psbt_str)); + let launch = modal.launch(true); + self.modal = Some(PsbtModal::Export(modal)); + return launch; + } + } + Message::View(view::Message::ImportExport(ImportExportMessage::Close)) => { + if matches!(self.modal, Some(PsbtModal::Export(_))) { + self.modal = None; + } + } + Message::View(view::Message::ImportExport(m)) => { + if let Some(PsbtModal::Export(modal)) = self.modal.as_mut() { + return modal.update(m); + } + } Message::View(view::Message::Spend(view::SpendTxMessage::Cancel)) => { if let Some(PsbtModal::Sign(SignModal { display_modal, .. })) = &mut self.modal { *display_modal = false; diff --git a/liana-gui/src/app/view/message.rs b/liana-gui/src/app/view/message.rs index d4a2833b..04a4780f 100644 --- a/liana-gui/src/app/view/message.rs +++ b/liana-gui/src/app/view/message.rs @@ -25,6 +25,7 @@ pub enum Message { ShowQrCode(usize), ImportExport(ImportExportMessage), HideRescanWarning, + ExportPsbt, } impl Close for Message { diff --git a/liana-gui/src/app/view/psbt.rs b/liana-gui/src/app/view/psbt.rs index 2114b625..4d22bf55 100644 --- a/liana-gui/src/app/view/psbt.rs +++ b/liana-gui/src/app/view/psbt.rs @@ -339,10 +339,10 @@ pub fn spend_overview_view<'a>( .spacing(5) .push( button::secondary( - Some(icon::clipboard_icon()), - "Copy", + Some(icon::backup_icon()), + "Export", ) - .on_press(Message::Clipboard(tx.psbt.to_string())), + .on_press(Message::ExportPsbt), ) .push( button::secondary( From 9658cfde3fdbdd5823c3fd9014253bbf2901368a Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 8 Apr 2025 15:13:25 +0200 Subject: [PATCH 3/7] export: finalyze PSBT import --- liana-gui/src/app/state/export.rs | 6 +++--- liana-gui/src/export.rs | 25 +++++++++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index 269044bf..f4d82d24 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -81,7 +81,7 @@ impl ExportModal { ImportExportType::ExportProcessBackup(..) | ImportExportType::ExportLabels => { "Export Labels" } - ImportExportType::ImportPsbt => "Import PSBT", + ImportExportType::ImportPsbt(_) => "Import PSBT", ImportExportType::ImportDescriptor => "Import Descriptor", ImportExportType::ImportBackup(..) => "Restore Backup", ImportExportType::WalletFromBackup => "Import existing wallet from backup", @@ -105,7 +105,7 @@ impl ExportModal { .to_string(); format!("liana-{}.txt", checksum) } - ImportExportType::ImportPsbt => "psbt.psbt".into(), + ImportExportType::ImportPsbt(_) => "psbt.psbt".into(), ImportExportType::ImportDescriptor => "descriptor.txt".into(), ImportExportType::ExportLabels => format!("liana-labels-{date}.jsonl"), ImportExportType::ExportBackup(_) | ImportExportType::ExportProcessBackup(..) => { @@ -156,7 +156,7 @@ impl ExportModal { } Progress::None => {} Progress::Psbt(_) => { - if self.import_export_type == ImportExportType::ImportPsbt { + if matches!(self.import_export_type, ImportExportType::ImportPsbt(_)) { self.state = ImportExportState::Ended; } // TODO: forward PSBT diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index df1361b6..5c63421b 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -123,6 +123,7 @@ pub enum Error { Backup(backup::Error), ParseXpub, XpubNetwork, + TxidNotMatch, } impl Display for Error { @@ -144,6 +145,7 @@ impl Display for Error { Error::Backup(e) => write!(f, "Backup: {e}"), Error::ParseXpub => write!(f, "Failed to parse Xpub from file"), Error::XpubNetwork => write!(f, "Xpub is for another network"), + Error::TxidNotMatch => write!(f, "The imported PSBT txid do not match this PSBT"), } } } @@ -162,7 +164,7 @@ pub enum ImportExportType { WalletFromBackup, Descriptor(LianaDescriptor), ExportLabels, - ImportPsbt, + ImportPsbt(Option), ImportXpub(Network), ImportDescriptor, } @@ -178,7 +180,7 @@ impl ImportExportType { | ImportExportType::ExportXpub(_) | ImportExportType::ExportLabels => "Export successful!", ImportExportType::ImportBackup(_, _) - | ImportExportType::ImportPsbt + | ImportExportType::ImportPsbt(_) | ImportExportType::ImportXpub(_) | ImportExportType::WalletFromBackup | ImportExportType::ImportDescriptor => "Import successful", @@ -294,7 +296,7 @@ impl Export { export_descriptor(&sender, path, descriptor).await } ImportExportType::ExportLabels => export_labels(&sender, daemon, path).await, - ImportExportType::ImportPsbt => import_psbt(&sender, path).await, + ImportExportType::ImportPsbt(txid) => import_psbt(daemon, &sender, path, txid).await, ImportExportType::ImportXpub(network) => import_xpub(&sender, path, network).await, ImportExportType::ImportDescriptor => import_descriptor(&sender, path).await, ImportExportType::ExportBackup(str) => export_string(&sender, path, str).await, @@ -580,14 +582,29 @@ pub async fn export_string( Ok(()) } -pub async fn import_psbt(sender: &UnboundedSender, path: PathBuf) -> Result<(), Error> { +pub async fn import_psbt( + daemon: Option>, + sender: &UnboundedSender, + path: PathBuf, + txid: Option, +) -> Result<(), Error> { let mut file = File::open(&path)?; + let daemon = daemon.ok_or(Error::DaemonMissing)?; let mut psbt_str = String::new(); file.read_to_string(&mut psbt_str)?; psbt_str = psbt_str.trim().to_string(); let psbt = Psbt::from_str(&psbt_str).map_err(|_| Error::ParsePsbt)?; + send_progress!(sender, Progress(50.0)); + + if let Some(txid) = txid { + if psbt.unsigned_tx.compute_txid() != txid { + return Err(Error::TxidNotMatch); + } + } + + daemon.update_spend_tx(&psbt).await?; send_progress!(sender, Progress(100.0)); send_progress!(sender, Psbt(psbt)); From 71684e7039252384d18146c06b79d6f7deb9e0e4 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 8 Apr 2025 15:13:38 +0200 Subject: [PATCH 4/7] gui: integrate import PSBT from file --- liana-gui/src/app/state/export.rs | 7 +- liana-gui/src/app/state/psbt.rs | 129 +++++---------------------- liana-gui/src/app/state/psbts.rs | 143 ++++++++++-------------------- liana-gui/src/app/view/message.rs | 1 + liana-gui/src/app/view/psbt.rs | 6 +- liana-gui/src/app/view/psbts.rs | 4 +- liana-gui/src/export.rs | 2 +- 7 files changed, 77 insertions(+), 215 deletions(-) diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index f4d82d24..b0e26efe 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -155,12 +155,6 @@ impl ExportModal { self.error = Some(e.clone()); } Progress::None => {} - Progress::Psbt(_) => { - if matches!(self.import_export_type, ImportExportType::ImportPsbt(_)) { - self.state = ImportExportState::Ended; - } - // TODO: forward PSBT - } Progress::Xpub(xpub_str) => { if matches!(self.import_export_type, ImportExportType::ExportXpub(_)) { self.state = ImportExportState::Ended; @@ -181,6 +175,7 @@ impl ExportModal { }); } Progress::WalletFromBackup(_) => {} + Progress::Psbt(_) => {} }, ImportExportMessage::TimedOut => { self.stop(ImportExportState::TimedOut); diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index 48a6c637..fffe8062 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -1,6 +1,5 @@ use std::collections::{HashMap, HashSet}; use std::path::PathBuf; -use std::str::FromStr; use std::sync::Arc; use iced::Subscription; @@ -13,13 +12,10 @@ use liana::{ use lianad::commands::CoinStatus; use liana_ui::component::toast; -use liana_ui::{ - component::{form, modal}, - widget::Element, -}; +use liana_ui::{component::modal, widget::Element}; use crate::daemon::model::LabelsLoader; -use crate::export::{ImportExportMessage, ImportExportType}; +use crate::export::{ImportExportMessage, ImportExportType, Progress}; use crate::{ app::{ cache::Cache, @@ -59,7 +55,6 @@ pub trait Modal { pub enum PsbtModal { Save(SaveModal), Sign(SignModal), - Update(UpdateModal), Broadcast(BroadcastModal), Delete(DeleteModal), Export(ExportModal), @@ -70,7 +65,6 @@ impl<'a> AsRef for PsbtModal { match &self { Self::Save(a) => a, Self::Sign(a) => a, - Self::Update(a) => a, Self::Broadcast(a) => a, Self::Delete(a) => a, Self::Export(a) => a, @@ -83,7 +77,6 @@ impl<'a> AsMut for PsbtModal { match self { Self::Save(a) => a, Self::Sign(a) => a, - Self::Update(a) => a, Self::Broadcast(a) => a, Self::Delete(a) => a, Self::Export(a) => a, @@ -150,6 +143,17 @@ impl PsbtState { return launch; } } + Message::View(view::Message::ImportPsbt) => { + if self.modal.is_none() { + let modal = ExportModal::new( + Some(daemon.clone()), + ImportExportType::ImportPsbt(Some(self.tx.psbt.unsigned_tx.compute_txid())), + ); + let launch = modal.launch(false); + self.modal = Some(PsbtModal::Export(modal)); + return launch; + } + } Message::View(view::Message::ImportExport(ImportExportMessage::Close)) => { if matches!(self.modal, Some(PsbtModal::Export(_))) { self.modal = None; @@ -188,12 +192,6 @@ impl PsbtState { self.modal = Some(PsbtModal::Sign(modal)); return cmd; } - Message::View(view::Message::Spend(view::SpendTxMessage::EditPsbt)) => { - let modal = UpdateModal::new(self.wallet.clone(), self.tx.psbt.to_string()); - let cmd = modal.load(daemon); - self.modal = Some(PsbtModal::Update(modal)); - return cmd; - } Message::View(view::Message::Spend(view::SpendTxMessage::Broadcast)) => { let outpoints: Vec<_> = self.tx.coins.keys().cloned().collect(); return Task::perform( @@ -246,6 +244,16 @@ impl PsbtState { self.warning = Some(e); } }, + Message::Export(ImportExportMessage::Progress(Progress::Psbt(psbt))) => { + merge_signatures(&mut self.tx.psbt, &psbt); + self.tx.sigs = self + .wallet + .main_descriptor + .partial_spend_info(&self.tx.psbt) + // FIXME: we should not unwrap here, it seems importing a PSBT w/ an + // non-owned input will trigger a panic + .unwrap(); + } _ => { if let Some(modal) = self.modal.as_mut() { return modal.as_mut().update(daemon.clone(), message, &mut self.tx); @@ -658,97 +666,6 @@ async fn sign_psbt( Ok(psbt) } -pub struct UpdateModal { - wallet: Arc, - psbt: String, - updated: form::Value, - processing: bool, - error: Option, - success: bool, -} - -impl UpdateModal { - pub fn new(wallet: Arc, psbt: String) -> Self { - Self { - wallet, - psbt, - updated: form::Value::default(), - processing: false, - error: None, - success: false, - } - } -} - -impl Modal for UpdateModal { - fn view<'a>(&'a self, content: Element<'a, view::Message>) -> Element<'a, view::Message> { - modal::Modal::new( - content, - if self.success { - view::psbt::update_spend_success_view() - } else { - view::psbt::update_spend_view( - self.psbt.clone(), - &self.updated, - self.error.as_ref(), - self.processing, - ) - }, - ) - .on_blur(Some(view::Message::Spend(view::SpendTxMessage::Cancel))) - .into() - } - - fn update( - &mut self, - daemon: Arc, - message: Message, - tx: &mut SpendTx, - ) -> Task { - match message { - Message::Updated(res) => { - self.processing = false; - match res { - Ok(()) => { - self.success = true; - self.error = None; - let psbt = Psbt::from_str(&self.updated.value).expect("Already checked"); - merge_signatures(&mut tx.psbt, &psbt); - tx.sigs = self - .wallet - .main_descriptor - .partial_spend_info(&tx.psbt) - .unwrap(); - } - Err(e) => self.error = e.into(), - } - } - Message::View(view::Message::ImportSpend(view::ImportSpendMessage::PsbtEdited(s))) => { - self.updated.value = s; - if let Ok(psbt) = Psbt::from_str(&self.updated.value) { - self.updated.valid = - tx.psbt.unsigned_tx.compute_txid() == psbt.unsigned_tx.compute_txid(); - } else { - self.updated.valid = false; - } - } - Message::View(view::Message::ImportSpend(view::ImportSpendMessage::Confirm)) => { - self.processing = true; - self.error = None; - if let Ok(updated) = Psbt::from_str(&self.updated.value) { - return Task::perform( - async move { daemon.update_spend_tx(&updated).await.map_err(|e| e.into()) }, - Message::Updated, - ); - } - } - _ => {} - } - - Task::none() - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/liana-gui/src/app/state/psbts.rs b/liana-gui/src/app/state/psbts.rs index 173a2981..e6781e54 100644 --- a/liana-gui/src/app/state/psbts.rs +++ b/liana-gui/src/app/state/psbts.rs @@ -1,18 +1,14 @@ -use std::str::FromStr; use std::sync::Arc; use iced::{Subscription, Task}; -use liana::miniscript::bitcoin::psbt::Psbt; -use liana_ui::{ - component::{form, modal}, - widget::Element, -}; +use liana_ui::widget::Element; -use super::{psbt, State}; +use super::{export::ExportModal, psbt, State}; use crate::{ app::{cache::Cache, error::Error, menu::Menu, message::Message, view, wallet::Wallet}, daemon::{model::SpendTx, Daemon}, + export::{ImportExportMessage, ImportExportType}, }; pub struct PsbtsPanel { @@ -20,7 +16,7 @@ pub struct PsbtsPanel { selected_tx: Option, spend_txs: Vec, warning: Option, - import_tx: Option, + modal: Option, } impl PsbtsPanel { @@ -30,7 +26,7 @@ impl PsbtsPanel { spend_txs: Vec::new(), warning: None, selected_tx: None, - import_tx: None, + modal: None, } } @@ -38,7 +34,7 @@ impl PsbtsPanel { let psbt_state = psbt::PsbtState::new(self.wallet.clone(), spend_tx, true); self.selected_tx = Some(psbt_state); self.warning = None; - self.import_tx = None; + self.modal = None; } } @@ -53,14 +49,8 @@ impl State for PsbtsPanel { self.warning.as_ref(), view::psbts::psbts_view(&self.spend_txs), ); - if let Some(import_tx) = &self.import_tx { - modal::Modal::new(list_view, import_tx.view()) - .on_blur(if import_tx.processing { - None - } else { - Some(view::Message::Close) - }) - .into() + if let Some(modal) = &self.modal { + modal.view(list_view) } else { list_view } @@ -99,9 +89,32 @@ impl State for PsbtsPanel { } } }, - Message::View(view::Message::ImportSpend(view::ImportSpendMessage::Import)) => { - if self.import_tx.is_none() { - self.import_tx = Some(ImportPsbtModal::new()); + Message::View(view::Message::ImportPsbt) => { + if let Some(tx) = &mut self.selected_tx { + return tx.update(daemon, cache, message); + } else if self.modal.is_none() { + let modal = + ExportModal::new(Some(daemon.clone()), ImportExportType::ImportPsbt(None)); + let launch = modal.launch(false); + self.modal = Some(modal); + return launch; + } + } + Message::View(view::Message::ImportExport(ImportExportMessage::Close)) => { + if let Some(tx) = &mut self.selected_tx { + return tx.update(daemon, cache, message); + } else if self.modal.is_some() { + self.modal = None; + return Task::perform(async {}, |_| Message::View(view::Message::Reload)); + } + } + Message::View(view::Message::ImportExport(m)) => { + let m = m.clone(); + if let Some(tx) = &mut self.selected_tx { + let message = Message::View(view::Message::ImportExport(m)); + return tx.update(daemon, cache, message); + } else if let Some(modal) = self.modal.as_mut() { + return modal.update(m.clone()); } } Message::View(view::Message::Select(i)) => { @@ -116,10 +129,6 @@ impl State for PsbtsPanel { if let Some(tx) = &mut self.selected_tx { return tx.update(daemon, cache, message); } - - if let Some(import_tx) = &mut self.import_tx { - return import_tx.update(daemon, cache, message); - } } } Task::none() @@ -128,6 +137,17 @@ impl State for PsbtsPanel { fn subscription(&self) -> Subscription { if let Some(psbt) = &self.selected_tx { psbt.subscription() + } else if let Some(modal) = &self.modal { + modal + .subscription() + .map(|s| { + s.map(|m| { + Message::View(view::Message::ImportExport(ImportExportMessage::Progress( + m, + ))) + }) + }) + .unwrap_or(Subscription::none()) } else { Subscription::none() } @@ -140,7 +160,7 @@ impl State for PsbtsPanel { ) -> Task { self.wallet = wallet; self.selected_tx = None; - self.import_tx = None; + self.modal = None; let daemon = daemon.clone(); Task::perform( async move { @@ -159,74 +179,3 @@ impl From for Box { Box::new(s) } } - -pub struct ImportPsbtModal { - imported: form::Value, - processing: bool, - error: Option, - success: bool, -} - -impl ImportPsbtModal { - pub fn new() -> Self { - Self { - imported: form::Value::default(), - processing: false, - error: None, - success: false, - } - } -} - -impl ImportPsbtModal { - fn view<'a>(&self) -> Element<'a, view::Message> { - if self.success { - view::psbts::import_psbt_success_view() - } else { - view::psbts::import_psbt_view(&self.imported, self.error.as_ref(), self.processing) - } - } - - fn update( - &mut self, - daemon: Arc, - _cache: &Cache, - message: Message, - ) -> Task { - match message { - Message::Updated(res) => { - self.processing = false; - match res { - Ok(()) => { - self.success = true; - self.error = None; - } - Err(e) => self.error = e.into(), - } - } - Message::View(view::Message::ImportSpend(view::ImportSpendMessage::PsbtEdited(s))) => { - self.imported.value = s; - self.imported.valid = Psbt::from_str(&self.imported.value).ok().is_some(); - } - Message::View(view::Message::ImportSpend(view::ImportSpendMessage::Confirm)) => { - if self.imported.valid { - self.processing = true; - self.error = None; - let imported = Psbt::from_str(&self.imported.value).expect("Already checked"); - return Task::perform( - async move { - daemon - .update_spend_tx(&imported) - .await - .map_err(|e| e.into()) - }, - Message::Updated, - ); - } - } - _ => {} - } - - Task::none() - } -} diff --git a/liana-gui/src/app/view/message.rs b/liana-gui/src/app/view/message.rs index 04a4780f..543d8018 100644 --- a/liana-gui/src/app/view/message.rs +++ b/liana-gui/src/app/view/message.rs @@ -26,6 +26,7 @@ pub enum Message { ImportExport(ImportExportMessage), HideRescanWarning, ExportPsbt, + ImportPsbt, } impl Close for Message { diff --git a/liana-gui/src/app/view/psbt.rs b/liana-gui/src/app/view/psbt.rs index 4d22bf55..81927130 100644 --- a/liana-gui/src/app/view/psbt.rs +++ b/liana-gui/src/app/view/psbt.rs @@ -346,10 +346,10 @@ pub fn spend_overview_view<'a>( ) .push( button::secondary( - Some(icon::import_icon()), - "Update", + Some(icon::restore_icon()), + "Import", ) - .on_press(Message::Spend(SpendTxMessage::EditPsbt)), + .on_press(Message::ImportPsbt), ), ) .align_y(Alignment::Center), diff --git a/liana-gui/src/app/view/psbts.rs b/liana-gui/src/app/view/psbts.rs index adc0fa40..b0d7def2 100644 --- a/liana-gui/src/app/view/psbts.rs +++ b/liana-gui/src/app/view/psbts.rs @@ -68,8 +68,8 @@ pub fn psbts_view(spend_txs: &[SpendTx]) -> Element<'_, Message> { .spacing(10) .push(Container::new(h3("PSBTs")).width(Length::Fill)) .push( - button::secondary(Some(icon::import_icon()), "Import") - .on_press(Message::ImportSpend(ImportSpendMessage::Import)), + button::secondary(Some(icon::restore_icon()), "Import") + .on_press(Message::ImportPsbt), ) .push( button::secondary(Some(icon::plus_icon()), "New") diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index 5c63421b..91669782 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -605,9 +605,9 @@ pub async fn import_psbt( } daemon.update_spend_tx(&psbt).await?; + send_progress!(sender, Psbt(psbt)); send_progress!(sender, Progress(100.0)); - send_progress!(sender, Psbt(psbt)); Ok(()) } From 3d9440d3ac70451a85b96e780b71d540ec811415 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 8 Apr 2025 15:25:49 +0200 Subject: [PATCH 5/7] export:cleanup --- liana-gui/src/app/state/export.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index b0e26efe..73892306 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -167,7 +167,6 @@ impl ExportModal { if self.import_export_type == ImportExportType::ImportDescriptor { self.state = ImportExportState::Ended; } - // TODO: forward Descriptor } Progress::UpdateAliases(map) => { return Task::perform(async {}, move |_| { From a1918f5eb6009c5e4526edcc57f82f5a1ccec37f Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Mon, 14 Apr 2025 20:26:55 +0200 Subject: [PATCH 6/7] export: do not panic if import an insane PSBT --- liana-gui/src/app/state/psbt.rs | 4 +--- liana-gui/src/export.rs | 9 ++++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index fffe8062..6f1de0c2 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -250,9 +250,7 @@ impl PsbtState { .wallet .main_descriptor .partial_spend_info(&self.tx.psbt) - // FIXME: we should not unwrap here, it seems importing a PSBT w/ an - // non-owned input will trigger a panic - .unwrap(); + .expect("already check in psbt import logic"); } _ => { if let Some(modal) = self.modal.as_mut() { diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index 91669782..d5364f63 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -124,6 +124,7 @@ pub enum Error { ParseXpub, XpubNetwork, TxidNotMatch, + InsanePsbt, } impl Display for Error { @@ -145,7 +146,8 @@ impl Display for Error { Error::Backup(e) => write!(f, "Backup: {e}"), Error::ParseXpub => write!(f, "Failed to parse Xpub from file"), Error::XpubNetwork => write!(f, "Xpub is for another network"), - Error::TxidNotMatch => write!(f, "The imported PSBT txid do not match this PSBT"), + Error::TxidNotMatch => write!(f, "The imported PSBT txid doesn't match this PSBT"), + Error::InsanePsbt => write!(f, "The Psbt is not sane"), } } } @@ -591,12 +593,17 @@ pub async fn import_psbt( let mut file = File::open(&path)?; let daemon = daemon.ok_or(Error::DaemonMissing)?; + let descr = daemon.get_info().await?.descriptors.main; + let mut psbt_str = String::new(); file.read_to_string(&mut psbt_str)?; psbt_str = psbt_str.trim().to_string(); let psbt = Psbt::from_str(&psbt_str).map_err(|_| Error::ParsePsbt)?; send_progress!(sender, Progress(50.0)); + descr + .partial_spend_info(&psbt) + .map_err(|_| Error::InsanePsbt)?; if let Some(txid) = txid { if psbt.unsigned_tx.compute_txid() != txid { From d86bd20a2fbcec0c3c37dd0cabb5ea02130a88f6 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Thu, 17 Apr 2025 19:15:54 +0200 Subject: [PATCH 7/7] export: better wording when user try to import psbt w/ non owned coins --- liana-gui/src/export.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index d5364f63..52015dc9 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -125,6 +125,7 @@ pub enum Error { XpubNetwork, TxidNotMatch, InsanePsbt, + OutpointNotOwned, } impl Display for Error { @@ -148,6 +149,10 @@ impl Display for Error { Error::XpubNetwork => write!(f, "Xpub is for another network"), Error::TxidNotMatch => write!(f, "The imported PSBT txid doesn't match this PSBT"), Error::InsanePsbt => write!(f, "The Psbt is not sane"), + Error::OutpointNotOwned => write!( + f, + "Import failed. The PSBT either doesn't belong to the wallet or has already been spent." + ), } } } @@ -605,13 +610,24 @@ pub async fn import_psbt( .partial_spend_info(&psbt) .map_err(|_| Error::InsanePsbt)?; - if let Some(txid) = txid { - if psbt.unsigned_tx.compute_txid() != txid { + if let Some(txid) = &txid { + if psbt.unsigned_tx.compute_txid() != *txid { return Err(Error::TxidNotMatch); } } - daemon.update_spend_tx(&psbt).await?; + let e = daemon.update_spend_tx(&psbt).await; + if let (None, Err(error)) = (txid, &e) { + if let DaemonError::Unexpected(e) = error { + if e.contains("Unknown outpoint") { + return Err(Error::OutpointNotOwned); + } else { + return Err(Error::Daemon(error.to_string())); + } + } + } else { + e?; + } send_progress!(sender, Psbt(psbt)); send_progress!(sender, Progress(100.0));