gui: new module settings

This commit is contained in:
edouard 2023-01-25 16:07:59 +01:00
parent 689f19a4f2
commit bf1e9e4b80
20 changed files with 650 additions and 319 deletions

View File

@ -1,6 +1,7 @@
use crate::daemon::model::{Coin, SpendTx};
use liana::miniscript::bitcoin::Network;
#[derive(Debug)]
pub struct Cache {
pub network: Network,
pub blockheight: i32,

View File

@ -11,19 +11,19 @@ pub struct Config {
/// Use iced debug feature if true.
pub debug: Option<bool>,
/// hardware wallets config.
#[serde(default)]
pub hardware_wallets: Vec<HardwareWalletConfig>,
/// LEGACY: Use Settings module instead.
pub hardware_wallets: Option<Vec<HardwareWalletConfig>>,
}
pub const DEFAULT_FILE_NAME: &str = "gui.toml";
impl Config {
pub fn new(daemon_config_path: PathBuf, hardware_wallets: Vec<HardwareWalletConfig>) -> Self {
pub fn new(daemon_config_path: PathBuf) -> Self {
Self {
daemon_config_path,
log_level: None,
debug: None,
hardware_wallets,
hardware_wallets: None,
}
}
@ -40,14 +40,6 @@ impl Config {
})?;
Ok(config)
}
pub fn default_path() -> Result<PathBuf, ConfigError> {
let mut datadir = default_datadir().map_err(|_| {
ConfigError::Unexpected("Could not locate the default datadir directory.".to_owned())
})?;
datadir.push(DEFAULT_FILE_NAME);
Ok(datadir)
}
}
#[derive(PartialEq, Eq, Debug, Clone)]

View File

@ -2,6 +2,7 @@ pub mod cache;
pub mod config;
pub mod menu;
pub mod message;
pub mod settings;
pub mod state;
pub mod view;
pub mod wallet;
@ -31,14 +32,14 @@ pub struct App {
state: Box<dyn State>,
cache: Cache,
config: Config,
wallet: Wallet,
wallet: Arc<Wallet>,
daemon: Arc<dyn Daemon + Sync + Send>,
}
impl App {
pub fn new(
cache: Cache,
wallet: Wallet,
wallet: Arc<Wallet>,
config: Config,
daemon: Arc<dyn Daemon + Sync + Send>,
) -> (App, Command<Message>) {
@ -72,22 +73,15 @@ impl App {
.into(),
menu::Menu::Recovery => RecoveryPanel::new(
self.wallet.clone(),
self.config.clone(),
&self.cache.coins,
self.wallet.main_descriptor.timelock_value(),
self.cache.blockheight as u32,
)
.into(),
menu::Menu::Receive => ReceivePanel::default().into(),
menu::Menu::Spend => SpendPanel::new(
self.wallet.clone(),
self.config.clone(),
&self.cache.spend_txs,
)
.into(),
menu::Menu::Spend => SpendPanel::new(self.wallet.clone(), &self.cache.spend_txs).into(),
menu::Menu::CreateSpendTx => CreateSpendPanel::new(
self.wallet.clone(),
self.config.clone(),
&self.cache.coins,
self.cache.blockheight as u32,
)

75
gui/src/app/settings.rs Normal file
View File

@ -0,0 +1,75 @@
use std::collections::HashMap;
use std::path::Path;
use liana::miniscript::bitcoin::util::bip32::Fingerprint;
use serde::{Deserialize, Serialize};
use crate::hw::HardwareWalletConfig;
///! Settings is the module to handle the GUI settings file.
///! The settings file is used by the GUI to store useful information.
pub const DEFAULT_FILE_NAME: &str = "settings.json";
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Settings {
pub wallets: Vec<WalletSetting>,
}
impl Settings {
pub fn from_file(path: &Path) -> Result<Self, SettingsError> {
let config = 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))
})
})?;
Ok(config)
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WalletSetting {
pub name: String,
pub descriptor_checksum: String,
#[serde(default)]
pub keys: Vec<KeySetting>,
#[serde(default)]
pub hardware_wallets: Vec<HardwareWalletConfig>,
}
impl WalletSetting {
pub fn keys_aliases(&self) -> HashMap<Fingerprint, String> {
let mut map = HashMap::new();
for key in self.keys.clone() {
map.insert(key.master_fingerprint, key.name);
}
map
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct KeySetting {
pub name: String,
pub master_fingerprint: Fingerprint,
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum SettingsError {
NotFound,
ReadingFile(String),
Unexpected(String),
}
impl std::fmt::Display for SettingsError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::NotFound => write!(f, "Settings file not found"),
Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e),
Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
}
}
}

View File

