From 30228c6490c45b2b775ac6974b40998dac565208 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Mon, 12 May 2025 17:51:41 +0200 Subject: [PATCH] refac delete wallet modal and desc backup --- liana-gui/src/app/settings.rs | 47 ++++- liana-gui/src/app/wallet.rs | 10 +- liana-gui/src/backup.rs | 56 +----- liana-gui/src/delete.rs | 82 ++++++++ liana-gui/src/dir.rs | 8 +- liana-gui/src/installer/message.rs | 2 +- liana-gui/src/installer/mod.rs | 188 ++++++++++-------- .../src/installer/step/descriptor/mod.rs | 2 +- liana-gui/src/installer/step/mod.rs | 2 +- liana-gui/src/launcher.rs | 70 ++++--- liana-gui/src/lib.rs | 1 + liana-gui/src/loader.rs | 4 +- .../src/services/connect/client/cache.rs | 63 ++++++ 13 files changed, 355 insertions(+), 180 deletions(-) create mode 100644 liana-gui/src/delete.rs diff --git a/liana-gui/src/app/settings.rs b/liana-gui/src/app/settings.rs index 0fc72217..518c5c29 100644 --- a/liana-gui/src/app/settings.rs +++ b/liana-gui/src/app/settings.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use async_fd_lock::LockWrite; +use liana::descriptors::LianaDescriptor; use std::io::SeekFrom; use tokio::fs::OpenOptions; use tokio::io::AsyncSeekExt; @@ -190,11 +191,49 @@ impl WalletSettings { } } - pub fn wallet_id(&self) -> String { - if let Some(t) = self.pinned_at { - format!("{}-{}", self.descriptor_checksum, t) + pub fn wallet_id(&self) -> WalletId { + WalletId { + timestamp: self.pinned_at, + descriptor_checksum: self.descriptor_checksum.clone(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WalletId { + pub timestamp: Option, + pub descriptor_checksum: String, +} + +impl WalletId { + pub fn new(descriptor_checksum: String, timestamp: Option) -> Self { + WalletId { + timestamp, + descriptor_checksum, + } + } + pub fn generate(descriptor: &LianaDescriptor) -> Self { + WalletId { + timestamp: Some(chrono::Utc::now().timestamp()), + descriptor_checksum: descriptor + .to_string() + .split_once('#') + .map(|(_, checksum)| checksum) + .expect("LianaDescriptor.to_string() always include the checksum") + .to_string(), + } + } + pub fn is_legacy(&self) -> bool { + self.timestamp.is_none() + } +} + +impl std::fmt::Display for WalletId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(t) = self.timestamp { + write!(f, "{}-{}", self.descriptor_checksum, t) } else { - self.descriptor_checksum.clone() + write!(f, "{}", self.descriptor_checksum) } } } diff --git a/liana-gui/src/app/wallet.rs b/liana-gui/src/app/wallet.rs index a87a8360..fad98487 100644 --- a/liana-gui/src/app/wallet.rs +++ b/liana-gui/src/app/wallet.rs @@ -11,7 +11,7 @@ use liana::{miniscript::bitcoin, signer::HotSigner}; use liana::descriptors::LianaDescriptor; use liana::miniscript::bitcoin::bip32::Fingerprint; -use super::settings::WalletSettings; +use super::settings::{WalletId, WalletSettings}; const DEFAULT_WALLET_NAME: &str = "Liana"; @@ -72,12 +72,8 @@ impl Wallet { } // To match with WalletSettings.wallet_id - pub fn id(&self) -> String { - if let Some(t) = self.pinned_at { - format!("{}-{}", self.descriptor_checksum, t) - } else { - self.descriptor_checksum.clone() - } + pub fn id(&self) -> WalletId { + WalletId::new(self.descriptor_checksum.clone(), self.pinned_at) } pub fn with_pinned_at(mut self, pinned_at: Option) -> Self { diff --git a/liana-gui/src/backup.rs b/liana-gui/src/backup.rs index ba721f2a..fd436668 100644 --- a/liana-gui/src/backup.rs +++ b/liana-gui/src/backup.rs @@ -20,16 +20,13 @@ use tokio::sync::mpsc::UnboundedSender; use crate::{ app::{ settings::{Settings, WalletSettings}, - wallet::Wallet, + wallet::{wallet_name, Wallet}, Config, }, daemon::{model::HistoryTransaction, Daemon, DaemonBackend, DaemonError}, dir::LianaDirectory, export::Progress, - installer::{ - extract_daemon_config, extract_local_gui_settings, extract_remote_gui_settings, Context, - RemoteBackend, - }, + installer::Context, services::connect::client::backend::api::DEFAULT_LIMIT, VERSION, }; @@ -116,54 +113,23 @@ impl Backup { /// /// # Arguments /// * `ctx` - the installer context - /// * `timestamp` - whether to record the current timestamp as wallet creation time - /// (we should want to set timestamp = false for a wallet import for instance) - pub async fn from_installer(ctx: Context, timestamp: bool) -> Result { - let descriptor = ctx - .descriptor - .clone() - .ok_or(Error::DescriptorMissing)? - .to_string(); + pub async fn from_installer_descriptor_step(ctx: Context) -> Result { + let descriptor = ctx.descriptor.clone().ok_or(Error::DescriptorMissing)?; let now = now(); + let name = Some(wallet_name(&descriptor)); - let mut account = Account::new(descriptor); - - let mut proprietary = serde_json::Map::new(); - proprietary.insert(LIANA_VERSION_KEY.to_string(), liana_version().into()); - - let settings = match &ctx.remote_backend { - // This append while user is importing a wallet already created on Liana-Connect. - RemoteBackend::WithWallet(backend) => extract_remote_gui_settings(&ctx, backend).await, - // Other cases are about wallet creation, the ctx contains all the keys aliases and - // descriptor registration hmacs. - _ => extract_local_gui_settings(&ctx), - }; - - let config = - extract_daemon_config(&ctx, &settings).map_err(|e| Error::Daemon(e.to_string()))?; - if let Ok(config) = serde_json::to_value(config) { - proprietary.insert(CONFIG_KEY.to_string(), config); - } - - let name = { - let name = settings.name.clone(); - if let Ok(settings) = serde_json::to_value(settings) { - proprietary.insert(SETTINGS_KEY.to_string(), settings); - } - Some(name) - }; + let mut account = Account::new(descriptor.to_string()); + account.name = name.clone(); + account.timestamp = Some(now); + account + .proprietary + .insert(LIANA_VERSION_KEY.to_string(), liana_version().into()); ctx.keys.iter().for_each(|(k, s)| { account.keys.insert(*k, s.to_backup()); }); - account.proprietary = proprietary; - account.name = name.clone(); - if timestamp { - account.timestamp = Some(now); - } - Ok(Backup { name, accounts: vec![account], diff --git a/liana-gui/src/delete.rs b/liana-gui/src/delete.rs new file mode 100644 index 00000000..dd3c27b1 --- /dev/null +++ b/liana-gui/src/delete.rs @@ -0,0 +1,82 @@ +use std::collections::HashSet; + +use crate::{ + app::settings::{self, SettingsError, WalletId}, + dir::NetworkDirectory, + services::connect::client::cache::{self, ConnectCacheError}, +}; + +pub enum DeleteError { + Io(std::io::Error), + Settings(SettingsError), + Connect(ConnectCacheError), +} + +impl std::fmt::Display for DeleteError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Io(e) => write!(f, "{}", e), + Self::Settings(e) => write!(f, "{}", e), + Self::Connect(e) => write!(f, "{}", e), + } + } +} + +impl From for DeleteError { + fn from(value: std::io::Error) -> Self { + DeleteError::Io(value) + } +} + +fn ignore_not_found(result: std::io::Result) -> std::io::Result> { + match result { + Ok(value) => Ok(Some(value)), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(err) => Err(err), + } +} + +pub async fn delete_wallet( + network_dir: &NetworkDirectory, + wallet_id: &WalletId, +) -> Result<(), DeleteError> { + let lianad_directory = network_dir.lianad_data_directory(wallet_id); + if !wallet_id.is_legacy() { + ignore_not_found(tokio::fs::remove_dir_all(lianad_directory.path()).await)?; + } else { + // if this is a legacy wallet, then it is the only wallet in the network directory. + ignore_not_found(tokio::fs::remove_file(lianad_directory.sqlite_db_file_path()).await)?; + ignore_not_found( + tokio::fs::remove_dir_all(lianad_directory.lianad_watchonly_wallet_path()).await, + )?; + ignore_not_found( + tokio::fs::remove_file(lianad_directory.path().join("daemon.toml")).await, + )?; + } + + let mut remaining_accounts = HashSet::::new(); + settings::update_settings_file(network_dir, |mut settings| { + settings + .wallets + .retain(|settings| settings.wallet_id() != *wallet_id); + remaining_accounts = settings + .wallets + .iter() + .filter_map(|settings| { + settings + .remote_backend_auth + .as_ref() + .map(|auth| auth.email.clone()) + }) + .collect(); + settings + }) + .await + .map_err(DeleteError::Settings)?; + + cache::filter_connect_cache(network_dir, &remaining_accounts) + .await + .map_err(DeleteError::Connect)?; + + Ok(()) +} diff --git a/liana-gui/src/dir.rs b/liana-gui/src/dir.rs index 9ced7df7..e8eb8616 100644 --- a/liana-gui/src/dir.rs +++ b/liana-gui/src/dir.rs @@ -1,4 +1,4 @@ -use crate::app::settings::WalletSettings; +use crate::app::settings::WalletId; use liana::miniscript::bitcoin::Network; use lianad::datadir::DataDirectory; use std::path::{Path, PathBuf}; @@ -86,11 +86,11 @@ impl NetworkDirectory { pub fn path(&self) -> &Path { self.0.as_path() } - pub fn lianad_data_directory(&self, settings: &WalletSettings) -> DataDirectory { + pub fn lianad_data_directory(&self, wallet_id: &WalletId) -> DataDirectory { let mut path = self.0.clone(); - if let Some(t) = settings.pinned_at { + if !wallet_id.is_legacy() { path.push("data"); - path.push(format!("{}-{}", settings.descriptor_checksum, t)) + path.push(wallet_id.to_string()); } DataDirectory::new(path) } diff --git a/liana-gui/src/installer/message.rs b/liana-gui/src/installer/message.rs index 804d8616..2eac96c6 100644 --- a/liana-gui/src/installer/message.rs +++ b/liana-gui/src/installer/message.rs @@ -43,7 +43,7 @@ pub enum Message { Reload, Select(usize), UseHotSigner, - Installed(Result), + Installed(settings::WalletId, Result), CreateTaprootDescriptor(bool), SelectDescriptorTemplate(context::DescriptorTemplate), SelectBackend(SelectBackend), diff --git a/liana-gui/src/installer/mod.rs b/liana-gui/src/installer/mod.rs index b67d6cca..e1776897 100644 --- a/liana-gui/src/installer/mod.rs +++ b/liana-gui/src/installer/mod.rs @@ -14,6 +14,7 @@ use liana_ui::{ }; use lianad::config::Config; use std::ops::Deref; +use tokio::runtime::Handle; use tracing::{error, info, warn}; use std::io::Write; @@ -23,11 +24,12 @@ use std::sync::{Arc, Mutex}; use crate::{ app::{ config as gui_config, - settings::{update_settings_file, AuthConfig, SettingsError, WalletSettings}, + settings::{update_settings_file, AuthConfig, SettingsError, WalletId, WalletSettings}, wallet::wallet_name, }, backup, daemon::DaemonError, + delete, dir::LianaDirectory, hw::{HardwareWalletConfig, HardwareWallets}, services::{ @@ -252,50 +254,65 @@ impl Installer { .get_mut(self.current) .expect("There is always a step") .update(&mut self.hws, message); + let wallet_id = WalletId::generate( + self.context + .descriptor + .as_ref() + .expect("Must be a descriptor at this point"), + ); + let context = self.context.clone(); + let signer = self.signer.clone(); match &self.context.remote_backend { RemoteBackend::WithoutWallet(backend) => Task::perform( - create_remote_wallet( - self.context.clone(), - self.signer.clone(), - backend.clone(), + with_wallet_id( + wallet_id.clone(), + create_remote_wallet(context, wallet_id, signer, backend.clone()), ), - Message::Installed, + |(id, res)| Message::Installed(id, res), ), RemoteBackend::WithWallet(backend) => Task::perform( - import_remote_wallet(self.context.clone(), backend.clone()), - Message::Installed, + with_wallet_id( + wallet_id.clone(), + import_remote_wallet(context, wallet_id, backend.clone()), + ), + |(id, res)| Message::Installed(id, res), ), RemoteBackend::None => Task::perform( - install_local_wallet(self.context.clone(), self.signer.clone()), - Message::Installed, + with_wallet_id( + wallet_id.clone(), + install_local_wallet(context, wallet_id, signer), + ), + |(id, res)| Message::Installed(id, res), ), RemoteBackend::Undefined => unreachable!("Must be defined at this point"), } } - Message::Installed(Err(e)) => { + Message::Installed(wallet_id, Err(e)) => { let network_directory = self .context .liana_directory .network_directory(self.context.bitcoin_config.network); // In case of failure during install, block the thread to // deleted the data_dir/network directory in order to start clean again. - warn!("Installation failed. Cleaning up the leftover data directory."); - if let Err(e) = std::fs::remove_dir_all(network_directory.path()) { + warn!("Installation failed. Cleaning up the network directory."); + if let Err(e) = Handle::current() + .block_on(delete::delete_wallet(&network_directory, &wallet_id)) + { error!( - "Failed to completely delete the data directory (path: '{}'): {}", + "Failed to completely clean the network directory (path: '{}'): {}", network_directory.path().to_string_lossy(), e ); } else { warn!( - "Successfully deleted data directory at '{}'.", + "Successfully cleaned network directory at '{}'.", network_directory.path().to_string_lossy() ); }; self.steps .get_mut(self.current) .expect("There is always a step") - .update(&mut self.hws, Message::Installed(Err(e))) + .update(&mut self.hws, Message::Installed(wallet_id, Err(e))) } Message::WalletFromBackup((ks, backup)) => { self.context.keys = ks; @@ -359,8 +376,16 @@ pub fn daemon_check(cfg: lianad::config::Config) -> Result<(), Error> { } } +async fn with_wallet_id(wallet_id: WalletId, res: F) -> (WalletId, Result) +where + F: std::future::Future>, +{ + (wallet_id, res.await) +} + pub async fn install_local_wallet( ctx: Context, + wallet_id: WalletId, signer: Arc>, ) -> Result { let network_datadir = ctx @@ -370,7 +395,31 @@ pub async fn install_local_wallet( .init() .map_err(|e| Error::Unexpected(format!("Failed to create datadir path: {}", e)))?; - let wallet_settings = extract_local_gui_settings(&ctx); + let descriptor = ctx + .descriptor + .as_ref() + .expect("Context must have a descriptor at this point"); + + let hardware_wallets = ctx + .hws + .iter() + .filter_map(|(kind, fingerprint, token)| { + token + .as_ref() + .map(|token| HardwareWalletConfig::new(kind, *fingerprint, token)) + }) + .collect(); + + let wallet_settings = WalletSettings { + name: wallet_name(descriptor), + pinned_at: wallet_id.timestamp, + descriptor_checksum: wallet_id.descriptor_checksum, + keys: ctx.keys.values().cloned().collect(), + hardware_wallets, + remote_backend_auth: None, + start_internal_bitcoind: Some(ctx.internal_bitcoind.is_some()), + }; + let cfg: lianad::config::Config = extract_daemon_config(&ctx, &wallet_settings)?; daemon_check(cfg.clone())?; @@ -384,7 +433,7 @@ pub async fn install_local_wallet( // create lianad configuration file create_and_write_file( &network_datadir - .lianad_data_directory(&wallet_settings) + .lianad_data_directory(&wallet_settings.wallet_id()) .path() .join("daemon.toml"), daemon_config.to_string().as_bytes(), @@ -447,6 +496,7 @@ pub async fn install_local_wallet( pub async fn create_remote_wallet( ctx: Context, + wallet_id: WalletId, signer: Arc>, remote_backend: BackendClient, ) -> Result { @@ -545,7 +595,20 @@ pub async fn create_remote_wallet( let remote_backend = remote_backend.connect_wallet(wallet).0; // create liana GUI settings file - let wallet_settings = extract_remote_gui_settings(&ctx, &remote_backend).await; + // if the wallet is using the remote backend, then the hardware wallet settings and + // keys will be store on the remote backend side and not in the settings file. + let wallet_settings = WalletSettings { + name: wallet_name(descriptor), + descriptor_checksum: wallet_id.descriptor_checksum, + pinned_at: wallet_id.timestamp, + keys: Vec::new(), + hardware_wallets: Vec::new(), + remote_backend_auth: Some(AuthConfig::new( + remote_backend.user_email().to_string(), + remote_backend.wallet_id(), + )), + start_internal_bitcoind: None, + }; update_settings_file(&network_datadir, |mut settings| { settings.wallets.push(wallet_settings.clone()); settings @@ -576,6 +639,7 @@ pub async fn create_remote_wallet( pub async fn import_remote_wallet( ctx: Context, + wallet_id: WalletId, backend: BackendWalletClient, ) -> Result { tracing::info!("Importing wallet from remote backend"); @@ -594,7 +658,24 @@ pub async fn import_remote_wallet( .map_err(|e| Error::Unexpected(format!("Failed to create datadir path: {}", e)))?; // create liana GUI settings file - let wallet_settings = extract_remote_gui_settings(&ctx, &backend).await; + // if the wallet is using the remote backend, then the hardware wallet settings and + // keys will be store on the remote backend side and not in the settings file. + let wallet_settings = WalletSettings { + name: wallet_name( + ctx.descriptor + .as_ref() + .expect("Context must have a descriptor at this point"), + ), + descriptor_checksum: wallet_id.descriptor_checksum, + pinned_at: wallet_id.timestamp, + keys: Vec::new(), + hardware_wallets: Vec::new(), + remote_backend_auth: Some(AuthConfig::new( + backend.user_email().to_string(), + backend.wallet_id(), + )), + start_internal_bitcoind: None, + }; update_settings_file(&network_datadir, |mut settings| { settings.wallets.push(wallet_settings.clone()); settings @@ -646,76 +727,11 @@ pub fn create_and_write_file(path: &Path, data: &[u8]) -> Result<(), Error> { Ok(()) } -// if the wallet is using the remote backend, then the hardware wallet settings and -// keys will be store on the remote backend side and not in the settings file. -pub async fn extract_remote_gui_settings( - ctx: &Context, - backend: &BackendWalletClient, -) -> WalletSettings { - let descriptor = ctx - .descriptor - .as_ref() - .expect("Context must have a descriptor at this point"); - - let descriptor_checksum = descriptor - .to_string() - .split_once('#') - .map(|(_, checksum)| checksum) - .expect("LianaDescriptor.to_string() always include the checksum") - .to_string(); - - WalletSettings { - name: wallet_name(descriptor), - descriptor_checksum, - pinned_at: Some(chrono::Utc::now().timestamp()), - keys: Vec::new(), - hardware_wallets: Vec::new(), - remote_backend_auth: Some(AuthConfig::new( - backend.user_email().to_string(), - backend.wallet_id(), - )), - start_internal_bitcoind: None, - } -} - -pub fn extract_local_gui_settings(ctx: &Context) -> WalletSettings { - let descriptor = ctx - .descriptor - .as_ref() - .expect("Context must have a descriptor at this point"); - - let descriptor_checksum = descriptor - .to_string() - .split_once('#') - .map(|(_, checksum)| checksum) - .expect("LianaDescriptor.to_string() always include the checksum") - .to_string(); - - let hardware_wallets = ctx - .hws - .iter() - .filter_map(|(kind, fingerprint, token)| { - token - .as_ref() - .map(|token| HardwareWalletConfig::new(kind, *fingerprint, token)) - }) - .collect(); - WalletSettings { - name: wallet_name(descriptor), - pinned_at: Some(chrono::Utc::now().timestamp()), - descriptor_checksum, - keys: ctx.keys.values().cloned().collect(), - hardware_wallets, - remote_backend_auth: None, - start_internal_bitcoind: Some(ctx.internal_bitcoind.is_some()), - } -} - pub fn extract_daemon_config(ctx: &Context, settings: &WalletSettings) -> Result { let data_directory = ctx .liana_directory .network_directory(ctx.bitcoin_config.network) - .lianad_data_directory(settings); + .lianad_data_directory(&settings.wallet_id()); data_directory .init() .map_err(|e| Error::CannotCreateDatadir(e.to_string()))?; diff --git a/liana-gui/src/installer/step/descriptor/mod.rs b/liana-gui/src/installer/step/descriptor/mod.rs index 7933fb77..a5ba04b1 100644 --- a/liana-gui/src/installer/step/descriptor/mod.rs +++ b/liana-gui/src/installer/step/descriptor/mod.rs @@ -379,7 +379,7 @@ impl Step for BackupDescriptor { let ctx = ctx.clone(); return Task::perform( async move { - let backup = Backup::from_installer(ctx, true).await?; + let backup = Backup::from_installer_descriptor_step(ctx).await?; serde_json::to_string_pretty(&backup).map_err(|_| backup::Error::Json) }, Message::ExportWallet, diff --git a/liana-gui/src/installer/step/mod.rs b/liana-gui/src/installer/step/mod.rs index 616869c3..fbca9973 100644 --- a/liana-gui/src/installer/step/mod.rs +++ b/liana-gui/src/installer/step/mod.rs @@ -151,7 +151,7 @@ impl Step for Final { }, ); } - Message::Installed(res) => match res { + Message::Installed(_, res) => match res { Err(e) => { self.generating = false; self.wallet_settings = None; diff --git a/liana-gui/src/launcher.rs b/liana-gui/src/launcher.rs index 105de7b6..1c1ae55e 100644 --- a/liana-gui/src/launcher.rs +++ b/liana-gui/src/launcher.rs @@ -11,9 +11,11 @@ use liana_ui::{ widget::*, }; use lianad::config::ConfigError; +use tokio::runtime::Handle; use crate::{ app::{self, settings::WalletSettings}, + delete::{delete_wallet, DeleteError}, dir::{LianaDirectory, NetworkDirectory}, installer::UserFlow, }; @@ -94,19 +96,21 @@ impl Launcher { Message::Install(d, n, UserFlow::ShareXpubs) }) } - Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::ShowModal)) => { - let wallet_datadir = self.datadir_path.network_directory(self.network); - let config_path = wallet_datadir.path().join(app::config::DEFAULT_FILE_NAME); - let internal_bitcoind = if let Ok(cfg) = app::Config::from_file(&config_path) { - Some(cfg.start_internal_bitcoind) - } else { - None - }; - self.delete_wallet_modal = Some(DeleteWalletModal::new( - self.network, - wallet_datadir, - internal_bitcoind, - )); + Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::ShowModal(i))) => { + if let State::Wallets { wallets, .. } = &self.state { + let wallet_datadir = self.datadir_path.network_directory(self.network); + let config_path = wallet_datadir.path().join(app::config::DEFAULT_FILE_NAME); + let internal_bitcoind = if let Ok(cfg) = app::Config::from_file(&config_path) { + Some(cfg.start_internal_bitcoind) + } else { + None + }; + self.delete_wallet_modal = Some(DeleteWalletModal::new( + wallet_datadir, + wallets[i].clone(), + internal_bitcoind, + )); + } Task::none() } Message::View(ViewMessage::SelectNetwork(network)) => { @@ -116,7 +120,8 @@ impl Launcher { } Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::Deleted)) => { self.state = State::NoWallet; - Task::none() + let network_dir = self.datadir_path.network_directory(self.network); + Task::perform(check_network_datadir(network_dir), Message::Checked) } Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::CloseModal)) => { @@ -365,7 +370,7 @@ fn wallets_list_item( Button::new(icon::trash_icon()) .style(theme::button::secondary) .padding(10) - .on_press(ViewMessage::DeleteWallet(DeleteWalletMessage::ShowModal)), + .on_press(ViewMessage::DeleteWallet(DeleteWalletMessage::ShowModal(i))), ), ) .into() @@ -395,16 +400,16 @@ pub enum ViewMessage { #[derive(Debug, Clone)] pub enum DeleteWalletMessage { - ShowModal, + ShowModal(usize), CloseModal, Confirm, Deleted, } struct DeleteWalletModal { - network: Network, - wallet_datadir: NetworkDirectory, - warning: Option, + network_directory: NetworkDirectory, + wallet_settings: WalletSettings, + warning: Option, deleted: bool, // `None` means we were not able to determine whether wallet uses internal bitcoind. internal_bitcoind: Option, @@ -412,13 +417,13 @@ struct DeleteWalletModal { impl DeleteWalletModal { fn new( - network: Network, - wallet_datadir: NetworkDirectory, + network_directory: NetworkDirectory, + wallet_settings: WalletSettings, internal_bitcoind: Option, ) -> Self { Self { - network, - wallet_datadir, + wallet_settings, + network_directory, warning: None, deleted: false, internal_bitcoind, @@ -428,7 +433,10 @@ impl DeleteWalletModal { fn update(&mut self, message: Message) -> Task { if let Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::Confirm)) = message { self.warning = None; - if let Err(e) = std::fs::remove_dir_all(self.wallet_datadir.path()) { + if let Err(e) = Handle::current().block_on(delete_wallet( + &self.network_directory, + &self.wallet_settings.wallet_id(), + )) { self.warning = Some(e); } else { self.deleted = true; @@ -439,6 +447,7 @@ impl DeleteWalletModal { } Task::none() } + fn view(&self) -> Element { let mut confirm_button = button::secondary(None, "Delete wallet") .width(Length::Fixed(200.0)) @@ -449,8 +458,8 @@ impl DeleteWalletModal { } // Use separate `Row`s for help text in order to have better spacing. let help_text_1 = format!( - "Are you sure you want to delete the configuration and all associated data for the network {}?", - &self.network + "Are you sure you want to delete the configuration and all associated data for the wallet Liana-{}?", + &self.wallet_settings.descriptor_checksum, ); let help_text_2 = match self.internal_bitcoind { Some(true) => Some("(The Liana-managed Bitcoin node for this network will not be affected by this action.)"), @@ -464,9 +473,12 @@ impl DeleteWalletModal { Column::new() .spacing(10) .push(Container::new( - h4_bold(format!("Delete configuration for {}", &self.network)) - .style(theme::text::destructive) - .width(Length::Fill), + h4_bold(format!( + "Delete configuration for Liana-{}", + &self.wallet_settings.descriptor_checksum + )) + .style(theme::text::destructive) + .width(Length::Fill), )) .push(Row::new().push(text(help_text_1))) .push_maybe( diff --git a/liana-gui/src/lib.rs b/liana-gui/src/lib.rs index 0b81af2b..9cc6959a 100644 --- a/liana-gui/src/lib.rs +++ b/liana-gui/src/lib.rs @@ -1,6 +1,7 @@ pub mod app; pub mod backup; pub mod daemon; +pub mod delete; pub mod dir; pub mod download; pub mod export; diff --git a/liana-gui/src/loader.rs b/liana-gui/src/loader.rs index 604d2b2a..079a0de2 100644 --- a/liana-gui/src/loader.rs +++ b/liana-gui/src/loader.rs @@ -123,7 +123,7 @@ impl Loader { ) -> (Self, Task) { let socket_path = datadir_path .network_directory(network) - .lianad_data_directory(&wallet_settings) + .lianad_data_directory(&wallet_settings.wallet_id()) .lianad_rpc_socket_path(); ( Loader { @@ -559,7 +559,7 @@ pub async fn start_bitcoind_and_daemon( ) -> StartedResult { let mut config_path = liana_datadir_path .network_directory(network) - .lianad_data_directory(&settings) + .lianad_data_directory(&settings.wallet_id()) .path() .to_path_buf(); config_path.push("daemon.toml"); diff --git a/liana-gui/src/services/connect/client/cache.rs b/liana-gui/src/services/connect/client/cache.rs index 1a5885a7..610ac985 100644 --- a/liana-gui/src/services/connect/client/cache.rs +++ b/liana-gui/src/services/connect/client/cache.rs @@ -1,6 +1,7 @@ use crate::dir::NetworkDirectory; use async_fd_lock::LockWrite; use serde::{Deserialize, Serialize}; +use std::collections::HashSet; use std::io::SeekFrom; use tokio::fs::OpenOptions; use tokio::io::AsyncSeekExt; @@ -138,6 +139,68 @@ pub async fn update_connect_cache( Ok(tokens) } +pub async fn filter_connect_cache( + network_dir: &NetworkDirectory, + emails: &HashSet, +) -> Result<(), ConnectCacheError> { + let mut path = network_dir.path().to_path_buf(); + path.push(CONNECT_CACHE_FILENAME); + + let file_exists = tokio::fs::try_exists(&path).await.unwrap_or(false); + + let mut file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(&path) + .await + .map_err(|e| ConnectCacheError::ReadingFile(format!("Opening file: {}", e)))? + .lock_write() + .await + .map_err(|e| ConnectCacheError::ReadingFile(format!("Locking file: {:?}", e)))?; + + let mut cache = if file_exists { + let mut file_content = Vec::new(); + file.read_to_end(&mut file_content) + .await + .map_err(|e| ConnectCacheError::ReadingFile(format!("Reading file content: {}", e)))?; + + match serde_json::from_slice::(&file_content) { + Ok(cache) => cache, + Err(e) => { + tracing::warn!("Something wrong with Liana-Connect cache file: {:?}", e); + tracing::warn!("Liana-Connect cache file is reset"); + ConnectCache::default() + } + } + } else { + ConnectCache::default() + }; + + cache.accounts.retain(|a| emails.contains(&a.email)); + + let content = serde_json::to_vec_pretty(&cache).map_err(|e| { + ConnectCacheError::WritingFile(format!("Failed to serialize settings: {}", e)) + })?; + + file.seek(SeekFrom::Start(0)).await.map_err(|e| { + ConnectCacheError::WritingFile(format!("Failed to seek to start of file: {}", e)) + })?; + + file.write_all(&content).await.map_err(|e| { + tracing::warn!("failed to write to file: {:?}", e); + ConnectCacheError::WritingFile(e.to_string()) + })?; + + file.inner_mut() + .set_len(content.len() as u64) + .await + .map_err(|e| ConnectCacheError::WritingFile(format!("Failed to truncate file: {}", e)))?; + + Ok(()) +} + #[derive(Debug, Clone)] pub enum ConnectCacheError { NotFound,