From 158651ebe7fbbf07fa8f8fbd74aa927ea53034ce Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Thu, 30 Jan 2025 07:22:18 +0100 Subject: [PATCH] import: implement import from file for psbt & descriptor --- liana-gui/src/app/message.rs | 4 +- liana-gui/src/app/state/export.rs | 90 +++++++++------ liana-gui/src/app/state/transactions.rs | 14 ++- liana-gui/src/app/view/export.rs | 45 ++++---- liana-gui/src/app/view/message.rs | 4 +- liana-gui/src/app/view/transactions.rs | 7 +- liana-gui/src/export.rs | 145 ++++++++++++++++-------- 7 files changed, 194 insertions(+), 115 deletions(-) diff --git a/liana-gui/src/app/message.rs b/liana-gui/src/app/message.rs index dad49c93..96c68dc6 100644 --- a/liana-gui/src/app/message.rs +++ b/liana-gui/src/app/message.rs @@ -11,7 +11,7 @@ use lianad::config::Config as DaemonConfig; use crate::{ app::{cache::Cache, error::Error, view, wallet::Wallet}, daemon::model::*, - export::ExportMessage, + export::ImportExportMessage, hw::HardwareWalletMessage, }; @@ -47,5 +47,5 @@ pub enum Message { LabelsUpdated(Result>, Error>), BroadcastModal(Result, Error>), RbfModal(Box, bool, Result, Error>), - Export(ExportMessage), + Export(ImportExportMessage), } diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index 4c4954a9..e133f002 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -13,40 +13,40 @@ use crate::{ view::{self, export::export_modal}, }, daemon::Daemon, - export::{self, get_path, ExportMessage, ExportProgress, ExportState, ExportType}, + export::{self, get_path, ImportExportMessage, ImportExportState, ImportExportType, Progress}, }; #[derive(Debug)] pub struct ExportModal { path: Option, handle: Option>>>, - state: ExportState, + state: ImportExportState, error: Option, daemon: Arc, - export_type: ExportType, + import_export_type: ImportExportType, } impl ExportModal { #[allow(clippy::new_without_default)] - pub fn new(daemon: Arc, export_type: ExportType) -> Self { + pub fn new(daemon: Arc, export_type: ImportExportType) -> Self { Self { path: None, handle: None, - state: ExportState::Init, + state: ImportExportState::Init, error: None, daemon, - export_type, + import_export_type: export_type, } } pub fn default_filename(&self) -> String { - match &self.export_type { - ExportType::Transactions => { + match &self.import_export_type { + ImportExportType::Transactions => { let date = chrono::Local::now().format("%Y-%m-%dT%H-%M-%S"); format!("liana-txs-{date}.csv") } - ExportType::Psbt(_) => "psbt.psbt".into(), - ExportType::Descriptor(descriptor) => { + ImportExportType::ExportPsbt(_) => "psbt.psbt".into(), + ImportExportType::Descriptor(descriptor) => { let checksum = descriptor .to_string() .split_once('#') @@ -55,48 +55,64 @@ impl ExportModal { .to_string(); format!("liana-{}.descriptor", checksum) } + ImportExportType::ImportPsbt => "psbt.psbt".into(), + ImportExportType::ImportDescriptor => "descriptor.descriptor".into(), } } pub fn launch(&self) -> Task { Task::perform(get_path(self.default_filename()), |m| { - app::message::Message::View(view::Message::Export(ExportMessage::Path(m))) + app::message::Message::View(view::Message::ImportExport(ImportExportMessage::Path(m))) }) } - pub fn update(&mut self, message: ExportMessage) -> Task { + pub fn update(&mut self, message: ImportExportMessage) -> Task { match message { - ExportMessage::ExportProgress(m) => match m { - ExportProgress::Started(handle) => { + ImportExportMessage::Progress(m) => match m { + Progress::Started(handle) => { self.handle = Some(handle); - self.state = ExportState::Progress(0.0); + self.state = ImportExportState::Progress(0.0); } - ExportProgress::Progress(p) => { - if let ExportState::Progress(_) = self.state { - self.state = ExportState::Progress(p); + Progress::Progress(p) => { + if let ImportExportState::Progress(_) = self.state { + self.state = ImportExportState::Progress(p); } } - ExportProgress::Finished | ExportProgress::Ended => self.state = ExportState::Ended, - ExportProgress::Error(e) => self.error = Some(e), - ExportProgress::None => {} + Progress::Finished | Progress::Ended => self.state = ImportExportState::Ended, + Progress::Error(e) => self.error = Some(e), + Progress::None => {} + Progress::Psbt(_) => { + if self.import_export_type == ImportExportType::ImportPsbt { + self.state = ImportExportState::Ended; + } + // TODO: forward PSBT + } + Progress::Descriptor(_) => { + if self.import_export_type == ImportExportType::ImportDescriptor { + self.state = ImportExportState::Ended; + } + // TODO: forward Descriptor + } }, - ExportMessage::TimedOut => { - self.stop(ExportState::TimedOut); + ImportExportMessage::TimedOut => { + self.stop(ImportExportState::TimedOut); } - ExportMessage::UserStop => { - self.stop(ExportState::Aborted); + ImportExportMessage::UserStop => { + self.stop(ImportExportState::Aborted); } - ExportMessage::Path(p) => { + ImportExportMessage::Path(p) => { if let Some(path) = p { self.path = Some(path); self.start(); } else { return Task::perform(async {}, |_| { - app::message::Message::View(view::Message::Export(ExportMessage::Close)) + app::message::Message::View(view::Message::ImportExport( + ImportExportMessage::Close, + )) }); } } - ExportMessage::Close | ExportMessage::Open => { /* unreachable */ } + ImportExportMessage::Close | ImportExportMessage::Open => { /* unreachable */ } } Task::none() } @@ -106,36 +122,36 @@ impl ExportModal { export_modal(&self.state, self.error.as_ref(), "Transactions"), ); match self.state { - ExportState::TimedOut - | ExportState::Aborted - | ExportState::Ended - | ExportState::Closed => modal.on_blur(Some(view::Message::Close)), + ImportExportState::TimedOut + | ImportExportState::Aborted + | ImportExportState::Ended + | ImportExportState::Closed => modal.on_blur(Some(view::Message::Close)), _ => modal, } .into() } pub fn start(&mut self) { - self.state = ExportState::Started; + self.state = ImportExportState::Started; } - pub fn stop(&mut self, state: ExportState) { + pub fn stop(&mut self, state: ImportExportState) { if let Some(handle) = self.handle.take() { handle.lock().expect("poisoned").abort(); self.state = state; } } - pub fn subscription(&self) -> Option> { + pub fn subscription(&self) -> Option> { if let Some(path) = &self.path { match &self.state { - ExportState::Started | ExportState::Progress(_) => { + ImportExportState::Started | ImportExportState::Progress(_) => { Some(iced::Subscription::run_with_id( "transactions", export::export_subscription( self.daemon.clone(), path.to_path_buf(), - ExportType::Transactions, + self.import_export_type.clone(), ), )) } diff --git a/liana-gui/src/app/state/transactions.rs b/liana-gui/src/app/state/transactions.rs index c0e4c7f8..dfbf4353 100644 --- a/liana-gui/src/app/state/transactions.rs +++ b/liana-gui/src/app/state/transactions.rs @@ -28,7 +28,7 @@ use crate::{ wallet::Wallet, }, daemon::model::{self, LabelsLoader}, - export::{ExportMessage, ExportType}, + export::{ImportExportMessage, ImportExportType}, }; use crate::daemon::{ @@ -266,18 +266,18 @@ impl State for TransactionsPanel { ); } } - Message::View(view::Message::Export(ExportMessage::Open)) => { + Message::View(view::Message::ImportExport(ImportExportMessage::Open)) => { if let TransactionsModal::None = &self.modal { self.modal = TransactionsModal::Export(ExportModal::new( daemon, - ExportType::Transactions, + ImportExportType::Transactions, )); if let TransactionsModal::Export(m) = &self.modal { return m.launch(); } } } - Message::View(view::Message::Export(ExportMessage::Close)) => { + Message::View(view::Message::ImportExport(ImportExportMessage::Close)) => { if let TransactionsModal::Export(_) = &self.modal { self.modal = TransactionsModal::None; } @@ -286,7 +286,7 @@ impl State for TransactionsPanel { return match &mut self.modal { TransactionsModal::CreateRbf(modal) => modal.update(daemon, _cache, message), TransactionsModal::Export(modal) => { - if let Message::View(view::Message::Export(m)) = msg { + if let Message::View(view::Message::ImportExport(m)) = msg { modal.update(m.clone()) } else { Task::none() @@ -330,7 +330,9 @@ impl State for TransactionsPanel { if let TransactionsModal::Export(modal) = &self.modal { if let Some(sub) = modal.subscription() { return sub.map(|m| { - Message::View(view::Message::Export(ExportMessage::ExportProgress(m))) + Message::View(view::Message::ImportExport(ImportExportMessage::Progress( + m, + ))) }); } } diff --git a/liana-gui/src/app/view/export.rs b/liana-gui/src/app/view/export.rs index af9e9b2d..900cee8b 100644 --- a/liana-gui/src/app/view/export.rs +++ b/liana-gui/src/app/view/export.rs @@ -11,21 +11,21 @@ use liana_ui::{ widget::Element, }; -use crate::export::{Error, ExportMessage}; -use crate::{app::view::message::Message, export::ExportState}; +use crate::export::{Error, ImportExportMessage}; +use crate::{app::view::message::Message, export::ImportExportState}; /// Return the modal view for an export task pub fn export_modal<'a>( - state: &ExportState, + state: &ImportExportState, error: Option<&'a Error>, export_type: &str, ) -> Element<'a, Message> { let button = match state { - ExportState::Started | ExportState::Progress(_) => { - Some(button::secondary(None, "Cancel").on_press(ExportMessage::UserStop.into())) + ImportExportState::Started | ImportExportState::Progress(_) => { + Some(button::secondary(None, "Cancel").on_press(ImportExportMessage::UserStop.into())) } - ExportState::Ended | ExportState::TimedOut | ExportState::Aborted => { - Some(button::secondary(None, "Close").on_press(ExportMessage::Close.into())) + ImportExportState::Ended | ImportExportState::TimedOut | ImportExportState::Aborted => { + Some(button::secondary(None, "Close").on_press(ImportExportMessage::Close.into())) } _ => None, }; @@ -33,26 +33,29 @@ pub fn export_modal<'a>( format!("{:?}", error) } else { match state { - ExportState::Init => "".to_string(), - ExportState::ChoosePath => { + ImportExportState::Init => "".to_string(), + ImportExportState::ChoosePath => { "Select the path you want to export in the popup window...".into() } - ExportState::Path(_) => "".into(), - ExportState::Started => "Starting export...".into(), - ExportState::Progress(p) => format!("Progress: {}%", p.round()), - ExportState::TimedOut => "Export failed: timeout".into(), - ExportState::Aborted => "Export canceled".into(), - ExportState::Ended => "Export successful!".into(), - ExportState::Closed => "".into(), + ImportExportState::Path(_) => "".into(), + ImportExportState::Started => "Starting export...".into(), + ImportExportState::Progress(p) => format!("Progress: {}%", p.round()), + ImportExportState::TimedOut => "Export failed: timeout".into(), + ImportExportState::Aborted => "Export canceled".into(), + ImportExportState::Ended => "Export successful!".into(), + ImportExportState::Closed => "".into(), } }; let p = match state { - ExportState::Init => 0.0, - ExportState::ChoosePath | ExportState::Path(_) | ExportState::Started => 5.0, - ExportState::Progress(p) => *p, - ExportState::TimedOut | ExportState::Aborted | ExportState::Ended | ExportState::Closed => { - 100.0 + ImportExportState::Init => 0.0, + ImportExportState::ChoosePath | ImportExportState::Path(_) | ImportExportState::Started => { + 5.0 } + ImportExportState::Progress(p) => *p, + ImportExportState::TimedOut + | ImportExportState::Aborted + | ImportExportState::Ended + | ImportExportState::Closed => 100.0, }; let progress_bar_row = Row::new() .push(Space::with_width(30)) diff --git a/liana-gui/src/app/view/message.rs b/liana-gui/src/app/view/message.rs index 345dd1dd..a1e796c8 100644 --- a/liana-gui/src/app/view/message.rs +++ b/liana-gui/src/app/view/message.rs @@ -1,4 +1,4 @@ -use crate::{app::menu::Menu, export::ExportMessage, node::bitcoind::RpcAuthType}; +use crate::{app::menu::Menu, export::ImportExportMessage, node::bitcoind::RpcAuthType}; use liana::miniscript::bitcoin::{bip32::Fingerprint, OutPoint}; #[derive(Debug, Clone)] @@ -19,7 +19,7 @@ pub enum Message { SelectHardwareWallet(usize), CreateRbf(CreateRbfMessage), ShowQrCode(usize), - Export(ExportMessage), + ImportExport(ImportExportMessage), } #[derive(Debug, Clone)] diff --git a/liana-gui/src/app/view/transactions.rs b/liana-gui/src/app/view/transactions.rs index 1694deb1..e9cee700 100644 --- a/liana-gui/src/app/view/transactions.rs +++ b/liana-gui/src/app/view/transactions.rs @@ -25,7 +25,7 @@ use crate::{ }, }, daemon::model::{HistoryTransaction, Txid}, - export::ExportMessage, + export::ImportExportMessage, }; pub fn transactions_view<'a>( @@ -44,7 +44,10 @@ pub fn transactions_view<'a>( Row::new() .push(Container::new(h3("Transactions"))) .push(Space::with_width(Length::Fill)) - .push(button::secondary(None, "Export").on_press(ExportMessage::Open.into())), + .push( + button::secondary(None, "Export") + .on_press(ImportExportMessage::Open.into()), + ), ) .push( Column::new() diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index 5a3cd65f..8ec294a5 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -1,8 +1,9 @@ use std::{ collections::HashMap, fs::{self, File}, - io::Write, + io::{Read, Write}, path::PathBuf, + str::FromStr, sync::{ mpsc::{channel, Receiver, Sender}, Arc, Mutex, @@ -13,7 +14,7 @@ use std::{ use chrono::{DateTime, Duration, Utc}; use liana::{ descriptors::LianaDescriptor, - miniscript::bitcoin::{Amount, Txid}, + miniscript::bitcoin::{Amount, Psbt, Txid}, }; use tokio::{ task::{JoinError, JoinHandle}, @@ -33,26 +34,26 @@ use crate::{ macro_rules! send_error { ($sender:ident, $error:ident) => { - if let Err(e) = $sender.send(ExportProgress::Error(Error::$error)) { - tracing::error!("ExportState::start() fail to send msg: {}", e); + if let Err(e) = $sender.send(Progress::Error(Error::$error)) { + tracing::error!("Import/Export fail to send msg: {}", e); } }; ($sender:ident, $error:expr) => { - if let Err(e) = $sender.send(ExportProgress::Error($error)) { - tracing::error!("ExportState::start() fail to send msg: {}", e); + if let Err(e) = $sender.send(Progress::Error($error)) { + tracing::error!("Import/Export fail to send msg: {}", e); } }; } macro_rules! send_progress { ($sender:ident, $progress:ident) => { - if let Err(e) = $sender.send(ExportProgress::$progress) { - tracing::error!("ExportState::start() fail to send msg: {}", e); + if let Err(e) = $sender.send(Progress::$progress) { + tracing::error!("ImportExport fail to send msg: {}", e); } }; ($sender:ident, $progress:ident($val:expr)) => { - if let Err(e) = $sender.send(ExportProgress::$progress($val)) { - tracing::error!("ExportState::start() fail to send msg: {}", e); + if let Err(e) = $sender.send(Progress::$progress($val)) { + tracing::error!("ImportExport fail to send msg: {}", e); } }; } @@ -83,23 +84,23 @@ macro_rules! open_file { } #[derive(Debug, Clone)] -pub enum ExportMessage { +pub enum ImportExportMessage { Open, - ExportProgress(ExportProgress), + Progress(Progress), TimedOut, UserStop, Path(Option), Close, } -impl From for view::Message { - fn from(value: ExportMessage) -> Self { - Self::Export(value) +impl From for view::Message { + fn from(value: ImportExportMessage) -> Self { + Self::ImportExport(value) } } #[derive(Debug, PartialEq)] -pub enum ExportState { +pub enum ImportExportState { Init, ChoosePath, Path(PathBuf), @@ -121,13 +122,18 @@ pub enum Error { NoParentDir, Daemon(String), TxTimeMissing, + DaemonMissing, + ParsePsbt, + ParseDescriptor, } #[derive(Debug, PartialEq, Clone)] -pub enum ExportType { +pub enum ImportExportType { Transactions, - Psbt(String), + ExportPsbt(String), Descriptor(LianaDescriptor), + ImportPsbt, + ImportDescriptor, } impl From for Error { @@ -156,29 +162,31 @@ pub enum Status { } #[derive(Debug, Clone)] -pub enum ExportProgress { +pub enum Progress { Started(Arc>>), Progress(f32), Ended, Finished, Error(Error), None, + Psbt(Psbt), + Descriptor(LianaDescriptor), } pub struct Export { - pub receiver: Receiver, - pub sender: Option>, + pub receiver: Receiver, + pub sender: Option>, pub handle: Option>>>, - pub daemon: Arc, + pub daemon: Option>, pub path: Box, - pub export_type: ExportType, + pub export_type: ImportExportType, } impl Export { pub fn new( - daemon: Arc, + daemon: Option>, path: Box, - export_type: ExportType, + export_type: ImportExportType, ) -> Self { let (sender, receiver) = channel(); Export { @@ -192,15 +200,17 @@ impl Export { } pub async fn export_logic( - export_type: ExportType, - sender: Sender, - daemon: Arc, + export_type: ImportExportType, + sender: Sender, + daemon: Option>, path: PathBuf, ) { match export_type { - ExportType::Transactions => export_transactions(sender, daemon, path).await, - ExportType::Psbt(psbt) => export_psbt(sender, path, psbt), - ExportType::Descriptor(descriptor) => export_descriptor(sender, path, descriptor), + ImportExportType::Transactions => export_transactions(sender, daemon, path).await, + ImportExportType::ExportPsbt(psbt) => export_psbt(sender, path, psbt), + ImportExportType::Descriptor(descriptor) => export_descriptor(sender, path, descriptor), + ImportExportType::ImportPsbt => import_psbt(sender, path), + ImportExportType::ImportDescriptor => import_descriptor(sender, path), }; } @@ -238,10 +248,10 @@ impl Export { pub fn export_subscription( daemon: Arc, path: PathBuf, - export_type: ExportType, -) -> impl Stream { + export_type: ImportExportType, +) -> impl Stream { iced::stream::channel(100, move |mut output| async move { - let mut state = Export::new(daemon, Box::new(path), export_type); + let mut state = Export::new(Some(daemon), Box::new(path), export_type); loop { match state.state() { Status::Init => { @@ -269,7 +279,7 @@ pub fn export_subscription( let handle = match state.handle.take() { Some(h) => h, None => { - if let Err(e) = output.send(ExportProgress::Error(Error::HandleLost)).await { + if let Err(e) = output.send(Progress::Error(Error::HandleLost)).await { tracing::error!("export_subscription() fail to send message: {}", e); } continue; @@ -278,9 +288,9 @@ pub fn export_subscription( let msg = { let h = handle.lock().expect("should not fail"); if h.is_finished() { - Some(ExportProgress::Finished) + Some(Progress::Finished) } else if disconnected { - Some(ExportProgress::Error(Error::ChannelLost)) + Some(Progress::Error(Error::ChannelLost)) } else { None } @@ -299,11 +309,18 @@ pub fn export_subscription( } pub async fn export_transactions( - sender: Sender, - daemon: Arc, + sender: Sender, + daemon: Option>, path: PathBuf, ) { async move { + let daemon = match daemon { + Some(d) => d, + None => { + send_error!(sender, Error::DaemonMissing); + return; + } + }; let mut file = open_file!(path, sender); let header = "Date,Label,Value,Fee,Txid,Block\n".to_string(); @@ -456,11 +473,7 @@ pub async fn export_transactions( .await; } -pub fn export_descriptor( - sender: Sender, - path: PathBuf, - descriptor: LianaDescriptor, -) { +pub fn export_descriptor(sender: Sender, path: PathBuf, descriptor: LianaDescriptor) { let mut file = open_file!(path, sender); let descr_string = descriptor.to_string(); @@ -472,7 +485,7 @@ pub fn export_descriptor( send_progress!(sender, Ended); } -pub fn export_psbt(sender: Sender, path: PathBuf, psbt: String) { +pub fn export_psbt(sender: Sender, path: PathBuf, psbt: String) { let mut file = open_file!(path, sender); if let Err(e) = file.write_all(psbt.as_bytes()) { @@ -483,6 +496,48 @@ pub fn export_psbt(sender: Sender, path: PathBuf, psbt: String) send_progress!(sender, Ended); } +pub fn import_psbt(sender: Sender, path: PathBuf) { + let mut file = open_file!(path, sender); + + let mut psbt_str = String::new(); + if let Err(e) = file.read_to_string(&mut psbt_str) { + send_error!(sender, e.into()); + return; + } + + let psbt = match Psbt::from_str(&psbt_str) { + Ok(psbt) => psbt, + Err(_) => { + send_error!(sender, Error::ParsePsbt); + return; + } + }; + + send_progress!(sender, Progress(100.0)); + send_progress!(sender, Psbt(psbt)); +} + +pub fn import_descriptor(sender: Sender, path: PathBuf) { + let mut file = open_file!(path, sender); + + let mut descr_str = String::new(); + if let Err(e) = file.read_to_string(&mut descr_str) { + send_error!(sender, e.into()); + return; + } + + let descriptor = match LianaDescriptor::from_str(&descr_str) { + Ok(psbt) => psbt, + Err(_) => { + send_error!(sender, Error::ParseDescriptor); + return; + } + }; + + send_progress!(sender, Progress(100.0)); + send_progress!(sender, Descriptor(descriptor)); +} + pub async fn get_path(filename: String) -> Option { rfd::AsyncFileDialog::new() .set_title("Choose a location to export...")