gui: separate settings panels, add wallet settings

This commit is contained in:
edouard 2023-03-15 18:29:01 +01:00
parent 2c7cd2b0ca
commit ae8df0dd4c
20 changed files with 982 additions and 239 deletions

View File

@ -1,21 +1,27 @@
use crate::daemon::DaemonError;
use liana::config::ConfigError;
use std::convert::From;
use std::io::ErrorKind;
use liana::config::ConfigError;
use crate::{
app::{settings::SettingsError, wallet::WalletError},
daemon::DaemonError,
};
#[derive(Debug)]
pub enum Error {
Config(String),
Wallet(WalletError),
Daemon(DaemonError),
Unexpected(String),
HardwareWallet(async_hwi::Error),
HotSigner(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Config(e) => write!(f, "{}", e),
Self::Wallet(e) => write!(f, "{}", e),
Self::Daemon(e) => match e {
DaemonError::Unexpected(e) => write!(f, "{}", e),
DaemonError::NoAnswer => write!(f, "Daemon did not answer"),
@ -41,7 +47,6 @@ impl std::fmt::Display for Error {
},
Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
Self::HardwareWallet(e) => write!(f, "{}", e),
Self::HotSigner(e) => write!(f, "{}", e),
}
}
}
@ -52,6 +57,18 @@ impl From<ConfigError> for Error {
}
}
impl From<WalletError> for Error {
fn from(error: WalletError) -> Self {
Error::Wallet(error)
}
}
impl From<SettingsError> for Error {
fn from(error: SettingsError) -> Self {
Error::Wallet(WalletError::Settings(error))
}
}
impl From<DaemonError> for Error {
fn from(error: DaemonError) -> Self {
Error::Daemon(error)

View File

@ -1,3 +1,5 @@
use std::sync::Arc;
use liana::{
config::Config as DaemonConfig,
miniscript::bitcoin::{
@ -7,7 +9,7 @@ use liana::{
};
use crate::{
app::{error::Error, view},
app::{error::Error, view, wallet::Wallet},
daemon::model::*,
hw::HardwareWallet,
};
@ -18,6 +20,8 @@ pub enum Message {
View(view::Message),
LoadDaemonConfig(Box<DaemonConfig>),
DaemonConfigLoaded(Result<(), Error>),
LoadWallet,
WalletLoaded(Result<Arc<Wallet>, Error>),
Info(Result<GetInfoResult, Error>),
ReceiveAddress(Result<Address, Error>),
Coins(Result<Vec<Coin>, Error>),
@ -25,6 +29,7 @@ pub enum Message {
Psbt(Result<Psbt, Error>),
Recovery(Result<SpendTx, Error>),
Signed(Result<(Psbt, Fingerprint), Error>),
WalletRegistered(Result<Fingerprint, Error>),
Updated(Result<(), Error>),
Saved(Result<(), Error>),
StartRescan(Result<(), Error>),

View File

@ -18,7 +18,7 @@ use std::time::Duration;
use iced::{clipboard, time, Command, Element, Subscription};
use tracing::{info, warn};
pub use liana::config::Config as DaemonConfig;
pub use liana::{config::Config as DaemonConfig, miniscript::bitcoin};
pub use config::Config;
pub use message::Message;
@ -31,6 +31,7 @@ use crate::{
};
pub struct App {
data_dir: PathBuf,
state: Box<dyn State>,
cache: Cache,
config: Config,
@ -44,11 +45,13 @@ impl App {
wallet: Arc<Wallet>,
config: Config,
daemon: Arc<dyn Daemon + Sync + Send>,
data_dir: PathBuf,
) -> (App, Command<Message>) {
let state: Box<dyn State> = Home::new(wallet.clone(), &cache.coins).into();
let cmd = state.load(daemon.clone());
(
Self {
data_dir,
state,
cache,
config,
@ -61,12 +64,9 @@ impl App {
fn load_state(&mut self, menu: &Menu) -> Command<Message> {
self.state = match menu {
menu::Menu::Settings => state::SettingsState::new(
self.daemon.config().cloned(),
&self.cache,
self.daemon.is_external(),
)
.into(),
menu::Menu::Settings => {
state::SettingsState::new(self.data_dir.clone(), self.wallet.clone()).into()
}
menu::Menu::Home => Home::new(self.wallet.clone(), &self.cache.coins).into(),
menu::Menu::Coins => CoinsPanel::new(
&self.cache.coins,
@ -145,6 +145,10 @@ impl App {
let res = self.load_daemon_config(&path, *cfg);
self.update(Message::DaemonConfigLoaded(res))
}
Message::LoadWallet => {
let res = self.load_wallet();
self.update(Message::WalletLoaded(res))
}
Message::View(view::Message::Menu(menu)) => self.load_state(&menu),
Message::View(view::Message::Clipboard(text)) => clipboard::write(text),
_ => self.state.update(self.daemon.clone(), &self.cache, message),
@ -181,6 +185,18 @@ impl App {
Ok(())
}
pub fn load_wallet(&mut self) -> Result<Arc<Wallet>, Error> {
let wallet = Wallet::new(self.wallet.main_descriptor.clone()).load_settings(
&self.config,
&self.data_dir,
self.cache.network,
)?;
self.wallet = Arc::new(wallet);
Ok(self.wallet.clone())
}
pub fn view(&self) -> Element<Message> {
self.state.view(&self.cache).map(Message::View)
}

View File

@ -1,10 +1,12 @@
use std::collections::HashMap;
use std::path::Path;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use liana::miniscript::bitcoin::util::bip32::Fingerprint;
use liana::miniscript::bitcoin::{util::bip32::Fingerprint, Network};
use serde::{Deserialize, Serialize};
use crate::hw::HardwareWalletConfig;
use crate::{app::wallet::Wallet, hw::HardwareWalletConfig};
///! Settings is the module to handle the GUI settings file.
///! The settings file is used by the GUI to store useful information.
@ -16,7 +18,11 @@ pub struct Settings {
}
impl Settings {
pub fn from_file(path: &Path) -> Result<Self, SettingsError> {
pub fn from_file(datadir: PathBuf, network: Network) -> Result<Self, SettingsError> {
let mut path = datadir;
path.push(network.to_string());
path.push(DEFAULT_FILE_NAME);
let config = std::fs::read(path)
.map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => SettingsError::NotFound,
@ -29,6 +35,26 @@ impl Settings {
})?;
Ok(config)
}
pub fn to_file(&self, datadir: PathBuf, network: Network) -> Result<(), SettingsError> {
let mut path = datadir;
path.push(network.to_string());
path.push(DEFAULT_FILE_NAME);
let content = serde_json::to_string_pretty(&self).map_err(|e| {
SettingsError::WritingFile(format!("Failed to serialize settings: {}", e))
})?;
let mut settings_file = OpenOptions::new()
.write(true)
.open(path)
.map_err(|e| SettingsError::WritingFile(e.to_string()))?;
settings_file.write_all(content.as_bytes()).map_err(|e| {
tracing::warn!("failed to write to file: {:?}", e);
SettingsError::WritingFile(e.to_string())
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
@ -51,6 +77,25 @@ impl WalletSetting {
}
}
impl From<&Wallet> for WalletSetting {
fn from(w: &Wallet) -> WalletSetting {
Self {
name: w.name.clone(),
hardware_wallets: w.hardware_wallets.clone(),
keys: w
.keys_aliases
.clone()
.into_iter()
.map(|(master_fingerprint, name)| KeySetting {
name,
master_fingerprint,
})
.collect(),
descriptor_checksum: w.descriptor_checksum(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct KeySetting {
pub name: String,
@ -61,6 +106,7 @@ pub struct KeySetting {
pub enum SettingsError {
NotFound,
ReadingFile(String),
WritingFile(String),
Unexpected(String),
}
@ -69,6 +115,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::WritingFile(e) => write!(f, "Error while writing file: {}", e),
Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
}
}

View File

@ -26,10 +26,12 @@ pub trait State {
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message>;
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
cache: &Cache,
message: Message,
) -> Command<Message>;
_daemon: Arc<dyn Daemon + Sync + Send>,
_cache: &Cache,
_message: Message,
) -> Command<Message> {
Command::none()
}
fn subscription(&self) -> Subscription<Message> {
Subscription::none()
}

View File

@ -11,34 +11,22 @@ use tracing::info;
use liana::config::{BitcoinConfig, BitcoindConfig, Config};
use crate::{
app::{cache::Cache, error::Error, message::Message, state::State, view},
app::{cache::Cache, error::Error, message::Message, state::settings::Setting, view, State},
daemon::Daemon,
ui::component::form,
};
trait Setting: std::fmt::Debug {
fn edited(&mut self, success: bool);
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
cache: &Cache,
message: view::SettingsMessage,
) -> Command<Message>;
fn view<'a>(&self, cache: &'a Cache, can_edit: bool) -> Element<'a, view::SettingsMessage>;
}
#[derive(Debug)]
pub struct SettingsState {
pub struct BitcoindSettingsState {
warning: Option<Error>,
config_updated: bool,
daemon_is_external: bool,
daemon_version: Option<String>,
settings: Vec<Box<dyn Setting>>,
current: Option<usize>,
}
impl SettingsState {
impl BitcoindSettingsState {
pub fn new(config: Option<Config>, cache: &Cache, daemon_is_external: bool) -> Self {
let settings = if let Some(config) = &config {
vec![
@ -53,12 +41,7 @@ impl SettingsState {
vec![RescanSetting::new(cache.rescan_progress).into()]
};
SettingsState {
daemon_version: if !daemon_is_external {
Some(liana::VERSION.to_string())
} else {
None
},
BitcoindSettingsState {
daemon_is_external,
warning: None,
config_updated: false,
@ -69,7 +52,7 @@ impl SettingsState {
}
}
impl State for SettingsState {
impl State for BitcoindSettingsState {
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
@ -101,17 +84,16 @@ impl State for SettingsState {
Message::Info(res) => match res {
Err(e) => self.warning = Some(e),
Ok(info) => {
self.daemon_version = Some(info.version);
if info.rescan_progress == Some(1.0) {
self.settings[1].edited(true);
}
}
},
Message::View(view::Message::Settings(i, msg)) => {
Message::View(view::Message::Settings(view::SettingsMessage::Edit(i, msg))) => {
if let Some(setting) = self.settings.get_mut(i) {
match msg {
view::SettingsMessage::Edit => self.current = Some(i),
view::SettingsMessage::CancelEdit => self.current = None,
view::SettingsEditMessage::Select => self.current = Some(i),
view::SettingsEditMessage::Cancel => self.current = None,
_ => {}
};
return setting.update(daemon, cache, msg);
@ -124,36 +106,24 @@ impl State for SettingsState {
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> {
let can_edit = self.current.is_none() && !self.daemon_is_external;
view::settings::list(
self.daemon_version.as_ref(),
view::settings::bitcoind_settings(
cache,
self.warning.as_ref(),
self.settings
.iter()
.enumerate()
.map(|(i, setting)| {
setting
.view(cache, can_edit)
.map(move |msg| view::Message::Settings(i, msg))
setting.view(cache, can_edit).map(move |msg| {
view::Message::Settings(view::SettingsMessage::Edit(i, msg))
})
})
.collect(),
)
}
fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
if self.daemon_version.is_none() {
Command::perform(
async move { daemon.get_info().map_err(|e| e.into()) },
Message::Info,
)
} else {
Command::none()
}
}
}
impl From<SettingsState> for Box<dyn State> {
fn from(s: SettingsState) -> Box<dyn State> {
impl From<BitcoindSettingsState> for Box<dyn State> {
fn from(s: BitcoindSettingsState) -> Box<dyn State> {
Box::new(s)
}
}
@ -207,20 +177,20 @@ impl Setting for BitcoindSettings {
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
_cache: &Cache,
message: view::SettingsMessage,
message: view::SettingsEditMessage,
) -> Command<Message> {
match message {
view::SettingsMessage::Edit => {
view::SettingsEditMessage::Select => {
if !self.processing {
self.edit = true;
}
}
view::SettingsMessage::CancelEdit => {
view::SettingsEditMessage::Cancel => {
if !self.processing {
self.edit = false;
}
}
view::SettingsMessage::FieldEdited(field, value) => {
view::SettingsEditMessage::FieldEdited(field, value) => {
if !self.processing {
match field {
"socket_address" => self.addr.value = value,
@ -229,7 +199,7 @@ impl Setting for BitcoindSettings {
}
}
}
view::SettingsMessage::ConfirmEdit => {
view::SettingsEditMessage::Confirm => {
let new_addr = SocketAddr::from_str(&self.addr.value);
self.addr.valid = new_addr.is_ok();
let new_path = PathBuf::from_str(&self.cookie_path.value);
@ -251,7 +221,7 @@ impl Setting for BitcoindSettings {
Command::none()
}
fn view<'a>(&self, cache: &'a Cache, can_edit: bool) -> Element<'a, view::SettingsMessage> {
fn view<'a>(&self, cache: &'a Cache, can_edit: bool) -> Element<'a, view::SettingsEditMessage> {
if self.edit {
view::settings::bitcoind_edit(
self.bitcoin_config.network,
@ -307,20 +277,20 @@ impl Setting for RescanSetting {
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
_cache: &Cache,
message: view::SettingsMessage,
message: view::SettingsEditMessage,
) -> Command<Message> {
match message {
view::SettingsMessage::Edit => {
view::SettingsEditMessage::Select => {
if !self.processing {
self.edit = true;
}
}
view::SettingsMessage::CancelEdit => {
view::SettingsEditMessage::Cancel => {
if !self.processing {
self.edit = false;
}
}
view::SettingsMessage::FieldEdited(field, value) => {
view::SettingsEditMessage::FieldEdited(field, value) => {
if !self.processing && (value.is_empty() || u32::from_str(&value).is_ok()) {
match field {
"rescan_year" => self.year.value = value,
@ -330,7 +300,7 @@ impl Setting for RescanSetting {
}
}
}
view::SettingsMessage::ConfirmEdit => {
view::SettingsEditMessage::Confirm => {
let date_time = NaiveDate::from_ymd(
i32::from_str(&self.year.value).unwrap_or(1),
u32::from_str(&self.month.value).unwrap_or(1),
@ -349,7 +319,7 @@ impl Setting for RescanSetting {
Command::none()
}
fn view<'a>(&self, cache: &'a Cache, can_edit: bool) -> Element<'a, view::SettingsMessage> {
fn view<'a>(&self, cache: &'a Cache, can_edit: bool) -> Element<'a, view::SettingsEditMessage> {
view::settings::rescan(
&self.year,
&self.month,

View File

@ -0,0 +1,158 @@
mod bitcoind;
mod wallet;
use std::convert::From;
use std::path::PathBuf;
use std::sync::Arc;
use iced::{Command, Element};
use bitcoind::BitcoindSettingsState;
use wallet::WalletSettingsState;
use crate::{
app::{cache::Cache, error::Error, message::Message, state::State, view, wallet::Wallet},
daemon::Daemon,
};
trait Setting: std::fmt::Debug {
fn edited(&mut self, success: bool);
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
cache: &Cache,
message: view::SettingsEditMessage,
) -> Command<Message>;
fn view<'a>(&self, cache: &'a Cache, can_edit: bool) -> Element<'a, view::SettingsEditMessage>;
}
pub struct SettingsState {
data_dir: PathBuf,
wallet: Arc<Wallet>,
setting: Option<Box<dyn State>>,
}
impl SettingsState {
pub fn new(data_dir: PathBuf, wallet: Arc<Wallet>) -> Self {
Self {
data_dir,
wallet,
setting: None,
}
}
}
impl State for SettingsState {
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
cache: &Cache,
message: Message,
) -> Command<Message> {
match &message {
Message::View(view::Message::Settings(view::SettingsMessage::EditBitcoindSettings)) => {
self.setting = Some(
BitcoindSettingsState::new(
daemon.config().cloned(),
cache,
daemon.is_external(),
)
.into(),
);
self.setting
.as_mut()
.map(|s| s.load(daemon))
.unwrap_or_else(Command::none)
}
Message::View(view::Message::Settings(view::SettingsMessage::AboutSection)) => {
self.setting = Some(AboutSettingsState::default().into());
self.setting
.as_mut()
.map(|s| s.load(daemon))
.unwrap_or_else(Command::none)
}
Message::View(view::Message::Settings(view::SettingsMessage::EditWalletSettings)) => {
self.setting = Some(
WalletSettingsState::new(self.data_dir.clone(), self.wallet.clone()).into(),
);
self.setting
.as_mut()
.map(|s| s.load(daemon))
.unwrap_or_else(Command::none)
}
_ => self
.setting
.as_mut()
.map(|s| s.update(daemon, cache, message))
.unwrap_or_else(Command::none),
}
}
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> {
if let Some(setting) = &self.setting {
setting.view(cache)
} else {
view::settings::list(cache)
}
}
}
impl From<SettingsState> for Box<dyn State> {
fn from(s: SettingsState) -> Box<dyn State> {
Box::new(s)
}
}
#[derive(Default)]
pub struct AboutSettingsState {
daemon_version: Option<String>,
warning: Option<Error>,
}
impl AboutSettingsState {
pub fn new(daemon_is_external: bool) -> Self {
AboutSettingsState {
daemon_version: if !daemon_is_external {
Some(liana::VERSION.to_string())
} else {
None
},
warning: None,
}
}
}
impl State for AboutSettingsState {
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> {
view::settings::about_section(cache, self.warning.as_ref(), self.daemon_version.as_ref())
}
fn update(
&mut self,
_daemon: Arc<dyn Daemon + Sync + Send>,
_cache: &Cache,
message: Message,
) -> Command<Message> {
if let Message::Info(res) = message {
match res {
Ok(info) => self.daemon_version = Some(info.version),
Err(e) => self.warning = Some(e),
}
}
Command::none()
}
fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
Command::perform(
async move { daemon.get_info().map_err(|e| e.into()) },
Message::Info,
)
}
}
impl From<AboutSettingsState> for Box<dyn State> {
fn from(s: AboutSettingsState) -> Box<dyn State> {
Box::new(s)
}
}

View File

@ -0,0 +1,258 @@
use std::collections::HashSet;
use std::convert::From;
use std::path::PathBuf;
use std::sync::Arc;
use iced::{Command, Element};
use liana::miniscript::bitcoin::{hashes::hex::ToHex, util::bip32::Fingerprint, Network};
use crate::{
app::{
cache::Cache, error::Error, message::Message, settings, state::State, view, wallet::Wallet,
},
daemon::Daemon,
hw::{list_hardware_wallets, HardwareWallet, HardwareWalletConfig},
ui::component::modal,
};
pub struct WalletSettingsState {
data_dir: PathBuf,
warning: Option<Error>,
descriptor: String,
wallet: Arc<Wallet>,
modal: Option<RegisterWalletModal>,
}
impl WalletSettingsState {
pub fn new(data_dir: PathBuf, wallet: Arc<Wallet>) -> Self {
WalletSettingsState {
data_dir,
descriptor: wallet.main_descriptor.to_string(),
wallet,
warning: None,
modal: None,
}
}
}
impl State for WalletSettingsState {
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> {
let content =
view::settings::wallet_settings(cache, self.warning.as_ref(), &self.descriptor);
if let Some(m) = &self.modal {
modal::Modal::new(content, m.view())
.on_blur(Some(view::Message::Close))
.into()
} else {
content
}
}
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
cache: &Cache,
message: Message,
) -> Command<Message> {
match message {
Message::WalletLoaded(res) => {
match res {
Ok(wallet) => {
if let Some(modal) = &mut self.modal {
modal.wallet = wallet.clone();
}
self.wallet = wallet;
}
Err(e) => self.warning = Some(e),
};
Command::none()
}
Message::View(view::Message::Close) => {
self.modal = None;
Command::none()
}
Message::View(view::Message::Settings(view::SettingsMessage::RegisterWallet)) => {
self.modal = Some(RegisterWalletModal::new(
self.data_dir.clone(),
self.wallet.clone(),
));
self.modal
.as_ref()
.map(|m| m.load(daemon))
.unwrap_or_else(Command::none)
}
_ => self
.modal
.as_mut()
.map(|m| m.update(daemon, cache, message))
.unwrap_or_else(Command::none),
}
}
fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
Command::perform(
async move { daemon.get_info().map_err(|e| e.into()) },
Message::Info,
)
}
}
impl From<WalletSettingsState> for Box<dyn State> {
fn from(s: WalletSettingsState) -> Box<dyn State> {
Box::new(s)
}
}
pub struct RegisterWalletModal {
data_dir: PathBuf,
wallet: Arc<Wallet>,
warning: Option<Error>,
chosen_hw: Option<usize>,
hws: Vec<HardwareWallet>,
registered: HashSet<Fingerprint>,
processing: bool,
}
impl RegisterWalletModal {
pub fn new(data_dir: PathBuf, wallet: Arc<Wallet>) -> Self {
let mut registered = HashSet::new();
for hw in &wallet.hardware_wallets {
registered.insert(hw.fingerprint);
}
Self {
data_dir,
wallet,
warning: None,
chosen_hw: None,
hws: Vec::new(),
processing: false,
registered,
}
}
}
impl RegisterWalletModal {
fn view(&self) -> Element<view::Message> {
view::settings::register_wallet_modal(
self.warning.as_ref(),
&self.hws,
self.processing,
self.chosen_hw,
&self.registered,
)
}
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
cache: &Cache,
message: Message,
) -> Command<Message> {
match message {
Message::View(view::Message::Reload) => {
self.hws = Vec::new();
self.chosen_hw = None;
self.warning = None;
self.load(daemon)
}
Message::ConnectedHardwareWallets(hws) => {
self.hws = hws;
Command::none()
}
Message::WalletRegistered(res) => {
self.processing = false;
self.chosen_hw = None;
match res {
Ok(fingerprint) => {
self.registered.insert(fingerprint);
return Command::perform(async {}, |_| Message::LoadWallet);
}
Err(e) => self.warning = Some(e),
}
Command::none()
}
Message::View(view::Message::SelectHardwareWallet(i)) => {
if let Some(HardwareWallet::Supported {
fingerprint,
device,
..
}) = self.hws.get(i)
{
self.chosen_hw = Some(i);
self.processing = true;
Command::perform(
register_wallet(
self.data_dir.clone(),
cache.network,
device.clone(),
*fingerprint,
self.wallet.clone(),
),
Message::WalletRegistered,
)
} else {
Command::none()
}
}
_ => Command::none(),
}
}
fn load(&self, _daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
Command::perform(
list_hws(self.wallet.clone()),
Message::ConnectedHardwareWallets,
)
}
}
async fn register_wallet(
data_dir: PathBuf,
network: Network,
hw: std::sync::Arc<dyn async_hwi::HWI + Send + Sync>,
fingerprint: Fingerprint,
wallet: Arc<Wallet>,
) -> Result<Fingerprint, Error> {
let hmac = hw
.register_wallet(&wallet.name, &wallet.main_descriptor.to_string())
.await
.map_err(Error::from)?;
if let Some(hmac) = hmac {
let mut settings = settings::Settings::from_file(data_dir.clone(), network)?;
let checksum = wallet.descriptor_checksum();
if let Some(wallet_setting) = settings
.wallets
.iter_mut()
.find(|w| w.descriptor_checksum == checksum)
{
let kind = hw.device_kind().to_string();
if let Some(hw_config) = wallet_setting
.hardware_wallets
.iter_mut()
.find(|cfg| cfg.kind == kind && cfg.fingerprint == fingerprint)
{
hw_config.token = hmac.to_hex();
} else {
wallet_setting.hardware_wallets.push(HardwareWalletConfig {
kind,
token: hmac.to_hex(),
fingerprint,
})
}
}
settings.to_file(data_dir, network)?;
}
Ok(fingerprint)
}
async fn list_hws(wallet: Arc<Wallet>) -> Vec<HardwareWallet> {
list_hardware_wallets(
&wallet.hardware_wallets,
Some((&wallet.name, &wallet.main_descriptor.to_string())),
)
.await
}

View File

@ -11,7 +11,12 @@ use liana::{
use crate::{
app::{
cache::Cache, error::Error, message::Message, view, view::spend::detail, wallet::Wallet,
cache::Cache,
error::Error,
message::Message,
view,
view::spend::detail,
wallet::{Wallet, WalletError},
},
daemon::{
model::{SpendStatus, SpendTx},
@ -295,7 +300,7 @@ impl Action for SignAction {
tx: &mut SpendTx,
) -> Command<Message> {
match message {
Message::View(view::Message::Spend(view::SpendTxMessage::SelectHardwareWallet(i))) => {
Message::View(view::Message::SelectHardwareWallet(i)) => {
if let Some(HardwareWallet::Supported {
fingerprint,
device,
@ -389,12 +394,12 @@ async fn sign_psbt_with_hot_signer(
psbt: Psbt,
) -> Result<(Psbt, Fingerprint), Error> {
if let Some(signer) = &wallet.signer {
let psbt = signer
.sign_psbt(psbt)
.map_err(|e| Error::HotSigner(format!("Hot signer failed to sign psbt: {}", e)))?;
let psbt = signer.sign_psbt(psbt).map_err(|e| {
WalletError::HotSigner(format!("Hot signer failed to sign psbt: {}", e))
})?;
Ok((psbt, signer.fingerprint()))
} else {
Err(Error::HotSigner("Hot signer not loaded".to_string()))
Err(WalletError::HotSigner("Hot signer not loaded".to_string()).into())
}
}

View File

@ -17,13 +17,13 @@ use crate::{
},
};
pub fn hw_list_view(
pub fn hw_list_view<'a>(
i: usize,
hw: &HardwareWallet,
hw: &'a HardwareWallet,
chosen: bool,
processing: bool,
signed: bool,
) -> Element<Message> {
status: Option<&'a str>,
) -> Element<'a, Message> {
let mut bttn = Button::new(
Row::new()
.push(
@ -72,17 +72,13 @@ pub fn hw_list_view(
} else {
None
})
.push_maybe(if signed {
Some(
Row::new()
.align_items(Alignment::Center)
.spacing(5)
.push(icon::circle_check_icon().style(color::SUCCESS))
.push(text("Signed").style(color::SUCCESS)),
)
} else {
None
})
.push_maybe(status.map(|v| {
Row::new()
.align_items(Alignment::Center)
.spacing(5)
.push(icon::circle_check_icon().style(color::SUCCESS))
.push(text(v).style(color::SUCCESS))
}))
.align_items(Alignment::Center)
.width(Length::Fill),
)
@ -90,7 +86,7 @@ pub fn hw_list_view(
.style(button::Style::Border.into())
.width(Length::Fill);
if !processing && hw.is_supported() {
bttn = bttn.on_press(Message::Spend(SpendTxMessage::SelectHardwareWallet(i)));
bttn = bttn.on_press(Message::SelectHardwareWallet(i));
}
Container::new(bttn)
.width(Length::Fill)

View File

@ -7,12 +7,13 @@ pub enum Message {
Menu(Menu),
Close,
Select(usize),
Settings(usize, SettingsMessage),
Settings(SettingsMessage),
CreateSpend(CreateSpendMessage),
ImportSpend(ImportSpendMessage),
Spend(SpendTxMessage),
Next,
Previous,
SelectHardwareWallet(usize),
}
#[derive(Debug, Clone)]
@ -41,7 +42,6 @@ pub enum SpendTxMessage {
Confirm,
Cancel,
SelectHotSigner,
SelectHardwareWallet(usize),
EditPsbt,
PsbtEdited(String),
Next,
@ -49,8 +49,17 @@ pub enum SpendTxMessage {
#[derive(Debug, Clone)]
pub enum SettingsMessage {
Edit,
FieldEdited(&'static str, String),
CancelEdit,
ConfirmEdit,
EditBitcoindSettings,
EditWalletSettings,
AboutSection,
RegisterWallet,
Edit(usize, SettingsEditMessage),
}
#[derive(Debug, Clone)]
pub enum SettingsEditMessage {
Select,
FieldEdited(&'static str, String),
Cancel,
Confirm,
}

View File

@ -1,30 +1,120 @@
use std::collections::HashSet;
use std::str::FromStr;
use iced::{
alignment,
widget::{self, Column, Container, ProgressBar, Row, Space},
widget::{self, Button, Column, Container, ProgressBar, Row, Space},
Alignment, Element, Length,
};
use liana::miniscript::bitcoin;
use liana::miniscript::bitcoin::{util::bip32::Fingerprint, Network};
use super::{
dashboard,
message::{Message, SettingsMessage},
};
use super::{dashboard, message::*};
use crate::{
app::{cache::Cache, error::Error, menu::Menu},
app::{
cache::Cache,
error::Error,
menu::Menu,
view::{hw, warning::warn},
},
hw::HardwareWallet,
ui::{
color,
component::{badge, button, card, form, separation, text::*},
component::{badge, button, card, form, separation, text::*, tooltip::tooltip},
icon,
util::Collection,
},
};
pub fn list<'a>(
lianad_version: Option<&'a String>,
pub fn list(cache: &Cache) -> Element<Message> {
dashboard(
&Menu::Settings,
cache,
None,
Column::new()
.spacing(20)
.width(Length::Fill)
.push(
Button::new(text("Settings").size(30).bold())
.style(button::Style::Transparent.into())
.on_press(Message::Menu(Menu::Settings)))
.push(
Container::new(
Button::new(
Row::new()
.push(badge::Badge::new(icon::bitcoin_icon()))
.push(text("Bitcoind").bold())
.padding(10)
.spacing(20)
.align_items(Alignment::Center)
.width(Length::Fill),
)
.width(Length::Fill)
.style(button::Style::Border.into())
.on_press(Message::Settings(SettingsMessage::EditBitcoindSettings))
)
.width(Length::Fill)
.style(card::SimpleCardStyle)
)
.push(
Container::new(
Button::new(
Row::new()
.push(badge::Badge::new(icon::wallet_icon()))
.push(text("Wallet").bold())
.padding(10)
.spacing(20)
.align_items(Alignment::Center)
.width(Length::Fill),
)
.width(Length::Fill)
.style(button::Style::Border.into())
.on_press(Message::Settings(SettingsMessage::EditWalletSettings))
)
.width(Length::Fill)
.style(card::SimpleCardStyle)
)
.push(
Container::new(
Button::new(
Row::new()
.push(badge::Badge::new(icon::recovery_icon()))
.push(text("Recovery").bold())
.push(tooltip("In case of loss of the main key, the recovery key can move the funds after a certain time."))
.padding(10)
.spacing(20)
.align_items(Alignment::Center)
.width(Length::Fill),
)
.width(Length::Fill)
.style(button::Style::Border.into())
.on_press(Message::Menu(Menu::Recovery))
)
.width(Length::Fill)
.style(card::SimpleCardStyle)
)
.push(
Container::new(
Button::new(
Row::new()
.push(badge::Badge::new(icon::tooltip_icon()))
.push(text("About").bold())
.padding(10)
.spacing(20)
.align_items(Alignment::Center)
.width(Length::Fill),
)
.width(Length::Fill)
.style(button::Style::Border.into())
.on_press(Message::Settings(SettingsMessage::AboutSection))
)
.width(Length::Fill)
.style(card::SimpleCardStyle)
)
)
}
pub fn bitcoind_settings<'a>(
cache: &'a Cache,
warning: Option<&Error>,
settings: Vec<Element<'a, Message>>,
@ -33,36 +123,62 @@ pub fn list<'a>(
&Menu::Settings,
cache,
warning,
widget::Column::with_children(settings)
Column::new()
.spacing(20)
.push(card::simple(
Column::new()
.push(
Row::new()
.spacing(10)
.align_items(Alignment::Center)
.push(
Row::new()
.push(badge::Badge::new(icon::recovery_icon()))
.push(text("Recovery").bold())
.padding(10)
.spacing(20)
.align_items(Alignment::Center)
.width(Length::Fill),
Button::new(text("Settings").size(30).bold())
.style(button::Style::Transparent.into())
.on_press(Message::Menu(Menu::Settings)),
)
.push(separation().width(Length::Fill))
.push(Space::with_height(Length::Units(10)))
.push(text("In case of loss of the main key, the recovery key can move the funds after a certain time."))
.push(Space::with_height(Length::Units(10)))
.push(icon::chevron_right().size(30))
.push(
Row::new()
.push(Space::with_width(Length::Fill))
.push(button::primary(None, "Recover funds").on_press(Message::Menu(Menu::Recovery))),
Button::new(text("Bitcoind").size(30).bold())
.style(button::Style::Transparent.into())
.on_press(Message::Settings(SettingsMessage::EditBitcoindSettings)),
),
))
)
.push(widget::Column::with_children(settings).spacing(20)),
)
}
pub fn about_section<'a>(
cache: &'a Cache,
warning: Option<&Error>,
lianad_version: Option<&String>,
) -> Element<'a, Message> {
dashboard(
&Menu::Settings,
cache,
warning,
Column::new()
.spacing(20)
.push(
Row::new()
.spacing(10)
.align_items(Alignment::Center)
.push(
Button::new(text("Settings").size(30).bold())
.style(button::Style::Transparent.into())
.on_press(Message::Menu(Menu::Settings)),
)
.push(icon::chevron_right().size(30))
.push(
Button::new(text("About").size(30).bold())
.style(button::Style::Transparent.into())
.on_press(Message::Settings(SettingsMessage::AboutSection)),
),
)
.push(
card::simple(
Column::new()
.push(
Row::new()
.push(badge::Badge::new(icon::tooltip_icon()))
.push(text("About").bold())
.push(text("Version").bold())
.padding(10)
.spacing(20)
.align_items(Alignment::Center)
@ -71,22 +187,28 @@ pub fn list<'a>(
.push(separation().width(Length::Fill))
.push(Space::with_height(Length::Units(10)))
.push(
Row::new().push(Space::with_width(Length::Fill)).push(Column::new()
.push(text(format!("liana-gui v{}", crate::VERSION)))
.push_maybe(lianad_version.map(|version| text(format!("lianad v{}", version)))))
)
).width(Length::Fill)
)
Row::new().push(Space::with_width(Length::Fill)).push(
Column::new()
.push(text(format!("liana-gui v{}", crate::VERSION)))
.push_maybe(
lianad_version
.map(|version| text(format!("lianad v{}", version))),
),
),
),
)
.width(Length::Fill),
),
)
}
pub fn bitcoind_edit<'a>(
network: bitcoin::Network,
network: Network,
blockheight: i32,
addr: &form::Value<String>,
cookie_path: &form::Value<String>,
processing: bool,
) -> Element<'a, SettingsMessage> {
) -> Element<'a, SettingsEditMessage> {
let mut col = Column::new().spacing(20);
if blockheight != 0 {
col = col
@ -124,7 +246,7 @@ pub fn bitcoind_edit<'a>(
.push(text("Cookie file path:").bold().small())
.push(
form::Form::new("Cookie file path", cookie_path, |value| {
SettingsMessage::FieldEdited("cookie_file_path", value)
SettingsEditMessage::FieldEdited("cookie_file_path", value)
})
.warning("Please enter a valid filesystem path")
.size(20)
@ -137,7 +259,7 @@ pub fn bitcoind_edit<'a>(
.push(text("Socket address:").bold().small())
.push(
form::Form::new("Socket address:", addr, |value| {
SettingsMessage::FieldEdited("socket_address", value)
SettingsEditMessage::FieldEdited("socket_address", value)
})
.warning("Please enter a valid address")
.size(20)
@ -149,8 +271,8 @@ pub fn bitcoind_edit<'a>(
let mut cancel_button = button::transparent(None, " Cancel ").padding(5);
let mut confirm_button = button::primary(None, " Save ").padding(5);
if !processing {
cancel_button = cancel_button.on_press(SettingsMessage::CancelEdit);
confirm_button = confirm_button.on_press(SettingsMessage::ConfirmEdit);
cancel_button = cancel_button.on_press(SettingsEditMessage::Cancel);
confirm_button = confirm_button.on_press(SettingsEditMessage::Confirm);
}
card::simple(Container::new(
@ -184,12 +306,12 @@ pub fn bitcoind_edit<'a>(
}
pub fn bitcoind<'a>(
network: bitcoin::Network,
network: Network,
config: &liana::config::BitcoindConfig,
blockheight: i32,
is_running: Option<bool>,
can_edit: bool,
) -> Element<'a, SettingsMessage> {
) -> Element<'a, SettingsEditMessage> {
let mut col = Column::new().spacing(20);
if blockheight != 0 {
col = col
@ -254,7 +376,7 @@ pub fn bitcoind<'a>(
.push(if can_edit {
widget::Button::new(icon::pencil_icon())
.style(button::Style::TransparentBorder.into())
.on_press(SettingsMessage::Edit)
.on_press(SettingsEditMessage::Select)
} else {
widget::Button::new(icon::pencil_icon())
.style(button::Style::TransparentBorder.into())
@ -299,7 +421,7 @@ pub fn rescan<'a>(
success: bool,
processing: bool,
can_edit: bool,
) -> Element<'a, SettingsMessage> {
) -> Element<'a, SettingsEditMessage> {
card::simple(Container::new(
Column::new()
.push(
@ -332,7 +454,7 @@ pub fn rescan<'a>(
.push(text("Year:").bold().small())
.push(
form::Form::new("2022", year, |value| {
SettingsMessage::FieldEdited("rescan_year", value)
SettingsEditMessage::FieldEdited("rescan_year", value)
})
.size(20)
.padding(5),
@ -340,7 +462,7 @@ pub fn rescan<'a>(
.push(text("Month:").bold().small())
.push(
form::Form::new("12", month, |value| {
SettingsMessage::FieldEdited("rescan_month", value)
SettingsEditMessage::FieldEdited("rescan_month", value)
})
.size(20)
.padding(5),
@ -348,7 +470,7 @@ pub fn rescan<'a>(
.push(text("Day:").bold().small())
.push(
form::Form::new("31", day, |value| {
SettingsMessage::FieldEdited("rescan_day", value)
SettingsEditMessage::FieldEdited("rescan_day", value)
})
.size(20)
.padding(5),
@ -367,7 +489,7 @@ pub fn rescan<'a>(
{
Row::new().push(Column::new().width(Length::Fill)).push(
button::primary(None, "Start rescan")
.on_press(SettingsMessage::ConfirmEdit)
.on_press(SettingsEditMessage::Confirm)
.width(Length::Shrink),
)
} else if processing {
@ -396,3 +518,103 @@ fn is_ok_and<T, E>(res: &Result<T, E>, f: impl FnOnce(&T) -> bool) -> bool {
false
}
}
pub fn wallet_settings<'a>(
cache: &'a Cache,
warning: Option<&Error>,
descriptor: &'a str,
) -> Element<'a, Message> {
dashboard(
&Menu::Settings,
cache,
warning,
Column::new()
.spacing(20)
.push(
Row::new()
.spacing(10)
.align_items(Alignment::Center)
.push(
Button::new(text("Settings").size(30).bold())
.style(button::Style::Transparent.into())
.on_press(Message::Menu(Menu::Settings)),
)
.push(icon::chevron_right().size(30))
.push(
Button::new(text("Wallet").size(30).bold())
.style(button::Style::Transparent.into())
.on_press(Message::Settings(SettingsMessage::AboutSection)),
),
)
.push(card::simple(
Column::new()
.push(text("Wallet descriptor:").small().bold())
.push(text(descriptor.to_owned()).small())
.push(
Row::new()
.spacing(10)
.push(Column::new().width(Length::Fill))
.push(
button::border(Some(icon::clipboard_icon()), "Copy")
.on_press(Message::Clipboard(descriptor.to_owned())),
)
.push(
button::primary(
Some(icon::chip_icon()),
"Register on hardware device",
)
.on_press(Message::Settings(SettingsMessage::RegisterWallet)),
),
)
.spacing(10),
)),
)
}
pub fn register_wallet_modal<'a>(
warning: Option<&Error>,
hws: &'a [HardwareWallet],
processing: bool,
chosen_hw: Option<usize>,
registered: &HashSet<Fingerprint>,
) -> Element<'a, Message> {
Column::new()
.push_maybe(warning.map(|w| warn(Some(w))))
.push(card::simple(
Column::new()
.push(
Column::new()
.push(
Row::new()
.push(text("Select device:").bold().width(Length::Fill))
.push(button::border(None, "Refresh").on_press(Message::Reload))
.align_items(Alignment::Center),
)
.spacing(10)
.push(hws.iter().enumerate().fold(
Column::new().spacing(10),
|col, (i, hw)| {
col.push(hw::hw_list_view(
i,
hw,
Some(i) == chosen_hw,
processing,
hw.fingerprint().and_then(|f| {
if registered.contains(&f) {
Some("Registered")
} else {
None
}
}),
))
},
))
.width(Length::Fill),
)
.spacing(20)
.width(Length::Fill)
.align_items(Alignment::Center),
))
.width(Length::Units(500))
.into()
}

View File

@ -737,9 +737,13 @@ pub fn sign_action<'a>(
hw,
Some(i) == chosen_hw,
processing,
hw.fingerprint()
.map(|f| signed.contains(&f))
.unwrap_or(false),
hw.fingerprint().and_then(|f| {
if signed.contains(&f) {
Some("Signed")
} else {
None
}
}),
))
},
))

View File

@ -18,6 +18,7 @@ impl From<&Error> for WarningMessage {
fn from(error: &Error) -> WarningMessage {
match error {
Error::Config(e) => WarningMessage(e.to_owned()),
Error::Wallet(_) => WarningMessage("Wallet error".to_string()),
Error::Daemon(e) => match e {
DaemonError::Rpc(code, _) => {
if *code == RpcErrorCode::JSONRPC2_INVALID_PARAMS as i32 {
@ -37,7 +38,6 @@ impl From<&Error> for WarningMessage {
},
Error::Unexpected(_) => WarningMessage("Unknown error".to_string()),
Error::HardwareWallet(_) => WarningMessage("Hardware wallet error".to_string()),
Error::HotSigner(_) => WarningMessage("Hot signer error".to_string()),
}
}
}

View File

@ -1,6 +1,13 @@
use std::collections::{HashMap, HashSet};
use std::path::Path;
use crate::{hw::HardwareWalletConfig, signer::Signer};
use crate::{
app::{config::Config, settings},
hw::HardwareWalletConfig,
signer::Signer,
};
use liana::{miniscript::bitcoin, signer::HotSigner};
use liana::descriptors::MultipathDescriptor;
use liana::miniscript::bitcoin::util::bip32::Fingerprint;
@ -17,17 +24,7 @@ pub struct Wallet {
}
impl Wallet {
pub fn new(name: String, main_descriptor: MultipathDescriptor) -> Self {
Self {
name,
main_descriptor,
keys_aliases: HashMap::new(),
hardware_wallets: Vec::new(),
signer: None,
}
}
pub fn legacy(main_descriptor: MultipathDescriptor) -> Self {
pub fn new(main_descriptor: MultipathDescriptor) -> Self {
Self {
name: DEFAULT_WALLET_NAME.to_string(),
main_descriptor,
@ -63,4 +60,86 @@ 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_settings(
self,
gui_config: &Config,
datadir_path: &Path,
network: bitcoin::Network,
) -> Result<Self, WalletError> {
let gui_config_hws = gui_config
.hardware_wallets
.as_ref()
.cloned()
.unwrap_or_default();
let mut wallet = match settings::Settings::from_file(datadir_path.to_path_buf(), network) {
Ok(settings) => {
if let Some(wallet_setting) = settings.wallets.first() {
self.with_hardware_wallets(wallet_setting.hardware_wallets.clone())
.with_key_aliases(wallet_setting.keys_aliases())
} else {
self.with_hardware_wallets(gui_config_hws)
}
}
Err(settings::SettingsError::NotFound) => self.with_hardware_wallets(gui_config_hws),
Err(e) => return Err(e.into()),
};
let hot_signers = match HotSigner::from_datadir(datadir_path, network) {
Ok(signers) => signers,
Err(e) => match e {
liana::signer::SignerError::MnemonicStorage(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
Vec::new()
} else {
return Err(WalletError::HotSigner(e.to_string()));
}
}
_ => return Err(WalletError::HotSigner(e.to_string())),
},
};
let curve = bitcoin::secp256k1::Secp256k1::signing_only();
let keys = wallet.descriptor_keys();
if let Some(hot_signer) = hot_signers
.into_iter()
.find(|s| keys.contains(&s.fingerprint(&curve)))
{
wallet = wallet.with_signer(Signer::new(hot_signer));
}
Ok(wallet)
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum WalletError {
Settings(settings::SettingsError),
HotSigner(String),
}
impl std::fmt::Display for WalletError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Settings(e) => write!(f, "Failed to load settings: {}", e),
Self::HotSigner(e) => write!(f, "Failed to load hot signer: {}", e),
}
}
}
impl From<settings::SettingsError> for WalletError {
fn from(error: settings::SettingsError) -> Self {
WalletError::Settings(error)
}
}

View File

@ -58,15 +58,15 @@ impl HardwareWallet {
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HardwareWalletConfig {
pub kind: String,
pub fingerprint: String,
pub fingerprint: Fingerprint,
pub token: String,
}
impl HardwareWalletConfig {
pub fn new(kind: &async_hwi::DeviceKind, fingerprint: &Fingerprint, token: &[u8; 32]) -> Self {
pub fn new(kind: &async_hwi::DeviceKind, fingerprint: Fingerprint, token: &[u8; 32]) -> Self {
Self {
kind: kind.to_string(),
fingerprint: fingerprint.to_string(),
fingerprint,
token: token.to_hex(),
}
}
@ -116,7 +116,7 @@ pub async fn list_hardware_wallets(
name,
descriptor,
cfg.iter()
.find(|cfg| cfg.fingerprint == fingerprint.to_string())
.find(|cfg| cfg.fingerprint == fingerprint)
.map(|cfg| cfg.token()),
)
.expect("Configuration must be correct");
@ -166,7 +166,7 @@ pub async fn list_hardware_wallets(
name,
descriptor,
cfg.iter()
.find(|cfg| cfg.fingerprint == fingerprint.to_string())
.find(|cfg| cfg.fingerprint == fingerprint)
.map(|cfg| cfg.token()),
)
.expect("Configuration must be correct");

View File

@ -56,7 +56,7 @@ impl Context {
.filter_map(|(kind, fingerprint, token)| {
token
.as_ref()
.map(|token| HardwareWalletConfig::new(kind, fingerprint, token))
.map(|token| HardwareWalletConfig::new(kind, *fingerprint, token))
})
.collect();
Settings {

View File

@ -13,7 +13,6 @@ use tracing::{debug, info};
use liana::{
config::{Config, ConfigError},
miniscript::bitcoin,
signer::HotSigner,
StartupError,
};
@ -21,11 +20,9 @@ use crate::{
app::{
cache::Cache,
config::Config as GUIConfig,
settings::{self, Settings},
wallet::Wallet,
wallet::{Wallet, WalletError},
},
daemon::{client, embedded::EmbeddedDaemon, model::*, Daemon, DaemonError},
signer::Signer,
ui::{
component::{button, notification, text::*},
icon,
@ -237,51 +234,9 @@ pub async fn load_application(
spend_txs,
..Default::default()
};
let settings_path = settings_path(&datadir_path, network);
let gui_config_hws = gui_config
.hardware_wallets
.as_ref()
.cloned()
.unwrap_or_default();
let mut wallet = match Settings::from_file(&settings_path) {
Ok(settings) => {
if let Some(wallet_setting) = settings.wallets.first() {
Wallet::new(wallet_setting.name.clone(), info.descriptors.main)
.with_hardware_wallets(wallet_setting.hardware_wallets.clone())
.with_key_aliases(wallet_setting.keys_aliases())
} else {
Wallet::legacy(info.descriptors.main).with_hardware_wallets(gui_config_hws)
}
}
Err(settings::SettingsError::NotFound) => {
Wallet::legacy(info.descriptors.main).with_hardware_wallets(gui_config_hws)
}
Err(e) => return Err(e.into()),
};
let hot_signers = match HotSigner::from_datadir(&datadir_path, network) {
Ok(signers) => signers,
Err(e) => match e {
liana::signer::SignerError::MnemonicStorage(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
Vec::new()
} else {
return Err(Error::HotSigner(e.to_string()));
}
}
_ => return Err(Error::HotSigner(e.to_string())),
},
};
let curve = bitcoin::secp256k1::Secp256k1::signing_only();
let keys = wallet.descriptor_keys();
if let Some(hot_signer) = hot_signers
.into_iter()
.find(|s| keys.contains(&s.fingerprint(&curve)))
{
wallet = wallet.with_signer(Signer::new(hot_signer));
}
let wallet =
Wallet::new(info.descriptors.main).load_settings(&gui_config, &datadir_path, network)?;
Ok((Arc::new(wallet), cache, daemon))
}
@ -402,26 +357,24 @@ async fn sync(
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum Error {
Settings(settings::SettingsError),
Wallet(WalletError),
Config(ConfigError),
Daemon(DaemonError),
HotSigner(String),
}
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::Wallet(e) => write!(f, "Wallet error: {}", e),
Self::Daemon(e) => write!(f, "Liana daemon error: {}", e),
Self::HotSigner(e) => write!(f, "Failed to load hot signer: {}", e),
}
}
}
impl From<settings::SettingsError> for Error {
fn from(error: settings::SettingsError) -> Self {
Error::Settings(error)
impl From<WalletError> for Error {
fn from(error: WalletError) -> Self {
Error::Wallet(error)
}
}
@ -444,11 +397,3 @@ fn socket_path(datadir: &Path, network: bitcoin::Network) -> PathBuf {
path.push("lianad_rpc");
path
}
/// default liana settings path is .liana/bitcoin/settings.json
fn settings_path(datadir: &Path, network: bitcoin::Network) -> PathBuf {
let mut path = datadir.to_path_buf();
path.push(network.to_string());
path.push(settings::DEFAULT_FILE_NAME);
path
}

View File

@ -214,7 +214,13 @@ impl Application for GUI {
Command::none()
}
loader::Message::Synced(Ok((wallet, cache, daemon))) => {
let (app, command) = App::new(cache, wallet, loader.gui_config.clone(), daemon);
let (app, command) = App::new(
cache,
wallet,
loader.gui_config.clone(),
daemon,
loader.datadir_path.clone(),
);
self.state = State::App(app);
command.map(|msg| Message::Run(Box::new(msg)))
}

View File

@ -17,6 +17,10 @@ pub fn arrow_down() -> Text<'static> {
icon('\u{F128}')
}
pub fn chevron_right() -> Text<'static> {
icon('\u{F285}')
}
pub fn recovery_icon() -> Text<'static> {
icon('\u{F467}')
}