@ -39,7 +39,7 @@ pub trait State {
}
pub struct Home {
wallet: Wallet,
wallet: Arc<Wallet>,
balance: Amount,
recovery_warning: Option<(Amount, usize)>,
recovery_alert: Option<(Amount, usize)>,
@ -50,7 +50,7 @@ pub struct Home {
}
impl Home {
pub fn new(wallet: Wallet, coins: &[Coin]) -> Self {
pub fn new(wallet: Arc<Wallet>, coins: &[Coin]) -> Self {
Self {
wallet,
balance: Amount::from_sat(

View File

@ -6,7 +6,6 @@ use iced::{Command, Element};
use crate::{
app::{
cache::Cache,
config::Config,
error::Error,
menu::Menu,
message::Message,
@ -25,8 +24,7 @@ use crate::{
use liana::miniscript::bitcoin::{Address, Amount, Network};
pub struct RecoveryPanel {
wallet: Wallet,
config: Config,
wallet: Arc<Wallet>,
locked_coins: (usize, Amount),
recoverable_coins: (usize, Amount),
warning: Option<Error>,
@ -38,13 +36,7 @@ pub struct RecoveryPanel {
}
impl RecoveryPanel {
pub fn new(
wallet: Wallet,
config: Config,
coins: &[Coin],
timelock: u32,
blockheight: u32,
) -> Self {
pub fn new(wallet: Arc<Wallet>, coins: &[Coin], timelock: u32, blockheight: u32) -> Self {
let mut locked_coins = (0, Amount::from_sat(0));
let mut recoverable_coins = (0, Amount::from_sat(0));
for coin in coins {
@ -61,7 +53,6 @@ impl RecoveryPanel {
}
Self {
wallet,
config,
locked_coins,
recoverable_coins,
warning: None,
@ -123,12 +114,7 @@ impl State for RecoveryPanel {
},
Message::Recovery(res) => match res {
Ok(tx) => {
self.generated = Some(detail::SpendTxState::new(
self.wallet.clone(),
self.config.clone(),
tx,
false,
))
self.generated = Some(detail::SpendTxState::new(self.wallet.clone(), tx, false))
}
Err(e) => self.warning = Some(e),
},

View File

@ -8,8 +8,7 @@ use liana::miniscript::bitcoin::{
use crate::{
app::{
cache::Cache, config::Config, error::Error, message::Message, view, view::spend::detail,
wallet::Wallet,
cache::Cache, error::Error, message::Message, view, view::spend::detail, wallet::Wallet,
},
daemon::{
model::{SpendStatus, SpendTx},
@ -39,19 +38,17 @@ trait Action {
}
pub struct SpendTxState {
wallet: Wallet,
config: Config,
wallet: Arc<Wallet>,
tx: SpendTx,
saved: bool,
action: Option<Box<dyn Action>>,
}
impl SpendTxState {
pub fn new(wallet: Wallet, config: Config, tx: SpendTx, saved: bool) -> Self {
pub fn new(wallet: Arc<Wallet>, tx: SpendTx, saved: bool) -> Self {
Self {
wallet,
action: None,
config,
tx,
saved,
}
@ -80,7 +77,7 @@ impl SpendTxState {
self.action = Some(Box::new(DeleteAction::default()));
}
view::SpendTxMessage::Sign => {
let action = SignAction::new(self.config.clone());
let action = SignAction::new();
let cmd = action.load(&self.wallet, daemon);
self.action = Some(Box::new(action));
return cmd;
@ -252,7 +249,6 @@ impl Action for DeleteAction {
}
pub struct SignAction {
config: Config,
chosen_hw: Option<usize>,
processing: bool,
hws: Vec<HardwareWallet>,
@ -261,9 +257,8 @@ pub struct SignAction {
}
impl SignAction {
pub fn new(config: Config) -> Self {
pub fn new() -> Self {
Self {
config,
chosen_hw: None,
processing: false,
hws: Vec::new(),
@ -279,13 +274,8 @@ impl Action for SignAction {
}
fn load(&self, wallet: &Wallet, _daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
let config = self.config.clone();
let desc = wallet.main_descriptor.to_string();
let name = wallet.name.clone();
Command::perform(
list_hws(config, name, desc),
Message::ConnectedHardwareWallets,
)
let wallet = wallet.clone();
Command::perform(list_hws(wallet), Message::ConnectedHardwareWallets)
}
fn update(
&mut self,
@ -350,8 +340,12 @@ impl Action for SignAction {
}
}
async fn list_hws(config: Config, wallet_name: String, descriptor: String) -> Vec<HardwareWallet> {
list_hardware_wallets(&config.hardware_wallets, Some((&wallet_name, &descriptor))).await
async fn list_hws(wallet: Wallet) -> Vec<HardwareWallet> {
list_hardware_wallets(
&wallet.hardware_wallets,
Some((&wallet.name, &wallet.main_descriptor.to_string())),
)
.await
}
async fn sign_psbt(

View File

@ -8,10 +8,7 @@ use liana::miniscript::bitcoin::{consensus, util::psbt::Psbt};
use super::{redirect, State};
use crate::{
app::{
cache::Cache, config::Config, error::Error, menu::Menu, message::Message, view,
wallet::Wallet,
},
app::{cache::Cache, error::Error, menu::Menu, message::Message, view, wallet::Wallet},
daemon::{
model::{Coin, SpendTx},
Daemon,
@ -20,8 +17,7 @@ use crate::{
};
pub struct SpendPanel {
wallet: Wallet,
config: Config,
wallet: Arc<Wallet>,
selected_tx: Option<detail::SpendTxState>,
spend_txs: Vec<SpendTx>,
warning: Option<Error>,
@ -29,10 +25,9 @@ pub struct SpendPanel {
}
impl SpendPanel {
pub fn new(wallet: Wallet, config: Config, spend_txs: &[SpendTx]) -> Self {
pub fn new(wallet: Arc<Wallet>, spend_txs: &[SpendTx]) -> Self {
Self {
wallet,
config,
spend_txs: spend_txs.to_vec(),
warning: None,
selected_tx: None,
@ -97,12 +92,7 @@ impl State for SpendPanel {
}
Message::View(view::Message::Select(i)) => {
if let Some(tx) = self.spend_txs.get(i) {
let tx = detail::SpendTxState::new(
self.wallet.clone(),
self.config.clone(),
tx.clone(),
true,
);
let tx = detail::SpendTxState::new(self.wallet.clone(), tx.clone(), true);
let cmd = tx.load(daemon);
self.selected_tx = Some(tx);
return cmd;
@ -143,7 +133,7 @@ pub struct CreateSpendPanel {
}
impl CreateSpendPanel {
pub fn new(wallet: Wallet, config: Config, coins: &[Coin], blockheight: u32) -> Self {
pub fn new(wallet: Arc<Wallet>, coins: &[Coin], blockheight: u32) -> Self {
let descriptor = wallet.main_descriptor.clone();
let timelock = descriptor.timelock_value();
Self {
@ -157,7 +147,7 @@ impl CreateSpendPanel {
timelock,
blockheight,
)),
Box::new(step::SaveSpend::new(wallet, config)),
Box::new(step::SaveSpend::new(wallet)),
],
}
}

View File

@ -12,8 +12,7 @@ use liana::{
use crate::{
app::{
cache::Cache, config::Config, error::Error, message::Message, state::spend::detail, view,
wallet::Wallet,
cache::Cache, error::Error, message::Message, state::spend::detail, view, wallet::Wallet,
},
daemon::{
model::{remaining_sequence, Coin, SpendTx},
@ -430,16 +429,14 @@ impl Step for ChooseCoins {
}
pub struct SaveSpend {
wallet: Wallet,
config: Config,
wallet: Arc<Wallet>,
spend: Option<detail::SpendTxState>,
}
impl SaveSpend {
pub fn new(wallet: Wallet, config: Config) -> Self {
pub fn new(wallet: Arc<Wallet>) -> Self {
Self {
wallet,
config,
spend: None,
}
}
@ -455,7 +452,6 @@ impl Step for SaveSpend {
.unwrap();
self.spend = Some(detail::SpendTxState::new(
self.wallet.clone(),
self.config.clone(),
SpendTx::new(psbt, draft.inputs.clone(), sigs),
false,
));

View File

@ -1,16 +1,46 @@
use liana::descriptors::MultipathDescriptor;
use std::collections::HashMap;
#[derive(Clone)]
use crate::hw::HardwareWalletConfig;
use liana::descriptors::MultipathDescriptor;
use liana::miniscript::bitcoin::util::bip32::Fingerprint;
pub const DEFAULT_WALLET_NAME: &str = "Liana";
#[derive(Debug, Clone)]
pub struct Wallet {
pub name: String,
pub main_descriptor: MultipathDescriptor,
pub keys_aliases: HashMap<Fingerprint, String>,
pub hardware_wallets: Vec<HardwareWalletConfig>,
}
impl Wallet {
pub fn new(main_descriptor: MultipathDescriptor) -> Self {
pub fn new(name: String, main_descriptor: MultipathDescriptor) -> Self {
Self {
name: "Liana".to_string(),
name,
main_descriptor,
keys_aliases: HashMap::new(),
hardware_wallets: Vec::new(),
}
}
pub fn legacy(main_descriptor: MultipathDescriptor) -> Self {
Self {
name: DEFAULT_WALLET_NAME.to_string(),
main_descriptor,
keys_aliases: HashMap::new(),
hardware_wallets: Vec::new(),
}
}
pub fn with_key_aliases(mut self, aliases: HashMap<Fingerprint, String>) -> Self {
self.keys_aliases = aliases;
self
}
pub fn with_harware_wallets(mut self, hardware_wallets: Vec<HardwareWalletConfig>) -> Self {
self.hardware_wallets = hardware_wallets;
self
}
}

View File

@ -2,7 +2,7 @@ use std::convert::TryFrom;
use liana::config::Config as LianaConfig;
use super::step::Context;
use super::Context;
pub const DEFAULT_FILE_NAME: &str = "daemon.toml";

View File

@ -0,0 +1,87 @@
use std::path::PathBuf;
use std::time::Duration;
use crate::{
app::{
settings::{KeySetting, Settings, WalletSetting},
wallet::DEFAULT_WALLET_NAME,
},
hw::HardwareWalletConfig,
};
use async_hwi::DeviceKind;
use liana::{
config::Config,
config::{BitcoinConfig, BitcoindConfig},
descriptors::MultipathDescriptor,
miniscript::bitcoin,
};
#[derive(Clone)]
pub struct Context {
pub bitcoin_config: BitcoinConfig,
pub bitcoind_config: Option<BitcoindConfig>,
pub descriptor: Option<MultipathDescriptor>,
pub keys: Vec<KeySetting>,
pub hws: Vec<(
DeviceKind,
bitcoin::util::bip32::Fingerprint,
Option<[u8; 32]>,
)>,
pub data_dir: PathBuf,
}
impl Context {
pub fn new(network: bitcoin::Network, data_dir: PathBuf) -> Self {
Self {
bitcoin_config: BitcoinConfig {
network,
poll_interval_secs: Duration::from_secs(30),
},
hws: Vec::new(),
keys: Vec::new(),
bitcoind_config: None,
descriptor: None,
data_dir,
}
}
pub fn extract_gui_settings(&self) -> Settings {
let hardware_wallets = self
.hws
.iter()
.filter_map(|(kind, fingerprint, token)| {
token
.as_ref()
.map(|token| HardwareWalletConfig::new(kind, fingerprint, token))
})
.collect();
Settings {
wallets: vec![WalletSetting {
name: DEFAULT_WALLET_NAME.to_string(),
descriptor_checksum: self
.descriptor
.as_ref()
.unwrap()
.to_string()
.split_once('#')
.map(|(_, checksum)| checksum)
.unwrap()
.to_string(),
keys: self.keys.clone(),
hardware_wallets,
}],
}
}
pub fn extract_daemon_config(&self) -> Config {
Config {
#[cfg(unix)]
daemon: false,
log_level: log::LevelFilter::Info,
main_descriptor: self.descriptor.clone().unwrap(),
data_dir: Some(self.data_dir.clone()),
bitcoin_config: self.bitcoin_config.clone(),
bitcoind_config: self.bitcoind_config.clone(),
}
}
}

View File

@ -44,6 +44,7 @@ pub enum DefineDescriptor {
Key(bool, usize, DefineKey),
HWXpubImported(Result<DescriptorPublicKey, Error>),
XPubEdited(String),
EditName,
NameEdited(String),
SequenceEdited(String),
ThresholdEdited(bool, usize),

View File

@ -1,4 +1,4 @@
mod config;
mod context;
mod message;
mod prompt;
mod step;
@ -7,18 +7,16 @@ mod view;
use iced::{clipboard, Command, Element, Subscription};
use liana::miniscript::bitcoin;
use std::convert::TryInto;
use context::Context;
use std::io::Write;
use std::path::PathBuf;
use crate::{
app::config as gui_config, hw::HardwareWalletConfig, installer::config::DEFAULT_FILE_NAME,
};
use crate::app::{config as gui_config, settings as gui_settings};
pub use message::Message;
use step::{
BackupDescriptor, Context, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor,
ParticipateXpub, RegisterDescriptor, Step, Welcome,
BackupDescriptor, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor, ParticipateXpub,
RegisterDescriptor, Step, Welcome,
};
pub struct Installer {
@ -164,19 +162,7 @@ impl Installer {
}
pub async fn install(ctx: Context) -> Result<PathBuf, Error> {
let hardware_wallets = ctx
.hws
.iter()
.filter_map(|(kind, fingerprint, token)| {
token
.as_ref()
.map(|token| HardwareWalletConfig::new(kind, fingerprint, token))
})
.collect();
let mut cfg: liana::config::Config = ctx
.try_into()
.expect("Everything should be checked at this point");
let mut cfg: liana::config::Config = ctx.extract_daemon_config();
// Start Daemon to check correctness of installation
let daemon = liana::DaemonHandle::start_default(cfg.clone()).map_err(|e| {
Error::Unexpected(format!("Failed to start daemon with entered config: {}", e))
@ -191,42 +177,55 @@ pub async fn install(ctx: Context) -> Result<PathBuf, Error> {
let mut datadir_path = cfg.data_dir.clone().unwrap();
datadir_path.push(cfg.bitcoin_config.network.to_string());
// create lianad configuration file
let mut daemon_config_path = datadir_path.clone();
daemon_config_path.push(DEFAULT_FILE_NAME);
let mut daemon_config_file = std::fs::File::create(&daemon_config_path)
.map_err(|e| Error::CannotCreateFile(e.to_string()))?;
// Step needed because of ValueAfterTable error in the toml serialize implementation.
let daemon_config =
toml::Value::try_from(&cfg).expect("daemon::Config has a proper Serialize implementation");
daemon_config_file
.write_all(daemon_config.to_string().as_bytes())
.map_err(|e| Error::CannotWriteToFile(e.to_string()))?;
// create lianad configuration file
let daemon_config_path = create_and_write_file(
datadir_path.clone(),
"daemon.toml",
daemon_config.to_string().as_bytes(),
)?;
// create liana GUI configuration file
let mut gui_config_path = datadir_path;
gui_config_path.push(gui_config::DEFAULT_FILE_NAME);
let mut gui_config_file = std::fs::File::create(&gui_config_path)
.map_err(|e| Error::CannotCreateFile(e.to_string()))?;
let gui_config_path = create_and_write_file(
datadir_path.clone(),
gui_config::DEFAULT_FILE_NAME,
toml::to_string(&gui_config::Config::new(
daemon_config_path.canonicalize().map_err(|e| {
Error::Unexpected(format!("Failed to canonicalize daemon config path: {}", e))
})?,
))
.unwrap()
.as_bytes(),
)?;
gui_config_file
.write_all(
toml::to_string(&gui_config::Config::new(
daemon_config_path.canonicalize().map_err(|e| {
Error::Unexpected(format!("Failed to canonicalize daemon config path: {}", e))
})?,
hardware_wallets,
))
.unwrap()
.as_bytes(),
)
.map_err(|e| Error::CannotWriteToFile(e.to_string()))?;
// create liana GUI settings file
let settings: gui_settings::Settings = ctx.extract_gui_settings();
create_and_write_file(
datadir_path,
gui_settings::DEFAULT_FILE_NAME,
serde_json::to_string_pretty(&settings).unwrap().as_bytes(),
)?;
Ok(gui_config_path)
}
pub fn create_and_write_file(
mut network_datadir: PathBuf,
file_name: &str,
data: &[u8],
) -> Result<PathBuf, Error> {
network_datadir.push(file_name);
let path = network_datadir;
let mut file =
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)
}
#[derive(Debug, Clone)]
pub enum Error {
CannotCreateDatadir(String),

View File

@ -4,3 +4,5 @@ pub const DEFINE_DESCRIPTOR_PRIMATRY_PATH_TOOLTIP: &str =
"This is the keys that can spend received coins immediately,\n with no time restriction.";
pub const DEFINE_DESCRIPTOR_SEQUENCE_TOOLTIP: &str =
"Number of blocks after a coin is received \nfor which the recovery path is not available";
pub const DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP: &str =
"The alias is applied on all the keys derived from the same seed";

View File

@ -1,4 +1,4 @@
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::str::FromStr;
@ -7,7 +7,7 @@ use liana::{
descriptors::{LianaDescKeys, MultipathDescriptor},
miniscript::{
bitcoin::{
util::bip32::{DerivationPath, Fingerprint},
util::bip32::{ChildNumber, DerivationPath, Fingerprint},
Network,
},
descriptor::{DerivPaths, DescriptorMultiXKey, DescriptorPublicKey, Wildcard},
@ -15,6 +15,7 @@ use liana::{
};
use crate::{
app::settings::KeySetting,
hw::{list_hardware_wallets, HardwareWallet},
installer::{
message::{self, Message},
@ -24,9 +25,6 @@ use crate::{
ui::component::{form, modal::Modal},
};
const LIANA_STANDARD_PATH: &str = "m/48'/0'/0'/2'";
const LIANA_TESTNET_STANDARD_PATH: &str = "m/48'/1'/0'/2'";
pub trait DescriptorKeyModal {
fn processing(&self) -> bool {
false
@ -48,8 +46,6 @@ pub struct DefineDescriptor {
sequence: form::Value<String>,
modal: Option<Box<dyn DescriptorKeyModal>>,
name_indexes: (usize, usize),
error: Option<String>,
}
@ -59,11 +55,10 @@ impl DefineDescriptor {
network: Network::Bitcoin,
data_dir: None,
network_valid: true,
spending_keys: vec![DescriptorKey::new("Key 1".to_string())],
spending_keys: vec![DescriptorKey::default()],
spending_threshold: 1,
recovery_keys: vec![DescriptorKey::new("Recovery key 1".to_string())],
recovery_keys: vec![DescriptorKey::default()],
recovery_threshold: 1,
name_indexes: (1, 1),
sequence: form::Value::default(),
modal: None,
error: None,
@ -79,18 +74,22 @@ impl DefineDescriptor {
}
// TODO: Improve algo
// Mark as duplicate every defined key that have the same name but not the same fingerprint.
// And every undefined_key that have a same name than an other key.
fn check_for_duplicate(&mut self) {
let mut all_keys = HashSet::new();
let mut duplicate_keys = HashSet::new();
let mut all_names = HashSet::new();
let mut all_names: HashMap<String, Fingerprint> = HashMap::new();
let mut duplicate_names = HashSet::new();
for spending_key in &self.spending_keys {
if all_names.contains(&spending_key.name) {
duplicate_names.insert(spending_key.name.clone());
} else {
all_names.insert(spending_key.name.clone());
}
if let Some(key) = &spending_key.key {
if let Some(fg) = all_names.get(&spending_key.name) {
if fg != &key.master_fingerprint() {
duplicate_names.insert(spending_key.name.clone());
}
} else {
all_names.insert(spending_key.name.clone(), key.master_fingerprint());
}
if all_keys.contains(key) {
duplicate_keys.insert(key.clone());
} else {
@ -99,12 +98,14 @@ impl DefineDescriptor {
}
}
for recovery_key in &self.recovery_keys {
if all_names.contains(&recovery_key.name) {
duplicate_names.insert(recovery_key.name.clone());
} else {
all_names.insert(recovery_key.name.clone());
}
if let Some(key) = &recovery_key.key {
if let Some(fg) = all_names.get(&recovery_key.name) {
if fg != &key.master_fingerprint() {
duplicate_names.insert(recovery_key.name.clone());
}
} else {
all_names.insert(recovery_key.name.clone(), key.master_fingerprint());
}
if all_keys.contains(key) {
duplicate_keys.insert(key.clone());
} else {
@ -124,6 +125,69 @@ impl DefineDescriptor {
}
}
}
fn edit_alias_for_key_with_same_fingerprint(&mut self, name: String, fingerprint: Fingerprint) {
for spending_key in &mut self.spending_keys {
if spending_key.key.as_ref().map(|k| k.master_fingerprint()) == Some(fingerprint) {
spending_key.name = name.clone();
}
}
for recovery_key in &mut self.recovery_keys {
if recovery_key.key.as_ref().map(|k| k.master_fingerprint()) == Some(fingerprint) {
recovery_key.name = name.clone();
}
}
}
/// Returns the maximum account index per key fingerprint
fn fingerprint_account_index_mappping(&self) -> HashMap<Fingerprint, ChildNumber> {
let mut mapping = HashMap::new();
let update_mapping =
|keys: &[DescriptorKey], mapping: &mut HashMap<Fingerprint, ChildNumber>| {
for key in keys {
if let Some(DescriptorPublicKey::MultiXPub(key)) = key.key.as_ref() {
if let Some((fingerprint, derivation_path)) = key.origin.as_ref() {
let index = if derivation_path.len() >= 4 {
if derivation_path[0].to_string() == "48'" {
Some(derivation_path[2])
} else {
None
}
} else {
None
};
if let Some(index) = index {
if let Some(previous_index) = mapping.get(fingerprint) {
if index > *previous_index {
mapping.insert(*fingerprint, index);
}
} else {
mapping.insert(*fingerprint, index);
}
}
}
}
}
};
update_mapping(&self.spending_keys, &mut mapping);
update_mapping(&self.recovery_keys, &mut mapping);
mapping
}
fn keys_aliases(&self) -> HashMap<Fingerprint, String> {
let mut map = HashMap::new();
for spending_key in &self.spending_keys {
if let Some(key) = spending_key.key.as_ref() {
map.insert(key.master_fingerprint(), spending_key.name.clone());
}
}
for recovery_key in &self.recovery_keys {
if let Some(key) = recovery_key.key.as_ref() {
map.insert(key.master_fingerprint(), recovery_key.name.clone());
}
}
map
}
}
impl Step for DefineDescriptor {
@ -164,16 +228,10 @@ impl Step for DefineDescriptor {
}
message::DefineDescriptor::AddKey(is_recovery) => {
if is_recovery {
self.name_indexes.0 += 1;
self.recovery_keys.push(DescriptorKey::new(format!(
"Recovery key {}",
self.name_indexes.0,
)));
self.recovery_keys.push(DescriptorKey::default());
self.recovery_threshold += 1;
} else {
self.name_indexes.1 += 1;
self.spending_keys
.push(DescriptorKey::new(format!("Key {}", self.name_indexes.1,)));
self.spending_keys.push(DescriptorKey::default());
self.spending_threshold += 1;
}
}
@ -182,6 +240,10 @@ impl Step for DefineDescriptor {
return Command::perform(async move { key }, Message::Clibpboard);
}
message::DefineKey::Edited(name, imported_key) => {
self.edit_alias_for_key_with_same_fingerprint(
name.clone(),
imported_key.master_fingerprint(),
);
if is_recovery {
if let Some(recovery_key) = self.recovery_keys.get_mut(i) {
recovery_key.name = name;
@ -207,8 +269,15 @@ impl Step for DefineDescriptor {
k.to_string().trim_end_matches("/<0;1>/*").to_string()
})
.unwrap_or_else(|| "".to_string());
let modal =
EditXpubModal::new(name, key, i, is_recovery, self.network);
let modal = EditXpubModal::new(
name,
key,
i,
is_recovery,
self.network,
self.fingerprint_account_index_mappping(),
self.keys_aliases(),
);
let cmd = modal.load();
self.modal = Some(Box::new(modal));
return cmd;
@ -220,8 +289,15 @@ impl Step for DefineDescriptor {
.as_ref()
.map(|k| k.to_string().trim_end_matches("/<0;1>/*").to_string())
.unwrap_or_else(|| "".to_string());
let modal =
EditXpubModal::new(name, key, i, is_recovery, self.network);
let modal = EditXpubModal::new(
name,
key,
i,
is_recovery,
self.network,
self.fingerprint_account_index_mappping(),
self.keys_aliases(),
);
let cmd = modal.load();
self.modal = Some(Box::new(modal));
return cmd;
@ -268,17 +344,36 @@ impl Step for DefineDescriptor {
fn apply(&mut self, ctx: &mut Context) -> bool {
ctx.bitcoin_config.network = self.network;
let spending_keys: Vec<DescriptorPublicKey> = self
.spending_keys
.iter()
.filter_map(|k| k.key.clone())
.collect();
ctx.keys = Vec::new();
let mut spending_keys: Vec<DescriptorPublicKey> = Vec::new();
for spending_key in self.spending_keys.iter().clone() {
if let Some(key) = spending_key.key.as_ref() {
if let DescriptorPublicKey::MultiXPub(xpub) = key {
if let Some((master_fingerprint, _)) = xpub.origin {
ctx.keys.push(KeySetting {
master_fingerprint,
name: spending_key.name.clone(),
});
}
}
spending_keys.push(key.clone());
}
}
let recovery_keys: Vec<DescriptorPublicKey> = self
.recovery_keys
.iter()
.filter_map(|k| k.key.clone())
.collect();
let mut recovery_keys: Vec<DescriptorPublicKey> = Vec::new();
for recovery_key in self.recovery_keys.iter().clone() {
if let Some(key) = recovery_key.key.as_ref() {
if let DescriptorPublicKey::MultiXPub(xpub) = key {
if let Some((master_fingerprint, _)) = xpub.origin {
ctx.keys.push(KeySetting {
master_fingerprint,
name: recovery_key.name.clone(),
});
}
}
recovery_keys.push(key.clone());
}
}
let sequence = self.sequence.value.parse::<u16>();
self.sequence.valid = sequence.is_ok();
@ -378,17 +473,19 @@ pub struct DescriptorKey {
pub duplicate_name: bool,
}
impl DescriptorKey {
pub fn new(name: String) -> Self {
impl Default for DescriptorKey {
fn default() -> Self {
Self {
name,
name: "".to_string(),
valid: true,
key: None,
duplicate_key: false,
duplicate_name: false,
}
}
}
impl DescriptorKey {
pub fn check_network(&mut self, network: Network) {
if let Some(key) = &self.key {
self.valid = check_key_network(key, network);
@ -397,7 +494,7 @@ impl DescriptorKey {
pub fn view(&self) -> Element<message::DefineKey> {
match &self.key {
None => view::undefined_descriptor_key(&self.name),
None => view::undefined_descriptor_key(),
Some(_) => view::defined_descriptor_key(
&self.name,
self.valid,
@ -447,8 +544,12 @@ pub struct EditXpubModal {
error: Option<Error>,
processing: bool,
keys_aliases: HashMap<Fingerprint, String>,
account_indexes: HashMap<Fingerprint, ChildNumber>,
form_name: form::Value<String>,
form_xpub: form::Value<String>,
edit_name: bool,
chosen_hw: Option<usize>,
hws: Vec<HardwareWallet>,
@ -461,6 +562,8 @@ impl EditXpubModal {
key_index: usize,
is_recovery: bool,
network: Network,
account_indexes: HashMap<Fingerprint, ChildNumber>,
keys_aliases: HashMap<Fingerprint, String>,
) -> Self {
Self {
form_name: form::Value {
@ -471,6 +574,8 @@ impl EditXpubModal {
valid: true,
value: key,
},
keys_aliases,
account_indexes,
is_recovery,
key_index,
chosen_hw: None,
@ -478,6 +583,7 @@ impl EditXpubModal {
hws: Vec::new(),
error: None,
network,
edit_name: false,
}
}
fn load(&self) -> Command<Message> {
@ -500,8 +606,14 @@ impl DescriptorKeyModal for EditXpubModal {
let device = hw.device.clone();
self.chosen_hw = Some(i);
self.processing = true;
// If another account n exists, the key is retrieved for the account n+1
let account_index = self
.account_indexes
.get(&hw.fingerprint)
.map(|account_index| account_index.increment().unwrap())
.unwrap_or_else(|| ChildNumber::from_hardened_idx(0).unwrap());
return Command::perform(
get_extended_pubkey(device, hw.fingerprint, self.network),
get_extended_pubkey(device, hw.fingerprint, self.network, account_index),
|res| {
Message::DefineDescriptor(message::DefineDescriptor::HWXpubImported(
res,
@ -520,6 +632,14 @@ impl DescriptorKeyModal for EditXpubModal {
self.processing = false;
match res {
Ok(key) => {
if let Some(alias) = self.keys_aliases.get(&key.master_fingerprint()) {
self.form_name.valid = true;
self.form_name.value = alias.clone();
self.edit_name = false;
} else {
self.edit_name = true;
}
self.form_xpub.valid = true;
self.form_xpub.value =
key.to_string().trim_end_matches("/<0;1>/*").to_string();
}
@ -528,13 +648,32 @@ impl DescriptorKeyModal for EditXpubModal {
}
}
}
Message::DefineDescriptor(message::DefineDescriptor::EditName) => {
self.edit_name = true;
}
Message::DefineDescriptor(message::DefineDescriptor::NameEdited(name)) => {
self.form_name.valid = true;
self.form_name.value = name;
}
Message::DefineDescriptor(message::DefineDescriptor::XPubEdited(s)) => {
self.form_xpub.valid =
DescriptorPublicKey::from_str(&format!("{}/<0;1>/*", s)).is_ok();
if let Ok(DescriptorPublicKey::MultiXPub(key)) =
DescriptorPublicKey::from_str(&format!("{}/<0;1>/*", s))
{
if let Some((fingerprint, _)) = key.origin {
self.form_xpub.valid = true;
if let Some(alias) = self.keys_aliases.get(&fingerprint) {
self.form_name.valid = true;
self.form_name.value = alias.clone();
self.edit_name = false;
} else {
self.edit_name = true;
}
} else {
self.form_xpub.valid = false;
}
} else {
self.form_xpub.valid = false;
}
self.form_xpub.value = s;
}
Message::DefineDescriptor(message::DefineDescriptor::ConfirmXpub) => {
@ -570,19 +709,25 @@ impl DescriptorKeyModal for EditXpubModal {
self.chosen_hw,
&self.form_xpub,
&self.form_name,
self.edit_name,
)
}
}
/// LIANA_STANDARD_PATH: m/48'/0'/0'/2';
/// LIANA_TESTNET_STANDARD_PATH: m/48'/1'/0'/2';
async fn get_extended_pubkey(
hw: std::sync::Arc<dyn async_hwi::HWI + Send + Sync>,
fingerprint: Fingerprint,
network: Network,
account_index: ChildNumber,
) -> Result<DescriptorPublicKey, Error> {
let derivation_path = DerivationPath::from_str(if network == Network::Bitcoin {
LIANA_STANDARD_PATH
} else {
LIANA_TESTNET_STANDARD_PATH
let derivation_path = DerivationPath::from_str(&{
if network == Network::Bitcoin {
format!("m/48'/0'/{}/2'", account_index)
} else {
format!("m/48'/1'/{}/2'", account_index)
}
})
.unwrap();
let xkey = hw
@ -667,7 +812,12 @@ impl Step for ParticipateXpub {
self.processing = true;
self.error = None;
return Command::perform(
get_extended_pubkey(device, hw.fingerprint, self.network),
get_extended_pubkey(
device,
hw.fingerprint,
self.network,
ChildNumber::from_hardened_idx(0).unwrap(),
),
Message::ImportXpub,
);
}

View File

@ -5,19 +5,14 @@ pub use descriptor::{
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;
use async_hwi::DeviceKind;
use iced::{Command, Element};
use liana::{
config::{BitcoinConfig, BitcoindConfig},
descriptors::MultipathDescriptor,
miniscript::bitcoin,
};
use liana::{config::BitcoindConfig, miniscript::bitcoin};
use crate::ui::component::form;
use crate::installer::{
context::Context,
message::{self, Message},
view,
};
@ -39,34 +34,6 @@ pub trait Step {
}
}
#[derive(Clone)]
pub struct Context {
pub bitcoin_config: BitcoinConfig,
pub bitcoind_config: Option<BitcoindConfig>,
pub descriptor: Option<MultipathDescriptor>,
pub hws: Vec<(
DeviceKind,
bitcoin::util::bip32::Fingerprint,
Option<[u8; 32]>,
)>,
pub data_dir: PathBuf,
}
impl Context {
pub fn new(network: bitcoin::Network, data_dir: PathBuf) -> Self {
Self {
bitcoin_config: BitcoinConfig {
network,
poll_interval_secs: Duration::from_secs(30),
},
hws: Vec::new(),
bitcoind_config: None,
descriptor: None,
data_dir,
}
}
}
#[derive(Default)]
pub struct Welcome {}

View File

@ -8,9 +8,9 @@ use liana::miniscript::bitcoin;
use crate::{
hw::HardwareWallet,
installer::{
context::Context,
message::{self, Message},
step::Context,
Error,
prompt, Error,
},
ui::{
color,
@ -171,9 +171,7 @@ pub fn define_descriptor<'a>(
.spacing(10)
.push(Space::with_width(Length::Units(40)))
.push(text("Primary path:").bold())
.push(tooltip(
super::prompt::DEFINE_DESCRIPTOR_PRIMATRY_PATH_TOOLTIP,
)),
.push(tooltip(prompt::DEFINE_DESCRIPTOR_PRIMATRY_PATH_TOOLTIP)),
)
.push(separation().width(Length::Fill))
.push(
@ -294,7 +292,7 @@ pub fn define_descriptor<'a>(
Row::new()
.spacing(10)
.push(text("Blocks before recovery:").bold())
.push(tooltip(super::prompt::DEFINE_DESCRIPTOR_SEQUENCE_TOOLTIP)),
.push(tooltip(prompt::DEFINE_DESCRIPTOR_SEQUENCE_TOOLTIP)),
)
.push(
Container::new(
@ -319,7 +317,7 @@ pub fn define_descriptor<'a>(
layout(
progress,
Column::new()
.push(Space::with_height(Length::Units(50)))
.push(Space::with_height(Length::Units(30)))
.push(text("Create the wallet").bold().size(50))
.push(
Column::new()
@ -620,7 +618,7 @@ pub fn backup_descriptor<'a>(
)
.push(
Column::new()
.push(text(super::prompt::BACKUP_DESCRIPTOR_MESSAGE))
.push(text(prompt::BACKUP_DESCRIPTOR_MESSAGE))
.push(collapse::Collapse::new(
|| {
Button::new(
@ -680,7 +678,7 @@ pub fn backup_descriptor<'a>(
}
pub fn help_backup<'a>() -> Element<'a, Message> {
text(super::prompt::BACKUP_DESCRIPTOR_HELP).small().into()
text(prompt::BACKUP_DESCRIPTOR_HELP).small().into()
}
pub fn define_bitcoin<'a>(
@ -854,7 +852,7 @@ pub fn install<'a>(
layout(progress, col)
}
pub fn undefined_descriptor_key(name: &str) -> Element<message::DefineKey> {
pub fn undefined_descriptor_key<'a>() -> Element<'a, message::DefineKey> {
card::simple(
Column::new()
.width(Length::Fill)
@ -875,8 +873,13 @@ pub fn undefined_descriptor_key(name: &str) -> Element<message::DefineKey> {
.spacing(15)
.align_items(Alignment::Center)
.push(
Scrollable::new(text(name).bold())
.horizontal_scroll(Properties::new().width(2).scroller_width(2)),
Scrollable::new(
icon::key_icon()
.style(color::DARK_GREY)
.size(50)
.width(Length::Units(50)),
)
.horizontal_scroll(Properties::new().width(2).scroller_width(2)),
)
.push(icon::circle_check_icon().style(color::FOREGROUND).size(50)),
)
@ -884,8 +887,7 @@ pub fn undefined_descriptor_key(name: &str) -> Element<message::DefineKey> {
.align_y(alignment::Vertical::Center),
)
.push(
button::border(Some(icon::pencil_icon()), "Edit")
.on_press(message::DefineKey::Edit),
button::border(Some(icon::pencil_icon()), "Set").on_press(message::DefineKey::Edit),
)
.push(Space::with_height(Length::Units(5))),
)
@ -967,7 +969,7 @@ pub fn defined_descriptor_key(
.height(Length::Units(200))
.width(Length::Units(200)),
)
.push(text("Key is a duplicate").small().style(color::ALERT))
.push(text("Duplicate key").small().style(color::ALERT))
.into()
} else if duplicate_name {
Column::new()
@ -978,7 +980,7 @@ pub fn defined_descriptor_key(
.height(Length::Units(200))
.width(Length::Units(200)),
)
.push(text("Name is a duplicate").small().style(color::ALERT))
.push(text("Duplicate name").small().style(color::ALERT))
.into()
} else {
card::simple(col)
@ -989,6 +991,7 @@ pub fn defined_descriptor_key(
}
}
#[allow(clippy::too_many_arguments)]
pub fn edit_key_modal<'a>(
network: bitcoin::Network,
hws: &[HardwareWallet],
@ -996,62 +999,14 @@ pub fn edit_key_modal<'a>(
processing: bool,
chosen_hw: Option<usize>,
form_xpub: &form::Value<String>,
form_name: &form::Value<String>,
form_name: &'a form::Value<String>,
edit_name: bool,
) -> Element<'a, Message> {
Column::new()
.push_maybe(error.map(|e| card::error("Failed to import xpub", e.to_string())))
.push(card::simple(
Column::new()
.spacing(25)
.push(
Container::new(
Row::new()
.spacing(5)
.push(icon::pencil_icon())
.push(text("Edit")),
)
.width(Length::Fill)
.align_x(alignment::Horizontal::Center),
)
.push(
Column::new()
.spacing(5)
.push(text("Edit name:").bold())
.push(
form::Form::new("Name", form_name, |msg| {
Message::DefineDescriptor(message::DefineDescriptor::NameEdited(
msg,
))
})
.warning("Please enter correct name")
.size(20)
.padding(10),
),
)
.push(
Column::new()
.spacing(5)
.push(text("Enter an extended public key:").bold())
.push(
Row::new()
.push(
form::Form::new("Extended public key", form_xpub, |msg| {
Message::DefineDescriptor(
message::DefineDescriptor::XPubEdited(msg),
)
})
.warning(if network == bitcoin::Network::Bitcoin {
"Please enter correct xpub"
} else {
"Please enter correct tpub"
})
.size(20)
.padding(10),
)
.spacing(10)
.push(Container::new(text("/<0;1>/*")).padding(5)),
),
)
.push(if !hws.is_empty() {
Column::new()
.push(
@ -1059,7 +1014,7 @@ pub fn edit_key_modal<'a>(
.spacing(10)
.align_items(Alignment::Center)
.push(
Container::new(text("Or select a hardware wallet:").bold())
Container::new(text("Select a hardware wallet:").bold())
.width(Length::Fill),
)
.push(
@ -1100,6 +1055,75 @@ pub fn edit_key_modal<'a>(
)
.width(Length::Fill)
})
.push(
Column::new()
.spacing(5)
.push(text("Or enter an extended public key:").bold())
.push(
Row::new()
.push(
form::Form::new("Extended public key", form_xpub, |msg| {
Message::DefineDescriptor(
message::DefineDescriptor::XPubEdited(msg),
)
})
.warning(if network == bitcoin::Network::Bitcoin {
"Please enter correct xpub with origin"
} else {
"Please enter correct tpub with origin"
})
.size(20)
.padding(10),
)
.spacing(10)
.push(Container::new(text("/<0;1>/*")).padding(5)),
),
)
.push(
if !edit_name && !form_xpub.value.is_empty() && form_xpub.valid {
Column::new().push(
Row::new()
.push(
Column::new()
.spacing(5)
.width(Length::Fill)
.push(
Row::new()
.spacing(5)
.push(text("Fingerprint alias:").bold())
.push(tooltip(
prompt::DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP,
)),
)
.push(text(&form_name.value)),
)
.push(button::border(Some(icon::pencil_icon()), "Edit").on_press(
Message::DefineDescriptor(message::DefineDescriptor::EditName),
)),
)
} else if !form_xpub.value.is_empty() && form_xpub.valid {
Column::new()
.spacing(5)
.push(
Row::new()
.spacing(5)
.push(text("Fingerprint alias:").bold())
.push(tooltip(prompt::DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP)),
)
.push(
form::Form::new("Alias", form_name, |msg| {
Message::DefineDescriptor(
message::DefineDescriptor::NameEdited(msg),
)
})
.warning("Please enter correct alias")
.size(20)
.padding(10),
)
} else {
Column::new()
},
)
.push(
if form_xpub.valid && !form_xpub.value.is_empty() && !form_name.value.is_empty()
{

View File

@ -17,7 +17,12 @@ use liana::{
};
use crate::{
app::config::{default_datadir, Config as GUIConfig},
app::{
cache::Cache,
config::{default_datadir, Config as GUIConfig},
settings::{self, Settings},
wallet::Wallet,
},
daemon::{client, embedded::EmbeddedDaemon, model::*, Daemon, DaemonError},
ui::{
component::{button, notification, text::*},
@ -30,6 +35,7 @@ type Lianad = client::Lianad<client::jsonrpc::JsonRPCClient>;
pub struct Loader {
pub datadir_path: Option<PathBuf>,
pub network: bitcoin::Network,
pub gui_config: GUIConfig,
pub daemon_started: bool,
@ -47,16 +53,12 @@ pub enum Step {
Error(Box<Error>),
}
#[allow(clippy::type_complexity)]
#[derive(Debug)]
pub enum Message {
View(ViewMessage),
Syncing(Result<GetInfoResult, DaemonError>),
Synced(
GetInfoResult,
Vec<Coin>,
Vec<SpendTx>,
Arc<dyn Daemon + Sync + Send>,
),
Synced(Result<(Arc<Wallet>, Cache, Arc<dyn Daemon + Sync + Send>), Error>),
Started(Result<Arc<dyn Daemon + Sync + Send>, Error>),
Loaded(Result<Arc<dyn Daemon + Sync + Send>, Error>),
Failure(DaemonError),
@ -75,6 +77,7 @@ impl Loader {
.unwrap();
(
Loader {
network: daemon_config.bitcoin_config.network,
datadir_path,
daemon_config: daemon_config.clone(),
gui_config,
@ -141,18 +144,47 @@ impl Loader {
Ok(info) => {
if (info.sync - 1.0_f64).abs() < f64::EPSILON {
let daemon = daemon.clone();
let settings_path =
settings_path(&self.datadir_path, self.network).unwrap();
let gui_config_hws = self
.gui_config
.hardware_wallets
.as_ref()
.cloned()
.unwrap_or_default();
return Command::perform(
async move {
let coins = daemon
.list_coins()
.map(|res| res.coins)
.unwrap_or_else(|_| Vec::new());
let spend_txs = daemon
.list_spend_transactions()
.unwrap_or_else(|_| Vec::new());
(info, coins, spend_txs, daemon)
let coins = daemon.list_coins().map(|res| res.coins)?;
let spend_txs = daemon.list_spend_transactions()?;
let cache = Cache {
network: info.network,
blockheight: info.block_height,
coins,
spend_txs,
..Default::default()
};
let wallet = match Settings::from_file(&settings_path) {
Ok(settings) => {
if let Some(wallet_setting) = settings.wallets.first() {
Wallet::legacy(info.descriptors.main)
.with_harware_wallets(
wallet_setting.hardware_wallets.clone(),
)
.with_key_aliases(wallet_setting.keys_aliases())
} else {
Wallet::legacy(info.descriptors.main)
.with_harware_wallets(gui_config_hws)
}
}
Err(settings::SettingsError::NotFound) => {
Wallet::legacy(info.descriptors.main)
.with_harware_wallets(gui_config_hws)
}
Err(e) => return Err(e.into()),
};
Ok((Arc::new(wallet), cache, daemon))
},
|res| Message::Synced(res.0, res.1, res.2, res.3),
Message::Synced,
);
} else {
*progress = info.sync
@ -333,6 +365,7 @@ async fn sync(
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum Error {
Settings(settings::SettingsError),
Config(ConfigError),
Daemon(DaemonError),
}
@ -340,12 +373,19 @@ pub enum Error {
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Settings(e) => write!(f, "Settings error: {}", e),
Self::Config(e) => write!(f, "Config error: {}", e),
Self::Daemon(e) => write!(f, "Liana daemon error: {}", e),
}
}
}
impl From<settings::SettingsError> for Error {
fn from(error: settings::SettingsError) -> Self {
Error::Settings(error)
}
}
impl From<ConfigError> for Error {
fn from(error: ConfigError) -> Self {
Error::Config(error)
@ -372,3 +412,18 @@ fn socket_path(
path.push("lianad_rpc");
Ok(path)
}
/// default liana settings path is .liana/bitcoin/settings.json
fn settings_path(
datadir: &Option<PathBuf>,
network: bitcoin::Network,
) -> Result<PathBuf, ConfigError> {
let mut path = if let Some(ref datadir) = datadir {
datadir.clone()
} else {
default_datadir().map_err(|_| ConfigError::DatadirNotFound)?
};
path.push(network.to_string());
path.push(settings::DEFAULT_FILE_NAME);
Ok(path)
}

View File

@ -11,9 +11,7 @@ use liana::{config::Config as DaemonConfig, miniscript::bitcoin};
use liana_gui::{
app::{
self,
cache::Cache,
config::{default_datadir, ConfigError},
wallet::Wallet,
App,
},
installer::{self, Installer},
@ -205,17 +203,7 @@ impl Application for GUI {
)));
Command::none()
}
loader::Message::Synced(info, coins, spend_txs, daemon) => {
let cache = Cache {
network: info.network,
blockheight: info.block_height,
coins,
spend_txs,
..Default::default()
};
let wallet = Wallet::new(info.descriptors.main);
loader::Message::Synced(Ok((wallet, cache, daemon))) => {
let (app, command) = App::new(cache, wallet, loader.gui_config.clone(), daemon);
self.state = State::App(app);
command.map(|msg| Message::Run(Box::new(msg)))