Add login page to connect to remote backend
This commit is contained in:
parent
224aa66231
commit
92ee94edbc
10
gui/Cargo.lock
generated
10
gui/Cargo.lock
generated
@ -1286,6 +1286,15 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "email_address"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.33"
|
||||
@ -2609,6 +2618,7 @@ dependencies = [
|
||||
"bitcoin_hashes 0.12.0",
|
||||
"chrono",
|
||||
"dirs 3.0.2",
|
||||
"email_address",
|
||||
"flate2",
|
||||
"hex",
|
||||
"iced",
|
||||
|
||||
@ -24,6 +24,9 @@ hex = "0.4.3"
|
||||
iced = { version = "0.12.1", default-features = false, features = ["tokio", "svg", "qr_code", "image", "lazy", "wgpu", "advanced"] }
|
||||
iced_runtime = "0.12.1"
|
||||
|
||||
# Used to verify RFC-compliance of an email
|
||||
email_address = "0.2.7"
|
||||
|
||||
tokio = {version = "1.21.0", features = ["signal"]}
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing_subscriber::filter;
|
||||
|
||||
@ -47,6 +49,26 @@ impl Config {
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn to_file(&self, path: &Path) -> Result<(), ConfigError> {
|
||||
let content = toml::to_string(&self)
|
||||
.map_err(|e| ConfigError::WritingFile(format!("Failed to serialize config: {}", e)))?;
|
||||
|
||||
let mut config_file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(path)
|
||||
.map_err(|e| ConfigError::WritingFile(e.to_string()))?;
|
||||
|
||||
config_file.write_all(content.as_bytes()).map_err(|e| {
|
||||
tracing::warn!("failed to write to file: {:?}", e);
|
||||
ConfigError::WritingFile(e.to_string())
|
||||
})?;
|
||||
|
||||
tracing::info!("Done writing gui configuration file");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// TODO: Deserialize directly in the struct.
|
||||
pub fn log_level(&self) -> Result<filter::LevelFilter, ConfigError> {
|
||||
if let Some(level) = &self.log_level {
|
||||
@ -72,6 +94,7 @@ pub enum ConfigError {
|
||||
InvalidField(&'static str, String),
|
||||
NotFound,
|
||||
ReadingFile(String),
|
||||
WritingFile(String),
|
||||
Unexpected(String),
|
||||
}
|
||||
|
||||
@ -83,6 +106,7 @@ impl std::fmt::Display for ConfigError {
|
||||
write!(f, "Config field {} is invalid: {}", field, message)
|
||||
}
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
@ -255,11 +255,12 @@ impl App {
|
||||
Message::Tick => {
|
||||
let daemon = self.daemon.clone();
|
||||
let datadir_path = self.cache.datadir_path.clone();
|
||||
let network = self.cache.network;
|
||||
Command::perform(
|
||||
async move {
|
||||
// we check every 10 second if the daemon poller is alive
|
||||
// or if the access token is not expired.
|
||||
daemon.is_alive().await?;
|
||||
daemon.is_alive(&datadir_path, network).await?;
|
||||
|
||||
let info = daemon.get_info().await?;
|
||||
let coins = daemon
|
||||
|
||||
@ -8,7 +8,7 @@ use std::path::PathBuf;
|
||||
use liana::miniscript::bitcoin::{bip32::Fingerprint, Network};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{app::wallet::Wallet, hw::HardwareWalletConfig};
|
||||
use crate::hw::HardwareWalletConfig;
|
||||
|
||||
pub const DEFAULT_FILE_NAME: &str = "settings.json";
|
||||
|
||||
@ -59,14 +59,26 @@ impl Settings {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct AuthConfig {
|
||||
pub email: String,
|
||||
pub wallet_id: String,
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct WalletSetting {
|
||||
pub name: String,
|
||||
pub descriptor_checksum: String,
|
||||
// if wallet is using remote backend, then this information is stored on the remote backend
|
||||
// wallet metadata
|
||||
#[serde(default)]
|
||||
pub keys: Vec<KeySetting>,
|
||||
// if wallet is using remote backend, then this information is stored on the remote backend
|
||||
// wallet metadata
|
||||
#[serde(default)]
|
||||
pub hardware_wallets: Vec<HardwareWalletConfig>,
|
||||
pub remote_backend_auth: Option<AuthConfig>,
|
||||
}
|
||||
|
||||
impl WalletSetting {
|
||||
@ -79,25 +91,6 @@ 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,
|
||||
|
||||
@ -105,7 +105,23 @@ impl Wallet {
|
||||
}
|
||||
Err(settings::SettingsError::NotFound) => {
|
||||
let s = settings::Settings {
|
||||
wallets: vec![settings::WalletSetting::from(&self)],
|
||||
wallets: vec![settings::WalletSetting {
|
||||
name: self.name.clone(),
|
||||
hardware_wallets: self.hardware_wallets.clone(),
|
||||
keys: self
|
||||
.keys_aliases
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(master_fingerprint, name)| settings::KeySetting {
|
||||
name,
|
||||
master_fingerprint,
|
||||
})
|
||||
.collect(),
|
||||
descriptor_checksum: self.descriptor_checksum(),
|
||||
// Only local wallet from previous version of Liana GUI may not have a
|
||||
// settings.json file
|
||||
remote_backend_auth: None,
|
||||
}],
|
||||
};
|
||||
|
||||
tracing::info!("Settings file not found, creating one");
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Debug;
|
||||
use std::iter::FromIterator;
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use liana::commands::{CoinStatus, CreateRecoveryResult};
|
||||
@ -15,7 +16,7 @@ pub mod jsonrpc;
|
||||
use liana::{
|
||||
commands::LabelItem,
|
||||
config::Config,
|
||||
miniscript::bitcoin::{address, psbt::Psbt, Address, OutPoint, Txid},
|
||||
miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid},
|
||||
};
|
||||
|
||||
use super::{model::*, Daemon, DaemonBackend, DaemonError};
|
||||
@ -63,7 +64,7 @@ impl<C: Client + Send + Sync + Debug> Daemon for Lianad<C> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn is_alive(&self) -> Result<(), DaemonError> {
|
||||
async fn is_alive(&self, _datadir: &Path, _network: Network) -> Result<(), DaemonError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::{model::*, Daemon, DaemonBackend, DaemonError};
|
||||
@ -6,7 +7,7 @@ use async_trait::async_trait;
|
||||
use liana::{
|
||||
commands::{CoinStatus, LabelItem},
|
||||
config::Config,
|
||||
miniscript::bitcoin::{address, psbt::Psbt, Address, OutPoint, Txid},
|
||||
miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid},
|
||||
DaemonControl, DaemonHandle,
|
||||
};
|
||||
|
||||
@ -57,7 +58,7 @@ impl Daemon for EmbeddedDaemon {
|
||||
Some(&self.config)
|
||||
}
|
||||
|
||||
async fn is_alive(&self) -> Result<(), DaemonError> {
|
||||
async fn is_alive(&self, _datadir: &Path, _network: Network) -> Result<(), DaemonError> {
|
||||
let mut handle = self.handle.lock().await;
|
||||
if let Some(h) = handle.as_ref() {
|
||||
if h.is_alive() {
|
||||
|
||||
@ -7,6 +7,7 @@ use std::convert::TryInto;
|
||||
use std::fmt::Debug;
|
||||
use std::io::ErrorKind;
|
||||
use std::iter::FromIterator;
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
@ -14,7 +15,7 @@ use liana::{
|
||||
commands::{CoinStatus, LabelItem, TransactionInfo},
|
||||
config::Config,
|
||||
miniscript::bitcoin::{
|
||||
address, bip32::Fingerprint, psbt::Psbt, secp256k1, Address, OutPoint, Txid,
|
||||
address, bip32::Fingerprint, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid,
|
||||
},
|
||||
StartupError,
|
||||
};
|
||||
@ -70,7 +71,7 @@ pub enum DaemonBackend {
|
||||
pub trait Daemon: Debug {
|
||||
fn backend(&self) -> DaemonBackend;
|
||||
fn config(&self) -> Option<&Config>;
|
||||
async fn is_alive(&self) -> Result<(), DaemonError>;
|
||||
async fn is_alive(&self, datadir: &Path, network: Network) -> Result<(), DaemonError>;
|
||||
async fn stop(&self) -> Result<(), DaemonError>;
|
||||
async fn get_info(&self) -> Result<model::GetInfoResult, DaemonError>;
|
||||
async fn get_new_address(&self) -> Result<model::GetAddressResult, DaemonError>;
|
||||
|
||||
18
gui/src/datadir.rs
Normal file
18
gui/src/datadir.rs
Normal file
@ -0,0 +1,18 @@
|
||||
pub fn create_directory(datadir_path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[cfg(unix)]
|
||||
return {
|
||||
use std::fs::DirBuilder;
|
||||
use std::os::unix::fs::DirBuilderExt;
|
||||
|
||||
let mut builder = DirBuilder::new();
|
||||
builder.mode(0o700).recursive(true).create(datadir_path)?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
// TODO: permissions on Windows..
|
||||
#[cfg(not(unix))]
|
||||
return {
|
||||
std::fs::create_dir_all(datadir_path)?;
|
||||
Ok(())
|
||||
};
|
||||
}
|
||||
@ -3,22 +3,35 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
settings::{KeySetting, Settings, WalletSetting},
|
||||
wallet::wallet_name,
|
||||
},
|
||||
app::settings::KeySetting,
|
||||
bitcoind::{Bitcoind, InternalBitcoindConfig},
|
||||
hw::HardwareWalletConfig,
|
||||
lianalite::client::backend::{BackendClient, BackendWalletClient},
|
||||
signer::Signer,
|
||||
};
|
||||
use async_hwi::DeviceKind;
|
||||
use liana::{
|
||||
config::Config,
|
||||
config::{BitcoinConfig, BitcoindConfig},
|
||||
descriptors::LianaDescriptor,
|
||||
miniscript::bitcoin,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RemoteBackend {
|
||||
// The installer will have to create a wallet from the created descriptor.
|
||||
WithoutWallet(BackendClient),
|
||||
// The installer will have to fetch the wallet and only install the missing configuration files.
|
||||
WithWallet(BackendWalletClient),
|
||||
}
|
||||
|
||||
impl RemoteBackend {
|
||||
pub fn user_email(&self) -> &str {
|
||||
match self {
|
||||
Self::WithWallet(b) => b.user_email(),
|
||||
Self::WithoutWallet(b) => b.user_email(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Context {
|
||||
pub bitcoin_config: BitcoinConfig,
|
||||
@ -27,6 +40,7 @@ pub struct Context {
|
||||
pub keys: Vec<KeySetting>,
|
||||
pub hws: Vec<(DeviceKind, bitcoin::bip32::Fingerprint, Option<[u8; 32]>)>,
|
||||
pub data_dir: PathBuf,
|
||||
pub network: bitcoin::Network,
|
||||
pub hw_is_used: bool,
|
||||
// In case a user entered a mnemonic,
|
||||
// we dont want to override the generated signer with it.
|
||||
@ -34,10 +48,15 @@ pub struct Context {
|
||||
pub bitcoind_is_external: bool,
|
||||
pub internal_bitcoind_config: Option<InternalBitcoindConfig>,
|
||||
pub internal_bitcoind: Option<Bitcoind>,
|
||||
pub remote_backend: Option<RemoteBackend>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new(network: bitcoin::Network, data_dir: PathBuf) -> Self {
|
||||
pub fn new(
|
||||
network: bitcoin::Network,
|
||||
data_dir: PathBuf,
|
||||
remote_backend: Option<RemoteBackend>,
|
||||
) -> Self {
|
||||
Self {
|
||||
bitcoin_config: BitcoinConfig {
|
||||
network,
|
||||
@ -48,52 +67,13 @@ impl Context {
|
||||
bitcoind_config: None,
|
||||
descriptor: None,
|
||||
data_dir,
|
||||
network,
|
||||
hw_is_used: false,
|
||||
recovered_signer: None,
|
||||
bitcoind_is_external: true,
|
||||
internal_bitcoind_config: None,
|
||||
internal_bitcoind: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_gui_settings(&self) -> Settings {
|
||||
let hardware_wallets = self
|
||||
.hws
|
||||
.iter()
|
||||
.filter_map(|(kind, fingerprint, token)| {
|
||||
token
|
||||
.as_ref()
|
||||
.map(|token| HardwareWalletConfig::new(kind, *fingerprint, token))
|
||||
})
|
||||
.collect();
|
||||
let descriptor = self
|
||||
.descriptor
|
||||
.as_ref()
|
||||
.expect("Must be a descriptor at this point");
|
||||
Settings {
|
||||
wallets: vec![WalletSetting {
|
||||
name: wallet_name(descriptor),
|
||||
descriptor_checksum: descriptor
|
||||
.to_string()
|
||||
.split_once('#')
|
||||
.map(|(_, checksum)| checksum)
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
keys: self.keys.clone(),
|
||||
hardware_wallets,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_daemon_config(&self) -> Config {
|
||||
Config {
|
||||
#[cfg(unix)]
|
||||
daemon: false,
|
||||
log_level: log::LevelFilter::Info,
|
||||
main_descriptor: self.descriptor.clone().unwrap(),
|
||||
data_dir: Some(self.data_dir.clone()),
|
||||
bitcoin_config: self.bitcoin_config.clone(),
|
||||
bitcoind_config: self.bitcoind_config.clone(),
|
||||
remote_backend,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
use liana::miniscript::{bitcoin::bip32::Fingerprint, DescriptorPublicKey};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::Error;
|
||||
use super::{context, Error};
|
||||
use crate::{
|
||||
bitcoind::{Bitcoind, ConfigField, RpcAuthType},
|
||||
download::Progress,
|
||||
hw::HardwareWalletMessage,
|
||||
lianalite::client::{auth::AuthClient, backend::api},
|
||||
};
|
||||
use async_hwi::{DeviceKind, Version};
|
||||
|
||||
@ -28,6 +29,7 @@ pub enum Message {
|
||||
UseHotSigner,
|
||||
Installed(Result<PathBuf, Error>),
|
||||
CreateTaprootDescriptor(bool),
|
||||
SelectBackend(SelectBackend),
|
||||
SelectBitcoindType(SelectBitcoindTypeMsg),
|
||||
InternalBitcoind(InternalBitcoindMsg),
|
||||
DefineBitcoind(DefineBitcoind),
|
||||
@ -39,6 +41,21 @@ pub enum Message {
|
||||
ImportMnemonic(bool),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SelectBackend {
|
||||
// view messages
|
||||
RequestOTP,
|
||||
EditEmail,
|
||||
EmailEdited(String),
|
||||
OTPEdited(String),
|
||||
ContinueWithRemoteBackend,
|
||||
ContinueWithLocalWallet,
|
||||
// Commands messages
|
||||
OTPRequested(Result<(AuthClient, String), Error>),
|
||||
OTPResent(Result<(), Error>),
|
||||
Connected(Result<(context::RemoteBackend, Option<api::Wallet>), Error>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DefineBitcoind {
|
||||
ConfigFieldEdited(ConfigField, String),
|
||||
|
||||
@ -5,34 +5,49 @@ mod step;
|
||||
mod view;
|
||||
|
||||
use iced::{clipboard, Command, Subscription};
|
||||
use liana::miniscript::bitcoin::{self, Network};
|
||||
use liana::{
|
||||
config::Config,
|
||||
miniscript::bitcoin::{self, Network},
|
||||
};
|
||||
use liana_ui::{
|
||||
component::network_banner,
|
||||
widget::{Column, Element},
|
||||
};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use context::Context;
|
||||
use context::{Context, RemoteBackend};
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{
|
||||
app::{config as gui_config, settings as gui_settings},
|
||||
hw::HardwareWallets,
|
||||
app::{
|
||||
config as gui_config, settings as gui_settings,
|
||||
settings::{AuthConfig, Settings, SettingsError, WalletSetting},
|
||||
wallet::wallet_name,
|
||||
},
|
||||
daemon::DaemonError,
|
||||
datadir::create_directory,
|
||||
hw::{HardwareWalletConfig, HardwareWallets},
|
||||
lianalite::client::{
|
||||
auth::AuthError,
|
||||
backend::{BackendClient, BackendWalletClient},
|
||||
},
|
||||
signer::Signer,
|
||||
};
|
||||
|
||||
pub use message::Message;
|
||||
use step::{
|
||||
BackupDescriptor, BackupMnemonic, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor,
|
||||
InternalBitcoindStep, RecoverMnemonic, RegisterDescriptor, SelectBitcoindTypeStep, ShareXpubs,
|
||||
Step, Welcome,
|
||||
BackupDescriptor, BackupMnemonic, ChooseBackend, DefineBitcoind, DefineDescriptor, Final,
|
||||
ImportDescriptor, InternalBitcoindStep, RecoverMnemonic, RegisterDescriptor,
|
||||
SelectBitcoindTypeStep, ShareXpubs, Step, Welcome,
|
||||
};
|
||||
|
||||
pub struct Installer {
|
||||
network: bitcoin::Network,
|
||||
pub network: bitcoin::Network,
|
||||
pub datadir: PathBuf,
|
||||
|
||||
current: usize,
|
||||
steps: Vec<Box<dyn Step>>,
|
||||
hws: HardwareWallets,
|
||||
@ -62,14 +77,20 @@ impl Installer {
|
||||
pub fn new(
|
||||
destination_path: PathBuf,
|
||||
network: bitcoin::Network,
|
||||
remote_backend: Option<BackendClient>,
|
||||
) -> (Installer, Command<Message>) {
|
||||
(
|
||||
Installer {
|
||||
network,
|
||||
datadir: destination_path.clone(),
|
||||
current: 0,
|
||||
hws: HardwareWallets::new(destination_path.clone(), network),
|
||||
steps: vec![Welcome::default().into()],
|
||||
context: Context::new(network, destination_path),
|
||||
context: Context::new(
|
||||
network,
|
||||
destination_path,
|
||||
remote_backend.map(RemoteBackend::WithoutWallet),
|
||||
),
|
||||
signer: Arc::new(Mutex::new(Signer::generate(network).unwrap())),
|
||||
},
|
||||
Command::none(),
|
||||
@ -148,6 +169,7 @@ impl Installer {
|
||||
BackupMnemonic::new(self.signer.clone()).into(),
|
||||
BackupDescriptor::default().into(),
|
||||
RegisterDescriptor::new_create_wallet().into(),
|
||||
ChooseBackend::new(self.network).into(),
|
||||
SelectBitcoindTypeStep::new().into(),
|
||||
InternalBitcoindStep::new(&self.context.data_dir).into(),
|
||||
DefineBitcoind::new().into(),
|
||||
@ -165,6 +187,7 @@ impl Installer {
|
||||
Message::ImportWallet => {
|
||||
self.steps = vec![
|
||||
Welcome::default().into(),
|
||||
ChooseBackend::new(self.network).into(),
|
||||
ImportDescriptor::new(self.network).into(),
|
||||
RecoverMnemonic::default().into(),
|
||||
RegisterDescriptor::new_import_wallet().into(),
|
||||
@ -173,6 +196,7 @@ impl Installer {
|
||||
DefineBitcoind::new().into(),
|
||||
Final::new().into(),
|
||||
];
|
||||
|
||||
self.next()
|
||||
}
|
||||
Message::HardwareWallets(msg) => match self.hws.update(msg) {
|
||||
@ -194,10 +218,24 @@ impl Installer {
|
||||
.get_mut(self.current)
|
||||
.expect("There is always a step")
|
||||
.update(&mut self.hws, message);
|
||||
Command::perform(
|
||||
install(self.context.clone(), self.signer.clone()),
|
||||
Message::Installed,
|
||||
)
|
||||
match &self.context.remote_backend {
|
||||
Some(RemoteBackend::WithoutWallet(backend)) => Command::perform(
|
||||
create_remote_wallet(
|
||||
self.context.clone(),
|
||||
self.signer.clone(),
|
||||
backend.clone(),
|
||||
),
|
||||
Message::Installed,
|
||||
),
|
||||
Some(RemoteBackend::WithWallet(backend)) => Command::perform(
|
||||
import_remote_wallet(self.context.clone(), backend.clone()),
|
||||
Message::Installed,
|
||||
),
|
||||
None => Command::perform(
|
||||
install_local_wallet(self.context.clone(), self.signer.clone()),
|
||||
Message::Installed,
|
||||
),
|
||||
}
|
||||
}
|
||||
Message::Installed(Err(e)) => {
|
||||
let mut data_dir = self.context.data_dir.clone();
|
||||
@ -252,7 +290,11 @@ impl Installer {
|
||||
.steps
|
||||
.get(self.current)
|
||||
.expect("There is always a step")
|
||||
.view(&self.hws, self.progress());
|
||||
.view(
|
||||
&self.hws,
|
||||
self.progress(),
|
||||
self.context.remote_backend.as_ref().map(|b| b.user_email()),
|
||||
);
|
||||
|
||||
if self.network != Network::Bitcoin {
|
||||
Column::with_children(vec![network_banner(self.network).into(), content]).into()
|
||||
@ -275,8 +317,11 @@ pub fn daemon_check(cfg: liana::config::Config) -> Result<(), Error> {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn install(ctx: Context, signer: Arc<Mutex<Signer>>) -> Result<PathBuf, Error> {
|
||||
let mut cfg: liana::config::Config = ctx.extract_daemon_config();
|
||||
pub async fn install_local_wallet(
|
||||
ctx: Context,
|
||||
signer: Arc<Mutex<Signer>>,
|
||||
) -> Result<PathBuf, Error> {
|
||||
let mut cfg: liana::config::Config = extract_daemon_config(&ctx);
|
||||
let data_dir = cfg.data_dir.unwrap();
|
||||
|
||||
let data_dir = data_dir
|
||||
@ -290,6 +335,8 @@ pub async fn install(ctx: Context, signer: Arc<Mutex<Signer>>) -> Result<PathBuf
|
||||
|
||||
let mut network_datadir_path = data_dir;
|
||||
network_datadir_path.push(cfg.bitcoin_config.network.to_string());
|
||||
create_directory(&network_datadir_path)
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to create datadir path: {}", e)))?;
|
||||
|
||||
// Step needed because of ValueAfterTable error in the toml serialize implementation.
|
||||
let daemon_config = toml::Value::try_from(&cfg)
|
||||
@ -350,7 +397,7 @@ pub async fn install(ctx: Context, signer: Arc<Mutex<Signer>>) -> Result<PathBuf
|
||||
info!("Gui configuration file created");
|
||||
|
||||
// create liana GUI settings file
|
||||
let settings: gui_settings::Settings = ctx.extract_gui_settings();
|
||||
let settings: gui_settings::Settings = extract_local_gui_settings(&ctx).await;
|
||||
create_and_write_file(
|
||||
network_datadir_path,
|
||||
gui_settings::DEFAULT_FILE_NAME,
|
||||
@ -364,6 +411,171 @@ pub async fn install(ctx: Context, signer: Arc<Mutex<Signer>>) -> Result<PathBuf
|
||||
Ok(gui_config_path)
|
||||
}
|
||||
|
||||
pub async fn create_remote_wallet(
|
||||
ctx: Context,
|
||||
signer: Arc<Mutex<Signer>>,
|
||||
remote_backend: BackendClient,
|
||||
) -> Result<PathBuf, Error> {
|
||||
let data_dir = ctx
|
||||
.data_dir
|
||||
.canonicalize()
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to canonicalize datadir path: {}", e)))?;
|
||||
|
||||
let mut network_datadir_path = data_dir.clone();
|
||||
network_datadir_path.push(ctx.network.to_string());
|
||||
create_directory(&network_datadir_path)
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to create datadir path: {}", e)))?;
|
||||
|
||||
let descriptor = ctx
|
||||
.descriptor
|
||||
.as_ref()
|
||||
.expect("There must be a descriptor at this point");
|
||||
|
||||
if descriptor
|
||||
.to_string()
|
||||
.contains(&signer.lock().unwrap().fingerprint().to_string())
|
||||
{
|
||||
signer
|
||||
.lock()
|
||||
.unwrap()
|
||||
.store(&data_dir, ctx.network)
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?;
|
||||
|
||||
info!("Hot signer mnemonic stored");
|
||||
}
|
||||
|
||||
if let Some(signer) = &ctx.recovered_signer {
|
||||
signer
|
||||
.store(&data_dir, ctx.network)
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?;
|
||||
|
||||
info!("Recovered signer mnemonic stored");
|
||||
}
|
||||
|
||||
let mut network_datadir_path = data_dir;
|
||||
network_datadir_path.push(ctx.network.to_string());
|
||||
|
||||
// create liana GUI configuration file
|
||||
let gui_config_path = create_and_write_file(
|
||||
network_datadir_path.clone(),
|
||||
gui_config::DEFAULT_FILE_NAME,
|
||||
toml::to_string(&gui_config::Config {
|
||||
daemon_config_path: None,
|
||||
daemon_rpc_path: None,
|
||||
log_level: Some("info".to_string()),
|
||||
debug: Some(false),
|
||||
start_internal_bitcoind: false,
|
||||
})
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to serialize gui config: {}", e)))?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
|
||||
info!("Gui configuration file created");
|
||||
|
||||
let wallet = remote_backend
|
||||
.create_wallet(&wallet_name(descriptor), descriptor)
|
||||
.await
|
||||
.map_err(|e| Error::Unexpected(e.to_string()))?;
|
||||
|
||||
let hws: Vec<HardwareWalletConfig> = ctx
|
||||
.hws
|
||||
.iter()
|
||||
.filter_map(|(kind, fingerprint, token)| {
|
||||
token
|
||||
.as_ref()
|
||||
.map(|token| HardwareWalletConfig::new(kind, *fingerprint, token))
|
||||
})
|
||||
.collect();
|
||||
let descriptor_str = descriptor.to_string();
|
||||
let aliases = ctx
|
||||
.keys
|
||||
.iter()
|
||||
.filter_map(|k| {
|
||||
if descriptor_str.contains(&k.master_fingerprint.to_string()) {
|
||||
Some((k.master_fingerprint, k.name.to_string()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
remote_backend
|
||||
.update_wallet_metadata(&wallet.id, &aliases, &hws)
|
||||
.await
|
||||
.map_err(|e| Error::Unexpected(e.to_string()))?;
|
||||
|
||||
let remote_backend = remote_backend.connect_wallet(wallet).0;
|
||||
|
||||
// create liana GUI settings file
|
||||
let settings: gui_settings::Settings = extract_remote_gui_settings(&ctx, &remote_backend).await;
|
||||
create_and_write_file(
|
||||
network_datadir_path.clone(),
|
||||
gui_settings::DEFAULT_FILE_NAME,
|
||||
serde_json::to_string_pretty(&settings)
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to serialize settings: {}", e)))?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
|
||||
info!("Settings file created");
|
||||
|
||||
Ok(gui_config_path)
|
||||
}
|
||||
|
||||
pub async fn import_remote_wallet(
|
||||
ctx: Context,
|
||||
backend: BackendWalletClient,
|
||||
) -> Result<PathBuf, Error> {
|
||||
tracing::info!("Importing wallet from remote backend");
|
||||
|
||||
let data_dir = ctx
|
||||
.data_dir
|
||||
.canonicalize()
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to canonicalize datadir path: {}", e)))?;
|
||||
|
||||
if let Some(signer) = &ctx.recovered_signer {
|
||||
signer
|
||||
.store(&data_dir, ctx.network)
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?;
|
||||
|
||||
info!("Recovered signer mnemonic stored");
|
||||
}
|
||||
|
||||
let mut network_datadir_path = data_dir;
|
||||
network_datadir_path.push(ctx.network.to_string());
|
||||
create_directory(&network_datadir_path)
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to create datadir path: {}", e)))?;
|
||||
|
||||
// create liana GUI settings file
|
||||
let settings: gui_settings::Settings = extract_remote_gui_settings(&ctx, &backend).await;
|
||||
create_and_write_file(
|
||||
network_datadir_path.clone(),
|
||||
gui_settings::DEFAULT_FILE_NAME,
|
||||
serde_json::to_string_pretty(&settings)
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to serialize settings: {}", e)))?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
|
||||
info!("Settings file created");
|
||||
|
||||
// create liana GUI configuration file
|
||||
let gui_config_path = create_and_write_file(
|
||||
network_datadir_path.clone(),
|
||||
gui_config::DEFAULT_FILE_NAME,
|
||||
toml::to_string(&gui_config::Config {
|
||||
daemon_config_path: None,
|
||||
daemon_rpc_path: None,
|
||||
log_level: Some("info".to_string()),
|
||||
debug: Some(false),
|
||||
start_internal_bitcoind: false,
|
||||
})
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to serialize gui config: {}", e)))?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
|
||||
info!("Gui configuration file created");
|
||||
|
||||
Ok(gui_config_path)
|
||||
}
|
||||
|
||||
pub fn create_and_write_file(
|
||||
mut network_datadir: PathBuf,
|
||||
file_name: &str,
|
||||
@ -378,8 +590,93 @@ pub fn create_and_write_file(
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
// if the wallet is using the remote backend, then the hardware wallet settings and
|
||||
// keys will be store on the remote backend side and not in the settings file.
|
||||
pub async fn extract_remote_gui_settings(ctx: &Context, backend: &BackendWalletClient) -> Settings {
|
||||
let descriptor = ctx
|
||||
.descriptor
|
||||
.as_ref()
|
||||
.expect("Context must have a descriptor at this point");
|
||||
|
||||
let descriptor_checksum = descriptor
|
||||
.to_string()
|
||||
.split_once('#')
|
||||
.map(|(_, checksum)| checksum)
|
||||
.expect("LianaDescriptor.to_string() always include the checksum")
|
||||
.to_string();
|
||||
|
||||
let auth = backend.inner_client().auth.read().await;
|
||||
|
||||
Settings {
|
||||
wallets: vec![WalletSetting {
|
||||
name: wallet_name(descriptor),
|
||||
descriptor_checksum,
|
||||
keys: Vec::new(),
|
||||
hardware_wallets: Vec::new(),
|
||||
remote_backend_auth: Some(AuthConfig {
|
||||
email: backend.user_email().to_string(),
|
||||
wallet_id: backend.wallet_id(),
|
||||
refresh_token: auth.refresh_token.clone(),
|
||||
}),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn extract_local_gui_settings(ctx: &Context) -> Settings {
|
||||
let descriptor = ctx
|
||||
.descriptor
|
||||
.as_ref()
|
||||
.expect("Context must have a descriptor at this point");
|
||||
|
||||
let descriptor_checksum = descriptor
|
||||
.to_string()
|
||||
.split_once('#')
|
||||
.map(|(_, checksum)| checksum)
|
||||
.expect("LianaDescriptor.to_string() always include the checksum")
|
||||
.to_string();
|
||||
|
||||
let hardware_wallets = ctx
|
||||
.hws
|
||||
.iter()
|
||||
.filter_map(|(kind, fingerprint, token)| {
|
||||
token
|
||||
.as_ref()
|
||||
.map(|token| HardwareWalletConfig::new(kind, *fingerprint, token))
|
||||
})
|
||||
.collect();
|
||||
Settings {
|
||||
wallets: vec![WalletSetting {
|
||||
name: wallet_name(descriptor),
|
||||
descriptor_checksum,
|
||||
keys: ctx.keys.clone(),
|
||||
hardware_wallets,
|
||||
remote_backend_auth: None,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_daemon_config(ctx: &Context) -> Config {
|
||||
Config {
|
||||
#[cfg(unix)]
|
||||
daemon: false,
|
||||
log_level: log::LevelFilter::Info,
|
||||
main_descriptor: ctx
|
||||
.descriptor
|
||||
.clone()
|
||||
.expect("Context must have a descriptor at this point"),
|
||||
data_dir: Some(ctx.data_dir.clone()),
|
||||
bitcoin_config: ctx.bitcoin_config.clone(),
|
||||
bitcoind_config: ctx.bitcoind_config.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Error {
|
||||
Auth(AuthError),
|
||||
// DaemonError does not implement Clone.
|
||||
// TODO: maybe Arc is overkill
|
||||
Backend(Arc<DaemonError>),
|
||||
Settings(SettingsError),
|
||||
Bitcoind(String),
|
||||
CannotCreateDatadir(String),
|
||||
CannotCreateFile(String),
|
||||
@ -407,9 +704,30 @@ impl From<async_hwi::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DaemonError> for Error {
|
||||
fn from(value: DaemonError) -> Self {
|
||||
Self::Backend(Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AuthError> for Error {
|
||||
fn from(value: AuthError) -> Self {
|
||||
Self::Auth(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SettingsError> for Error {
|
||||
fn from(value: SettingsError) -> Self {
|
||||
Self::Settings(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Auth(e) => write!(f, "Authentification error: {}", e),
|
||||
Self::Backend(e) => write!(f, "Remote backend error: {}", e),
|
||||
Self::Settings(e) => write!(f, "Settings file error: {}", e),
|
||||
Self::Bitcoind(e) => write!(f, "Failed to ping bitcoind: {}", e),
|
||||
Self::CannotCreateDatadir(e) => write!(f, "Failed to create datadir: {}", e),
|
||||
Self::CannotGetAvailablePort(e) => write!(f, "Failed to get available port: {}", e),
|
||||
|
||||
307
gui/src/installer/step/backend.rs
Normal file
307
gui/src/installer/step/backend.rs
Normal file
@ -0,0 +1,307 @@
|
||||
use iced::Command;
|
||||
|
||||
use liana::miniscript::bitcoin::Network;
|
||||
use liana_ui::{component::form, widget::Element};
|
||||
|
||||
use crate::{
|
||||
hw::HardwareWallets,
|
||||
installer::{
|
||||
context::{self, Context, RemoteBackend},
|
||||
message::{self, Message},
|
||||
step::Step,
|
||||
view, Error,
|
||||
},
|
||||
lianalite::client::{
|
||||
self,
|
||||
auth::{AuthClient, AuthError},
|
||||
backend::{api, BackendClient},
|
||||
},
|
||||
};
|
||||
|
||||
pub enum ConnectionStep {
|
||||
EnterEmail {
|
||||
email: form::Value<String>,
|
||||
},
|
||||
EnterOtp {
|
||||
client: AuthClient,
|
||||
backend_api_url: String,
|
||||
email: String,
|
||||
otp: form::Value<String>,
|
||||
},
|
||||
Connected {
|
||||
email: String,
|
||||
remote_backend: context::RemoteBackend,
|
||||
wallet: Option<api::Wallet>,
|
||||
remote_backend_is_selected: bool,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct ChooseBackend {
|
||||
network: Network,
|
||||
processing: bool,
|
||||
step: ConnectionStep,
|
||||
connection_error: Option<Error>,
|
||||
auth_error: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl ChooseBackend {
|
||||
pub fn new(network: Network) -> Self {
|
||||
Self {
|
||||
network,
|
||||
step: ConnectionStep::EnterEmail {
|
||||
email: form::Value::default(),
|
||||
},
|
||||
connection_error: None,
|
||||
auth_error: None,
|
||||
processing: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChooseBackend> for Box<dyn Step> {
|
||||
fn from(s: ChooseBackend) -> Box<dyn Step> {
|
||||
Box::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Step for ChooseBackend {
|
||||
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
|
||||
if matches!(
|
||||
message,
|
||||
Message::SelectBackend(message::SelectBackend::ContinueWithLocalWallet)
|
||||
) {
|
||||
if let ConnectionStep::Connected {
|
||||
remote_backend_is_selected,
|
||||
..
|
||||
} = &mut self.step
|
||||
{
|
||||
*remote_backend_is_selected = false;
|
||||
}
|
||||
return Command::perform(async move {}, |_| Message::Next);
|
||||
}
|
||||
match &mut self.step {
|
||||
ConnectionStep::EnterEmail { email } => match message {
|
||||
Message::SelectBackend(message::SelectBackend::EmailEdited(value)) => {
|
||||
email.valid = value.is_empty()
|
||||
|| email_address::EmailAddress::parse_with_options(
|
||||
&value,
|
||||
email_address::Options::default().with_required_tld(),
|
||||
)
|
||||
.is_ok();
|
||||
email.value = value;
|
||||
}
|
||||
Message::SelectBackend(message::SelectBackend::RequestOTP) => {
|
||||
if email.value.is_empty() {
|
||||
email.valid = false;
|
||||
} else if email.valid {
|
||||
let email = email.value.clone();
|
||||
let network = self.network;
|
||||
self.processing = true;
|
||||
self.connection_error = None;
|
||||
self.auth_error = None;
|
||||
return Command::perform(
|
||||
async move {
|
||||
let config =
|
||||
client::get_service_config(network).await.map_err(|e| {
|
||||
if e.status() == Some(reqwest::StatusCode::NOT_FOUND) {
|
||||
Error::Unexpected(
|
||||
"Remote servers are unresponsive".to_string(),
|
||||
)
|
||||
} else {
|
||||
Error::Unexpected(e.to_string())
|
||||
}
|
||||
})?;
|
||||
let client = AuthClient::new(
|
||||
config.auth_api_url,
|
||||
config.auth_api_public_key,
|
||||
email,
|
||||
);
|
||||
client.sign_in_otp().await?;
|
||||
Ok((client, config.backend_api_url))
|
||||
},
|
||||
|res| Message::SelectBackend(message::SelectBackend::OTPRequested(res)),
|
||||
);
|
||||
}
|
||||
}
|
||||
Message::SelectBackend(message::SelectBackend::OTPRequested(res)) => {
|
||||
self.processing = false;
|
||||
match res {
|
||||
Ok((client, backend_api_url)) => {
|
||||
self.step = ConnectionStep::EnterOtp {
|
||||
email: email.value.to_owned(),
|
||||
otp: form::Value::default(),
|
||||
client,
|
||||
backend_api_url,
|
||||
};
|
||||
}
|
||||
Err(e) => {
|
||||
self.connection_error = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ConnectionStep::EnterOtp {
|
||||
client,
|
||||
email,
|
||||
otp,
|
||||
backend_api_url,
|
||||
} => match message {
|
||||
Message::SelectBackend(message::SelectBackend::EditEmail) => {
|
||||
self.step = ConnectionStep::EnterEmail {
|
||||
email: form::Value {
|
||||
value: email.clone(),
|
||||
valid: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
Message::SelectBackend(message::SelectBackend::RequestOTP) => {
|
||||
*otp = form::Value::default();
|
||||
let client = client.clone();
|
||||
self.processing = true;
|
||||
self.connection_error = None;
|
||||
self.auth_error = None;
|
||||
return Command::perform(
|
||||
async move {
|
||||
client.resend_otp().await?;
|
||||
Ok(())
|
||||
},
|
||||
message::SelectBackend::OTPResent,
|
||||
)
|
||||
.map(Message::SelectBackend);
|
||||
}
|
||||
Message::SelectBackend(message::SelectBackend::OTPResent(res)) => {
|
||||
self.processing = false;
|
||||
if let Err(e) = res {
|
||||
self.connection_error = Some(e);
|
||||
}
|
||||
}
|
||||
Message::SelectBackend(message::SelectBackend::OTPEdited(value)) => {
|
||||
otp.value = value.trim().to_string();
|
||||
if otp.value.len() == 6 {
|
||||
let client = client.clone();
|
||||
let otp = otp.value.clone();
|
||||
let backend_api_url = backend_api_url.clone();
|
||||
self.processing = true;
|
||||
self.connection_error = None;
|
||||
self.auth_error = None;
|
||||
return Command::perform(
|
||||
async move { connect(client, otp, backend_api_url).await },
|
||||
message::SelectBackend::Connected,
|
||||
)
|
||||
.map(Message::SelectBackend);
|
||||
}
|
||||
}
|
||||
|
||||
Message::SelectBackend(message::SelectBackend::Connected(res)) => {
|
||||
self.processing = false;
|
||||
match res {
|
||||
Ok((remote_backend, wallet)) => {
|
||||
self.step = ConnectionStep::Connected {
|
||||
email: email.clone(),
|
||||
remote_backend,
|
||||
wallet,
|
||||
remote_backend_is_selected: false,
|
||||
};
|
||||
}
|
||||
Err(e) => {
|
||||
if let Error::Auth(AuthError { http_status, .. }) = e {
|
||||
if http_status == Some(403) {
|
||||
self.auth_error = Some("Token is expired or is invalid")
|
||||
} else {
|
||||
self.connection_error = Some(e);
|
||||
}
|
||||
} else {
|
||||
self.connection_error = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ConnectionStep::Connected {
|
||||
remote_backend_is_selected,
|
||||
..
|
||||
} => match message {
|
||||
Message::SelectBackend(message::SelectBackend::EditEmail) => {
|
||||
self.step = ConnectionStep::EnterEmail {
|
||||
email: form::Value::default(),
|
||||
}
|
||||
}
|
||||
Message::SelectBackend(message::SelectBackend::ContinueWithRemoteBackend) => {
|
||||
*remote_backend_is_selected = true;
|
||||
return Command::perform(async move {}, |_| Message::Next);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn apply(&mut self, ctx: &mut Context) -> bool {
|
||||
if let ConnectionStep::Connected {
|
||||
remote_backend,
|
||||
remote_backend_is_selected,
|
||||
..
|
||||
} = &self.step
|
||||
{
|
||||
if *remote_backend_is_selected {
|
||||
ctx.remote_backend = Some(remote_backend.clone());
|
||||
}
|
||||
} else {
|
||||
ctx.remote_backend = None;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn view<'a>(
|
||||
&'a self,
|
||||
_hws: &'a HardwareWallets,
|
||||
progress: (usize, usize),
|
||||
_email: Option<&'a str>,
|
||||
) -> Element<Message> {
|
||||
view::choose_backend(
|
||||
progress,
|
||||
match &self.step {
|
||||
ConnectionStep::EnterEmail { email } => view::connection_step_enter_email(
|
||||
email,
|
||||
self.processing,
|
||||
self.connection_error.as_ref(),
|
||||
self.auth_error,
|
||||
),
|
||||
ConnectionStep::EnterOtp { email, otp, .. } => view::connection_step_enter_otp(
|
||||
email,
|
||||
otp,
|
||||
self.processing,
|
||||
self.connection_error.as_ref(),
|
||||
self.auth_error,
|
||||
),
|
||||
ConnectionStep::Connected { email, wallet, .. } => view::connection_step_connected(
|
||||
email,
|
||||
self.processing,
|
||||
wallet.as_ref().map(|w| w.name.as_str()),
|
||||
self.connection_error.as_ref(),
|
||||
self.auth_error,
|
||||
),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect(
|
||||
auth: AuthClient,
|
||||
token: String,
|
||||
backend_api_url: String,
|
||||
) -> Result<(context::RemoteBackend, Option<api::Wallet>), Error> {
|
||||
let access = auth.verify_otp(token.trim_end()).await?;
|
||||
let client = BackendClient::connect(auth, backend_api_url, access.clone()).await?;
|
||||
|
||||
if !client.list_wallets().await?.is_empty() {
|
||||
let (wallet_client, wallet) = client.connect_first().await?;
|
||||
Ok((RemoteBackend::WithWallet(wallet_client), Some(wallet)))
|
||||
} else {
|
||||
Ok((RemoteBackend::WithoutWallet(client), None))
|
||||
}
|
||||
}
|
||||
@ -302,6 +302,9 @@ impl SelectBitcoindTypeStep {
|
||||
}
|
||||
|
||||
impl Step for SelectBitcoindTypeStep {
|
||||
fn skip(&self, ctx: &Context) -> bool {
|
||||
ctx.remote_backend.is_some()
|
||||
}
|
||||
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
|
||||
if let Message::SelectBitcoindType(msg) = message {
|
||||
match msg {
|
||||
@ -326,7 +329,12 @@ impl Step for SelectBitcoindTypeStep {
|
||||
true
|
||||
}
|
||||
|
||||
fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element<Message> {
|
||||
fn view(
|
||||
&self,
|
||||
_hws: &HardwareWallets,
|
||||
progress: (usize, usize),
|
||||
_email: Option<&str>,
|
||||
) -> Element<Message> {
|
||||
view::select_bitcoind_type(progress)
|
||||
}
|
||||
}
|
||||
@ -479,7 +487,12 @@ impl Step for DefineBitcoind {
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element<Message> {
|
||||
fn view(
|
||||
&self,
|
||||
_hws: &HardwareWallets,
|
||||
progress: (usize, usize),
|
||||
_email: Option<&str>,
|
||||
) -> Element<Message> {
|
||||
view::define_bitcoin(
|
||||
progress,
|
||||
&self.address,
|
||||
@ -494,7 +507,7 @@ impl Step for DefineBitcoind {
|
||||
}
|
||||
|
||||
fn skip(&self, ctx: &Context) -> bool {
|
||||
!ctx.bitcoind_is_external
|
||||
!ctx.bitcoind_is_external || ctx.remote_backend.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
@ -787,7 +800,12 @@ impl Step for InternalBitcoindStep {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element<Message> {
|
||||
fn view(
|
||||
&self,
|
||||
_hws: &HardwareWallets,
|
||||
progress: (usize, usize),
|
||||
_email: Option<&str>,
|
||||
) -> Element<Message> {
|
||||
view::start_internal_bitcoind(
|
||||
progress,
|
||||
self.exe_path.as_ref(),
|
||||
@ -806,7 +824,7 @@ impl Step for InternalBitcoindStep {
|
||||
}
|
||||
|
||||
fn skip(&self, ctx: &Context) -> bool {
|
||||
ctx.bitcoind_is_external
|
||||
ctx.bitcoind_is_external || ctx.remote_backend.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -548,10 +548,12 @@ impl Step for DefineDescriptor {
|
||||
&'a self,
|
||||
hws: &'a HardwareWallets,
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
) -> Element<'a, Message> {
|
||||
let aliases = self.setup.keys_aliases();
|
||||
let content = view::define_descriptor(
|
||||
progress,
|
||||
email,
|
||||
self.use_taproot,
|
||||
self.setup
|
||||
.spending_keys
|
||||
@ -1166,9 +1168,15 @@ impl Step for ImportDescriptor {
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element<Message> {
|
||||
fn view<'a>(
|
||||
&'a self,
|
||||
_hws: &'a HardwareWallets,
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
) -> Element<Message> {
|
||||
view::import_descriptor(
|
||||
progress,
|
||||
email,
|
||||
&self.imported_descriptor,
|
||||
self.wrong_network,
|
||||
self.error.as_ref(),
|
||||
@ -1310,10 +1318,12 @@ impl Step for RegisterDescriptor {
|
||||
&'a self,
|
||||
hws: &'a HardwareWallets,
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
) -> Element<'a, Message> {
|
||||
let desc = self.descriptor.as_ref().unwrap();
|
||||
view::register_descriptor(
|
||||
progress,
|
||||
email,
|
||||
desc.to_string(),
|
||||
&hws.list,
|
||||
&self.registered,
|
||||
@ -1364,9 +1374,14 @@ impl Step for BackupDescriptor {
|
||||
self.done = false;
|
||||
}
|
||||
}
|
||||
fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element<Message> {
|
||||
fn view<'a>(
|
||||
&'a self,
|
||||
_hws: &'a HardwareWallets,
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
) -> Element<Message> {
|
||||
let desc = self.descriptor.as_ref().unwrap();
|
||||
view::backup_descriptor(progress, desc.to_string(), self.done)
|
||||
view::backup_descriptor(progress, email, desc.to_string(), self.done)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1416,7 +1431,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_define_descriptor_use_hotkey() {
|
||||
let mut ctx = Context::new(Network::Signet, PathBuf::from_str("/").unwrap());
|
||||
let mut ctx = Context::new(Network::Signet, PathBuf::from_str("/").unwrap(), None);
|
||||
let sandbox: Sandbox<DefineDescriptor> = Sandbox::new(DefineDescriptor::new(
|
||||
Network::Bitcoin,
|
||||
Arc::new(Mutex::new(Signer::generate(Network::Bitcoin).unwrap())),
|
||||
@ -1498,7 +1513,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_define_descriptor_stores_if_hw_is_used() {
|
||||
let mut ctx = Context::new(Network::Testnet, PathBuf::from_str("/").unwrap());
|
||||
let mut ctx = Context::new(Network::Testnet, PathBuf::from_str("/").unwrap(), None);
|
||||
let sandbox: Sandbox<DefineDescriptor> = Sandbox::new(DefineDescriptor::new(
|
||||
Network::Testnet,
|
||||
Arc::new(Mutex::new(Signer::generate(Network::Testnet).unwrap())),
|
||||
|
||||
@ -51,8 +51,13 @@ impl Step for BackupMnemonic {
|
||||
false
|
||||
}
|
||||
}
|
||||
fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element<Message> {
|
||||
view::backup_mnemonic(progress, &self.words, self.done)
|
||||
fn view<'a>(
|
||||
&'a self,
|
||||
_hws: &'a HardwareWallets,
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
) -> Element<Message> {
|
||||
view::backup_mnemonic(progress, email, &self.words, self.done)
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,9 +168,15 @@ impl Step for RecoverMnemonic {
|
||||
ctx.recovered_signer = Some(Arc::new(signer));
|
||||
true
|
||||
}
|
||||
fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element<Message> {
|
||||
fn view<'a>(
|
||||
&'a self,
|
||||
_hws: &'a HardwareWallets,
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
) -> Element<Message> {
|
||||
view::recover_mnemonic(
|
||||
progress,
|
||||
email,
|
||||
&self.words,
|
||||
self.current,
|
||||
&self.suggestions,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
mod backend;
|
||||
mod bitcoind;
|
||||
mod descriptor;
|
||||
mod mnemonic;
|
||||
@ -9,6 +10,7 @@ pub use bitcoind::{
|
||||
|
||||
pub use descriptor::{BackupDescriptor, DefineDescriptor, ImportDescriptor, RegisterDescriptor};
|
||||
|
||||
pub use backend::ChooseBackend;
|
||||
pub use mnemonic::{BackupMnemonic, RecoverMnemonic};
|
||||
pub use share_xpubs::ShareXpubs;
|
||||
|
||||
@ -35,6 +37,7 @@ pub trait Step {
|
||||
&'a self,
|
||||
_hws: &'a HardwareWallets,
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
) -> Element<'a, Message>;
|
||||
|
||||
fn load_context(&mut self, _ctx: &Context) {}
|
||||
@ -54,7 +57,12 @@ pub trait Step {
|
||||
pub struct Welcome {}
|
||||
|
||||
impl Step for Welcome {
|
||||
fn view(&self, _hws: &HardwareWallets, _progress: (usize, usize)) -> Element<Message> {
|
||||
fn view<'a>(
|
||||
&'a self,
|
||||
_hws: &'a HardwareWallets,
|
||||
_progress: (usize, usize),
|
||||
_email: Option<&'a str>,
|
||||
) -> Element<Message> {
|
||||
view::welcome()
|
||||
}
|
||||
}
|
||||
@ -128,9 +136,15 @@ impl Step for Final {
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element<Message> {
|
||||
fn view<'a>(
|
||||
&'a self,
|
||||
_hws: &'a HardwareWallets,
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
) -> Element<Message> {
|
||||
view::install(
|
||||
progress,
|
||||
email,
|
||||
self.generating,
|
||||
self.config_path.as_ref(),
|
||||
self.warning.as_ref(),
|
||||
|
||||
@ -160,8 +160,14 @@ impl Step for ShareXpubs {
|
||||
true
|
||||
}
|
||||
|
||||
fn view<'a>(&'a self, hws: &'a HardwareWallets, _progress: (usize, usize)) -> Element<Message> {
|
||||
fn view<'a>(
|
||||
&'a self,
|
||||
hws: &'a HardwareWallets,
|
||||
_progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
) -> Element<Message> {
|
||||
view::share_xpubs(
|
||||
email,
|
||||
hws.list
|
||||
.iter()
|
||||
.enumerate()
|
||||
|
||||
@ -187,6 +187,7 @@ pub fn define_descriptor_advanced_settings<'a>(use_taproot: bool) -> Element<'a,
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn define_descriptor<'a>(
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
use_taproot: bool,
|
||||
spending_keys: Vec<Element<'a, Message>>,
|
||||
spending_threshold: usize,
|
||||
@ -253,6 +254,7 @@ pub fn define_descriptor<'a>(
|
||||
|
||||
layout(
|
||||
progress,
|
||||
email,
|
||||
"Create the wallet",
|
||||
Column::new()
|
||||
.push(collapse::Collapse::new(
|
||||
@ -376,6 +378,7 @@ pub fn recovery_path_view(
|
||||
|
||||
pub fn import_descriptor<'a>(
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
imported_descriptor: &form::Value<String>,
|
||||
wrong_network: bool,
|
||||
error: Option<&String>,
|
||||
@ -397,6 +400,7 @@ pub fn import_descriptor<'a>(
|
||||
.spacing(10);
|
||||
layout(
|
||||
progress,
|
||||
email,
|
||||
"Import the wallet",
|
||||
Column::new()
|
||||
.push(Column::new().spacing(20).push(col_descriptor).push(text(
|
||||
@ -605,11 +609,13 @@ pub fn hardware_wallet_xpubs<'a>(
|
||||
}
|
||||
|
||||
pub fn share_xpubs<'a>(
|
||||
email: Option<&'a str>,
|
||||
hws: Vec<Element<'a, Message>>,
|
||||
signer: Element<'a, Message>,
|
||||
) -> Element<'a, Message> {
|
||||
layout(
|
||||
(0, 0),
|
||||
email,
|
||||
"Share your public keys (Xpubs)",
|
||||
Column::new()
|
||||
.push(
|
||||
@ -637,6 +643,7 @@ pub fn share_xpubs<'a>(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn register_descriptor<'a>(
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
descriptor: String,
|
||||
hws: &'a [HardwareWallet],
|
||||
registered: &HashSet<bitcoin::bip32::Fingerprint>,
|
||||
@ -723,6 +730,7 @@ pub fn register_descriptor<'a>(
|
||||
};
|
||||
layout(
|
||||
progress,
|
||||
email,
|
||||
"Register descriptor",
|
||||
Column::new()
|
||||
.push_maybe((!created_desc).then_some(
|
||||
@ -785,13 +793,15 @@ pub fn register_descriptor<'a>(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn backup_descriptor<'a>(
|
||||
pub fn backup_descriptor(
|
||||
progress: (usize, usize),
|
||||
email: Option<&str>,
|
||||
descriptor: String,
|
||||
done: bool,
|
||||
) -> Element<'a, Message> {
|
||||
) -> Element<'_, Message> {
|
||||
layout(
|
||||
progress,
|
||||
email,
|
||||
"Backup your wallet descriptor",
|
||||
Column::new()
|
||||
.push(
|
||||
@ -982,6 +992,7 @@ pub fn define_bitcoin<'a>(
|
||||
};
|
||||
layout(
|
||||
progress,
|
||||
None,
|
||||
"Set up connection to the Bitcoin full node",
|
||||
Column::new()
|
||||
.push(col_address)
|
||||
@ -1040,6 +1051,7 @@ pub fn define_bitcoin<'a>(
|
||||
pub fn select_bitcoind_type<'a>(progress: (usize, usize)) -> Element<'a, Message> {
|
||||
layout(
|
||||
progress,
|
||||
None,
|
||||
"Bitcoin node management",
|
||||
Column::new().push(
|
||||
Row::new()
|
||||
@ -1146,6 +1158,7 @@ pub fn start_internal_bitcoind<'a>(
|
||||
};
|
||||
layout(
|
||||
progress,
|
||||
None,
|
||||
"Start Bitcoin full node",
|
||||
Column::new()
|
||||
.push_maybe(download_state.map(|s| {
|
||||
@ -1250,6 +1263,7 @@ pub fn start_internal_bitcoind<'a>(
|
||||
|
||||
pub fn install<'a>(
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
generating: bool,
|
||||
config_path: Option<&std::path::PathBuf>,
|
||||
warning: Option<&'a String>,
|
||||
@ -1261,6 +1275,7 @@ pub fn install<'a>(
|
||||
};
|
||||
layout(
|
||||
progress,
|
||||
email,
|
||||
"Finalize installation",
|
||||
Column::new()
|
||||
.push_maybe(warning.map(|e| card::invalid(text(e))))
|
||||
@ -1813,11 +1828,13 @@ pub fn key_list_view<'a>(
|
||||
|
||||
pub fn backup_mnemonic<'a>(
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
words: &'a [&'static str; 12],
|
||||
done: bool,
|
||||
) -> Element<'a, Message> {
|
||||
layout(
|
||||
progress,
|
||||
email,
|
||||
"Backup your mnemonic",
|
||||
Column::new()
|
||||
.push(text(prompt::MNEMONIC_HELP))
|
||||
@ -1853,6 +1870,7 @@ pub fn backup_mnemonic<'a>(
|
||||
|
||||
pub fn recover_mnemonic<'a>(
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
words: &'a [(String, bool); 12],
|
||||
current: usize,
|
||||
suggestions: &'a [String],
|
||||
@ -1861,6 +1879,7 @@ pub fn recover_mnemonic<'a>(
|
||||
) -> Element<'a, Message> {
|
||||
layout(
|
||||
progress,
|
||||
email,
|
||||
"Import Mnemonic",
|
||||
Column::new()
|
||||
.push(text(prompt::RECOVER_MNEMONIC_HELP))
|
||||
@ -1954,8 +1973,181 @@ pub fn recover_mnemonic<'a>(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn choose_backend(
|
||||
progress: (usize, usize),
|
||||
connection_step: Element<Message>,
|
||||
) -> Element<Message> {
|
||||
layout(
|
||||
progress,
|
||||
None,
|
||||
"Choose backend",
|
||||
Column::new()
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(20)
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::FillPortion(1))
|
||||
.push(image::liana_brand_grey().height(Length::Fixed(100.0)))
|
||||
.push(text::p2_medium(LIANA_DESC).style(color::GREY_3))
|
||||
.push(button::primary(None, "Install local wallet").on_press(
|
||||
Message::SelectBackend(
|
||||
message::SelectBackend::ContinueWithLocalWallet,
|
||||
),
|
||||
)),
|
||||
)
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::FillPortion(1))
|
||||
.push(image::wizardsardine().height(Length::Fixed(100.0)))
|
||||
.push(text::p2_medium(LIANALITE_DESC).style(color::GREY_3))
|
||||
.push(connection_step),
|
||||
),
|
||||
)
|
||||
.spacing(50),
|
||||
true,
|
||||
Some(Message::Previous),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn connection_step_enter_email<'a>(
|
||||
email: &form::Value<String>,
|
||||
processing: bool,
|
||||
connection_error: Option<&Error>,
|
||||
auth_error: Option<&'static str>,
|
||||
) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.push_maybe(connection_error.map(|e| text(e.to_string()).style(color::ORANGE)))
|
||||
.push_maybe(auth_error.map(|e| text(e.to_string()).style(color::ORANGE)))
|
||||
.push(
|
||||
form::Form::new_trimmed("email", email, |msg| {
|
||||
Message::SelectBackend(message::SelectBackend::EmailEdited(msg))
|
||||
})
|
||||
.size(text::P1_SIZE)
|
||||
.padding(10)
|
||||
.warning("Email is not valid"),
|
||||
)
|
||||
.push(
|
||||
button::primary(None, "Next").on_press_maybe(if processing || !email.valid {
|
||||
None
|
||||
} else {
|
||||
Some(Message::SelectBackend(message::SelectBackend::RequestOTP))
|
||||
}),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn connection_step_enter_otp<'a>(
|
||||
email: &'a str,
|
||||
otp: &form::Value<String>,
|
||||
processing: bool,
|
||||
connection_error: Option<&Error>,
|
||||
auth_error: Option<&'static str>,
|
||||
) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.push(text(email).style(color::GREEN))
|
||||
.push(text("An authentication was send to you mail"))
|
||||
.push_maybe(connection_error.map(|e| text(e.to_string()).style(color::ORANGE)))
|
||||
.push_maybe(auth_error.map(|e| text(e.to_string()).style(color::ORANGE)))
|
||||
.push(
|
||||
form::Form::new_trimmed("Token", otp, |msg| {
|
||||
Message::SelectBackend(message::SelectBackend::OTPEdited(msg))
|
||||
})
|
||||
.size(text::P1_SIZE)
|
||||
.padding(10)
|
||||
.warning("Token is not valid"),
|
||||
)
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
button::primary(Some(icon::previous_icon()), "Change Email")
|
||||
.on_press(Message::SelectBackend(message::SelectBackend::EditEmail)),
|
||||
)
|
||||
.push(
|
||||
button::primary(None, "Resend token").on_press_maybe(if processing {
|
||||
None
|
||||
} else {
|
||||
Some(Message::SelectBackend(message::SelectBackend::RequestOTP))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn connection_step_connected<'a>(
|
||||
email: &'a str,
|
||||
processing: bool,
|
||||
wallet_name: Option<&str>,
|
||||
connection_error: Option<&Error>,
|
||||
auth_error: Option<&'static str>,
|
||||
) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.push(text(email).style(color::GREEN))
|
||||
.push_maybe(connection_error.map(|e| text(e.to_string()).style(color::ORANGE)))
|
||||
.push_maybe(auth_error.map(|e| text(e.to_string()).style(color::ORANGE)))
|
||||
.push(if let Some(name) = wallet_name {
|
||||
Container::new(
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.push(text(format!("Wallet {} already exists", name)))
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
button::primary(Some(icon::previous_icon()), "Change Email")
|
||||
.on_press(Message::SelectBackend(
|
||||
message::SelectBackend::EditEmail,
|
||||
)),
|
||||
)
|
||||
.push(
|
||||
button::primary(None, "Continue with existing wallet")
|
||||
.on_press_maybe(if processing {
|
||||
None
|
||||
} else {
|
||||
Some(Message::SelectBackend(
|
||||
message::SelectBackend::ContinueWithRemoteBackend,
|
||||
))
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Container::new(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
button::primary(Some(icon::previous_icon()), "Change Email")
|
||||
.on_press(Message::SelectBackend(message::SelectBackend::EditEmail)),
|
||||
)
|
||||
.push(
|
||||
button::primary(None, "Continue").on_press_maybe(if processing {
|
||||
None
|
||||
} else {
|
||||
Some(Message::SelectBackend(
|
||||
message::SelectBackend::ContinueWithRemoteBackend,
|
||||
))
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
pub const LIANALITE_DESC: &str = "Use the connection to the Bitcoin network provided by Wizardsardine. This removes the need for running a Bitcoin full node on your machine. It also provides synchronisation of your wallet data (labels, transactions, etc..) across your machines and participants in your wallet. We will also keep a backup of your wallet descriptor for you. This is the most convenient option but has privacy implications: your data would be stored on our servers (but never shared with a third party).";
|
||||
|
||||
pub const LIANA_DESC: &str = "This option creates a wallet on your machine. The wallet will never access any of our servers, we would not even be able to know you use our wallet. This option requires a local Bitcoin full node. A full node is necessary to use Bitcoin in a sovereign way, but it is more accessible than it sounds. The Liana wallet can download and run one for you so you don't have to manage it yourself. It will never use more than a couple GB of disk space. The initial synchronisation of the node takes time and is computationally intensive, but past this point running a Bitcoin full node on your machine is seamless.";
|
||||
|
||||
fn layout<'a>(
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
title: &'static str,
|
||||
content: impl Into<Element<'a, Message>>,
|
||||
padding_left: bool,
|
||||
@ -1968,6 +2160,9 @@ fn layout<'a>(
|
||||
Container::new(scrollable(
|
||||
Column::new()
|
||||
.width(Length::Fill)
|
||||
.push(Row::new().push(Space::with_width(Length::Fill)).push_maybe(
|
||||
email.map(|e| Container::new(p1_regular(e).style(color::GREEN)).padding(20)),
|
||||
))
|
||||
.push(Space::with_height(Length::Fixed(100.0)))
|
||||
.push(
|
||||
Row::new()
|
||||
|
||||
@ -39,6 +39,7 @@ pub struct AuthClient {
|
||||
http: reqwest::Client,
|
||||
url: String,
|
||||
api_public_key: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -67,11 +68,12 @@ impl From<Error> for AuthError {
|
||||
}
|
||||
|
||||
impl AuthClient {
|
||||
pub fn new(url: String, api_public_key: String) -> Self {
|
||||
pub fn new(url: String, api_public_key: String, email: String) -> Self {
|
||||
AuthClient {
|
||||
http: reqwest::Client::new(),
|
||||
url,
|
||||
api_public_key,
|
||||
email,
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,11 +87,11 @@ impl AuthClient {
|
||||
req
|
||||
}
|
||||
|
||||
pub async fn sign_in_otp(&self, email: &str) -> Result<(), AuthError> {
|
||||
pub async fn sign_in_otp(&self) -> Result<(), AuthError> {
|
||||
let response: Response = self
|
||||
.request(Method::POST, &format!("{}/auth/v1/otp", self.url))
|
||||
.json(&SignInOtp {
|
||||
email,
|
||||
email: &self.email,
|
||||
create_user: true,
|
||||
})
|
||||
.send()
|
||||
@ -105,12 +107,12 @@ impl AuthClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn resend_otp(&self, email: &str) -> Result<Response, AuthError> {
|
||||
pub async fn resend_otp(&self) -> Result<Response, AuthError> {
|
||||
let response: Response = self
|
||||
.request(Method::POST, &format!("{}/auth/v1/resend", self.url))
|
||||
.json(&ResendOtp {
|
||||
email,
|
||||
kind: "email",
|
||||
email: &self.email,
|
||||
kind: "signup",
|
||||
})
|
||||
.send()
|
||||
.await?;
|
||||
@ -123,18 +125,14 @@ impl AuthClient {
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn verify_otp(
|
||||
&self,
|
||||
email: &str,
|
||||
token: &str,
|
||||
) -> Result<AccessTokenResponse, AuthError> {
|
||||
pub async fn verify_otp(&self, token: &str) -> Result<AccessTokenResponse, AuthError> {
|
||||
let response: Response = self
|
||||
.http
|
||||
.post(&format!("{}/auth/v1/verify", self.url))
|
||||
.header("apikey", &self.api_public_key)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&VerifyOtp {
|
||||
email,
|
||||
email: &self.email,
|
||||
token,
|
||||
kind: "email",
|
||||
})
|
||||
|
||||
@ -303,7 +303,7 @@ pub struct Address {
|
||||
}
|
||||
|
||||
pub mod payload {
|
||||
use liana::miniscript::bitcoin;
|
||||
use liana::{descriptors::LianaDescriptor, miniscript::bitcoin};
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
pub fn ser_to_string<T: std::fmt::Display, S: Serializer>(
|
||||
@ -313,6 +313,13 @@ pub mod payload {
|
||||
s.serialize_str(&field.to_string())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CreateWallet<'a> {
|
||||
pub name: &'a str,
|
||||
#[serde(serialize_with = "ser_to_string")]
|
||||
pub descriptor: &'a LianaDescriptor,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ImportPsbt {
|
||||
pub psbt: String,
|
||||
|
||||
@ -2,6 +2,7 @@ pub mod api;
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
@ -17,6 +18,7 @@ use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
app::settings::{AuthConfig, Settings},
|
||||
daemon::{model::*, Daemon, DaemonBackend, DaemonError},
|
||||
hw::HardwareWalletConfig,
|
||||
};
|
||||
@ -53,7 +55,7 @@ fn request<U: IntoUrl>(
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BackendClient {
|
||||
auth: Arc<RwLock<auth::AccessTokenResponse>>,
|
||||
pub auth: Arc<RwLock<auth::AccessTokenResponse>>,
|
||||
auth_client: auth::AuthClient,
|
||||
|
||||
url: String,
|
||||
@ -94,6 +96,10 @@ impl BackendClient {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn user_email(&self) -> &str {
|
||||
&self.auth_client.email
|
||||
}
|
||||
|
||||
pub async fn connect_first(self) -> Result<(BackendWalletClient, api::Wallet), DaemonError> {
|
||||
let wallets = self.list_wallets().await?;
|
||||
let first = wallets.first().cloned().ok_or(DaemonError::NoAnswer)?;
|
||||
@ -129,6 +135,119 @@ impl BackendClient {
|
||||
let list: api::ListWallets = response.json().await?;
|
||||
Ok(list.wallets)
|
||||
}
|
||||
|
||||
pub async fn create_wallet(
|
||||
&self,
|
||||
name: &str,
|
||||
descriptor: &LianaDescriptor,
|
||||
) -> Result<api::Wallet, DaemonError> {
|
||||
let response = self
|
||||
.request(Method::POST, &format!("{}/v1/wallets", self.url))
|
||||
.await
|
||||
.json(&api::payload::CreateWallet { name, descriptor })
|
||||
.send()
|
||||
.await?;
|
||||
if !response.status().is_success() {
|
||||
return Err(DaemonError::Http(
|
||||
Some(response.status().into()),
|
||||
response.text().await?,
|
||||
));
|
||||
}
|
||||
|
||||
let wallet: api::Wallet = response.json().await?;
|
||||
Ok(wallet)
|
||||
}
|
||||
|
||||
pub async fn update_wallet_metadata(
|
||||
&self,
|
||||
wallet_uuid: &str,
|
||||
fingerprint_aliases: &HashMap<Fingerprint, String>,
|
||||
hws: &[HardwareWalletConfig],
|
||||
) -> Result<(), DaemonError> {
|
||||
let wallets = self.list_wallets().await?;
|
||||
let wallet = wallets
|
||||
.iter()
|
||||
.find(|w| w.id == wallet_uuid)
|
||||
.ok_or(DaemonError::Http(
|
||||
Some(404),
|
||||
"No wallet exists for this uui".to_string(),
|
||||
))?;
|
||||
let ledger_kinds = [
|
||||
async_hwi::DeviceKind::Ledger.to_string(),
|
||||
async_hwi::DeviceKind::LedgerSimulator.to_string(),
|
||||
];
|
||||
for cfg in hws {
|
||||
if ledger_kinds.contains(&cfg.kind)
|
||||
&& !wallet.metadata.ledger_hmacs.iter().any(|ledger_hmac| {
|
||||
ledger_hmac.fingerprint == cfg.fingerprint && ledger_hmac.hmac == cfg.token
|
||||
})
|
||||
{
|
||||
let response: Response = self
|
||||
.request(
|
||||
Method::PATCH,
|
||||
&format!("{}/v1/wallets/{}", self.url, wallet_uuid),
|
||||
)
|
||||
.await
|
||||
.json(&api::payload::UpdateWallet {
|
||||
ledger_hmac: Some(api::payload::UpdateLedgerHmac {
|
||||
fingerprint: cfg.fingerprint.to_string(),
|
||||
hmac: cfg.token.clone(),
|
||||
}),
|
||||
fingerprint_aliases: None,
|
||||
})
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(DaemonError::Http(
|
||||
Some(response.status().into()),
|
||||
response.text().await?,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fingerprint_aliases.iter().any(|(fg, alias)| {
|
||||
!wallet
|
||||
.metadata
|
||||
.fingerprint_aliases
|
||||
.contains(&api::FingerprintAlias {
|
||||
alias: alias.to_string(),
|
||||
user_id: self.user_id.clone(),
|
||||
fingerprint: *fg,
|
||||
})
|
||||
}) {
|
||||
let response: Response = self
|
||||
.request(
|
||||
Method::PATCH,
|
||||
&format!("{}/v1/wallets/{}", self.url, wallet_uuid),
|
||||
)
|
||||
.await
|
||||
.json(&api::payload::UpdateWallet {
|
||||
ledger_hmac: None,
|
||||
fingerprint_aliases: Some(
|
||||
fingerprint_aliases
|
||||
.iter()
|
||||
.map(|(fg, alias)| api::payload::UpdateFingerprintAlias {
|
||||
fingerprint: fg.to_string(),
|
||||
alias: alias.to_string(),
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
})
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(DaemonError::Http(
|
||||
Some(response.status().into()),
|
||||
response.text().await?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -140,10 +259,22 @@ pub struct BackendWalletClient {
|
||||
}
|
||||
|
||||
impl BackendWalletClient {
|
||||
pub fn inner_client(&self) -> &BackendClient {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
pub fn user_id(&self) -> &str {
|
||||
&self.inner.user_id
|
||||
}
|
||||
|
||||
pub fn wallet_id(&self) -> String {
|
||||
self.wallet_uuid.clone()
|
||||
}
|
||||
|
||||
pub fn user_email(&self) -> &str {
|
||||
self.inner.user_email()
|
||||
}
|
||||
|
||||
async fn get_wallet(&self) -> Result<api::Wallet, DaemonError> {
|
||||
let list = self.inner.list_wallets().await?;
|
||||
let wallet = list
|
||||
@ -313,7 +444,7 @@ impl BackendWalletClient {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn auth(&self) -> AccessTokenResponse {
|
||||
pub async fn auth(&self) -> AccessTokenResponse {
|
||||
self.inner.auth.read().await.clone()
|
||||
}
|
||||
}
|
||||
@ -329,7 +460,7 @@ impl Daemon for BackendWalletClient {
|
||||
}
|
||||
|
||||
/// refresh the token if close to expiration.
|
||||
async fn is_alive(&self) -> Result<(), DaemonError> {
|
||||
async fn is_alive(&self, datadir: &Path, network: Network) -> Result<(), DaemonError> {
|
||||
let auth = self.auth().await;
|
||||
if auth.expires_at < Utc::now().timestamp() + 60 {
|
||||
match self.inner.auth.try_write() {
|
||||
@ -344,6 +475,39 @@ impl Daemon for BackendWalletClient {
|
||||
.refresh_token(&auth.refresh_token)
|
||||
.await?;
|
||||
|
||||
let mut settings = Settings::from_file(datadir.to_path_buf(), network)
|
||||
.map_err(|e| {
|
||||
DaemonError::Unexpected(format!(
|
||||
"Cannot access to settings.json file: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
if let Some(wallet_settings) = settings.wallets.iter_mut().find(|w| {
|
||||
if let Some(auth) = &w.remote_backend_auth {
|
||||
auth.wallet_id == self.wallet_uuid
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) {
|
||||
wallet_settings.remote_backend_auth = Some(AuthConfig {
|
||||
email: self.inner.auth_client.email.clone(),
|
||||
wallet_id: self.wallet_id(),
|
||||
refresh_token: new.refresh_token.clone(),
|
||||
});
|
||||
} else {
|
||||
tracing::info!("Wallet id was not found in the settings");
|
||||
}
|
||||
|
||||
settings
|
||||
.to_file(datadir.to_path_buf(), network)
|
||||
.map_err(|e| {
|
||||
DaemonError::Unexpected(format!(
|
||||
"Cannot access to settings.json file: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
*old = new;
|
||||
tracing::info!("Liana backend access was refreshed");
|
||||
}
|
||||
@ -856,84 +1020,9 @@ impl Daemon for BackendWalletClient {
|
||||
fingerprint_aliases: &HashMap<Fingerprint, String>,
|
||||
hws: &[HardwareWalletConfig],
|
||||
) -> Result<(), DaemonError> {
|
||||
let wallet = self.get_wallet().await?;
|
||||
let ledger_kinds = [
|
||||
async_hwi::DeviceKind::Ledger.to_string(),
|
||||
async_hwi::DeviceKind::LedgerSimulator.to_string(),
|
||||
];
|
||||
for cfg in hws {
|
||||
if ledger_kinds.contains(&cfg.kind)
|
||||
&& !wallet.metadata.ledger_hmacs.iter().any(|ledger_hmac| {
|
||||
ledger_hmac.fingerprint == cfg.fingerprint && ledger_hmac.hmac == cfg.token
|
||||
})
|
||||
{
|
||||
let response: Response = self
|
||||
.inner
|
||||
.request(
|
||||
Method::PATCH,
|
||||
&format!("{}/v1/wallets/{}", self.inner.url, self.wallet_uuid),
|
||||
)
|
||||
.await
|
||||
.json(&api::payload::UpdateWallet {
|
||||
ledger_hmac: Some(api::payload::UpdateLedgerHmac {
|
||||
fingerprint: cfg.fingerprint.to_string(),
|
||||
hmac: cfg.token.clone(),
|
||||
}),
|
||||
fingerprint_aliases: None,
|
||||
})
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(DaemonError::Http(
|
||||
Some(response.status().into()),
|
||||
response.text().await?,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fingerprint_aliases.iter().any(|(fg, alias)| {
|
||||
!wallet
|
||||
.metadata
|
||||
.fingerprint_aliases
|
||||
.contains(&api::FingerprintAlias {
|
||||
alias: alias.to_string(),
|
||||
user_id: self.inner.user_id.clone(),
|
||||
fingerprint: *fg,
|
||||
})
|
||||
}) {
|
||||
let response: Response = self
|
||||
.inner
|
||||
.request(
|
||||
Method::PATCH,
|
||||
&format!("{}/v1/wallets/{}", self.inner.url, self.wallet_uuid),
|
||||
)
|
||||
.await
|
||||
.json(&api::payload::UpdateWallet {
|
||||
ledger_hmac: None,
|
||||
fingerprint_aliases: Some(
|
||||
fingerprint_aliases
|
||||
.iter()
|
||||
.map(|(fg, alias)| api::payload::UpdateFingerprintAlias {
|
||||
fingerprint: fg.to_string(),
|
||||
alias: alias.to_string(),
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
})
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(DaemonError::Http(
|
||||
Some(response.status().into()),
|
||||
response.text().await?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
self.inner
|
||||
.update_wallet_metadata(&self.wallet_uuid, fingerprint_aliases, hws)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
537
gui/src/lianalite/login.rs
Normal file
537
gui/src/lianalite/login.rs
Normal file
@ -0,0 +1,537 @@
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use iced::{Alignment, Command, Length};
|
||||
|
||||
use liana::miniscript::bitcoin::Network;
|
||||
use liana_ui::{
|
||||
color,
|
||||
component::{button, form, network_banner, notification, text::*},
|
||||
icon, image,
|
||||
widget::*,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::settings::{AuthConfig, Settings, SettingsError, WalletSetting},
|
||||
daemon::DaemonError,
|
||||
};
|
||||
|
||||
use super::client::{
|
||||
auth::{AuthClient, AuthError},
|
||||
backend::{api, BackendClient, BackendWalletClient},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Error {
|
||||
Auth(AuthError),
|
||||
// DaemonError does not implement Clone.
|
||||
// TODO: maybe Arc is overkill
|
||||
Backend(Arc<DaemonError>),
|
||||
Settings(SettingsError),
|
||||
Unexpected(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Auth(e) => write!(f, "Authentication error: {}", e),
|
||||
Self::Backend(e) => write!(f, "Remote backend error: {}", e),
|
||||
Self::Settings(e) => write!(f, "Settings file error: {}", e),
|
||||
Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DaemonError> for Error {
|
||||
fn from(value: DaemonError) -> Self {
|
||||
Self::Backend(Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AuthError> for Error {
|
||||
fn from(value: AuthError) -> Self {
|
||||
Self::Auth(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SettingsError> for Error {
|
||||
fn from(value: SettingsError) -> Self {
|
||||
Self::Settings(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
View(ViewMessage),
|
||||
OTPRequested(Result<(AuthClient, String), Error>),
|
||||
OTPResent(Result<(), Error>),
|
||||
Connected(Result<BackendState, Error>),
|
||||
// redirect to the installer with the remote backend connection.
|
||||
Install(Option<BackendClient>),
|
||||
// redirect to the app runner with the remote backend connection.
|
||||
Run(Result<(BackendWalletClient, api::Wallet), Error>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ViewMessage {
|
||||
RequestOTP,
|
||||
EditEmail,
|
||||
EmailEdited(String),
|
||||
OTPEdited(String),
|
||||
BackToLauncher,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BackendState {
|
||||
NoWallet(BackendClient),
|
||||
WalletExists(BackendWalletClient, api::Wallet),
|
||||
}
|
||||
|
||||
pub struct LianaLiteLogin {
|
||||
pub datadir: PathBuf,
|
||||
pub network: Network,
|
||||
|
||||
processing: bool,
|
||||
step: ConnectionStep,
|
||||
|
||||
// Error due to connection
|
||||
connection_error: Option<Error>,
|
||||
// Authentification Error
|
||||
auth_error: Option<&'static str>,
|
||||
}
|
||||
|
||||
pub enum ConnectionStep {
|
||||
CheckingAuthFile,
|
||||
EnterEmail {
|
||||
email: form::Value<String>,
|
||||
},
|
||||
EnterOtp {
|
||||
client: AuthClient,
|
||||
backend_api_url: String,
|
||||
email: String,
|
||||
otp: form::Value<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl LianaLiteLogin {
|
||||
pub fn new(datadir: PathBuf, network: Network) -> (Self, Command<Message>) {
|
||||
(
|
||||
Self {
|
||||
network,
|
||||
datadir: datadir.clone(),
|
||||
step: ConnectionStep::CheckingAuthFile,
|
||||
connection_error: None,
|
||||
auth_error: None,
|
||||
processing: true,
|
||||
},
|
||||
Command::perform(
|
||||
async move {
|
||||
let auth_config = Settings::from_file(datadir.to_path_buf(), network)?
|
||||
.wallets
|
||||
.first()
|
||||
.cloned()
|
||||
.ok_or(SettingsError::NotFound)?
|
||||
.remote_backend_auth
|
||||
.ok_or(SettingsError::NotFound)?;
|
||||
let service_config = super::client::get_service_config(network)
|
||||
.await
|
||||
.map_err(|e| Error::Unexpected(e.to_string()))?;
|
||||
let client = AuthClient::new(
|
||||
service_config.auth_api_url,
|
||||
service_config.auth_api_public_key,
|
||||
auth_config.email,
|
||||
);
|
||||
connect_with_refresh_token(
|
||||
client,
|
||||
auth_config.refresh_token,
|
||||
service_config.backend_api_url,
|
||||
)
|
||||
.await
|
||||
},
|
||||
Message::Connected,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: Message) -> Command<Message> {
|
||||
match &mut self.step {
|
||||
ConnectionStep::CheckingAuthFile => {
|
||||
if let Message::Connected(res) = message {
|
||||
self.processing = false;
|
||||
match res {
|
||||
Ok(BackendState::NoWallet(_)) => {
|
||||
self.auth_error = Some("No wallet found for the given email");
|
||||
}
|
||||
Ok(BackendState::WalletExists(client, wallet)) => {
|
||||
return Command::perform(async move { (client, wallet) }, |(c, w)| {
|
||||
Message::Run(Ok((c, w)))
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
self.connection_error = Some(e);
|
||||
self.step = ConnectionStep::EnterEmail {
|
||||
email: form::Value::default(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ConnectionStep::EnterEmail { email } => match message {
|
||||
Message::View(ViewMessage::EmailEdited(value)) => {
|
||||
email.valid = value.is_empty()
|
||||
|| email_address::EmailAddress::parse_with_options(
|
||||
&value,
|
||||
email_address::Options::default().with_required_tld(),
|
||||
)
|
||||
.is_ok();
|
||||
email.value = value;
|
||||
}
|
||||
Message::View(ViewMessage::RequestOTP) => {
|
||||
if email.value.is_empty() {
|
||||
email.valid = false;
|
||||
} else if email.valid {
|
||||
let email = email.value.clone();
|
||||
let network = self.network;
|
||||
self.processing = true;
|
||||
self.connection_error = None;
|
||||
self.auth_error = None;
|
||||
return Command::perform(
|
||||
async move {
|
||||
let config = super::client::get_service_config(network)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if e.status() == Some(reqwest::StatusCode::NOT_FOUND) {
|
||||
Error::Unexpected(
|
||||
"Remote servers are unresponsive".to_string(),
|
||||
)
|
||||
} else {
|
||||
Error::Unexpected(e.to_string())
|
||||
}
|
||||
})?;
|
||||
let client = AuthClient::new(
|
||||
config.auth_api_url,
|
||||
config.auth_api_public_key,
|
||||
email,
|
||||
);
|
||||
client.sign_in_otp().await?;
|
||||
Ok((client, config.backend_api_url))
|
||||
},
|
||||
Message::OTPRequested,
|
||||
);
|
||||
}
|
||||
}
|
||||
Message::OTPRequested(res) => {
|
||||
self.processing = false;
|
||||
match res {
|
||||
Ok((client, backend_api_url)) => {
|
||||
self.step = ConnectionStep::EnterOtp {
|
||||
email: email.value.to_owned(),
|
||||
otp: form::Value::default(),
|
||||
client,
|
||||
backend_api_url,
|
||||
};
|
||||
}
|
||||
Err(e) => {
|
||||
self.connection_error = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ConnectionStep::EnterOtp {
|
||||
client,
|
||||
email,
|
||||
otp,
|
||||
backend_api_url,
|
||||
} => match message {
|
||||
Message::View(ViewMessage::EditEmail) => {
|
||||
self.step = ConnectionStep::EnterEmail {
|
||||
email: form::Value {
|
||||
value: email.clone(),
|
||||
valid: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
Message::View(ViewMessage::RequestOTP) => {
|
||||
*otp = form::Value::default();
|
||||
let client = client.clone();
|
||||
self.processing = true;
|
||||
self.connection_error = None;
|
||||
self.auth_error = None;
|
||||
return Command::perform(
|
||||
async move {
|
||||
client.resend_otp().await?;
|
||||
Ok(())
|
||||
},
|
||||
Message::OTPResent,
|
||||
);
|
||||
}
|
||||
Message::OTPResent(res) => match res {
|
||||
Ok(()) => {
|
||||
self.processing = false;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("{}", e);
|
||||
self.processing = false;
|
||||
self.connection_error = Some(e);
|
||||
}
|
||||
},
|
||||
Message::View(ViewMessage::OTPEdited(value)) => {
|
||||
otp.value = value.trim().to_string();
|
||||
otp.valid = true;
|
||||
if otp.value.len() == 6 {
|
||||
let client = client.clone();
|
||||
let otp = otp.value.clone();
|
||||
let backend_api_url = backend_api_url.clone();
|
||||
self.processing = true;
|
||||
self.connection_error = None;
|
||||
self.auth_error = None;
|
||||
return Command::perform(
|
||||
async move { connect(client, otp, backend_api_url).await },
|
||||
Message::Connected,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Message::Connected(res) => {
|
||||
self.processing = false;
|
||||
match res {
|
||||
Ok(BackendState::NoWallet(client)) => {
|
||||
return Command::perform(async move { Some(client) }, Message::Install);
|
||||
}
|
||||
Ok(BackendState::WalletExists(client, wallet)) => {
|
||||
let datadir = self.datadir.clone();
|
||||
let network = self.network;
|
||||
return Command::perform(
|
||||
async move {
|
||||
update_wallet_auth_settings(
|
||||
datadir,
|
||||
network,
|
||||
wallet.clone(),
|
||||
client.user_email().to_string(),
|
||||
client.auth().await.refresh_token,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((client, wallet))
|
||||
},
|
||||
Message::Run,
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("{}", e);
|
||||
if let Error::Auth(AuthError { http_status, .. }) = e {
|
||||
if http_status == Some(403) {
|
||||
self.auth_error = Some("Token is expired or is invalid")
|
||||
} else {
|
||||
self.connection_error = Some(e);
|
||||
}
|
||||
} else {
|
||||
self.connection_error = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Message::Run::Ok is handled by the upper level wrapping the LianaLiteLogin
|
||||
// state.
|
||||
Message::Run(Err(e)) => {
|
||||
self.connection_error = Some(e);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
pub fn view(&self) -> Element<Message> {
|
||||
let content = Into::<Element<ViewMessage>>::into(
|
||||
Container::new(
|
||||
Column::new()
|
||||
.spacing(100)
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
Column::new()
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(20)
|
||||
.width(Length::Fill)
|
||||
.push(image::wizardsardine().height(Length::Fixed(100.0)))
|
||||
.push(
|
||||
Column::new()
|
||||
.max_width(500)
|
||||
.spacing(20)
|
||||
.push(match &self.step {
|
||||
ConnectionStep::CheckingAuthFile => Column::new(),
|
||||
ConnectionStep::EnterEmail { email } => Column::new()
|
||||
.spacing(20)
|
||||
.push_maybe(
|
||||
self.auth_error
|
||||
.map(|e| text(e).style(color::ORANGE)),
|
||||
)
|
||||
.push(
|
||||
form::Form::new_trimmed("email", email, |msg| {
|
||||
ViewMessage::EmailEdited(msg)
|
||||
})
|
||||
.size(P1_SIZE)
|
||||
.padding(10)
|
||||
.warning("Email is not valid"),
|
||||
)
|
||||
.push(button::primary(None, "Next").on_press_maybe(
|
||||
if self.processing {
|
||||
None
|
||||
} else {
|
||||
Some(ViewMessage::RequestOTP)
|
||||
},
|
||||
)),
|
||||
ConnectionStep::EnterOtp { otp, .. } => Column::new()
|
||||
.push(text("An authentication was send to your email"))
|
||||
.push_maybe(
|
||||
self.auth_error
|
||||
.map(|e| text(e).style(color::ORANGE)),
|
||||
)
|
||||
.spacing(20)
|
||||
.push(
|
||||
form::Form::new_trimmed("Token", otp, |msg| {
|
||||
ViewMessage::OTPEdited(msg)
|
||||
})
|
||||
.size(P1_SIZE)
|
||||
.padding(10)
|
||||
.warning("Token is not valid"),
|
||||
)
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
button::primary(
|
||||
Some(icon::previous_icon()),
|
||||
"Change email",
|
||||
)
|
||||
.on_press(ViewMessage::EditEmail),
|
||||
)
|
||||
.push(
|
||||
button::primary(None, "Resend token")
|
||||
.on_press_maybe(if self.processing {
|
||||
None
|
||||
} else {
|
||||
Some(ViewMessage::RequestOTP)
|
||||
}),
|
||||
),
|
||||
),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.push_maybe(if !matches!(self.step, ConnectionStep::CheckingAuthFile) {
|
||||
Some(
|
||||
button::secondary(Some(icon::previous_icon()), "Change network")
|
||||
.width(Length::Fixed(200.0))
|
||||
.on_press(ViewMessage::BackToLauncher),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
)
|
||||
.padding(50)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.center_x()
|
||||
.center_y(),
|
||||
)
|
||||
.map(Message::View);
|
||||
|
||||
let mut col = Column::new();
|
||||
if self.network != Network::Bitcoin {
|
||||
col = col.push(network_banner(self.network));
|
||||
}
|
||||
if let Some(error) = &self.connection_error {
|
||||
col = col.push(
|
||||
notification::warning("Connection failed".to_string(), error.to_string())
|
||||
.width(Length::Fill),
|
||||
);
|
||||
}
|
||||
|
||||
col.push(content).into()
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_wallet_auth_settings(
|
||||
datadir: PathBuf,
|
||||
network: Network,
|
||||
wallet: api::Wallet,
|
||||
email: String,
|
||||
refresh_token: String,
|
||||
) -> Result<(), Error> {
|
||||
let mut settings = Settings::from_file(datadir.clone(), network)?;
|
||||
|
||||
let descriptor_checksum = wallet
|
||||
.descriptor
|
||||
.to_string()
|
||||
.split_once('#')
|
||||
.map(|(_, checksum)| checksum)
|
||||
.expect("Failed to get checksum from a valid LianaDescriptor")
|
||||
.to_string();
|
||||
|
||||
let remote_backend_auth = Some(AuthConfig {
|
||||
email,
|
||||
wallet_id: wallet.id.clone(),
|
||||
refresh_token,
|
||||
});
|
||||
|
||||
if let Some(wallet_settings) = settings.wallets.iter_mut().find(|w| {
|
||||
if let Some(auth) = &w.remote_backend_auth {
|
||||
auth.wallet_id == wallet.id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) {
|
||||
wallet_settings.remote_backend_auth = remote_backend_auth;
|
||||
} else {
|
||||
tracing::info!("Wallet id was not found in the settings, adding now the wallet settings to the settings.json file");
|
||||
settings.wallets.insert(
|
||||
0,
|
||||
WalletSetting {
|
||||
name: wallet.name,
|
||||
descriptor_checksum,
|
||||
keys: Vec::new(),
|
||||
hardware_wallets: Vec::new(),
|
||||
remote_backend_auth,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
settings.to_file(datadir, network).map_err(|e| {
|
||||
DaemonError::Unexpected(format!("Cannot access to settings.json file: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn connect(
|
||||
auth: AuthClient,
|
||||
token: String,
|
||||
backend_api_url: String,
|
||||
) -> Result<BackendState, Error> {
|
||||
let access = auth.verify_otp(token.trim_end()).await?;
|
||||
let client = BackendClient::connect(auth, backend_api_url, access.clone()).await?;
|
||||
|
||||
if !client.list_wallets().await?.is_empty() {
|
||||
let (wallet_client, wallet) = client.connect_first().await?;
|
||||
Ok(BackendState::WalletExists(wallet_client, wallet))
|
||||
} else {
|
||||
Ok(BackendState::NoWallet(client))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect_with_refresh_token(
|
||||
auth: AuthClient,
|
||||
refresh_token: String,
|
||||
backend_api_url: String,
|
||||
) -> Result<BackendState, Error> {
|
||||
let access = auth.refresh_token(&refresh_token).await?;
|
||||
let client = BackendClient::connect(auth, backend_api_url, access.clone()).await?;
|
||||
|
||||
if !client.list_wallets().await?.is_empty() {
|
||||
let (wallet_client, wallet) = client.connect_first().await?;
|
||||
Ok(BackendState::WalletExists(wallet_client, wallet))
|
||||
} else {
|
||||
Ok(BackendState::NoWallet(client))
|
||||
}
|
||||
}
|
||||
@ -1,86 +1,2 @@
|
||||
pub mod client;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use liana::miniscript::bitcoin::Network;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
auth: HashMap<String, NetworkAuthConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct NetworkAuthConfig {
|
||||
email: String,
|
||||
access_token: String,
|
||||
expires_at: i64,
|
||||
refresh_token: String,
|
||||
}
|
||||
|
||||
pub const DEFAULT_FILE_NAME: &str = "lite.json";
|
||||
|
||||
impl Config {
|
||||
pub fn file_path(datadir: PathBuf, network: Network) -> PathBuf {
|
||||
let mut path = datadir;
|
||||
path.push(network.to_string());
|
||||
path.push(DEFAULT_FILE_NAME);
|
||||
path
|
||||
}
|
||||
pub fn from_file(datadir: PathBuf, network: Network) -> Result<Self, ConfigError> {
|
||||
let path = Self::file_path(datadir, network);
|
||||
|
||||
let config = std::fs::read(path)
|
||||
.map_err(|e| match e.kind() {
|
||||
std::io::ErrorKind::NotFound => ConfigError::NotFound,
|
||||
_ => ConfigError::ReadingFile(format!("Reading settings file: {}", e)),
|
||||
})
|
||||
.and_then(|file_content| {
|
||||
serde_json::from_slice::<Config>(&file_content)
|
||||
.map_err(|e| ConfigError::ReadingFile(format!("Parsing settings file: {}", e)))
|
||||
})?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn to_file(&self, datadir: PathBuf, network: Network) -> Result<(), ConfigError> {
|
||||
let path = Self::file_path(datadir, network);
|
||||
|
||||
let content = serde_json::to_string_pretty(&self).map_err(|e| {
|
||||
ConfigError::WritingFile(format!("Failed to serialize settings: {}", e))
|
||||
})?;
|
||||
|
||||
let mut settings_file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(path)
|
||||
.map_err(|e| ConfigError::WritingFile(e.to_string()))?;
|
||||
|
||||
settings_file.write_all(content.as_bytes()).map_err(|e| {
|
||||
tracing::warn!("failed to write to file: {:?}", e);
|
||||
ConfigError::WritingFile(e.to_string())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub enum ConfigError {
|
||||
NotFound,
|
||||
ReadingFile(String),
|
||||
WritingFile(String),
|
||||
Unexpected(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ConfigError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NotFound => write!(f, "Settings file not found"),
|
||||
Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e),
|
||||
Self::WritingFile(e) => write!(f, "Error while writing file: {}", e),
|
||||
Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub mod login;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
pub mod app;
|
||||
pub mod bitcoind;
|
||||
pub mod daemon;
|
||||
pub mod datadir;
|
||||
pub mod download;
|
||||
pub mod hw;
|
||||
pub mod installer;
|
||||
|
||||
314
gui/src/main.rs
314
gui/src/main.rs
@ -27,10 +27,14 @@ use liana_gui::{
|
||||
wallet::Wallet,
|
||||
App,
|
||||
},
|
||||
datadir::{self, create_directory},
|
||||
hw::HardwareWalletConfig,
|
||||
installer::{self, Installer},
|
||||
launcher::{self, Launcher},
|
||||
lianalite::client::{auth::AuthClient, backend::BackendClient, get_service_config},
|
||||
lianalite::{
|
||||
client::{backend::api, backend::BackendWalletClient},
|
||||
login,
|
||||
},
|
||||
loader::{self, Loader},
|
||||
logger::Logger,
|
||||
VERSION,
|
||||
@ -41,8 +45,6 @@ enum Arg {
|
||||
ConfigPath(PathBuf),
|
||||
DatadirPath(PathBuf),
|
||||
Network(bitcoin::Network),
|
||||
Email(String),
|
||||
RefreshToken(String),
|
||||
}
|
||||
|
||||
fn parse_args(args: Vec<String>) -> Result<Vec<Arg>, Box<dyn Error>> {
|
||||
@ -85,18 +87,6 @@ Options:
|
||||
} else {
|
||||
return Err("missing arg to --datadir".into());
|
||||
}
|
||||
} else if arg == "--email" {
|
||||
if let Some(a) = args.get(i + 1) {
|
||||
res.push(Arg::Email(a.to_string()));
|
||||
} else {
|
||||
return Err("missing arg to --email".into());
|
||||
}
|
||||
} else if arg == "--refresh_token" {
|
||||
if let Some(a) = args.get(i + 1) {
|
||||
res.push(Arg::RefreshToken(a.to_string()));
|
||||
} else {
|
||||
return Err("missing arg to --access_token".into());
|
||||
}
|
||||
} else if arg.contains("--") {
|
||||
let network = bitcoin::Network::from_str(args[i].trim_start_matches("--"))?;
|
||||
res.push(Arg::Network(network));
|
||||
@ -117,6 +107,7 @@ enum State {
|
||||
Launcher(Box<Launcher>),
|
||||
Installer(Box<Installer>),
|
||||
Loader(Box<Loader>),
|
||||
Login(Box<login::LianaLiteLogin>),
|
||||
App(App),
|
||||
}
|
||||
|
||||
@ -133,6 +124,7 @@ pub enum Message {
|
||||
Install(Box<installer::Message>),
|
||||
Load(Box<loader::Message>),
|
||||
Run(Box<app::Message>),
|
||||
Login(Box<login::Message>),
|
||||
KeyPressed(Key),
|
||||
Event(iced::Event),
|
||||
}
|
||||
@ -177,7 +169,7 @@ impl Application for GUI {
|
||||
if !datadir_path.exists() {
|
||||
// datadir is created right before launching the installer
|
||||
// so logs can go in <datadir_path>/installer.log
|
||||
if let Err(e) = create_datadir(&datadir_path) {
|
||||
if let Err(e) = create_directory(&datadir_path) {
|
||||
error!("Failed to create datadir: {}", e);
|
||||
} else {
|
||||
info!(
|
||||
@ -190,7 +182,7 @@ impl Application for GUI {
|
||||
datadir_path.clone(),
|
||||
log_level.unwrap_or(LevelFilter::INFO),
|
||||
);
|
||||
let (install, command) = Installer::new(datadir_path, network);
|
||||
let (install, command) = Installer::new(datadir_path, network, None);
|
||||
cmds.push(command.map(|msg| Message::Install(Box::new(msg))));
|
||||
State::Installer(Box::new(install))
|
||||
}
|
||||
@ -204,99 +196,6 @@ impl Application for GUI {
|
||||
cmds.push(command.map(|msg| Message::Load(Box::new(msg))));
|
||||
State::Loader(Box::new(loader))
|
||||
}
|
||||
Config::RunWithRemoteBackend(email, refresh_token) => {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
// Spawn the root task
|
||||
let (wallet, client) = rt.block_on(async {
|
||||
let config = get_service_config(bitcoin::Network::Signet).await.unwrap();
|
||||
let backend_url = config.backend_api_url.to_owned();
|
||||
|
||||
let supabase_client =
|
||||
AuthClient::new(config.auth_api_url, config.auth_api_public_key);
|
||||
let access = match refresh_token {
|
||||
None => {
|
||||
supabase_client.sign_in_otp(&email).await.unwrap();
|
||||
|
||||
eprintln!("Please enter token:");
|
||||
let mut token = String::new();
|
||||
std::io::stdin()
|
||||
.read_line(&mut token)
|
||||
.expect("Failed to read line");
|
||||
|
||||
supabase_client
|
||||
.verify_otp(&email, token.trim_end())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
Some(token) => supabase_client.refresh_token(&token).await.unwrap(),
|
||||
};
|
||||
|
||||
let client =
|
||||
BackendClient::connect(supabase_client, backend_url, access.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
let (client, wallet) = client.connect_first().await.unwrap();
|
||||
eprintln!(
|
||||
"Connected, next time connect directly without otp verification with:"
|
||||
);
|
||||
eprintln!(
|
||||
"cargo run -- --email {} --refresh_token {}",
|
||||
email, access.refresh_token
|
||||
);
|
||||
|
||||
(wallet, client)
|
||||
});
|
||||
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 == client.user_id() {
|
||||
Some((a.fingerprint, a.alias))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let (app, command) = App::new(
|
||||
Cache {
|
||||
network: bitcoin::Network::Signet,
|
||||
coins: Vec::new(),
|
||||
rescan_progress: None,
|
||||
datadir_path: default_datadir().unwrap(),
|
||||
blockheight: wallet.tip_height.unwrap_or(0),
|
||||
},
|
||||
Arc::new(
|
||||
Wallet::new(wallet.descriptor)
|
||||
.with_name(wallet.name)
|
||||
.with_key_aliases(aliases)
|
||||
.with_hardware_wallets(hws),
|
||||
),
|
||||
app::Config {
|
||||
daemon_config_path: None,
|
||||
daemon_rpc_path: None,
|
||||
log_level: None,
|
||||
debug: None,
|
||||
start_internal_bitcoind: false,
|
||||
},
|
||||
Arc::new(client),
|
||||
default_datadir().unwrap(),
|
||||
None,
|
||||
);
|
||||
cmds.push(command.map(|msg| Message::Run(Box::new(msg))));
|
||||
State::App(app)
|
||||
}
|
||||
};
|
||||
(
|
||||
Self {
|
||||
@ -317,6 +216,7 @@ impl Application for GUI {
|
||||
State::Launcher(s) => s.stop(),
|
||||
State::Installer(s) => s.stop(),
|
||||
State::App(s) => s.stop(),
|
||||
State::Login(_) => {}
|
||||
};
|
||||
iced::window::close(iced::window::Id::MAIN)
|
||||
}
|
||||
@ -333,7 +233,7 @@ impl Application for GUI {
|
||||
if !datadir_path.exists() {
|
||||
// datadir is created right before launching the installer
|
||||
// so logs can go in <datadir_path>/installer.log
|
||||
if let Err(e) = create_datadir(&datadir_path) {
|
||||
if let Err(e) = datadir::create_directory(&datadir_path) {
|
||||
error!("Failed to create datadir: {}", e);
|
||||
} else {
|
||||
info!(
|
||||
@ -346,7 +246,8 @@ impl Application for GUI {
|
||||
datadir_path.clone(),
|
||||
self.log_level.unwrap_or(LevelFilter::INFO),
|
||||
);
|
||||
let (install, command) = Installer::new(datadir_path, network);
|
||||
|
||||
let (install, command) = Installer::new(datadir_path, network, None);
|
||||
self.state = State::Installer(Box::new(install));
|
||||
command.map(|msg| Message::Install(Box::new(msg)))
|
||||
}
|
||||
@ -357,38 +258,102 @@ impl Application for GUI {
|
||||
self.log_level
|
||||
.unwrap_or_else(|| cfg.log_level().unwrap_or(LevelFilter::INFO)),
|
||||
);
|
||||
let (loader, command) = Loader::new(datadir_path, cfg, network, None);
|
||||
self.state = State::Loader(Box::new(loader));
|
||||
command.map(|msg| Message::Load(Box::new(msg)))
|
||||
if app::settings::Settings::from_file(datadir_path.clone(), network).is_ok_and(
|
||||
{
|
||||
|s| {
|
||||
s.wallets.first().map(|w| w.remote_backend_auth.is_some())
|
||||
== Some(true)
|
||||
}
|
||||
},
|
||||
) {
|
||||
let (login, command) = login::LianaLiteLogin::new(datadir_path, network);
|
||||
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);
|
||||
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) => {
|
||||
let launcher = Launcher::new(l.datadir.clone());
|
||||
self.state = State::Launcher(Box::new(launcher));
|
||||
Command::none()
|
||||
}
|
||||
login::Message::Install(remote_backend) => {
|
||||
let (install, command) =
|
||||
Installer::new(l.datadir.clone(), l.network, remote_backend);
|
||||
self.state = State::Installer(Box::new(install));
|
||||
command.map(|msg| Message::Install(Box::new(msg)))
|
||||
}
|
||||
login::Message::Run(Ok((backend_client, wallet))) => {
|
||||
let config = app::Config::from_file(
|
||||
&l.datadir
|
||||
.join(l.network.to_string())
|
||||
.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(
|
||||
backend_client,
|
||||
wallet,
|
||||
l.datadir.clone(),
|
||||
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(path, internal_bitcoind) = *msg {
|
||||
let cfg = app::Config::from_file(&path).unwrap();
|
||||
let daemon_cfg =
|
||||
DaemonConfig::from_file(cfg.daemon_config_path.clone()).unwrap();
|
||||
let datadir_path = daemon_cfg
|
||||
.data_dir
|
||||
.as_ref()
|
||||
.expect("Installer must have set it")
|
||||
.clone();
|
||||
let settings = app::settings::Settings::from_file(i.datadir.clone(), i.network)
|
||||
.expect("A settings file was created");
|
||||
if settings
|
||||
.wallets
|
||||
.first()
|
||||
.map(|w| w.remote_backend_auth.is_some())
|
||||
== Some(true)
|
||||
{
|
||||
let (login, command) =
|
||||
login::LianaLiteLogin::new(i.datadir.clone(), i.network);
|
||||
self.state = State::Login(Box::new(login));
|
||||
command.map(|msg| Message::Login(Box::new(msg)))
|
||||
} else {
|
||||
let cfg = app::Config::from_file(&path).expect("A config file was created");
|
||||
let daemon_cfg =
|
||||
DaemonConfig::from_file(cfg.daemon_config_path.clone()).unwrap();
|
||||
let datadir_path = daemon_cfg
|
||||
.data_dir
|
||||
.as_ref()
|
||||
.expect("Installer must have set it")
|
||||
.clone();
|
||||
|
||||
self.logger.set_running_mode(
|
||||
datadir_path.clone(),
|
||||
daemon_cfg.bitcoin_config.network,
|
||||
self.log_level
|
||||
.unwrap_or_else(|| cfg.log_level().unwrap_or(LevelFilter::INFO)),
|
||||
);
|
||||
self.logger.remove_install_log_file(datadir_path.clone());
|
||||
let (loader, command) = Loader::new(
|
||||
datadir_path,
|
||||
cfg,
|
||||
daemon_cfg.bitcoin_config.network,
|
||||
internal_bitcoind,
|
||||
);
|
||||
self.state = State::Loader(Box::new(loader));
|
||||
command.map(|msg| Message::Load(Box::new(msg)))
|
||||
self.logger.set_running_mode(
|
||||
datadir_path.clone(),
|
||||
daemon_cfg.bitcoin_config.network,
|
||||
self.log_level
|
||||
.unwrap_or_else(|| cfg.log_level().unwrap_or(LevelFilter::INFO)),
|
||||
);
|
||||
self.logger.remove_install_log_file(datadir_path.clone());
|
||||
let (loader, command) = Loader::new(
|
||||
datadir_path,
|
||||
cfg,
|
||||
daemon_cfg.bitcoin_config.network,
|
||||
internal_bitcoind,
|
||||
);
|
||||
self.state = State::Loader(Box::new(loader));
|
||||
command.map(|msg| Message::Load(Box::new(msg)))
|
||||
}
|
||||
} else if let installer::Message::BackToLauncher = *msg {
|
||||
let launcher = Launcher::new(i.destination_path());
|
||||
self.state = State::Launcher(Box::new(launcher));
|
||||
@ -431,6 +396,7 @@ impl Application for GUI {
|
||||
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) {
|
||||
(
|
||||
@ -463,6 +429,7 @@ impl Application for GUI {
|
||||
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))),
|
||||
}
|
||||
}
|
||||
|
||||
@ -471,30 +438,59 @@ impl Application for GUI {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_datadir(datadir_path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[cfg(unix)]
|
||||
return {
|
||||
use std::fs::DirBuilder;
|
||||
use std::os::unix::fs::DirBuilderExt;
|
||||
|
||||
let mut builder = DirBuilder::new();
|
||||
builder.mode(0o700).recursive(true).create(datadir_path)?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
// TODO: permissions on Windows..
|
||||
#[cfg(not(unix))]
|
||||
return {
|
||||
std::fs::create_dir_all(datadir_path)?;
|
||||
Ok(())
|
||||
};
|
||||
pub fn create_app_with_remote_backend(
|
||||
remote_backend: BackendWalletClient,
|
||||
wallet: api::Wallet,
|
||||
datadir: PathBuf,
|
||||
config: app::Config,
|
||||
) -> (app::App, iced::Command<app::Message>) {
|
||||
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();
|
||||
App::new(
|
||||
Cache {
|
||||
network: bitcoin::Network::Signet,
|
||||
coins: Vec::new(),
|
||||
rescan_progress: None,
|
||||
datadir_path: default_datadir().unwrap(),
|
||||
blockheight: wallet.tip_height.unwrap_or(0),
|
||||
},
|
||||
Arc::new(
|
||||
Wallet::new(wallet.descriptor)
|
||||
.with_name(wallet.name)
|
||||
.with_key_aliases(aliases)
|
||||
.with_hardware_wallets(hws),
|
||||
),
|
||||
config,
|
||||
Arc::new(remote_backend),
|
||||
datadir,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub enum Config {
|
||||
Run(PathBuf, app::Config, bitcoin::Network),
|
||||
Launcher(PathBuf),
|
||||
Install(PathBuf, bitcoin::Network),
|
||||
RunWithRemoteBackend(String, Option<String>),
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@ -524,12 +520,6 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
let datadir_path = default_datadir().unwrap();
|
||||
Config::new(datadir_path, None)
|
||||
}
|
||||
[Arg::Email(email)] => Ok(Config::RunWithRemoteBackend(email.to_string(), None)),
|
||||
[Arg::Email(email), Arg::RefreshToken(token)]
|
||||
| [Arg::RefreshToken(token), Arg::Email(email)] => Ok(Config::RunWithRemoteBackend(
|
||||
email.to_string(),
|
||||
Some(token.to_string()),
|
||||
)),
|
||||
[Arg::Network(network)] => {
|
||||
let datadir_path = default_datadir().unwrap();
|
||||
Config::new(datadir_path, Some(*network))
|
||||
|
||||
@ -4,6 +4,7 @@ use iced::{widget::svg::Handle, window::icon};
|
||||
const LIANA_APP_ICON: &[u8] = include_bytes!("../static/logos/liana-app-icon.png");
|
||||
const LIANA_LOGO_GREY: &[u8] = include_bytes!("../static/logos/LIANA_SYMBOL_Gray.svg");
|
||||
const LIANA_BRAND_GREY: &[u8] = include_bytes!("../static/logos/LIANA_BRAND_Gray.svg");
|
||||
const WIZARDSARDINE_LETTERING: &[u8] = include_bytes!("../static/logos/logo-wizardsardine.svg");
|
||||
|
||||
pub fn liana_app_icon() -> icon::Icon {
|
||||
icon::from_file_data(LIANA_APP_ICON, None).unwrap()
|
||||
@ -19,6 +20,11 @@ pub fn liana_brand_grey() -> Svg {
|
||||
Svg::new(h)
|
||||
}
|
||||
|
||||
pub fn wizardsardine() -> Svg {
|
||||
let h = Handle::from_memory(WIZARDSARDINE_LETTERING.to_vec());
|
||||
Svg::new(h)
|
||||
}
|
||||
|
||||
const CREATE_NEW_WALLET_ICON: &[u8] = include_bytes!("../static/icons/blueprint.svg");
|
||||
|
||||
pub fn create_new_wallet_icon() -> Svg {
|
||||
|
||||
1
gui/ui/static/logos/logo-wizardsardine.svg
Normal file
1
gui/ui/static/logos/logo-wizardsardine.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 237 29"><path fill="#fff" d="M7.053 27.696 0 9.407h4.395l4.712 13.408 4.926-13.408h4.003l4.853 13.335L27.6 9.407h4.322l-7.016 18.29h-4.003l-4.926-13.618-4.926 13.617H7.049h.004ZM34.392 6.969V2.298h4.711v4.67h-4.711Zm.461 20.728V9.407h3.861v18.29h-3.86ZM42.495 27.696v-2.83l9.638-11.71h-9.496V9.408h14.952v2.795l-9.6 11.745h9.851v3.75H42.495ZM77.007 24.336v3.36H73.57v-2.228c-1.298 1.767-3.261 2.653-5.882 2.653-1.818 0-3.367-.548-4.643-1.644-1.276-1.096-1.912-2.515-1.912-4.261s.633-3.201 1.894-4.297c1.265-1.096 2.818-1.645 4.66-1.645h5.458v-3.288H62.41V9.415h14.6v14.928l-.004-.007Zm-8.326.744c1.156 0 2.193-.265 3.101-.795.91-.53 1.364-1.314 1.364-2.352v-2.795H68.68c-.967 0-1.803.276-2.497.831-.698.556-1.047 1.281-1.047 2.174s.349 1.612 1.047 2.142c.698.53 1.53.795 2.497.795ZM80.827 27.696V9.407h9.71v3.996h-5.845v14.29H80.83l-.004.003ZM112.285 27.697h-3.861v-3.22c-1.534 2.428-3.803 3.644-6.802 3.644-2.741 0-4.977-.925-6.715-2.776-1.737-1.851-2.603-4.12-2.603-6.81 0-2.689.873-4.928 2.622-6.79 1.748-1.862 3.98-2.795 6.696-2.795 2.999 0 5.268 1.216 6.802 3.644V0h3.861v27.697ZM98.023 22.71c1.145 1.11 2.592 1.662 4.341 1.662 1.748 0 3.195-.555 4.341-1.662 1.145-1.107 1.719-2.501 1.719-4.174 0-1.674-.578-3.067-1.738-4.174-1.156-1.107-2.599-1.662-4.322-1.662-1.723 0-3.196.559-4.341 1.68s-1.72 2.504-1.72 4.156c0 1.651.571 3.067 1.72 4.174ZM124.107 28.121c-2.126 0-3.904-.512-5.333-1.539-1.429-1.027-2.145-2.424-2.145-4.192h3.861c0 .777.331 1.405.993 1.873.661.472 1.559.708 2.693.708 1.04 0 1.891-.19 2.553-.567.661-.377.992-.918.992-1.626 0-.587-.265-1.085-.796-1.484-.531-.4-1.2-.7-2.003-.904-.804-.2-1.676-.446-2.622-.744a25.162 25.162 0 0 1-2.621-.973 4.889 4.889 0 0 1-2.003-1.625c-.531-.73-.796-1.616-.796-2.654 0-1.745.669-3.088 2.003-4.032s3.065-1.415 5.192-1.415c2.126 0 3.81.5 5.191 1.502 1.382 1.002 2.072 2.352 2.072 4.05h-3.861c0-.729-.309-1.313-.919-1.752-.615-.436-1.455-.654-2.516-.654-.993 0-1.785.182-2.374.548-.593.367-.887.868-.887 1.503 0 .755.371 1.343 1.116 1.768.745.424 1.647.762 2.712 1.008 1.062.247 2.127.53 3.188.85a6.14 6.14 0 0 1 2.712 1.644c.746.777 1.116 1.793 1.116 3.041 0 1.793-.698 3.184-2.09 4.174-1.392.991-3.203 1.485-5.42 1.485l-.008.007ZM150.098 24.336v3.36h-3.436v-2.228c-1.298 1.767-3.261 2.653-5.882 2.653-1.818 0-3.366-.548-4.642-1.644-1.276-1.096-1.913-2.515-1.913-4.261s.633-3.201 1.894-4.297c1.266-1.096 2.818-1.645 4.661-1.645h5.457v-3.288h-10.736V9.415h14.6v14.928l-.003-.007Zm-8.325.744c1.156 0 2.192-.265 3.101-.795.908-.53 1.363-1.314 1.363-2.352v-2.795h-4.464c-.967 0-1.804.276-2.498.831-.698.556-1.047 1.281-1.047 2.174s.349 1.612 1.047 2.142c.698.53 1.531.795 2.498.795ZM153.693 27.696V9.407h9.71v3.996h-5.846v14.29h-3.86l-.004.003ZM185.165 27.697h-3.86v-3.22c-1.535 2.428-3.803 3.644-6.802 3.644-2.742 0-4.977-.925-6.715-2.776-1.738-1.851-2.603-4.12-2.603-6.81 0-2.689.872-4.928 2.621-6.79s3.981-2.795 6.697-2.795c2.999 0 5.267 1.216 6.802 3.644V0h3.86v27.697Zm-14.262-4.987c1.146 1.11 2.593 1.662 4.341 1.662 1.749 0 3.196-.555 4.341-1.662 1.145-1.107 1.72-2.501 1.72-4.174 0-1.674-.579-3.067-1.738-4.174-1.156-1.107-2.6-1.662-4.323-1.662-1.723 0-3.195.559-4.341 1.68-1.145 1.121-1.719 2.504-1.719 4.156 0 1.651.571 3.067 1.719 4.174ZM188.535 6.969V2.298h4.712v4.67h-4.712Zm.462 20.728V9.407h3.861v18.29h-3.861ZM206.927 8.947c2.243 0 4.057.667 5.439 2 1.381 1.331 2.072 3.084 2.072 5.251v11.498h-3.861V17.472c0-1.437-.443-2.595-1.33-3.466s-2.073-1.31-3.563-1.31c-1.491 0-2.81.435-3.756 1.31-.945.871-1.417 2.05-1.417 3.539v10.151h-3.861V9.407h3.791v2.795c1.418-2.17 3.578-3.255 6.486-3.255ZM236.996 18.568v1.274h-15.323c.251 1.463.883 2.643 1.898 3.539 1.014.896 2.275 1.343 3.78 1.343.706 0 1.367-.095 1.985-.283.615-.189 1.113-.414 1.487-.672a6.64 6.64 0 0 0 .993-.83c.283-.295.48-.53.581-.709.102-.178.164-.3.19-.37l3.66.85a10.433 10.433 0 0 1-.992 1.593c-.389.519-.931 1.092-1.625 1.717-.694.624-1.589 1.132-2.687 1.52-1.094.389-2.316.585-3.661.585-2.759 0-5.06-.897-6.907-2.69-1.847-1.793-2.77-4.09-2.77-6.9 0-2.809.927-5.044 2.788-6.863 1.858-1.814 4.155-2.725 6.893-2.725 2.738 0 5.068.914 6.926 2.74 1.857 1.829 2.788 4.12 2.788 6.881h-.004Zm-15.323-1.379h11.321c-.204-1.437-.836-2.606-1.898-3.502-1.062-.897-2.33-1.343-3.814-1.343-1.392 0-2.606.432-3.642 1.292-1.036.86-1.695 2.047-1.967 3.553Z"/></svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
Loading…
x
Reference in New Issue
Block a user