diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index 47d2fded..1e2fadac 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( @@ -50,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", @@ -74,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(..) => { @@ -124,12 +155,6 @@ impl ExportModal { self.error = Some(e.clone()); } Progress::None => {} - Progress::Psbt(_) => { - if 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; @@ -142,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 |_| { @@ -150,6 +174,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 cc6f2a7e..6f1de0c2 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,12 +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, Progress}; use crate::{ app::{ cache::Cache, @@ -35,7 +32,9 @@ use crate::{ hw::{HardwareWallet, HardwareWallets}, }; -pub trait Action { +use super::export::ExportModal; + +pub trait Modal { fn load(&self, _daemon: Arc) -> Task { Task::none() } @@ -53,34 +52,34 @@ 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), + Broadcast(BroadcastModal), + Delete(DeleteModal), + Export(ExportModal), } -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, - Self::Update(a) => a, Self::Broadcast(a) => a, Self::Delete(a) => a, + Self::Export(a) => a, } } } -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, - Self::Update(a) => a, Self::Broadcast(a) => a, Self::Delete(a) => a, + Self::Export(a) => a, } } } @@ -92,7 +91,7 @@ pub struct PsbtState { pub saved: bool, pub warning: Option, pub labels_edited: LabelsEdited, - pub action: Option, + pub modal: Option, } impl PsbtState { @@ -102,27 +101,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() } @@ -135,38 +134,62 @@ 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::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; + } + } + 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(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)); - 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 cmd = modal.load(daemon); + self.modal = Some(PsbtModal::Sign(modal)); return cmd; } Message::View(view::Message::Spend(view::SpendTxMessage::Broadcast)) => { @@ -188,7 +211,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 +229,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() })); @@ -223,11 +244,17 @@ 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) + .expect("already check in psbt import logic"); + } _ => { - 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 +272,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 +281,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 +330,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 +385,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 +430,7 @@ impl Action for DeleteAction { } } -pub struct SignAction { +pub struct SignModal { wallet: Arc, hws: HardwareWallets, error: Option, @@ -413,7 +440,7 @@ pub struct SignAction { display_modal: bool, } -impl SignAction { +impl SignModal { pub fn new( signed: HashSet, wallet: Arc, @@ -433,7 +460,7 @@ impl SignAction { } } -impl Action for SignAction { +impl Modal for SignModal { fn subscription(&self) -> Subscription { self.hws.refresh().map(Message::HardwareWallets) } @@ -637,97 +664,6 @@ async fn sign_psbt( Ok(psbt) } -pub struct UpdateAction { - wallet: Arc, - psbt: String, - updated: form::Value, - processing: bool, - error: Option, - success: bool, -} - -impl UpdateAction { - pub fn new(wallet: Arc, psbt: String) -> Self { - Self { - wallet, - psbt, - updated: form::Value::default(), - processing: false, - error: None, - success: false, - } - } -} - -impl Action for UpdateAction { - 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/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 } diff --git a/liana-gui/src/app/view/message.rs b/liana-gui/src/app/view/message.rs index d4a2833b..543d8018 100644 --- a/liana-gui/src/app/view/message.rs +++ b/liana-gui/src/app/view/message.rs @@ -25,6 +25,8 @@ pub enum Message { ShowQrCode(usize), 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 2114b625..81927130 100644 --- a/liana-gui/src/app/view/psbt.rs +++ b/liana-gui/src/app/view/psbt.rs @@ -339,17 +339,17 @@ 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( - 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 df1361b6..52015dc9 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -123,6 +123,9 @@ pub enum Error { Backup(backup::Error), ParseXpub, XpubNetwork, + TxidNotMatch, + InsanePsbt, + OutpointNotOwned, } impl Display for Error { @@ -144,6 +147,12 @@ 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 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." + ), } } } @@ -162,7 +171,7 @@ pub enum ImportExportType { WalletFromBackup, Descriptor(LianaDescriptor), ExportLabels, - ImportPsbt, + ImportPsbt(Option), ImportXpub(Network), ImportDescriptor, } @@ -178,7 +187,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 +303,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,17 +589,48 @@ 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 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 { + return Err(Error::TxidNotMatch); + } + } + + 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)); - send_progress!(sender, Psbt(psbt)); Ok(()) }