diff --git a/liana-gui/src/app/error.rs b/liana-gui/src/app/error.rs index dad8c47c..ce49658e 100644 --- a/liana-gui/src/app/error.rs +++ b/liana-gui/src/app/error.rs @@ -7,7 +7,7 @@ use lianad::config::ConfigError; use crate::{ app::{settings::SettingsError, wallet::WalletError}, daemon::DaemonError, - export, + export::{self, RestoreBackupError}, }; #[derive(Debug)] @@ -20,6 +20,7 @@ pub enum Error { Desc(LianaDescError), Spend(SpendCreationError), ImportExport(export::Error), + RestoreBackup(RestoreBackupError), } impl std::fmt::Display for Error { @@ -61,6 +62,7 @@ impl std::fmt::Display for Error { Self::HardwareWallet(e) => write!(f, "error: {}\nPlease check if the device is still connected and unlocked with the correct firmware open for the current network and no other application is accessing the device.", e), Self::Desc(e) => write!(f, "Liana descriptor error: {}", e), Self::ImportExport(e) => write!(f, "{e}"), + Self::RestoreBackup(e) => write!(f, "{e}"), } } } diff --git a/liana-gui/src/app/state/settings/mod.rs b/liana-gui/src/app/state/settings/mod.rs index 4e73a94c..774de791 100644 --- a/liana-gui/src/app/state/settings/mod.rs +++ b/liana-gui/src/app/state/settings/mod.rs @@ -1,9 +1,9 @@ mod bitcoind; mod wallet; -use std::convert::From; use std::path::PathBuf; use std::sync::Arc; +use std::{collections::HashMap, convert::From}; use iced::Task; @@ -225,7 +225,16 @@ impl State for ImportExportSettingsState { Message::View(view::Message::ImportExport(m)) => { if let ImportExportMessage::UpdateAliases(aliases) = m { let mut wallet = (*self.wallet).clone(); - wallet.keys_aliases = aliases; + let mut ka = HashMap::new(); + let mut pk = HashMap::new(); + aliases.iter().for_each(|(k, ks)| { + ka.insert(*k, ks.name()); + if let Some(p_key) = &ks.provider_key { + pk.insert(*k, p_key.clone()); + } + }); + wallet.keys_aliases = ka; + wallet.provider_keys = pk; let wallet = Arc::new(wallet); return Task::perform(async {}, move |_| { Message::WalletUpdated(Ok(wallet.clone())) diff --git a/liana-gui/src/app/view/warning.rs b/liana-gui/src/app/view/warning.rs index 011ed430..1f371481 100644 --- a/liana-gui/src/app/view/warning.rs +++ b/liana-gui/src/app/view/warning.rs @@ -50,6 +50,7 @@ impl From<&Error> for WarningMessage { Error::Desc(e) => WarningMessage(format!("Descriptor analysis error: '{}'.", e)), Error::Spend(e) => WarningMessage(format!("Spend creation error: '{}'.", e)), Error::ImportExport(e) => WarningMessage(format!("{e}")), + Error::RestoreBackup(e) => WarningMessage(format!("Fail to restore backup: {e}")), } } } diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index 77026043..7a38f78f 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -31,8 +31,11 @@ use iced::futures::{SinkExt, Stream}; use crate::{ app::{ - settings::{KeySetting, Settings}, + cache::Cache, + settings::{self, KeySetting, Settings}, view, + wallet::Wallet, + Config, }, backup::{self, Backup}, daemon::{ @@ -40,6 +43,7 @@ use crate::{ Daemon, DaemonBackend, DaemonError, }, lianalite::client::backend::api::DEFAULT_LIMIT, + node::bitcoind::Bitcoind, }; const DUMP_LABELS_LIMIT: u32 = 100; @@ -117,7 +121,7 @@ pub enum ImportExportMessage { Close, Overwrite, Ignore, - UpdateAliases(HashMap), + UpdateAliases(HashMap), } impl From for view::Message { @@ -265,7 +269,7 @@ pub enum Progress { Descriptor(LianaDescriptor), LabelsConflict(SyncSender), KeyAliasesConflict(SyncSender), - UpdateAliases(HashMap), + UpdateAliases(HashMap), } pub struct Export { @@ -952,13 +956,16 @@ pub async fn import_backup( } settings.wallets.get_mut(0).expect("already checked").keys = - settings_aliases.into_values().collect(); + settings_aliases.clone().into_values().collect(); if settings.to_file(datadir.to_path_buf(), network).is_err() { send_error!( sender, Error::BackupImport("Fail to import keys aliases".into()) ); return; + } else { + // Update wallet state + send_progress!(sender, UpdateAliases(settings_aliases)); } } @@ -966,6 +973,165 @@ pub async fn import_backup( send_progress!(sender, Ended); } +#[derive(Debug)] +pub enum RestoreBackupError { + Daemon(DaemonError), + Network, + InvalidDescriptor, + WrongDescriptor, + NoAccount, + SeveralAccounts, + LianaConnectNotSupported, + GetLabels, + LabelsNotEmpty, + NotImplemented, + InvalidPsbt, +} + +impl Display for RestoreBackupError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RestoreBackupError::Daemon(e) => write!(f, "Daemon error during restore process: {e}"), + RestoreBackupError::Network => write!(f, "Backup & wallet network don't matches"), + RestoreBackupError::InvalidDescriptor => write!(f, "The backup descriptor is invalid"), + RestoreBackupError::WrongDescriptor => { + write!(f, "Backup & wallet descriptor don't matches") + } + RestoreBackupError::NoAccount => write!(f, "There is no account in the backup"), + RestoreBackupError::SeveralAccounts => { + write!(f, "There is several accounts in the backup") + } + RestoreBackupError::LianaConnectNotSupported => { + write!(f, "Restore a backup to Liana-connect is not yet supported") + } + RestoreBackupError::GetLabels => write!(f, "Fails to get labels during backup restore"), + RestoreBackupError::LabelsNotEmpty => write!( + f, + "Cannot load labels: there is already labels into the database" + ), + RestoreBackupError::NotImplemented => write!(f, "Not implemented"), + RestoreBackupError::InvalidPsbt => write!(f, "Psbt is invalid"), + } + } +} + +impl From for RestoreBackupError { + fn from(value: DaemonError) -> Self { + Self::Daemon(value) + } +} + +#[allow(unused)] +/// Import backup data if wallet created from a backup +/// - check if networks matches +/// - check if descriptors matches +/// - check if labels are empty +/// - update receive and change indexes +/// - parse psbt from backup +/// - import PSBTs +/// - import labels +pub async fn import_backup_at_launch( + cache: Cache, + wallet: Arc, + config: Config, + daemon: Arc, + datadir: PathBuf, + internal_bitcoind: Option, + backup: Backup, +) -> Result< + ( + Cache, + Arc, + Config, + Arc, + PathBuf, + Option, + ), + RestoreBackupError, +> { + // TODO: drop after support for restore to liana-connect + if matches!(daemon.backend(), DaemonBackend::RemoteBackend) { + return Err(RestoreBackupError::LianaConnectNotSupported); + } + + // get backend info + let info = daemon.get_info().await?; + + // check if networks matches + let network = info.network; + if backup.network != network { + return Err(RestoreBackupError::Network); + } + + // check if descriptors matches + let descriptor = info.descriptors.main; + let account = match backup.accounts.len() { + 0 => return Err(RestoreBackupError::NoAccount), + 1 => backup.accounts.first().expect("already checked"), + _ => return Err(RestoreBackupError::SeveralAccounts), + }; + + let backup_descriptor = LianaDescriptor::from_str(&account.descriptor) + .map_err(|_| RestoreBackupError::InvalidDescriptor)?; + + if backup_descriptor != descriptor { + return Err(RestoreBackupError::WrongDescriptor); + } + + // check there is no labels in DB + if account.labels.is_some() + && !daemon + .get_labels_bip329(0, u32::MAX) + .await + .map_err(|_| RestoreBackupError::GetLabels)? + .to_vec() + .is_empty() + { + return Err(RestoreBackupError::LabelsNotEmpty); + } + + // parse PSBTs + let mut psbts = Vec::new(); + for psbt_str in &account.psbts { + psbts.push(Psbt::from_str(psbt_str).map_err(|_| RestoreBackupError::InvalidPsbt)?); + } + + // update receive & change index + let db_receive = info.receive_index; + let i = account.receive_index.unwrap_or(0); + let receive = if db_receive < i { Some(i) } else { None }; + + let db_change = info.change_index; + let i = account.change_index.unwrap_or(0); + let change = if db_change < i { Some(i) } else { None }; + + daemon.update_deriv_indexes(receive, change).await?; + + // import labels + if let Some(labels) = account.labels.clone().map(|l| l.into_vec()) { + let labels: HashMap> = labels + .into_iter() + .filter_map(|l| { + if let Some((item, label)) = LabelItem::from_bip329(&l, network) { + Some((item, Some(label))) + } else { + None + } + }) + .collect(); + daemon.update_labels(&labels).await?; + } + + // import PSBTs + for psbt in psbts { + if let Err(e) = daemon.update_spend_tx(&psbt).await { + tracing::error!("Fail to restore PSBT: {e}") + } + } + + Ok((cache, wallet, config, daemon, datadir, internal_bitcoind)) +} + pub async fn export_labels( sender: Sender, daemon: Option>,