gui: separate settings panels, add wallet settings
This commit is contained in:
parent
2c7cd2b0ca
commit
ae8df0dd4c
@ -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)
|
||||
|
||||
@ -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>),
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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,
|
||||
158
gui/src/app/state/settings/mod.rs
Normal file
158
gui/src/app/state/settings/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
258
gui/src/app/state/settings/wallet.rs
Normal file
258
gui/src/app/state/settings/wallet.rs
Normal 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
|
||||
}
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}),
|
||||
))
|
||||
},
|
||||
))
|
||||
|
||||
@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)))
|
||||
}
|
||||
|
||||
@ -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}')
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user