Migrate gui to its own module

This commit is contained in:
edouardparis 2025-06-19 15:29:57 +02:00
parent c9cc6514cf
commit ce38ea1881
3 changed files with 486 additions and 471 deletions

480
liana-gui/src/gui/mod.rs Normal file
View File

@ -0,0 +1,480 @@
use std::{collections::HashMap, sync::Arc};
use iced::{
event::{self, Event},
keyboard,
widget::{focus_next, focus_previous},
Subscription, Task,
};
use tracing::{error, info};
use tracing_subscriber::filter::LevelFilter;
extern crate serde;
extern crate serde_json;
use liana::miniscript::bitcoin;
use liana_ui::widget::Element;
use lianad::commands::ListCoinsResult;
use crate::{
app::{
self,
cache::Cache,
settings::{update_settings_file, WalletSettings},
wallet::Wallet,
App,
},
dir::LianaDirectory,
export::import_backup_at_launch,
hw::HardwareWalletConfig,
installer::{self, Installer},
launcher::{self, Launcher},
loader::{self, Loader},
logger::Logger,
services::connect::{
client::backend::{api, BackendWalletClient},
login,
},
VERSION,
};
pub struct GUI {
state: State,
logger: Logger,
// if set up, it overrides the level filter of the logger.
log_level: Option<LevelFilter>,
}
enum State {
Launcher(Box<Launcher>),
Installer(Box<Installer>),
Loader(Box<Loader>),
Login(Box<login::LianaLiteLogin>),
App(App),
}
#[derive(Debug)]
pub enum Key {
Tab(bool),
}
#[derive(Debug)]
pub enum Message {
CtrlC,
FontLoaded(Result<(), iced::font::Error>),
Launch(Box<launcher::Message>),
Install(Box<installer::Message>),
Load(Box<loader::Message>),
Run(Box<app::Message>),
Login(Box<login::Message>),
KeyPressed(Key),
Event(iced::Event),
}
impl From<Result<(), iced::font::Error>> for Message {
fn from(value: Result<(), iced::font::Error>) -> Self {
Self::FontLoaded(value)
}
}
async fn ctrl_c() -> Result<(), ()> {
if let Err(e) = tokio::signal::ctrl_c().await {
error!("{}", e);
};
info!("Signal received, exiting");
Ok(())
}
impl GUI {
pub fn title(&self) -> String {
match &self.state {
State::Installer(_) => format!("Liana v{} Installer", VERSION),
State::App(a) => format!("Liana v{} {}", VERSION, a.title()),
_ => format!("Liana v{}", VERSION),
}
}
pub fn new((config, log_level): (Config, Option<LevelFilter>)) -> (GUI, Task<Message>) {
let logger = Logger::setup(log_level.unwrap_or(LevelFilter::INFO));
let mut cmds = vec![Task::perform(ctrl_c(), |_| Message::CtrlC)];
let (launcher, command) = Launcher::new(config.liana_directory, config.network);
cmds.push(command.map(|msg| Message::Launch(Box::new(msg))));
(
Self {
state: State::Launcher(Box::new(launcher)),
logger,
log_level,
},
Task::batch(cmds),
)
}
pub fn update(&mut self, message: Message) -> Task<Message> {
match (&mut self.state, message) {
(_, Message::CtrlC)
| (_, Message::Event(iced::Event::Window(iced::window::Event::CloseRequested))) => {
match &mut self.state {
State::Loader(s) => s.stop(),
State::Launcher(s) => s.stop(),
State::Installer(s) => s.stop(),
State::App(s) => s.stop(),
State::Login(_) => {}
};
iced::window::get_latest().and_then(iced::window::close)
}
(_, Message::KeyPressed(Key::Tab(shift))) => {
log::debug!("Tab pressed!");
if shift {
focus_previous()
} else {
focus_next()
}
}
(State::Launcher(l), Message::Launch(msg)) => match *msg {
launcher::Message::Install(datadir, network, init) => {
if !datadir.exists() {
// datadir is created right before launching the installer
// so logs can go in <datadir_path>/installer.log
if let Err(e) = datadir.init() {
error!("Failed to create datadir: {}", e);
} else {
info!(
"Created a fresh data directory at {}",
&datadir.path().to_string_lossy()
);
}
}
self.logger.set_installer_mode(
datadir.clone(),
self.log_level.unwrap_or(LevelFilter::INFO),
);
let (install, command) = Installer::new(datadir, network, None, init);
self.state = State::Installer(Box::new(install));
command.map(|msg| Message::Install(Box::new(msg)))
}
launcher::Message::Run(datadir_path, cfg, network, settings) => {
self.logger.set_running_mode(
datadir_path.clone(),
network,
self.log_level
.unwrap_or_else(|| cfg.log_level().unwrap_or(LevelFilter::INFO)),
);
if settings.remote_backend_auth.is_some() {
let (login, command) =
login::LianaLiteLogin::new(datadir_path, network, settings);
self.state = State::Login(Box::new(login));
command.map(|msg| Message::Login(Box::new(msg)))
} else {
let (loader, command) =
Loader::new(datadir_path, cfg, network, None, None, settings);
self.state = State::Loader(Box::new(loader));
command.map(|msg| Message::Load(Box::new(msg)))
}
}
_ => l.update(*msg).map(|msg| Message::Launch(Box::new(msg))),
},
(State::Login(l), Message::Login(msg)) => match *msg {
login::Message::View(login::ViewMessage::BackToLauncher(network)) => {
let (launcher, command) = Launcher::new(l.datadir.clone(), Some(network));
self.state = State::Launcher(Box::new(launcher));
command.map(|msg| Message::Launch(Box::new(msg)))
}
login::Message::Install(remote_backend) => {
let (install, command) = Installer::new(
l.datadir.clone(),
l.network,
remote_backend,
installer::UserFlow::CreateWallet,
);
self.state = State::Installer(Box::new(install));
command.map(|msg| Message::Install(Box::new(msg)))
}
login::Message::Run(Ok((backend_client, wallet, coins))) => {
let config = app::Config::from_file(
&l.datadir
.network_directory(l.network)
.path()
.join(app::config::DEFAULT_FILE_NAME),
)
.expect("A gui configuration file must be present");
self.logger.set_running_mode(
l.datadir.clone(),
l.network,
config.log_level().unwrap_or(LevelFilter::INFO),
);
let (app, command) = create_app_with_remote_backend(
l.settings.clone(),
backend_client,
wallet,
coins,
l.datadir.clone(),
l.network,
config,
);
self.state = State::App(app);
command.map(|msg| Message::Run(Box::new(msg)))
}
_ => l.update(*msg).map(|msg| Message::Login(Box::new(msg))),
},
(State::Installer(i), Message::Install(msg)) => {
if let installer::Message::Exit(settings, internal_bitcoind, remove_log) = *msg {
if settings.remote_backend_auth.is_some() {
let (login, command) =
login::LianaLiteLogin::new(i.datadir.clone(), i.network, *settings);
self.state = State::Login(Box::new(login));
command.map(|msg| Message::Login(Box::new(msg)))
} else {
let cfg = app::Config::from_file(
&i.datadir
.network_directory(i.network)
.path()
.join(app::config::DEFAULT_FILE_NAME),
)
.expect("A gui configuration file must be present");
self.logger.set_running_mode(
i.datadir.clone(),
i.network,
self.log_level
.unwrap_or_else(|| cfg.log_level().unwrap_or(LevelFilter::INFO)),
);
if remove_log {
self.logger.remove_install_log_file(i.datadir.clone());
}
let (loader, command) = Loader::new(
i.datadir.clone(),
cfg,
i.network,
internal_bitcoind,
i.context.backup.take(),
*settings,
);
self.state = State::Loader(Box::new(loader));
command.map(|msg| Message::Load(Box::new(msg)))
}
} else if let installer::Message::BackToLauncher(network) = *msg {
let (launcher, command) = Launcher::new(i.destination_path(), Some(network));
self.state = State::Launcher(Box::new(launcher));
command.map(|msg| Message::Launch(Box::new(msg)))
} else {
i.update(*msg).map(|msg| Message::Install(Box::new(msg)))
}
}
(State::Loader(loader), Message::Load(msg)) => match *msg {
loader::Message::View(loader::ViewMessage::SwitchNetwork) => {
let (launcher, command) =
Launcher::new(loader.datadir_path.clone(), Some(loader.network));
self.state = State::Launcher(Box::new(launcher));
command.map(|msg| Message::Launch(Box::new(msg)))
}
loader::Message::Synced(Ok((wallet, cache, daemon, bitcoind, backup))) => {
if let Some(backup) = backup {
let config = loader.gui_config.clone();
let datadir = loader.datadir_path.clone();
Task::perform(
async move {
import_backup_at_launch(
cache, wallet, config, daemon, datadir, bitcoind, backup,
)
.await
},
|r| {
let r = r.map_err(loader::Error::RestoreBackup);
Message::Load(Box::new(loader::Message::App(
r, /* restored_from_backup */ true,
)))
},
)
} else {
let (app, command) = App::new(
cache,
wallet,
loader.gui_config.clone(),
daemon,
loader.datadir_path.clone(),
bitcoind,
false,
);
self.state = State::App(app);
command.map(|msg| Message::Run(Box::new(msg)))
}
}
loader::Message::App(
Ok((cache, wallet, config, daemon, datadir, bitcoind)),
restored_from_backup,
) => {
let (app, command) = App::new(
cache,
wallet,
config,
daemon,
datadir,
bitcoind,
restored_from_backup,
);
self.state = State::App(app);
command.map(|msg| Message::Run(Box::new(msg)))
}
loader::Message::App(Err(e), _) => {
tracing::error!("Failed to import backup: {e}");
Task::none()
}
_ => loader.update(*msg).map(|msg| Message::Load(Box::new(msg))),
},
(State::App(i), Message::Run(msg)) => {
i.update(*msg).map(|msg| Message::Run(Box::new(msg)))
}
_ => Task::none(),
}
}
pub fn subscription(&self) -> Subscription<Message> {
Subscription::batch(vec![
match &self.state {
State::Installer(v) => v.subscription().map(|msg| Message::Install(Box::new(msg))),
State::Loader(v) => v.subscription().map(|msg| Message::Load(Box::new(msg))),
State::App(v) => v.subscription().map(|msg| Message::Run(Box::new(msg))),
State::Launcher(v) => v.subscription().map(|msg| Message::Launch(Box::new(msg))),
State::Login(_) => Subscription::none(),
},
iced::event::listen_with(|event, status, _| match (&event, status) {
(
Event::Keyboard(keyboard::Event::KeyPressed {
key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Tab),
modifiers,
..
}),
event::Status::Ignored,
) => Some(Message::KeyPressed(Key::Tab(modifiers.shift()))),
(
iced::Event::Window(iced::window::Event::CloseRequested),
event::Status::Ignored,
) => Some(Message::Event(event)),
_ => None,
}),
])
}
pub fn view(&self) -> Element<Message> {
match &self.state {
State::Installer(v) => v.view().map(|msg| Message::Install(Box::new(msg))),
State::App(v) => v.view().map(|msg| Message::Run(Box::new(msg))),
State::Launcher(v) => v.view().map(|msg| Message::Launch(Box::new(msg))),
State::Loader(v) => v.view().map(|msg| Message::Load(Box::new(msg))),
State::Login(v) => v.view().map(|msg| Message::Login(Box::new(msg))),
}
}
pub fn scale_factor(&self) -> f64 {
1.0
}
}
pub fn create_app_with_remote_backend(
wallet_settings: WalletSettings,
remote_backend: BackendWalletClient,
wallet: api::Wallet,
coins: ListCoinsResult,
liana_dir: LianaDirectory,
network: bitcoin::Network,
config: app::Config,
) -> (app::App, iced::Task<app::Message>) {
// If someone modified the wallet_alias on Liana-Connect,
// then the new alias is imported and stored in the settings file.
if wallet.metadata.wallet_alias != wallet_settings.alias {
let network_directory = liana_dir.network_directory(network);
if let Err(e) = tokio::runtime::Handle::current().block_on(async {
update_settings_file(&network_directory, |mut settings| {
if let Some(w) = settings
.wallets
.iter_mut()
.find(|w| w.wallet_id() == wallet_settings.wallet_id())
{
w.alias = wallet.metadata.wallet_alias.clone();
tracing::info!("Wallet alias was changed. Settings updated.");
}
settings
})
.await
}) {
tracing::error!("Failed to update wallet settings with remote alias: {}", e);
}
}
let hws: Vec<HardwareWalletConfig> = wallet
.metadata
.ledger_hmacs
.into_iter()
.map(|ledger_hmac| HardwareWalletConfig {
kind: async_hwi::DeviceKind::Ledger.to_string(),
fingerprint: ledger_hmac.fingerprint,
token: ledger_hmac.hmac,
})
.collect();
let aliases: HashMap<bitcoin::bip32::Fingerprint, String> = wallet
.metadata
.fingerprint_aliases
.into_iter()
.filter_map(|a| {
if a.user_id == remote_backend.user_id() {
Some((a.fingerprint, a.alias))
} else {
None
}
})
.collect();
let provider_keys: HashMap<_, _> = wallet
.metadata
.provider_keys
.into_iter()
.map(|pk| (pk.fingerprint, pk.into()))
.collect();
App::new(
Cache {
network,
coins: coins.coins,
rescan_progress: None,
sync_progress: 1.0, // Remote backend is always synced
datadir_path: liana_dir.clone(),
blockheight: wallet.tip_height.unwrap_or(0),
// We ignore last poll fields for remote backend.
last_poll_timestamp: None,
last_poll_at_startup: None,
},
Arc::new(
Wallet::new(wallet.descriptor)
.with_name(wallet.name)
.with_alias(wallet.metadata.wallet_alias)
.with_pinned_at(wallet_settings.pinned_at)
.with_key_aliases(aliases)
.with_provider_keys(provider_keys)
.with_hardware_wallets(hws)
.load_hotsigners(&liana_dir, network)
.expect("Datadir should be conform"),
),
config,
Arc::new(remote_backend),
liana_dir,
None,
false,
)
}
pub struct Config {
pub liana_directory: LianaDirectory,
network: Option<bitcoin::Network>,
}
impl Config {
pub fn new(liana_directory: LianaDirectory, network: Option<bitcoin::Network>) -> Self {
Self {
liana_directory,
network,
}
}
}

