Merge #1703: Multiple wallets
79fcbc41e27fd6ea1f0d4870832600476392f5e7 fix: remove unused method with_pin_date (edouardparis)
5baf2812040d5d7b0758e8f5058dc953a8adcb1b Remove wallet hot signer mnemonics on wallet delete. (edouardparis)
b6f45a42ee96399f4de70e5889ca32a3679466ae Delete settings file if no wallet (edouardparis)
30228c6490c45b2b775ac6974b40998dac565208 refac delete wallet modal and desc backup (edouardparis)
9a6218b3f1e5b6698d922eec91d51f7dff980790 Change lianad directory location (edouardparis)
58bccef4cdcae5bdbd5389dcea87e44e0cbdf267 Append wallet settings to file during creation (edouardparis)
e85f89dc7f1f83c8e74bf69a564671dfc4275848 Add wallet list to launcher (edouardparis)
8ddcece8204d6e0690cc5346a2d94c8f89d052c0 Add wallet id for settings filtering (edouardparis)
Pull request description:
ACKs for top commit:
jp1ac4:
tACK 79fcbc41e27fd6ea1f0d4870832600476392f5e7.
Tree-SHA512: d003d7eb690b22c6c2bd6124476b4a72692b6674e5d1c40f69d3eef86d205356a91afa5d18a4ba9ae26ad5751eff21bcdf99458bc54022872d3c21c102bd8ddf
This commit is contained in:
commit
c2da55444d
@ -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;
|
||||
@ -19,13 +20,31 @@ use crate::{
|
||||
services::{self, connect::client::backend},
|
||||
};
|
||||
|
||||
pub const DEFAULT_FILE_NAME: &str = "settings.json";
|
||||
pub const SETTINGS_FILE_NAME: &str = "settings.json";
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
pub struct Settings {
|
||||
pub wallets: Vec<WalletSettings>,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn from_file(network_dir: &NetworkDirectory) -> Result<Settings, SettingsError> {
|
||||
let mut path = network_dir.path().to_path_buf();
|
||||
path.push(SETTINGS_FILE_NAME);
|
||||
|
||||
std::fs::read(path)
|
||||
.map_err(|e| match e.kind() {
|
||||
std::io::ErrorKind::NotFound => SettingsError::NotFound,
|
||||
_ => SettingsError::ReadingFile(format!("Reading settings file: {}", e)),
|
||||
})
|
||||
.and_then(|file_content| {
|
||||
serde_json::from_slice::<Settings>(&file_content).map_err(|e| {
|
||||
SettingsError::ReadingFile(format!("Parsing settings file: {}", e))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_settings_file<F>(
|
||||
network_dir: &NetworkDirectory,
|
||||
updater: F,
|
||||
@ -33,7 +52,7 @@ pub async fn update_settings_file<F>(
|
||||
where
|
||||
F: FnOnce(Settings) -> Settings,
|
||||
{
|
||||
let path = network_dir.path().join(DEFAULT_FILE_NAME);
|
||||
let path = network_dir.path().join(SETTINGS_FILE_NAME);
|
||||
let file_exists = tokio::fs::try_exists(&path).await.unwrap_or(false);
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
@ -62,6 +81,13 @@ where
|
||||
|
||||
let settings = updater(settings);
|
||||
|
||||
if settings.wallets.is_empty() {
|
||||
tokio::fs::remove_file(&path)
|
||||
.await
|
||||
.map_err(|e| SettingsError::ReadingFile(e.to_string()))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let content = serde_json::to_vec_pretty(&settings)
|
||||
.map_err(|e| SettingsError::WritingFile(format!("Failed to serialize settings: {}", e)))?;
|
||||
|
||||
@ -107,6 +133,7 @@ impl AuthConfig {
|
||||
pub struct WalletSettings {
|
||||
pub name: String,
|
||||
pub descriptor_checksum: String,
|
||||
pub pinned_at: Option<i64>,
|
||||
// if wallet is using remote backend, then this information is stored on the remote backend
|
||||
// wallet metadata
|
||||
#[serde(default)]
|
||||
@ -129,20 +156,7 @@ impl WalletSettings {
|
||||
where
|
||||
F: FnMut(&WalletSettings) -> bool,
|
||||
{
|
||||
let mut path = network_dir.path().to_path_buf();
|
||||
path.push(DEFAULT_FILE_NAME);
|
||||
|
||||
std::fs::read(path)
|
||||
.map_err(|e| match e.kind() {
|
||||
std::io::ErrorKind::NotFound => SettingsError::NotFound,
|
||||
_ => SettingsError::ReadingFile(format!("Reading settings file: {}", e)),
|
||||
})
|
||||
.and_then(|file_content| {
|
||||
serde_json::from_slice::<Settings>(&file_content).map_err(|e| {
|
||||
SettingsError::ReadingFile(format!("Parsing settings file: {}", e))
|
||||
})
|
||||
})
|
||||
.map(|cache| cache.wallets.into_iter().find(selecter))
|
||||
Settings::from_file(network_dir).map(|cache| cache.wallets.into_iter().find(selecter))
|
||||
}
|
||||
|
||||
pub fn keys_aliases(&self) -> HashMap<Fingerprint, String> {
|
||||
@ -183,6 +197,52 @@ impl WalletSettings {
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
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<i64>,
|
||||
pub descriptor_checksum: String,
|
||||
}
|
||||
|
||||
impl WalletId {
|
||||
pub fn new(descriptor_checksum: String, timestamp: Option<i64>) -> 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 {
|
||||
write!(f, "{}", self.descriptor_checksum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
|
||||
@ -298,6 +358,7 @@ impl KeySetting {
|
||||
pub enum SettingsError {
|
||||
NotFound,
|
||||
ReadingFile(String),
|
||||
DeletingFile(String),
|
||||
WritingFile(String),
|
||||
Unexpected(String),
|
||||
}
|
||||
@ -306,6 +367,7 @@ impl std::fmt::Display for SettingsError {
|
||||
match self {
|
||||
Self::NotFound => write!(f, "Settings file not found"),
|
||||
Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e),
|
||||
Self::DeletingFile(e) => write!(f, "Error while deleting file: {}", e),
|
||||
Self::WritingFile(e) => write!(f, "Error while writing file: {}", e),
|
||||
Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
|
||||
}
|
||||
|
||||
@ -428,12 +428,12 @@ async fn register_wallet(
|
||||
|
||||
if daemon.backend() != DaemonBackend::RemoteBackend {
|
||||
let network_dir = data_dir.network_directory(network);
|
||||
let checksum = wallet.descriptor_checksum();
|
||||
let wallet_id = wallet.id();
|
||||
update_settings_file(&network_dir, |mut settings| {
|
||||
if let Some(wallet_setting) = settings
|
||||
.wallets
|
||||
.iter_mut()
|
||||
.find(|w| w.descriptor_checksum == checksum)
|
||||
.find(|w| w.wallet_id() == wallet_id)
|
||||
{
|
||||
if let Some(hw_config) = wallet_setting
|
||||
.hardware_wallets
|
||||
@ -479,12 +479,12 @@ pub async fn update_keys_aliases(
|
||||
) -> Result<Arc<Wallet>, Error> {
|
||||
if daemon.backend() != DaemonBackend::RemoteBackend {
|
||||
let network_dir = data_dir.network_directory(network);
|
||||
let checksum = wallet.descriptor_checksum();
|
||||
let wallet_id = wallet.id();
|
||||
update_settings_file(&network_dir, |mut settings| {
|
||||
if let Some(wallet_setting) = settings
|
||||
.wallets
|
||||
.iter_mut()
|
||||
.find(|w| w.descriptor_checksum == checksum)
|
||||
.find(|w| w.wallet_id() == wallet_id)
|
||||
{
|
||||
wallet_setting.keys = keys_aliases
|
||||
.iter()
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::dir::{LianaDirectory, NetworkDirectory};
|
||||
use crate::dir::LianaDirectory;
|
||||
use crate::{
|
||||
app::settings, daemon::DaemonBackend, hw::HardwareWalletConfig, node::NodeType, signer::Signer,
|
||||
};
|
||||
@ -11,6 +11,8 @@ use liana::{miniscript::bitcoin, signer::HotSigner};
|
||||
use liana::descriptors::LianaDescriptor;
|
||||
use liana::miniscript::bitcoin::bip32::Fingerprint;
|
||||
|
||||
use super::settings::{WalletId, WalletSettings};
|
||||
|
||||
const DEFAULT_WALLET_NAME: &str = "Liana";
|
||||
|
||||
pub fn wallet_name(main_descriptor: &LianaDescriptor) -> String {
|
||||
@ -31,6 +33,8 @@ pub fn wallet_name(main_descriptor: &LianaDescriptor) -> String {
|
||||
pub struct Wallet {
|
||||
pub name: String,
|
||||
pub main_descriptor: LianaDescriptor,
|
||||
pub descriptor_checksum: String,
|
||||
pub pinned_at: Option<i64>,
|
||||
// TODO: We could replace these two fields with `keys: HashMap<Fingerprint, settings::KeySetting>`.
|
||||
pub keys_aliases: HashMap<Fingerprint, String>,
|
||||
pub provider_keys: HashMap<Fingerprint, settings::ProviderKey>,
|
||||
@ -42,6 +46,13 @@ impl Wallet {
|
||||
pub fn new(main_descriptor: LianaDescriptor) -> Self {
|
||||
Self {
|
||||
name: wallet_name(&main_descriptor),
|
||||
descriptor_checksum: main_descriptor
|
||||
.to_string()
|
||||
.split_once('#')
|
||||
.map(|(_, checksum)| checksum)
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
pinned_at: None,
|
||||
main_descriptor,
|
||||
keys_aliases: HashMap::new(),
|
||||
provider_keys: HashMap::new(),
|
||||
@ -55,6 +66,16 @@ impl Wallet {
|
||||
self
|
||||
}
|
||||
|
||||
// To match with WalletSettings.wallet_id
|
||||
pub fn id(&self) -> WalletId {
|
||||
WalletId::new(self.descriptor_checksum.clone(), self.pinned_at)
|
||||
}
|
||||
|
||||
pub fn with_pinned_at(mut self, pinned_at: Option<i64>) -> Self {
|
||||
self.pinned_at = pinned_at;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_key_aliases(mut self, aliases: HashMap<Fingerprint, String>) -> Self {
|
||||
self.keys_aliases = aliases;
|
||||
self
|
||||
@ -92,26 +113,16 @@ impl Wallet {
|
||||
descriptor_keys
|
||||
}
|
||||
|
||||
pub fn descriptor_checksum(&self) -> String {
|
||||
self.main_descriptor
|
||||
.to_string()
|
||||
.split_once('#')
|
||||
.map(|(_, checksum)| checksum)
|
||||
.unwrap()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn load_from_settings(self, dir: &NetworkDirectory) -> Result<Self, WalletError> {
|
||||
if let Some(wallet_settings) = settings::WalletSettings::from_file(dir, |w| {
|
||||
w.descriptor_checksum == self.descriptor_checksum()
|
||||
})? {
|
||||
pub fn load_from_settings(self, wallet_settings: WalletSettings) -> Result<Self, WalletError> {
|
||||
if wallet_settings.descriptor_checksum != self.descriptor_checksum {
|
||||
Err(WalletError::WrongWalletLoaded)
|
||||
} else {
|
||||
Ok(self
|
||||
.with_key_aliases(wallet_settings.keys_aliases())
|
||||
.with_provider_keys(wallet_settings.provider_keys())
|
||||
.with_name(wallet_settings.name)
|
||||
.with_pinned_at(wallet_settings.pinned_at)
|
||||
.with_hardware_wallets(wallet_settings.hardware_wallets))
|
||||
} else {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,6 +183,7 @@ impl Wallet {
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum WalletError {
|
||||
WrongWalletLoaded,
|
||||
Settings(settings::SettingsError),
|
||||
HotSigner(String),
|
||||
}
|
||||
@ -179,6 +191,7 @@ pub enum WalletError {
|
||||
impl std::fmt::Display for WalletError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::WrongWalletLoaded => write!(f, "Wrong wallet was loaded"),
|
||||
Self::Settings(e) => write!(f, "Failed to load settings: {}", e),
|
||||
Self::HotSigner(e) => write!(f, "Failed to load hot signer: {}", e),
|
||||
}
|
||||
|
||||
@ -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,62 +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<Self, Error> {
|
||||
let descriptor = ctx
|
||||
.descriptor
|
||||
.clone()
|
||||
.ok_or(Error::DescriptorMissing)?
|
||||
.to_string();
|
||||
pub async fn from_installer_descriptor_step(ctx: Context) -> Result<Self, Error> {
|
||||
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 config = extract_daemon_config(&ctx).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 settings = if ctx.bitcoin_backend.is_some() {
|
||||
Some(extract_local_gui_settings(&ctx))
|
||||
} else {
|
||||
match &ctx.remote_backend {
|
||||
RemoteBackend::WithWallet(backend) => {
|
||||
Some(extract_remote_gui_settings(&ctx, backend).await)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
let name = if let Some(settings) = settings {
|
||||
assert_eq!(settings.wallets.len(), 1);
|
||||
if settings.wallets.len() != 1 {
|
||||
return Err(Error::NotSingleWallet);
|
||||
}
|
||||
let settings = settings.wallets.first().expect("only one wallet");
|
||||
let name = settings.name.clone();
|
||||
if let Ok(settings) = serde_json::to_value(settings) {
|
||||
proprietary.insert(SETTINGS_KEY.to_string(), settings);
|
||||
}
|
||||
Some(name)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
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],
|
||||
@ -199,10 +157,9 @@ impl Backup {
|
||||
let keys = wallet.keys();
|
||||
|
||||
let network_dir = datadir.network_directory(network);
|
||||
if let Some(settings) = WalletSettings::from_file(&network_dir, |settings| {
|
||||
wallet.descriptor_checksum() == settings.descriptor_checksum
|
||||
})
|
||||
.map_err(|_| Error::SettingsFromFile)?
|
||||
if let Some(settings) =
|
||||
WalletSettings::from_file(&network_dir, |settings| wallet.id() == settings.wallet_id())
|
||||
.map_err(|_| Error::SettingsFromFile)?
|
||||
{
|
||||
if let Ok(settings) = serde_json::to_value(settings) {
|
||||
proprietary.insert(SETTINGS_KEY.to_string(), settings);
|
||||
|
||||
90
liana-gui/src/delete.rs
Normal file
90
liana-gui/src/delete.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{
|
||||
app::settings::{self, SettingsError, WalletId},
|
||||
dir::NetworkDirectory,
|
||||
services::connect::client::cache::{self, ConnectCacheError},
|
||||
signer,
|
||||
};
|
||||
|
||||
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<std::io::Error> for DeleteError {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
DeleteError::Io(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn ignore_not_found<T>(result: std::io::Result<T>) -> std::io::Result<Option<T>> {
|
||||
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::<String>::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)?;
|
||||
|
||||
signer::delete_wallet_mnemonics(
|
||||
network_dir,
|
||||
&wallet_id.descriptor_checksum,
|
||||
wallet_id.timestamp,
|
||||
)
|
||||
.map_err(DeleteError::Io)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -1,4 +1,6 @@
|
||||
use crate::app::settings::WalletId;
|
||||
use liana::miniscript::bitcoin::Network;
|
||||
use lianad::datadir::DataDirectory;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@ -78,11 +80,20 @@ impl NetworkDirectory {
|
||||
self.0.as_path().exists()
|
||||
}
|
||||
pub fn init(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
create_directory(self.0.as_path())
|
||||
create_directory(self.0.as_path())?;
|
||||
create_directory(&self.0.as_path().join("data"))
|
||||
}
|
||||
pub fn path(&self) -> &Path {
|
||||
self.0.as_path()
|
||||
}
|
||||
pub fn lianad_data_directory(&self, wallet_id: &WalletId) -> DataDirectory {
|
||||
let mut path = self.0.clone();
|
||||
if !wallet_id.is_legacy() {
|
||||
path.push("data");
|
||||
path.push(wallet_id.to_string());
|
||||
}
|
||||
DataDirectory::new(path)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@ -2,7 +2,7 @@ use liana::miniscript::{
|
||||
bitcoin::{bip32::Fingerprint, Network},
|
||||
DescriptorPublicKey,
|
||||
};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::{context, Error};
|
||||
use crate::{
|
||||
@ -28,7 +28,11 @@ use crate::{
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
UserActionDone(bool),
|
||||
Exit(PathBuf, Option<Bitcoind>, /* remove log */ bool),
|
||||
Exit(
|
||||
Box<settings::WalletSettings>,
|
||||
Option<Bitcoind>,
|
||||
/* remove log */ bool,
|
||||
),
|
||||
Clibpboard(String),
|
||||
Next,
|
||||
Skip,
|
||||
@ -39,7 +43,7 @@ pub enum Message {
|
||||
Reload,
|
||||
Select(usize),
|
||||
UseHotSigner,
|
||||
Installed(Result<PathBuf, Error>),
|
||||
Installed(settings::WalletId, Result<settings::WalletSettings, Error>),
|
||||
CreateTaprootDescriptor(bool),
|
||||
SelectDescriptorTemplate(context::DescriptorTemplate),
|
||||
SelectBackend(SelectBackend),
|
||||
|
||||
@ -14,21 +14,23 @@ use liana_ui::{
|
||||
};
|
||||
use lianad::config::{BitcoinBackend, BitcoindConfig, BitcoindRpcAuth, Config};
|
||||
use std::ops::Deref;
|
||||
use tokio::runtime::Handle;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
config as gui_config,
|
||||
settings::{self as gui_settings, AuthConfig, Settings, SettingsError, WalletSettings},
|
||||
settings::{update_settings_file, AuthConfig, SettingsError, WalletId, WalletSettings},
|
||||
wallet::wallet_name,
|
||||
},
|
||||
backup,
|
||||
daemon::DaemonError,
|
||||
dir::{LianaDirectory, NetworkDirectory},
|
||||
delete,
|
||||
dir::LianaDirectory,
|
||||
hw::{HardwareWalletConfig, HardwareWallets},
|
||||
services::{
|
||||
self,
|
||||
@ -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,10 +376,18 @@ pub fn daemon_check(cfg: lianad::config::Config) -> Result<(), Error> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn with_wallet_id<F>(wallet_id: WalletId, res: F) -> (WalletId, Result<WalletSettings, Error>)
|
||||
where
|
||||
F: std::future::Future<Output = Result<WalletSettings, Error>>,
|
||||
{
|
||||
(wallet_id, res.await)
|
||||
}
|
||||
|
||||
pub async fn install_local_wallet(
|
||||
ctx: Context,
|
||||
wallet_id: WalletId,
|
||||
signer: Arc<Mutex<Signer>>,
|
||||
) -> Result<PathBuf, Error> {
|
||||
) -> Result<WalletSettings, Error> {
|
||||
let network_datadir = ctx
|
||||
.liana_directory
|
||||
.network_directory(ctx.bitcoin_config.network);
|
||||
@ -370,7 +395,32 @@ pub async fn install_local_wallet(
|
||||
.init()
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to create datadir path: {}", e)))?;
|
||||
|
||||
let cfg: lianad::config::Config = extract_daemon_config(&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.clone(),
|
||||
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())?;
|
||||
|
||||
@ -381,9 +431,11 @@ pub async fn install_local_wallet(
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to serialize daemon config: {}", e)))?;
|
||||
|
||||
// create lianad configuration file
|
||||
let _daemon_config_path = create_and_write_file(
|
||||
&network_datadir,
|
||||
"daemon.toml",
|
||||
create_and_write_file(
|
||||
&network_datadir
|
||||
.lianad_data_directory(&wallet_settings.wallet_id())
|
||||
.path()
|
||||
.join("daemon.toml"),
|
||||
daemon_config.to_string().as_bytes(),
|
||||
)?;
|
||||
|
||||
@ -397,7 +449,14 @@ pub async fn install_local_wallet(
|
||||
signer
|
||||
.lock()
|
||||
.unwrap()
|
||||
.store(&ctx.liana_directory, cfg.bitcoin_config.network)
|
||||
.store(
|
||||
&ctx.liana_directory,
|
||||
cfg.bitcoin_config.network,
|
||||
&wallet_id.descriptor_checksum,
|
||||
wallet_id
|
||||
.timestamp
|
||||
.expect("Every new wallet have now a timestamp"),
|
||||
)
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?;
|
||||
|
||||
info!("Hot signer mnemonic stored");
|
||||
@ -405,46 +464,56 @@ pub async fn install_local_wallet(
|
||||
|
||||
if let Some(signer) = &ctx.recovered_signer {
|
||||
signer
|
||||
.store(&ctx.liana_directory, cfg.bitcoin_config.network)
|
||||
.store(
|
||||
&ctx.liana_directory,
|
||||
cfg.bitcoin_config.network,
|
||||
&wallet_id.descriptor_checksum,
|
||||
wallet_id
|
||||
.timestamp
|
||||
.expect("Every new wallet have now a timestamp"),
|
||||
)
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?;
|
||||
|
||||
info!("Recovered signer mnemonic stored");
|
||||
}
|
||||
|
||||
// create liana GUI configuration file
|
||||
let gui_config_path = create_and_write_file(
|
||||
&network_datadir,
|
||||
gui_config::DEFAULT_FILE_NAME,
|
||||
toml::to_string(&gui_config::Config::new(
|
||||
// Installer started a bitcoind, it is expected that gui will start it on startup
|
||||
ctx.internal_bitcoind.is_some(),
|
||||
))
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to serialize gui config: {}", e)))?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
|
||||
info!("Gui configuration file created");
|
||||
let gui_config_path = network_datadir
|
||||
.path()
|
||||
.join(gui_config::DEFAULT_FILE_NAME)
|
||||
.to_path_buf();
|
||||
if !gui_config_path.exists() {
|
||||
create_and_write_file(
|
||||
&gui_config_path,
|
||||
toml::to_string(&gui_config::Config::new(
|
||||
// Installer started a bitcoind, it is expected that gui will start it on startup
|
||||
ctx.internal_bitcoind.is_some(),
|
||||
))
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to serialize gui config: {}", e)))?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
info!("Gui configuration file created");
|
||||
}
|
||||
|
||||
// create liana GUI settings file
|
||||
let settings: gui_settings::Settings = extract_local_gui_settings(&ctx);
|
||||
create_and_write_file(
|
||||
&network_datadir,
|
||||
gui_settings::DEFAULT_FILE_NAME,
|
||||
serde_json::to_string_pretty(&settings)
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to serialize settings: {}", e)))?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
update_settings_file(&network_datadir, |mut settings| {
|
||||
settings.wallets.push(wallet_settings.clone());
|
||||
settings
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::Unexpected(e.to_string()))?;
|
||||
|
||||
info!("Settings file created");
|
||||
|
||||
Ok(gui_config_path)
|
||||
Ok(wallet_settings)
|
||||
}
|
||||
|
||||
pub async fn create_remote_wallet(
|
||||
ctx: Context,
|
||||
wallet_id: WalletId,
|
||||
signer: Arc<Mutex<Signer>>,
|
||||
remote_backend: BackendClient,
|
||||
) -> Result<PathBuf, Error> {
|
||||
) -> Result<WalletSettings, Error> {
|
||||
let network_datadir = ctx.liana_directory.network_directory(ctx.network);
|
||||
network_datadir
|
||||
.init()
|
||||
@ -462,7 +531,14 @@ pub async fn create_remote_wallet(
|
||||
signer
|
||||
.lock()
|
||||
.unwrap()
|
||||
.store(&ctx.liana_directory, ctx.network)
|
||||
.store(
|
||||
&ctx.liana_directory,
|
||||
ctx.network,
|
||||
&wallet_id.descriptor_checksum,
|
||||
wallet_id
|
||||
.timestamp
|
||||
.expect("Every new wallet have now a timestamp"),
|
||||
)
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?;
|
||||
|
||||
info!("Hot signer mnemonic stored");
|
||||
@ -470,26 +546,33 @@ pub async fn create_remote_wallet(
|
||||
|
||||
if let Some(signer) = &ctx.recovered_signer {
|
||||
signer
|
||||
.store(&ctx.liana_directory, ctx.network)
|
||||
.store(
|
||||
&ctx.liana_directory,
|
||||
ctx.network,
|
||||
&wallet_id.descriptor_checksum,
|
||||
wallet_id
|
||||
.timestamp
|
||||
.expect("Every new wallet have now a timestamp"),
|
||||
)
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?;
|
||||
|
||||
info!("Recovered signer mnemonic stored");
|
||||
}
|
||||
|
||||
// create liana GUI configuration file
|
||||
let gui_config_path = create_and_write_file(
|
||||
&network_datadir,
|
||||
gui_config::DEFAULT_FILE_NAME,
|
||||
toml::to_string(&gui_config::Config {
|
||||
log_level: Some("info".to_string()),
|
||||
debug: Some(false),
|
||||
start_internal_bitcoind: false,
|
||||
})
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to serialize gui config: {}", e)))?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
|
||||
info!("Gui configuration file created");
|
||||
let gui_config_path = network_datadir
|
||||
.path()
|
||||
.join(gui_config::DEFAULT_FILE_NAME)
|
||||
.to_path_buf();
|
||||
if !gui_config_path.exists() {
|
||||
create_and_write_file(
|
||||
&gui_config_path,
|
||||
toml::to_string(&gui_config::Config::new(false))
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to serialize gui config: {}", e)))?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
info!("Gui configuration file created");
|
||||
}
|
||||
|
||||
let pks: Vec<_> = ctx
|
||||
.keys
|
||||
@ -540,14 +623,26 @@ pub async fn create_remote_wallet(
|
||||
let remote_backend = remote_backend.connect_wallet(wallet).0;
|
||||
|
||||
// create liana GUI settings file
|
||||
let settings: gui_settings::Settings = extract_remote_gui_settings(&ctx, &remote_backend).await;
|
||||
create_and_write_file(
|
||||
&network_datadir,
|
||||
gui_settings::DEFAULT_FILE_NAME,
|
||||
serde_json::to_string_pretty(&settings)
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to serialize settings: {}", e)))?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
// 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
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::Unexpected(e.to_string()))?;
|
||||
|
||||
info!("Settings file created");
|
||||
|
||||
@ -567,18 +662,26 @@ pub async fn create_remote_wallet(
|
||||
info!("Liana-Connect cache updated");
|
||||
};
|
||||
|
||||
Ok(gui_config_path)
|
||||
Ok(wallet_settings)
|
||||
}
|
||||
|
||||
pub async fn import_remote_wallet(
|
||||
ctx: Context,
|
||||
wallet_id: WalletId,
|
||||
backend: BackendWalletClient,
|
||||
) -> Result<PathBuf, Error> {
|
||||
) -> Result<WalletSettings, Error> {
|
||||
tracing::info!("Importing wallet from remote backend");
|
||||
|
||||
if let Some(signer) = &ctx.recovered_signer {
|
||||
signer
|
||||
.store(&ctx.liana_directory, ctx.network)
|
||||
.store(
|
||||
&ctx.liana_directory,
|
||||
ctx.network,
|
||||
&wallet_id.descriptor_checksum,
|
||||
wallet_id
|
||||
.timestamp
|
||||
.expect("Every new wallet have now a timestamp"),
|
||||
)
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?;
|
||||
|
||||
info!("Recovered signer mnemonic stored");
|
||||
@ -590,31 +693,47 @@ pub async fn import_remote_wallet(
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to create datadir path: {}", e)))?;
|
||||
|
||||
// create liana GUI settings file
|
||||
let settings: gui_settings::Settings = extract_remote_gui_settings(&ctx, &backend).await;
|
||||
create_and_write_file(
|
||||
&network_datadir,
|
||||
gui_settings::DEFAULT_FILE_NAME,
|
||||
serde_json::to_string_pretty(&settings)
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to serialize settings: {}", e)))?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
// 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
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::Unexpected(e.to_string()))?;
|
||||
|
||||
info!("Settings file created");
|
||||
|
||||
// create liana GUI configuration file
|
||||
let gui_config_path = create_and_write_file(
|
||||
&network_datadir,
|
||||
gui_config::DEFAULT_FILE_NAME,
|
||||
toml::to_string(&gui_config::Config {
|
||||
log_level: Some("info".to_string()),
|
||||
debug: Some(false),
|
||||
start_internal_bitcoind: false,
|
||||
})
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to serialize gui config: {}", e)))?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
|
||||
info!("Gui configuration file created");
|
||||
let gui_config_path = network_datadir
|
||||
.path()
|
||||
.join(gui_config::DEFAULT_FILE_NAME)
|
||||
.to_path_buf();
|
||||
if !gui_config_path.exists() {
|
||||
create_and_write_file(
|
||||
&gui_config_path,
|
||||
toml::to_string(&gui_config::Config::new(false))
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to serialize gui config: {}", e)))?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
info!("Gui configuration file created");
|
||||
}
|
||||
|
||||
let backend = backend.inner_client();
|
||||
if let Err(e) = update_connect_cache(
|
||||
@ -632,91 +751,27 @@ pub async fn import_remote_wallet(
|
||||
info!("Liana-Connect cache updated");
|
||||
};
|
||||
|
||||
Ok(gui_config_path)
|
||||
Ok(wallet_settings)
|
||||
}
|
||||
|
||||
pub fn create_and_write_file(
|
||||
network_datadir: &NetworkDirectory,
|
||||
file_name: &str,
|
||||
data: &[u8],
|
||||
) -> Result<PathBuf, Error> {
|
||||
let mut path = network_datadir.path().to_path_buf();
|
||||
path.push(file_name);
|
||||
pub fn create_and_write_file(path: &Path, data: &[u8]) -> Result<(), Error> {
|
||||
let mut file =
|
||||
std::fs::File::create(&path).map_err(|e| Error::CannotCreateFile(e.to_string()))?;
|
||||
std::fs::File::create(path).map_err(|e| Error::CannotCreateFile(e.to_string()))?;
|
||||
file.write_all(data)
|
||||
.map_err(|e| Error::CannotWriteToFile(e.to_string()))?;
|
||||
Ok(path)
|
||||
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) -> Settings {
|
||||
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();
|
||||
|
||||
Settings {
|
||||
wallets: vec![WalletSettings {
|
||||
name: wallet_name(descriptor),
|
||||
descriptor_checksum,
|
||||
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) -> Settings {
|
||||
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();
|
||||
Settings {
|
||||
wallets: vec![WalletSettings {
|
||||
name: wallet_name(descriptor),
|
||||
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) -> Result<Config, Error> {
|
||||
pub fn extract_daemon_config(ctx: &Context, settings: &WalletSettings) -> Result<Config, Error> {
|
||||
let data_directory = ctx
|
||||
.liana_directory
|
||||
.network_directory(ctx.bitcoin_config.network)
|
||||
.lianad_data_directory(&settings.wallet_id());
|
||||
data_directory
|
||||
.init()
|
||||
.map_err(|e| Error::CannotCreateDatadir(e.to_string()))?;
|
||||
|
||||
let data_directory = data_directory
|
||||
.path()
|
||||
.to_path_buf()
|
||||
.canonicalize()
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -21,14 +21,14 @@ pub use mnemonic::{BackupMnemonic, RecoverMnemonic};
|
||||
pub use share_xpubs::ShareXpubs;
|
||||
use tracing::warn;
|
||||
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use iced::{Subscription, Task};
|
||||
|
||||
use liana_ui::widget::*;
|
||||
|
||||
use crate::{
|
||||
app::settings::ProviderKey,
|
||||
app::settings::{ProviderKey, WalletSettings},
|
||||
hw::HardwareWallets,
|
||||
installer::{context::Context, message::Message, view},
|
||||
node::bitcoind::Bitcoind,
|
||||
@ -67,7 +67,7 @@ pub struct Final {
|
||||
generating: bool,
|
||||
internal_bitcoind: Option<Bitcoind>,
|
||||
warning: Option<String>,
|
||||
config_path: Option<PathBuf>,
|
||||
wallet_settings: Option<WalletSettings>,
|
||||
key_redemptions: HashMap<ProviderKey, Option<Result<(), services::keys::Error>>>,
|
||||
}
|
||||
|
||||
@ -77,7 +77,7 @@ impl Final {
|
||||
internal_bitcoind: None,
|
||||
generating: false,
|
||||
warning: None,
|
||||
config_path: None,
|
||||
wallet_settings: None,
|
||||
key_redemptions: HashMap::new(),
|
||||
}
|
||||
}
|
||||
@ -99,7 +99,7 @@ impl Step for Final {
|
||||
.collect();
|
||||
}
|
||||
fn load(&self) -> Task<Message> {
|
||||
if !self.generating && self.config_path.is_none() {
|
||||
if !self.generating && self.wallet_settings.is_none() {
|
||||
Task::perform(async {}, |_| Message::Install)
|
||||
} else {
|
||||
Task::none()
|
||||
@ -142,30 +142,30 @@ impl Step for Final {
|
||||
}
|
||||
// Now exit the installer whether or not any redemption errors occurred.
|
||||
let internal_bitcoind = self.internal_bitcoind.clone();
|
||||
let path = self.config_path.clone().expect("config path already set");
|
||||
let settings = self.wallet_settings.clone().expect("Install is done");
|
||||
// If there were any errors, don't remove the installer log.
|
||||
return Task::perform(
|
||||
async move { (path, internal_bitcoind, has_error) },
|
||||
|(path, internal_bitcoind, has_error)| {
|
||||
Message::Exit(path, internal_bitcoind, !has_error)
|
||||
async move { (settings, internal_bitcoind, has_error) },
|
||||
|(settings, internal_bitcoind, has_error)| {
|
||||
Message::Exit(Box::new(settings), internal_bitcoind, !has_error)
|
||||
},
|
||||
);
|
||||
}
|
||||
Message::Installed(res) => match res {
|
||||
Message::Installed(_, res) => match res {
|
||||
Err(e) => {
|
||||
self.generating = false;
|
||||
self.config_path = None;
|
||||
self.wallet_settings = None;
|
||||
self.warning = Some(e.to_string());
|
||||
}
|
||||
Ok(path) => {
|
||||
self.config_path = Some(path.clone());
|
||||
Ok(wallet_settings) => {
|
||||
self.wallet_settings = Some(wallet_settings);
|
||||
// Now redeem any provider keys.
|
||||
return Task::perform(async move {}, |_| Message::RedeemNextKey);
|
||||
}
|
||||
},
|
||||
Message::Install => {
|
||||
self.generating = true;
|
||||
self.config_path = None;
|
||||
self.wallet_settings = None;
|
||||
self.warning = None;
|
||||
}
|
||||
_ => {}
|
||||
@ -183,7 +183,7 @@ impl Step for Final {
|
||||
progress,
|
||||
email,
|
||||
self.generating,
|
||||
self.config_path.as_ref(),
|
||||
self.wallet_settings.is_some(),
|
||||
self.warning.as_ref(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -1485,7 +1485,7 @@ pub fn install<'a>(
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
generating: bool,
|
||||
config_path: Option<&std::path::PathBuf>,
|
||||
installed: bool,
|
||||
warning: Option<&'a String>,
|
||||
) -> Element<'a, Message> {
|
||||
let prev_msg = if !generating && warning.is_some() {
|
||||
@ -1501,7 +1501,7 @@ pub fn install<'a>(
|
||||
.push_maybe(warning.map(|e| card::invalid(text(e))))
|
||||
.push(if generating {
|
||||
Container::new(text("Installing..."))
|
||||
} else if config_path.is_some() {
|
||||
} else if installed {
|
||||
Container::new(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
|
||||
@ -11,9 +11,14 @@ use liana_ui::{
|
||||
widget::*,
|
||||
};
|
||||
use lianad::config::ConfigError;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::{
|
||||
app,
|
||||
app::{
|
||||
self,
|
||||
settings::{self, WalletSettings},
|
||||
},
|
||||
delete::{delete_wallet, DeleteError},
|
||||
dir::{LianaDirectory, NetworkDirectory},
|
||||
installer::UserFlow,
|
||||
};
|
||||
@ -28,10 +33,9 @@ const NETWORKS: [Network; 4] = [
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum State {
|
||||
Unchecked,
|
||||
Wallet {
|
||||
name: Option<String>,
|
||||
email: Option<String>,
|
||||
checksum: Option<String>,
|
||||
Wallets {
|
||||
wallets: Vec<WalletSettings>,
|
||||
add_wallet: bool,
|
||||
},
|
||||
NoWallet,
|
||||
}
|
||||
@ -49,7 +53,13 @@ impl Launcher {
|
||||
let network = network.unwrap_or(
|
||||
NETWORKS
|
||||
.iter()
|
||||
.find(|net| datadir_path.path().join(net.to_string()).exists())
|
||||
.find(|net| {
|
||||
datadir_path
|
||||
.path()
|
||||
.join(net.to_string())
|
||||
.join(settings::SETTINGS_FILE_NAME)
|
||||
.exists()
|
||||
})
|
||||
.cloned()
|
||||
.unwrap_or(Network::Bitcoin),
|
||||
);
|
||||
@ -95,19 +105,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)) => {
|
||||
@ -117,8 +129,10 @@ 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)) => {
|
||||
self.delete_wallet_modal = None;
|
||||
Task::none()
|
||||
@ -133,20 +147,32 @@ impl Launcher {
|
||||
Task::none()
|
||||
}
|
||||
},
|
||||
Message::View(ViewMessage::Run) => {
|
||||
if matches!(self.state, State::Wallet { .. }) {
|
||||
let datadir_path = self.datadir_path.clone();
|
||||
let mut path = self
|
||||
.datadir_path
|
||||
.network_directory(self.network)
|
||||
.path()
|
||||
.to_path_buf();
|
||||
path.push(app::config::DEFAULT_FILE_NAME);
|
||||
let cfg = app::Config::from_file(&path).expect("Already checked");
|
||||
let network = self.network;
|
||||
Task::perform(async move { (datadir_path.clone(), cfg, network) }, |m| {
|
||||
Message::Run(m.0, m.1, m.2)
|
||||
})
|
||||
Message::View(ViewMessage::AddWalletToList(add)) => {
|
||||
if let State::Wallets { add_wallet, .. } = &mut self.state {
|
||||
*add_wallet = add;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::View(ViewMessage::Run(index)) => {
|
||||
if let State::Wallets { wallets, .. } = &self.state {
|
||||
if let Some(settings) = wallets.get(index) {
|
||||
let datadir_path = self.datadir_path.clone();
|
||||
let mut path = self
|
||||
.datadir_path
|
||||
.network_directory(self.network)
|
||||
.path()
|
||||
.to_path_buf();
|
||||
path.push(app::config::DEFAULT_FILE_NAME);
|
||||
let cfg = app::Config::from_file(&path).expect("Already checked");
|
||||
let network = self.network;
|
||||
let settings = settings.clone();
|
||||
Task::perform(
|
||||
async move { (datadir_path.clone(), cfg, network, settings) },
|
||||
|m| Message::Run(m.0, m.1, m.2, m.3),
|
||||
)
|
||||
} else {
|
||||
Task::none()
|
||||
}
|
||||
} else {
|
||||
Task::none()
|
||||
}
|
||||
@ -170,6 +196,21 @@ impl Launcher {
|
||||
Container::new(image::liana_brand_grey().width(Length::Fixed(200.0)))
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push_maybe(if let State::Wallets { add_wallet, .. } = &self.state {
|
||||
if *add_wallet {
|
||||
Some(
|
||||
button::secondary(
|
||||
Some(icon::previous_icon()),
|
||||
"Back to wallet list",
|
||||
)
|
||||
.on_press(ViewMessage::AddWalletToList(false)),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.push(
|
||||
button::secondary(None, "Share Xpubs")
|
||||
.on_press(ViewMessage::ShareXpubs),
|
||||
@ -191,7 +232,7 @@ impl Launcher {
|
||||
Column::new()
|
||||
.align_x(Alignment::Center)
|
||||
.spacing(30)
|
||||
.push(if matches!(self.state, State::Wallet { .. }) {
|
||||
.push(if matches!(self.state, State::Wallets { .. }) {
|
||||
text("Welcome back").size(50).bold()
|
||||
} else {
|
||||
text("Welcome").size(50).bold()
|
||||
@ -199,122 +240,39 @@ impl Launcher {
|
||||
.push_maybe(self.error.as_ref().map(|e| card::simple(text(e))))
|
||||
.push(match &self.state {
|
||||
State::Unchecked => Column::new(),
|
||||
State::Wallet {
|
||||
email, checksum, ..
|
||||
} => Column::new().push(
|
||||
Row::new()
|
||||
.align_y(Alignment::Center)
|
||||
.spacing(20)
|
||||
.push(
|
||||
Container::new(
|
||||
Button::new(
|
||||
Column::new()
|
||||
.push(p1_bold(format!(
|
||||
"My Liana {} wallet",
|
||||
match self.network {
|
||||
Network::Bitcoin => "Bitcoin",
|
||||
Network::Signet => "Signet",
|
||||
Network::Testnet => "Testnet",
|
||||
Network::Regtest => "Regtest",
|
||||
_ => "",
|
||||
}
|
||||
)))
|
||||
.push_maybe(checksum.as_ref().map(
|
||||
|checksum| {
|
||||
p1_regular(format!(
|
||||
"Liana-{}",
|
||||
checksum
|
||||
))
|
||||
.style(theme::text::secondary)
|
||||
},
|
||||
))
|
||||
.push_maybe(email.as_ref().map(|email| {
|
||||
Row::new()
|
||||
.push(Space::with_width(
|
||||
Length::Fill,
|
||||
))
|
||||
.push(
|
||||
p1_regular(email).style(
|
||||
theme::text::secondary,
|
||||
),
|
||||
)
|
||||
})),
|
||||
State::Wallets {
|
||||
wallets,
|
||||
add_wallet,
|
||||
} => {
|
||||
if *add_wallet {
|
||||
Column::new().push(add_wallet_menu())
|
||||
} else {
|
||||
let col = wallets.iter().enumerate().fold(
|
||||
Column::new().spacing(20),
|
||||
|col, (i, settings)| {
|
||||
col.push(wallets_list_item(
|
||||
self.network,
|
||||
settings,
|
||||
i,
|
||||
))
|
||||
},
|
||||
);
|
||||
col.push(
|
||||
Column::new().push(
|
||||
button::secondary(
|
||||
Some(icon::plus_icon()),
|
||||
"Add wallet",
|
||||
)
|
||||
.on_press(ViewMessage::Run)
|
||||
.padding(15)
|
||||
.style(theme::button::container_border)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.style(theme::card::simple),
|
||||
)
|
||||
.push(
|
||||
Button::new(icon::trash_icon())
|
||||
.style(theme::button::secondary)
|
||||
.on_press(ViewMessage::AddWalletToList(true))
|
||||
.padding(10)
|
||||
.on_press(ViewMessage::DeleteWallet(
|
||||
DeleteWalletMessage::ShowModal,
|
||||
)),
|
||||
),
|
||||
),
|
||||
State::NoWallet => Column::new()
|
||||
.push(
|
||||
Row::new()
|
||||
.align_y(Alignment::End)
|
||||
.spacing(20)
|
||||
.push(
|
||||
Container::new(
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.align_x(Alignment::Center)
|
||||
.push(
|
||||
image::create_new_wallet_icon()
|
||||
.width(Length::Fixed(100.0)),
|
||||
)
|
||||
.push(
|
||||
p1_regular("Create a new Liana wallet")
|
||||
.style(theme::text::secondary),
|
||||
)
|
||||
.push(
|
||||
button::secondary(None, "Select")
|
||||
.width(Length::Fixed(200.0))
|
||||
.on_press(
|
||||
ViewMessage::CreateWallet,
|
||||
),
|
||||
)
|
||||
.align_x(Alignment::Center),
|
||||
)
|
||||
.padding(20),
|
||||
)
|
||||
.push(
|
||||
Container::new(
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.align_x(Alignment::Center)
|
||||
.push(
|
||||
image::restore_wallet_icon()
|
||||
.width(Length::Fixed(100.0)),
|
||||
)
|
||||
.push(
|
||||
p1_regular(
|
||||
"Add an existing Liana wallet",
|
||||
)
|
||||
.style(theme::text::secondary),
|
||||
)
|
||||
.push(
|
||||
button::secondary(None, "Select")
|
||||
.width(Length::Fixed(200.0))
|
||||
.on_press(
|
||||
ViewMessage::ImportWallet,
|
||||
),
|
||||
)
|
||||
.align_x(Alignment::Center),
|
||||
)
|
||||
.padding(20),
|
||||
.width(Length::Fixed(500.0)),
|
||||
),
|
||||
)
|
||||
.align_x(Alignment::Center),
|
||||
)
|
||||
}
|
||||
}
|
||||
State::NoWallet => Column::new().push(add_wallet_menu()),
|
||||
})
|
||||
.max_width(500),
|
||||
.align_x(Alignment::Center),
|
||||
)
|
||||
.center_x(Length::Fill),
|
||||
)
|
||||
@ -338,38 +296,129 @@ impl Launcher {
|
||||
}
|
||||
}
|
||||
|
||||
fn add_wallet_menu<'a>() -> Element<'a, ViewMessage> {
|
||||
Row::new()
|
||||
.align_y(Alignment::End)
|
||||
.spacing(20)
|
||||
.push(
|
||||
Container::new(
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.align_x(Alignment::Center)
|
||||
.push(image::create_new_wallet_icon().width(Length::Fixed(100.0)))
|
||||
.push(p1_regular("Create a new Liana wallet").style(theme::text::secondary))
|
||||
.push(
|
||||
button::secondary(None, "Select")
|
||||
.width(Length::Fixed(200.0))
|
||||
.on_press(ViewMessage::CreateWallet),
|
||||
)
|
||||
.align_x(Alignment::Center),
|
||||
)
|
||||
.padding(20),
|
||||
)
|
||||
.push(
|
||||
Container::new(
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.align_x(Alignment::Center)
|
||||
.push(image::restore_wallet_icon().width(Length::Fixed(100.0)))
|
||||
.push(p1_regular("Add an existing Liana wallet").style(theme::text::secondary))
|
||||
.push(
|
||||
button::secondary(None, "Select")
|
||||
.width(Length::Fixed(200.0))
|
||||
.on_press(ViewMessage::ImportWallet),
|
||||
)
|
||||
.align_x(Alignment::Center),
|
||||
)
|
||||
.padding(20),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn wallets_list_item(
|
||||
network: Network,
|
||||
settings: &WalletSettings,
|
||||
i: usize,
|
||||
) -> Element<ViewMessage> {
|
||||
Container::new(
|
||||
Row::new()
|
||||
.align_y(Alignment::Center)
|
||||
.spacing(20)
|
||||
.push(
|
||||
Container::new(
|
||||
Button::new(
|
||||
Column::new()
|
||||
.push(p1_bold(format!(
|
||||
"My Liana {} wallet",
|
||||
match network {
|
||||
Network::Bitcoin => "Bitcoin",
|
||||
Network::Signet => "Signet",
|
||||
Network::Testnet => "Testnet",
|
||||
Network::Regtest => "Regtest",
|
||||
_ => "",
|
||||
}
|
||||
)))
|
||||
.push(
|
||||
p1_regular(format!("Liana-{}", settings.descriptor_checksum))
|
||||
.style(theme::text::secondary),
|
||||
)
|
||||
.push_maybe(settings.remote_backend_auth.as_ref().map(|auth| {
|
||||
Row::new()
|
||||
.push(Space::with_width(Length::Fill))
|
||||
.push(p1_regular(&auth.email).style(theme::text::secondary))
|
||||
})),
|
||||
)
|
||||
.on_press(ViewMessage::Run(i))
|
||||
.padding(15)
|
||||
.style(theme::button::container_border)
|
||||
.width(Length::Fixed(500.0)),
|
||||
)
|
||||
.style(theme::card::simple),
|
||||
)
|
||||
.push(
|
||||
Button::new(icon::trash_icon())
|
||||
.style(theme::button::secondary)
|
||||
.padding(10)
|
||||
.on_press(ViewMessage::DeleteWallet(DeleteWalletMessage::ShowModal(i))),
|
||||
),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
View(ViewMessage),
|
||||
Install(LianaDirectory, Network, UserFlow),
|
||||
Checked(Result<State, String>),
|
||||
Run(LianaDirectory, app::config::Config, Network),
|
||||
Run(LianaDirectory, app::config::Config, Network, WalletSettings),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ViewMessage {
|
||||
ImportWallet,
|
||||
CreateWallet,
|
||||
AddWalletToList(bool),
|
||||
ShareXpubs,
|
||||
SelectNetwork(Network),
|
||||
StartInstall(Network),
|
||||
Check,
|
||||
Run,
|
||||
Run(usize),
|
||||
DeleteWallet(DeleteWalletMessage),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DeleteWalletMessage {
|
||||
ShowModal,
|
||||
ShowModal(usize),
|
||||
CloseModal,
|
||||
Confirm,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
struct DeleteWalletModal {
|
||||
network: Network,
|
||||
wallet_datadir: NetworkDirectory,
|
||||
warning: Option<std::io::Error>,
|
||||
network_directory: NetworkDirectory,
|
||||
wallet_settings: WalletSettings,
|
||||
warning: Option<DeleteError>,
|
||||
deleted: bool,
|
||||
// `None` means we were not able to determine whether wallet uses internal bitcoind.
|
||||
internal_bitcoind: Option<bool>,
|
||||
@ -377,13 +426,13 @@ struct DeleteWalletModal {
|
||||
|
||||
impl DeleteWalletModal {
|
||||
fn new(
|
||||
network: Network,
|
||||
wallet_datadir: NetworkDirectory,
|
||||
network_directory: NetworkDirectory,
|
||||
wallet_settings: WalletSettings,
|
||||
internal_bitcoind: Option<bool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
network,
|
||||
wallet_datadir,
|
||||
wallet_settings,
|
||||
network_directory,
|
||||
warning: None,
|
||||
deleted: false,
|
||||
internal_bitcoind,
|
||||
@ -393,7 +442,10 @@ impl DeleteWalletModal {
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
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;
|
||||
@ -404,6 +456,7 @@ impl DeleteWalletModal {
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
let mut confirm_button = button::secondary(None, "Delete wallet")
|
||||
.width(Length::Fixed(200.0))
|
||||
@ -414,8 +467,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.)"),
|
||||
@ -429,9 +482,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(
|
||||
@ -513,16 +569,18 @@ async fn check_network_datadir(path: NetworkDirectory) -> Result<State, String>
|
||||
})?;
|
||||
}
|
||||
|
||||
if let Ok(Some(wallet)) = app::settings::WalletSettings::from_file(&path, |_w| true) {
|
||||
return Ok(State::Wallet {
|
||||
name: Some(wallet.name),
|
||||
checksum: Some(wallet.descriptor_checksum),
|
||||
email: wallet.remote_backend_auth.map(|auth| auth.email),
|
||||
});
|
||||
match settings::Settings::from_file(&path) {
|
||||
Ok(s) => {
|
||||
if s.wallets.is_empty() {
|
||||
Ok(State::NoWallet)
|
||||
} else {
|
||||
Ok(State::Wallets {
|
||||
wallets: s.wallets,
|
||||
add_wallet: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
Err(settings::SettingsError::NotFound) => Ok(State::NoWallet),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
Ok(State::Wallet {
|
||||
name: None,
|
||||
checksum: None,
|
||||
email: None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -59,7 +59,7 @@ pub struct Loader {
|
||||
pub internal_bitcoind: Option<Bitcoind>,
|
||||
pub waiting_daemon_bitcoind: bool,
|
||||
pub backup: Option<Backup>,
|
||||
pub wallet_setting: Option<WalletSettings>,
|
||||
pub wallet_settings: WalletSettings,
|
||||
step: Step,
|
||||
}
|
||||
|
||||
@ -119,9 +119,12 @@ impl Loader {
|
||||
network: bitcoin::Network,
|
||||
internal_bitcoind: Option<Bitcoind>,
|
||||
backup: Option<Backup>,
|
||||
wallet_setting: Option<WalletSettings>,
|
||||
wallet_settings: WalletSettings,
|
||||
) -> (Self, Task<Message>) {
|
||||
let path = socket_path(&datadir_path, network);
|
||||
let socket_path = datadir_path
|
||||
.network_directory(network)
|
||||
.lianad_data_directory(&wallet_settings.wallet_id())
|
||||
.lianad_rpc_socket_path();
|
||||
(
|
||||
Loader {
|
||||
network,
|
||||
@ -131,21 +134,17 @@ impl Loader {
|
||||
daemon_started: false,
|
||||
internal_bitcoind,
|
||||
waiting_daemon_bitcoind: false,
|
||||
wallet_setting,
|
||||
wallet_settings,
|
||||
backup,
|
||||
},
|
||||
Task::perform(connect(path), Message::Loaded),
|
||||
Task::perform(connect(socket_path), Message::Loaded),
|
||||
)
|
||||
}
|
||||
|
||||
fn start_bitcoind(&self) -> bool {
|
||||
if self.internal_bitcoind.is_some() {
|
||||
false
|
||||
} else if let Some(start) = self
|
||||
.wallet_setting
|
||||
.as_ref()
|
||||
.and_then(|setting| setting.start_internal_bitcoind)
|
||||
{
|
||||
} else if let Some(start) = self.wallet_settings.start_internal_bitcoind {
|
||||
start
|
||||
} else {
|
||||
self.gui_config.start_internal_bitcoind
|
||||
@ -162,6 +161,7 @@ impl Loader {
|
||||
if info.block_height > 0 {
|
||||
return Task::perform(
|
||||
load_application(
|
||||
self.wallet_settings.clone(),
|
||||
daemon,
|
||||
info,
|
||||
self.datadir_path.clone(),
|
||||
@ -207,6 +207,7 @@ impl Loader {
|
||||
self.datadir_path.clone(),
|
||||
self.start_bitcoind(),
|
||||
self.network,
|
||||
self.wallet_settings.clone(),
|
||||
),
|
||||
Message::Started,
|
||||
);
|
||||
@ -256,6 +257,7 @@ impl Loader {
|
||||
if (info.sync - 1.0_f64).abs() < f64::EPSILON {
|
||||
return Task::perform(
|
||||
load_application(
|
||||
self.wallet_settings.clone(),
|
||||
daemon.clone(),
|
||||
info,
|
||||
self.datadir_path.clone(),
|
||||
@ -309,7 +311,7 @@ impl Loader {
|
||||
self.network,
|
||||
self.internal_bitcoind.clone(),
|
||||
self.backup.clone(),
|
||||
self.wallet_setting.clone(),
|
||||
self.wallet_settings.clone(),
|
||||
);
|
||||
*self = loader;
|
||||
cmd
|
||||
@ -406,6 +408,7 @@ fn get_bitcoind_log(log_path: PathBuf) -> impl Stream<Item = Option<String>> {
|
||||
}
|
||||
|
||||
pub async fn load_application(
|
||||
wallet_settings: WalletSettings,
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
info: GetInfoResult,
|
||||
datadir_path: LianaDirectory,
|
||||
@ -422,9 +425,8 @@ pub async fn load_application(
|
||||
),
|
||||
Error,
|
||||
> {
|
||||
let network_dir = datadir_path.network_directory(network);
|
||||
let wallet = Wallet::new(info.descriptors.main)
|
||||
.load_from_settings(&network_dir)?
|
||||
.load_from_settings(wallet_settings)?
|
||||
.load_hotsigners(&datadir_path, network)?;
|
||||
|
||||
let coins = coins_to_cache(daemon.clone()).await.map(|res| res.coins)?;
|
||||
@ -553,9 +555,11 @@ pub async fn start_bitcoind_and_daemon(
|
||||
liana_datadir_path: LianaDirectory,
|
||||
start_internal_bitcoind: bool,
|
||||
network: bitcoin::Network,
|
||||
settings: WalletSettings,
|
||||
) -> StartedResult {
|
||||
let mut config_path = liana_datadir_path
|
||||
.network_directory(network)
|
||||
.lianad_data_directory(&settings.wallet_id())
|
||||
.path()
|
||||
.to_path_buf();
|
||||
config_path.push("daemon.toml");
|
||||
@ -631,10 +635,3 @@ impl From<DaemonError> for Error {
|
||||
Error::Daemon(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// default lianad socket path is .liana/bitcoin/lianad_rpc
|
||||
fn socket_path(datadir: &LianaDirectory, network: bitcoin::Network) -> PathBuf {
|
||||
let mut path = datadir.network_directory(network).path().to_path_buf();
|
||||
path.push("lianad_rpc");
|
||||
path
|
||||
}
|
||||
|
||||
@ -200,34 +200,21 @@ impl GUI {
|
||||
self.state = State::Installer(Box::new(install));
|
||||
command.map(|msg| Message::Install(Box::new(msg)))
|
||||
}
|
||||
launcher::Message::Run(datadir_path, cfg, network) => {
|
||||
launcher::Message::Run(datadir_path, cfg, network, settings) => {
|
||||
self.logger.set_running_mode(
|
||||
datadir_path.clone(),
|
||||
network,
|
||||
self.log_level
|
||||
.unwrap_or_else(|| cfg.log_level().unwrap_or(LevelFilter::INFO)),
|
||||
);
|
||||
let network_dir = datadir_path.network_directory(network);
|
||||
if let Ok(settings) =
|
||||
app::settings::WalletSettings::from_file(&network_dir, |_w| true)
|
||||
{
|
||||
if let Some(setting) = settings
|
||||
.as_ref()
|
||||
.and_then(|w| w.remote_backend_auth.clone())
|
||||
{
|
||||
let (login, command) =
|
||||
login::LianaLiteLogin::new(datadir_path, network, setting);
|
||||
self.state = State::Login(Box::new(login));
|
||||
command.map(|msg| Message::Login(Box::new(msg)))
|
||||
} else {
|
||||
let (loader, command) =
|
||||
Loader::new(datadir_path, cfg, network, None, None, settings);
|
||||
self.state = State::Loader(Box::new(loader));
|
||||
command.map(|msg| Message::Load(Box::new(msg)))
|
||||
}
|
||||
if let Some(setting) = settings.remote_backend_auth {
|
||||
let (login, command) =
|
||||
login::LianaLiteLogin::new(datadir_path, network, setting);
|
||||
self.state = State::Login(Box::new(login));
|
||||
command.map(|msg| Message::Login(Box::new(msg)))
|
||||
} else {
|
||||
let (loader, command) =
|
||||
Loader::new(datadir_path, cfg, network, None, None, None);
|
||||
Loader::new(datadir_path, cfg, network, None, None, settings);
|
||||
self.state = State::Loader(Box::new(loader));
|
||||
command.map(|msg| Message::Load(Box::new(msg)))
|
||||
}
|
||||
@ -279,20 +266,20 @@ impl GUI {
|
||||
_ => l.update(*msg).map(|msg| Message::Login(Box::new(msg))),
|
||||
},
|
||||
(State::Installer(i), Message::Install(msg)) => {
|
||||
if let installer::Message::Exit(path, internal_bitcoind, remove_log) = *msg {
|
||||
let network_dir = i.datadir.network_directory(i.network);
|
||||
let settings = app::settings::WalletSettings::from_file(&network_dir, |_| true)
|
||||
.expect("A settings file was created");
|
||||
if let Some(setting) = settings
|
||||
.as_ref()
|
||||
.and_then(|w| w.remote_backend_auth.clone())
|
||||
{
|
||||
if let installer::Message::Exit(settings, internal_bitcoind, remove_log) = *msg {
|
||||
if let Some(auth) = settings.remote_backend_auth {
|
||||
let (login, command) =
|
||||
login::LianaLiteLogin::new(i.datadir.clone(), i.network, setting);
|
||||
login::LianaLiteLogin::new(i.datadir.clone(), i.network, auth);
|
||||
self.state = State::Login(Box::new(login));
|
||||
command.map(|msg| Message::Login(Box::new(msg)))
|
||||
} else {
|
||||
let cfg = app::Config::from_file(&path).expect("A config file was created");
|
||||
let cfg = app::Config::from_file(
|
||||
&i.datadir
|
||||
.network_directory(i.network)
|
||||
.path()
|
||||
.join(app::config::DEFAULT_FILE_NAME),
|
||||
)
|
||||
.expect("A gui configuration file must be present");
|
||||
|
||||
self.logger.set_running_mode(
|
||||
i.datadir.clone(),
|
||||
@ -310,7 +297,7 @@ impl GUI {
|
||||
i.network,
|
||||
internal_bitcoind,
|
||||
i.context.backup.take(),
|
||||
settings,
|
||||
*settings,
|
||||
);
|
||||
self.state = State::Loader(Box::new(loader));
|
||||
command.map(|msg| Message::Load(Box::new(msg)))
|
||||
|
||||
@ -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<String>,
|
||||
) -> 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::<ConnectCache>(&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,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
pub use liana::signer::SignerError;
|
||||
use std::str::FromStr;
|
||||
|
||||
use liana::{
|
||||
miniscript::bitcoin::{
|
||||
@ -6,10 +7,10 @@ use liana::{
|
||||
psbt::Psbt,
|
||||
secp256k1, Network,
|
||||
},
|
||||
signer::HotSigner,
|
||||
signer::{self, HotSigner},
|
||||
};
|
||||
|
||||
use crate::dir::LianaDirectory;
|
||||
use crate::dir::{LianaDirectory, NetworkDirectory};
|
||||
|
||||
pub struct Signer {
|
||||
curve: secp256k1::Secp256k1<secp256k1::All>,
|
||||
@ -62,7 +63,52 @@ impl Signer {
|
||||
&self,
|
||||
datadir_root: &LianaDirectory,
|
||||
network: Network,
|
||||
checksum: &str,
|
||||
timestamp: i64,
|
||||
) -> Result<(), SignerError> {
|
||||
self.key.store(datadir_root.path(), network, &self.curve)
|
||||
self.key.store(
|
||||
datadir_root.path(),
|
||||
network,
|
||||
&self.curve,
|
||||
Some((checksum.to_string(), timestamp)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_wallet_mnemonics(
|
||||
network_directory: &NetworkDirectory,
|
||||
descriptor_checksum: &str,
|
||||
pinned_at: Option<i64>,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let folder = network_directory
|
||||
.path()
|
||||
.join(signer::MNEMONICS_FOLDER_NAME)
|
||||
.to_path_buf();
|
||||
if folder.exists() {
|
||||
for entry in std::fs::read_dir(&folder)? {
|
||||
let path = entry?.path();
|
||||
if let Some(filename) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.and_then(|s| signer::MnemonicFileName::from_str(s).ok())
|
||||
{
|
||||
match (pinned_at, filename.descriptor_info) {
|
||||
// legacy wallet, we delete any mnemonic-{}.txt
|
||||
(None, None) => {
|
||||
std::fs::remove_file(&path)?;
|
||||
}
|
||||
// we delete any mnemonic-fg-sum-tim.txt that matches the descriptor_checksum
|
||||
// and timestamp
|
||||
(Some(t), Some(info)) => {
|
||||
if info.0 == descriptor_checksum && t == info.1 {
|
||||
std::fs::remove_file(&path)?;
|
||||
}
|
||||
}
|
||||
_ => { // The file is not related to the wallet}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ use std::{
|
||||
|
||||
use miniscript::bitcoin::{
|
||||
self,
|
||||
bip32::{self, Error as Bip32Error},
|
||||
bip32::{self, Error as Bip32Error, Fingerprint},
|
||||
ecdsa,
|
||||
hashes::Hash,
|
||||
key::TapTweak,
|
||||
@ -186,22 +186,26 @@ impl HotSigner {
|
||||
/// Store the mnemonic in a file within the given "data directory".
|
||||
/// The file is stored within a "mnemonics" folder, with the filename set to the fingerprint of
|
||||
/// the master xpub corresponding to this mnemonic.
|
||||
/// returns the filename
|
||||
pub fn store(
|
||||
&self,
|
||||
datadir_root: &path::Path,
|
||||
network: bitcoin::Network,
|
||||
secp: &secp256k1::Secp256k1<impl secp256k1::Signing>,
|
||||
descriptor_info: Option<(String, i64)>,
|
||||
) -> Result<(), SignerError> {
|
||||
let mut mnemonics_folder = Self::mnemonics_folder(datadir_root, network);
|
||||
let mnemonics_folder = Self::mnemonics_folder(datadir_root, network);
|
||||
if !mnemonics_folder.exists() {
|
||||
create_dir(&mnemonics_folder).map_err(SignerError::MnemonicStorage)?;
|
||||
}
|
||||
|
||||
// This will fail if a file with this fingerprint exists already.
|
||||
mnemonics_folder.push(format!("mnemonic-{:x}.txt", self.fingerprint(secp)));
|
||||
let mnemonic_path = mnemonics_folder;
|
||||
let mut mnemonic_file =
|
||||
create_file(&mnemonic_path).map_err(SignerError::MnemonicStorage)?;
|
||||
let filename = MnemonicFileName {
|
||||
fingerprint: self.fingerprint(secp),
|
||||
descriptor_info,
|
||||
};
|
||||
let mut mnemonic_file = create_file(&mnemonics_folder.join(filename.to_string()))
|
||||
.map_err(SignerError::MnemonicStorage)?;
|
||||
mnemonic_file
|
||||
.write_all(self.mnemonic_str().as_bytes())
|
||||
.map_err(SignerError::MnemonicStorage)?;
|
||||
@ -404,6 +408,95 @@ impl HotSigner {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MnemonicFileName {
|
||||
pub fingerprint: Fingerprint,
|
||||
pub descriptor_info: Option<(String, i64)>, // (descriptor_checksum, timestamp)
|
||||
}
|
||||
|
||||
impl fmt::Display for MnemonicFileName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self.descriptor_info {
|
||||
Some((checksum, timestamp)) => {
|
||||
write!(
|
||||
f,
|
||||
"mnemonic-{}-{}-{}.txt",
|
||||
self.fingerprint, checksum, timestamp
|
||||
)
|
||||
}
|
||||
None => {
|
||||
write!(f, "mnemonic-{}.txt", self.fingerprint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MnemonicFileNameError {
|
||||
InvalidFormat,
|
||||
InvalidFingerprint,
|
||||
InvalidTimestamp,
|
||||
}
|
||||
|
||||
impl fmt::Display for MnemonicFileNameError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
MnemonicFileNameError::InvalidFormat => write!(f, "Invalid mnemonic file name format"),
|
||||
MnemonicFileNameError::InvalidFingerprint => write!(f, "Invalid fingerprint format"),
|
||||
MnemonicFileNameError::InvalidTimestamp => write!(f, "Invalid timestamp format"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for MnemonicFileNameError {}
|
||||
|
||||
// Implementation of FromStr for MnemonicFileName
|
||||
impl FromStr for MnemonicFileName {
|
||||
type Err = MnemonicFileNameError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// Check if the string starts with "mnemonic-" and ends with ".txt"
|
||||
if !s.starts_with("mnemonic-") || !s.ends_with(".txt") {
|
||||
return Err(MnemonicFileNameError::InvalidFormat);
|
||||
}
|
||||
|
||||
let content = s
|
||||
.strip_prefix("mnemonic-")
|
||||
.expect("Already checked")
|
||||
.strip_suffix(".txt")
|
||||
.expect("Already checked");
|
||||
|
||||
let parts: Vec<&str> = content.split('-').collect();
|
||||
match parts.len() {
|
||||
1 => {
|
||||
// Only fingerprint
|
||||
let fingerprint = Fingerprint::from_str(parts[0])
|
||||
.map_err(|_| MnemonicFileNameError::InvalidFingerprint)?;
|
||||
|
||||
Ok(MnemonicFileName {
|
||||
fingerprint,
|
||||
descriptor_info: None,
|
||||
})
|
||||
}
|
||||
3 => {
|
||||
// Fingerprint + checksum + timestamp
|
||||
let fingerprint = Fingerprint::from_str(parts[0])
|
||||
.map_err(|_| MnemonicFileNameError::InvalidFingerprint)?;
|
||||
|
||||
let timestamp = parts[2]
|
||||
.parse::<i64>()
|
||||
.map_err(|_| MnemonicFileNameError::InvalidTimestamp)?;
|
||||
|
||||
Ok(MnemonicFileName {
|
||||
fingerprint,
|
||||
descriptor_info: Some((parts[1].to_string(), timestamp)),
|
||||
})
|
||||
}
|
||||
_ => Err(MnemonicFileNameError::InvalidFormat),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@ -471,7 +564,7 @@ mod tests {
|
||||
let words_set: HashSet<_> = (0..10)
|
||||
.map(|_| {
|
||||
let signer = HotSigner::generate(network).unwrap();
|
||||
signer.store(&tmp_dir, network, &secp).unwrap();
|
||||
signer.store(&tmp_dir, network, &secp, None).unwrap();
|
||||
signer.words()
|
||||
})
|
||||
.collect();
|
||||
@ -1087,4 +1180,71 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mnemonic_filename() {
|
||||
// Test to_string with descriptor info
|
||||
let fingerprint = Fingerprint::from_str("abcd1234").unwrap();
|
||||
let filename_with_info = MnemonicFileName {
|
||||
fingerprint,
|
||||
descriptor_info: Some(("def456".to_string(), 1620000000)),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
filename_with_info.to_string(),
|
||||
"mnemonic-abcd1234-def456-1620000000.txt"
|
||||
);
|
||||
|
||||
// Test to_string without descriptor info
|
||||
let filename_without_info = MnemonicFileName {
|
||||
fingerprint,
|
||||
descriptor_info: None,
|
||||
};
|
||||
|
||||
assert_eq!(filename_without_info.to_string(), "mnemonic-abcd1234.txt");
|
||||
|
||||
// Test from_str with descriptor info
|
||||
let input_with_info = "mnemonic-abcd1234-def456-1620000000.txt";
|
||||
let parsed_with_info = MnemonicFileName::from_str(input_with_info).unwrap();
|
||||
|
||||
assert_eq!(parsed_with_info.fingerprint, fingerprint);
|
||||
assert_eq!(
|
||||
parsed_with_info.descriptor_info,
|
||||
Some(("def456".to_string(), 1620000000))
|
||||
);
|
||||
|
||||
// Test from_str without descriptor info
|
||||
let input_without_info = "mnemonic-abcd1234.txt";
|
||||
let parsed_without_info = MnemonicFileName::from_str(input_without_info).unwrap();
|
||||
|
||||
assert_eq!(parsed_without_info.fingerprint, fingerprint);
|
||||
assert_eq!(parsed_without_info.descriptor_info, None);
|
||||
|
||||
// Test roundtrip with descriptor info
|
||||
let roundtrip_with_info =
|
||||
MnemonicFileName::from_str(&filename_with_info.to_string()).unwrap();
|
||||
assert_eq!(filename_with_info, roundtrip_with_info);
|
||||
|
||||
// Test roundtrip without descriptor info
|
||||
let roundtrip_without_info =
|
||||
MnemonicFileName::from_str(&filename_without_info.to_string()).unwrap();
|
||||
assert_eq!(filename_without_info, roundtrip_without_info);
|
||||
|
||||
// Test error cases
|
||||
|
||||
// Missing prefix
|
||||
assert!(MnemonicFileName::from_str("abcd1234.txt").is_err());
|
||||
|
||||
// Missing suffix
|
||||
assert!(MnemonicFileName::from_str("mnemonic-abcd1234").is_err());
|
||||
|
||||
// Wrong number of parts
|
||||
assert!(MnemonicFileName::from_str("mnemonic-abcd1234-def456.txt").is_err());
|
||||
|
||||
// Invalid fingerprint (assuming Fingerprint::from_str fails for "invalid")
|
||||
assert!(MnemonicFileName::from_str("mnemonic-invalid-def456-1620000000.txt").is_err());
|
||||
|
||||
// Invalid timestamp
|
||||
assert!(MnemonicFileName::from_str("mnemonic-abcd1234-def456-notanumber.txt").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user