View File

@ -5,6 +5,7 @@ pub mod delete;
pub mod dir;
pub mod download;
pub mod export;
pub mod gui;
pub mod hw;
pub mod installer;
pub mod launcher;

View File

@ -1,46 +1,22 @@
#![windows_subsystem = "windows"]
use std::{
collections::HashMap, error::Error, io::Write, path::PathBuf, process, str::FromStr, sync::Arc,
};
use std::{error::Error, io::Write, path::PathBuf, process, str::FromStr};
#[cfg(target_os = "linux")]
use iced::window::settings::PlatformSpecific;
use iced::{
event::{self, Event},
keyboard,
widget::{focus_next, focus_previous},
Settings, Size, Subscription, Task,
};
use tracing::{error, info};
use iced::{Settings, Size};
use tracing::error;
use tracing_subscriber::filter::LevelFilter;
extern crate serde;
extern crate serde_json;
use liana::miniscript::bitcoin;
use liana_ui::{component::text, font, image, theme, widget::Element};
use lianad::commands::ListCoinsResult;
use liana_ui::{component::text, font, image, theme};
use liana_gui::{
app::{
self,
cache::Cache,
settings::{update_settings_file, WalletSettings},
wallet::Wallet,
App,
},
dir::LianaDirectory,
export::import_backup_at_launch,
hw::HardwareWalletConfig,
installer::{self, Installer},
launcher::{self, Launcher},
loader::{self, Loader},
logger::Logger,
gui::{Config, GUI},
node::bitcoind::delete_all_bitcoind_locks_for_process,
services::connect::{
client::backend::{api, BackendWalletClient},
login,
},
VERSION,
};
@ -92,448 +68,6 @@ Options:
Ok(res)
}
pub struct GUI {
state: State,
logger: Logger,
// if set up, it overrides the level filter of the logger.
log_level: Option<LevelFilter>,
}
enum State {
Launcher(Box<Launcher>),
Installer(Box<Installer>),
Loader(Box<Loader>),
Login(Box<login::LianaLiteLogin>),
App(App),
}
#[derive(Debug)]
pub enum Key {
Tab(bool),
}
#[derive(Debug)]
pub enum Message {
CtrlC,
FontLoaded(Result<(), iced::font::Error>),
Launch(Box<launcher::Message>),
Install(Box<installer::Message>),
Load(Box<loader::Message>),
Run(Box<app::Message>),
Login(Box<login::Message>),
KeyPressed(Key),
Event(iced::Event),
}
impl From<Result<(), iced::font::Error>> for Message {
fn from(value: Result<(), iced::font::Error>) -> Self {
Self::FontLoaded(value)
}
}
async fn ctrl_c() -> Result<(), ()> {
if let Err(e) = tokio::signal::ctrl_c().await {
error!("{}", e);
};
info!("Signal received, exiting");
Ok(())
}
impl GUI {
fn title(&self) -> String {
match &self.state {
State::Installer(_) => format!("Liana v{} Installer", VERSION),
State::App(a) => format!("Liana v{} {}", VERSION, a.title()),
_ => format!("Liana v{}", VERSION),
}
}
fn new((config, log_level): (Config, Option<LevelFilter>)) -> (GUI, Task<Message>) {
let logger = Logger::setup(log_level.unwrap_or(LevelFilter::INFO));
let mut cmds = vec![Task::perform(ctrl_c(), |_| Message::CtrlC)];
let (launcher, command) = Launcher::new(config.liana_directory, config.network);
cmds.push(command.map(|msg| Message::Launch(Box::new(msg))));
(
Self {
state: State::Launcher(Box::new(launcher)),
logger,
log_level,
},
Task::batch(cmds),
)
}
fn update(&mut self, message: Message) -> Task<Message> {
match (&mut self.state, message) {
(_, Message::CtrlC)
| (_, Message::Event(iced::Event::Window(iced::window::Event::CloseRequested))) => {
match &mut self.state {
State::Loader(s) => s.stop(),
State::Launcher(s) => s.stop(),
State::Installer(s) => s.stop(),
State::App(s) => s.stop(),
State::Login(_) => {}
};
iced::window::get_latest().and_then(iced::window::close)
}
(_, Message::KeyPressed(Key::Tab(shift))) => {
log::debug!("Tab pressed!");
if shift {
focus_previous()
} else {
focus_next()
}
}
(State::Launcher(l), Message::Launch(msg)) => match *msg {
launcher::Message::Install(datadir, network, init) => {
if !datadir.exists() {
// datadir is created right before launching the installer
// so logs can go in <datadir_path>/installer.log
if let Err(e) = datadir.init() {
error!("Failed to create datadir: {}", e);
} else {
info!(
"Created a fresh data directory at {}",
&datadir.path().to_string_lossy()
);
}
}
self.logger.set_installer_mode(
datadir.clone(),
self.log_level.unwrap_or(LevelFilter::INFO),
);
let (install, command) = Installer::new(datadir, network, None, init);
self.state = State::Installer(Box::new(install));
command.map(|msg| Message::Install(Box::new(msg)))
}
launcher::Message::Run(datadir_path, cfg, network, settings) => {
self.logger.set_running_mode(
datadir_path.clone(),
network,
self.log_level
.unwrap_or_else(|| cfg.log_level().unwrap_or(LevelFilter::INFO)),
);
if settings.remote_backend_auth.is_some() {
let (login, command) =
login::LianaLiteLogin::new(datadir_path, network, settings);
self.state = State::Login(Box::new(login));
command.map(|msg| Message::Login(Box::new(msg)))
} else {
let (loader, command) =
Loader::new(datadir_path, cfg, network, None, None, settings);
self.state = State::Loader(Box::new(loader));
command.map(|msg| Message::Load(Box::new(msg)))
}
}
_ => l.update(*msg).map(|msg| Message::Launch(Box::new(msg))),
},
(State::Login(l), Message::Login(msg)) => match *msg {
login::Message::View(login::ViewMessage::BackToLauncher(network)) => {
let (launcher, command) = Launcher::new(l.datadir.clone(), Some(network));
self.state = State::Launcher(Box::new(launcher));
command.map(|msg| Message::Launch(Box::new(msg)))
}
login::Message::Install(remote_backend) => {
let (install, command) = Installer::new(
l.datadir.clone(),
l.network,
remote_backend,
installer::UserFlow::CreateWallet,
);
self.state = State::Installer(Box::new(install));
command.map(|msg| Message::Install(Box::new(msg)))
}
login::Message::Run(Ok((backend_client, wallet, coins))) => {
let config = app::Config::from_file(
&l.datadir
.network_directory(l.network)
.path()
.join(app::config::DEFAULT_FILE_NAME),
)
.expect("A gui configuration file must be present");
self.logger.set_running_mode(
l.datadir.clone(),
l.network,
config.log_level().unwrap_or(LevelFilter::INFO),
);
let (app, command) = create_app_with_remote_backend(
l.settings.clone(),
backend_client,
wallet,
coins,
l.datadir.clone(),
l.network,
config,
);
self.state = State::App(app);
command.map(|msg| Message::Run(Box::new(msg)))
}
_ => l.update(*msg).map(|msg| Message::Login(Box::new(msg))),
},
(State::Installer(i), Message::Install(msg)) => {
if let installer::Message::Exit(settings, internal_bitcoind, remove_log) = *msg {
if settings.remote_backend_auth.is_some() {
let (login, command) =
login::LianaLiteLogin::new(i.datadir.clone(), i.network, *settings);
self.state = State::Login(Box::new(login));
command.map(|msg| Message::Login(Box::new(msg)))
} else {
let cfg = app::Config::from_file(
&i.datadir
.network_directory(i.network)
.path()
.join(app::config::DEFAULT_FILE_NAME),
)
.expect("A gui configuration file must be present");
self.logger.set_running_mode(
i.datadir.clone(),
i.network,
self.log_level
.unwrap_or_else(|| cfg.log_level().unwrap_or(LevelFilter::INFO)),
);
if remove_log {
self.logger.remove_install_log_file(i.datadir.clone());
}
let (loader, command) = Loader::new(
i.datadir.clone(),
cfg,
i.network,
internal_bitcoind,
i.context.backup.take(),
*settings,
);
self.state = State::Loader(Box::new(loader));
command.map(|msg| Message::Load(Box::new(msg)))
}
} else if let installer::Message::BackToLauncher(network) = *msg {
let (launcher, command) = Launcher::new(i.destination_path(), Some(network));
self.state = State::Launcher(Box::new(launcher));
command.map(|msg| Message::Launch(Box::new(msg)))
} else {
i.update(*msg).map(|msg| Message::Install(Box::new(msg)))
}
}
(State::Loader(loader), Message::Load(msg)) => match *msg {
loader::Message::View(loader::ViewMessage::SwitchNetwork) => {
let (launcher, command) =
Launcher::new(loader.datadir_path.clone(), Some(loader.network));
self.state = State::Launcher(Box::new(launcher));
command.map(|msg| Message::Launch(Box::new(msg)))
}
loader::Message::Synced(Ok((wallet, cache, daemon, bitcoind, backup))) => {
if let Some(backup) = backup {
let config = loader.gui_config.clone();
let datadir = loader.datadir_path.clone();
Task::perform(
async move {
import_backup_at_launch(
cache, wallet, config, daemon, datadir, bitcoind, backup,
)
.await
},
|r| {
let r = r.map_err(loader::Error::RestoreBackup);
Message::Load(Box::new(loader::Message::App(
r, /* restored_from_backup */ true,
)))
},
)
} else {
let (app, command) = App::new(
cache,
wallet,
loader.gui_config.clone(),
daemon,
loader.datadir_path.clone(),
bitcoind,
false,
);
self.state = State::App(app);
command.map(|msg| Message::Run(Box::new(msg)))
}
}
loader::Message::App(
Ok((cache, wallet, config, daemon, datadir, bitcoind)),
restored_from_backup,
) => {
let (app, command) = App::new(
cache,
wallet,
config,
daemon,
datadir,
bitcoind,
restored_from_backup,
);
self.state = State::App(app);
command.map(|msg| Message::Run(Box::new(msg)))
}
loader::Message::App(Err(e), _) => {
tracing::error!("Failed to import backup: {e}");
Task::none()
}
_ => loader.update(*msg).map(|msg| Message::Load(Box::new(msg))),
},
(State::App(i), Message::Run(msg)) => {
i.update(*msg).map(|msg| Message::Run(Box::new(msg)))
}
_ => Task::none(),
}
}
fn subscription(&self) -> Subscription<Message> {
Subscription::batch(vec![
match &self.state {
State::Installer(v) => v.subscription().map(|msg| Message::Install(Box::new(msg))),
State::Loader(v) => v.subscription().map(|msg| Message::Load(Box::new(msg))),
State::App(v) => v.subscription().map(|msg| Message::Run(Box::new(msg))),
State::Launcher(v) => v.subscription().map(|msg| Message::Launch(Box::new(msg))),
State::Login(_) => Subscription::none(),
},
iced::event::listen_with(|event, status, _| match (&event, status) {
(
Event::Keyboard(keyboard::Event::KeyPressed {
key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Tab),
modifiers,
..
}),
event::Status::Ignored,
) => Some(Message::KeyPressed(Key::Tab(modifiers.shift()))),
(
iced::Event::Window(iced::window::Event::CloseRequested),
event::Status::Ignored,
) => Some(Message::Event(event)),
_ => None,
}),
])
}
fn view(&self) -> Element<Message> {
match &self.state {
State::Installer(v) => v.view().map(|msg| Message::Install(Box::new(msg))),
State::App(v) => v.view().map(|msg| Message::Run(Box::new(msg))),
State::Launcher(v) => v.view().map(|msg| Message::Launch(Box::new(msg))),
State::Loader(v) => v.view().map(|msg| Message::Load(Box::new(msg))),
State::Login(v) => v.view().map(|msg| Message::Login(Box::new(msg))),
}
}
fn scale_factor(&self) -> f64 {
1.0
}
}
pub fn create_app_with_remote_backend(
wallet_settings: WalletSettings,
remote_backend: BackendWalletClient,
wallet: api::Wallet,
coins: ListCoinsResult,
liana_dir: LianaDirectory,
network: bitcoin::Network,
config: app::Config,
) -> (app::App, iced::Task<app::Message>) {
// If someone modified the wallet_alias on Liana-Connect,
// then the new alias is imported and stored in the settings file.
if wallet.metadata.wallet_alias != wallet_settings.alias {
let network_directory = liana_dir.network_directory(network);
if let Err(e) = tokio::runtime::Handle::current().block_on(async {
update_settings_file(&network_directory, |mut settings| {
if let Some(w) = settings
.wallets
.iter_mut()
.find(|w| w.wallet_id() == wallet_settings.wallet_id())
{
w.alias = wallet.metadata.wallet_alias.clone();
tracing::info!("Wallet alias was changed. Settings updated.");
}
settings
})
.await
}) {
tracing::error!("Failed to update wallet settings with remote alias: {}", e);
}
}
let hws: Vec<HardwareWalletConfig> = wallet
.metadata
.ledger_hmacs
.into_iter()
.map(|ledger_hmac| HardwareWalletConfig {
kind: async_hwi::DeviceKind::Ledger.to_string(),
fingerprint: ledger_hmac.fingerprint,
token: ledger_hmac.hmac,
})
.collect();
let aliases: HashMap<bitcoin::bip32::Fingerprint, String> = wallet
.metadata
.fingerprint_aliases
.into_iter()
.filter_map(|a| {
if a.user_id == remote_backend.user_id() {
Some((a.fingerprint, a.alias))
} else {
None
}
})
.collect();
let provider_keys: HashMap<_, _> = wallet
.metadata
.provider_keys
.into_iter()
.map(|pk| (pk.fingerprint, pk.into()))
.collect();
App::new(
Cache {
network,
coins: coins.coins,
rescan_progress: None,
sync_progress: 1.0, // Remote backend is always synced
datadir_path: liana_dir.clone(),
blockheight: wallet.tip_height.unwrap_or(0),
// We ignore last poll fields for remote backend.
last_poll_timestamp: None,
last_poll_at_startup: None,
},
Arc::new(
Wallet::new(wallet.descriptor)
.with_name(wallet.name)
.with_alias(wallet.metadata.wallet_alias)
.with_pinned_at(wallet_settings.pinned_at)
.with_key_aliases(aliases)
.with_provider_keys(provider_keys)
.with_hardware_wallets(hws)
.load_hotsigners(&liana_dir, network)
.expect("Datadir should be conform"),
),
config,
Arc::new(remote_backend),
liana_dir,
None,
false,
)
}
pub struct Config {
liana_directory: LianaDirectory,
network: Option<bitcoin::Network>,
}
impl Config {
pub fn new(liana_directory: LianaDirectory, network: Option<bitcoin::Network>) -> Self {
Self {
liana_directory,
network,
}
}
}
fn main() -> Result<(), Box<dyn Error>> {
let args = parse_args(std::env::args().collect())?;
let config = match args.as_slice() {