From 5e2fe3b6c1e9099dc2f087b122bef1c398ebec62 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Tue, 28 May 2024 15:32:05 +0200 Subject: [PATCH] Add liana remote backend --- gui/src/app/error.rs | 7 +- gui/src/app/message.rs | 5 +- gui/src/app/mod.rs | 47 +++---- gui/src/app/state/settings/mod.rs | 4 +- gui/src/app/state/settings/wallet.rs | 163 ++++++++++++++---------- gui/src/app/view/warning.rs | 6 +- gui/src/app/wallet.rs | 27 ++-- gui/src/daemon/client/jsonrpc.rs | 8 +- gui/src/daemon/client/mod.rs | 6 +- gui/src/daemon/embedded.rs | 6 +- gui/src/daemon/mod.rs | 36 +++++- gui/src/lianalite/client/backend/mod.rs | 119 ++++++++--------- gui/src/loader.rs | 11 +- gui/src/main.rs | 123 +++++++++++++++++- 14 files changed, 381 insertions(+), 187 deletions(-) diff --git a/gui/src/app/error.rs b/gui/src/app/error.rs index b13f8ae7..2ca312d7 100644 --- a/gui/src/app/error.rs +++ b/gui/src/app/error.rs @@ -29,10 +29,10 @@ impl std::fmt::Display for Error { DaemonError::Unexpected(e) => write!(f, "{}", e), DaemonError::NoAnswer => write!(f, "Daemon did not answer"), DaemonError::DaemonStopped => write!(f, "Daemon stopped"), - DaemonError::Transport(Some(ErrorKind::ConnectionRefused), _) => { + DaemonError::RpcSocket(Some(ErrorKind::ConnectionRefused), _) => { write!(f, "Failed to connect to daemon") } - DaemonError::Transport(kind, e) => { + DaemonError::RpcSocket(kind, e) => { if let Some(k) = kind { write!(f, "{} [{:?}]", e, k) } else { @@ -48,6 +48,9 @@ impl std::fmt::Display for Error { DaemonError::Rpc(code, e) => { write!(f, "[{:?}] {}", code, e) } + DaemonError::Http(code, e) => { + write!(f, "[{:?}] {}", code, e) + } DaemonError::CoinSelectionError => write!(f, "{}", e), }, Self::Unexpected(e) => write!(f, "Unexpected error: {}", e), diff --git a/gui/src/app/message.rs b/gui/src/app/message.rs index 9b9de75d..97b77417 100644 --- a/gui/src/app/message.rs +++ b/gui/src/app/message.rs @@ -23,8 +23,7 @@ pub enum Message { View(view::Message), LoadDaemonConfig(Box), DaemonConfigLoaded(Result<(), Error>), - LoadWallet, - WalletLoaded(Result, Error>), + LoadWallet(Wallet), Info(Result), ReceiveAddress(Result<(Address, ChildNumber), Error>), Coins(Result, Error>), @@ -34,7 +33,7 @@ pub enum Message { RbfPsbt(Result), Recovery(Result), Signed(Fingerprint, Result), - WalletRegistered(Result), + WalletUpdated(Result, Error>), Updated(Result<(), Error>), Saved(Result<(), Error>), Verified(Fingerprint, Result<(), Error>), diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index 4f65b62f..c4793501 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -36,7 +36,7 @@ use state::{ use crate::{ app::{cache::Cache, error::Error, menu::Menu, wallet::Wallet}, bitcoind::Bitcoind, - daemon::{embedded::EmbeddedDaemon, Daemon}, + daemon::{embedded::EmbeddedDaemon, Daemon, DaemonBackend}, }; use self::state::SettingsState; @@ -116,7 +116,6 @@ impl Panels { } pub struct App { - data_dir: PathBuf, cache: Cache, config: Config, wallet: Arc, @@ -135,17 +134,11 @@ impl App { data_dir: PathBuf, internal_bitcoind: Option, ) -> (App, Command) { - let mut panels = Panels::new( - &cache, - wallet.clone(), - data_dir.clone(), - internal_bitcoind.as_ref(), - ); + let mut panels = Panels::new(&cache, wallet.clone(), data_dir, internal_bitcoind.as_ref()); let cmd = panels.home.reload(daemon.clone(), wallet.clone()); ( Self { panels, - data_dir, cache, config, daemon, @@ -218,14 +211,26 @@ impl App { pub fn subscription(&self) -> Subscription { Subscription::batch(vec![ - time::every(Duration::from_secs(10)).map(|_| Message::Tick), + time::every(Duration::from_secs( + // LianaLite has no rescan feature, the cache refresh loop is only + // to fetch the new block height tip which is only used to warn user + // about recovery availability. + if self.daemon.backend() == DaemonBackend::RemoteBackend { + 120 + // For the rescan feature, we set a higher frequency of cache refresh + // to give to user an up-to-date view of the rescan progress. + } else { + 10 + }, + )) + .map(|_| Message::Tick), self.panels.current().subscription(), ]) } pub fn stop(&mut self) { info!("Close requested"); - if !self.daemon.is_external() { + if self.daemon.backend() == DaemonBackend::EmbeddedLianad { if let Err(e) = Handle::current().block_on(async { self.daemon.stop().await }) { error!("{}", e); } else { @@ -245,6 +250,7 @@ impl App { 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?; let info = daemon.get_info().await?; @@ -276,9 +282,13 @@ impl App { let res = self.load_daemon_config(&path, *cfg); self.update(Message::DaemonConfigLoaded(res)) } - Message::LoadWallet => { - let res = self.load_wallet(); - self.update(Message::WalletLoaded(res)) + Message::WalletUpdated(Ok(wallet)) => { + self.wallet = wallet.clone(); + self.panels.current_mut().update( + self.daemon.clone(), + &self.cache, + Message::WalletUpdated(Ok(wallet)), + ) } Message::View(view::Message::Menu(menu)) => self.set_current_panel(menu), Message::View(view::Message::Clipboard(text)) => clipboard::write(text), @@ -313,15 +323,6 @@ impl App { }) } - pub fn load_wallet(&mut self) -> Result, Error> { - let wallet = Wallet::new(self.wallet.main_descriptor.clone()) - .load_settings(&self.data_dir, self.cache.network)?; - - self.wallet = Arc::new(wallet); - - Ok(self.wallet.clone()) - } - pub fn view(&self) -> Element { let content = self.panels.current().view(&self.cache).map(Message::View); if self.cache.network != bitcoin::Network::Bitcoin { diff --git a/gui/src/app/state/settings/mod.rs b/gui/src/app/state/settings/mod.rs index ff325e27..d3e72e6d 100644 --- a/gui/src/app/state/settings/mod.rs +++ b/gui/src/app/state/settings/mod.rs @@ -14,7 +14,7 @@ use wallet::WalletSettingsState; use crate::{ app::{cache::Cache, error::Error, message::Message, state::State, view, wallet::Wallet}, - daemon::Daemon, + daemon::{Daemon, DaemonBackend}, }; pub struct SettingsState { @@ -48,7 +48,7 @@ impl State for SettingsState { BitcoindSettingsState::new( daemon.config().cloned(), cache, - daemon.is_external(), + daemon.backend() != DaemonBackend::EmbeddedLianad, self.internal_bitcoind, ) .into(), diff --git a/gui/src/app/state/settings/wallet.rs b/gui/src/app/state/settings/wallet.rs index 3f2710d2..34690539 100644 --- a/gui/src/app/state/settings/wallet.rs +++ b/gui/src/app/state/settings/wallet.rs @@ -16,7 +16,7 @@ use crate::{ app::{ cache::Cache, error::Error, message::Message, settings, state::State, view, wallet::Wallet, }, - daemon::Daemon, + daemon::{Daemon, DaemonBackend}, hw::{HardwareWallet, HardwareWalletConfig, HardwareWallets}, }; @@ -106,30 +106,21 @@ impl State for WalletSettingsState { message: Message, ) -> Command { match message { - Message::Updated(res) => match res { - Ok(()) => { - self.processing = false; - self.updated = true; - Command::perform(async {}, |_| Message::LoadWallet) - } - Err(e) => { - self.processing = false; - self.warning = Some(e); + Message::WalletUpdated(res) => { + self.processing = false; + if let Some(modal) = &mut self.modal { + modal.update(daemon, cache, Message::WalletUpdated(res)) + } else { + match res { + Ok(wallet) => { + self.keys_aliases = Self::keys_aliases(&wallet); + self.wallet = wallet; + self.updated = true; + } + Err(e) => self.warning = Some(e), + }; Command::none() } - }, - Message::WalletLoaded(res) => { - match res { - Ok(wallet) => { - if let Some(modal) = &mut self.modal { - modal.wallet = wallet.clone(); - } - self.keys_aliases = Self::keys_aliases(&wallet); - self.wallet = wallet; - } - Err(e) => self.warning = Some(e), - }; - Command::none() } Message::View(view::Message::Settings( view::SettingsMessage::FingerprintAliasEdited(fg, value), @@ -156,8 +147,9 @@ impl State for WalletSettingsState { .iter() .map(|(fg, name)| (*fg, name.value.to_owned())) .collect(), + daemon, ), - Message::Updated, + Message::WalletUpdated, ) } Message::View(view::Message::Close) => { @@ -246,7 +238,7 @@ impl RegisterWalletModal { fn update( &mut self, - _daemon: Arc, + daemon: Arc, cache: &Cache, message: Message, ) -> Command { @@ -263,13 +255,16 @@ impl RegisterWalletModal { Command::none() } }, - Message::WalletRegistered(res) => { + Message::WalletUpdated(res) => { self.processing = false; self.chosen_hw = None; match res { - Ok(fingerprint) => { - self.registered.insert(fingerprint); - return Command::perform(async {}, |_| Message::LoadWallet); + Ok(wallet) => { + self.registered = HashSet::new(); + for hw in &wallet.hardware_wallets { + self.registered.insert(hw.fingerprint); + } + self.wallet = wallet; } Err(e) => { if !matches!(e, Error::HardwareWallet(async_hwi::Error::UserRefused)) { @@ -295,8 +290,9 @@ impl RegisterWalletModal { device.clone(), *fingerprint, self.wallet.clone(), + daemon, ), - Message::WalletRegistered, + Message::WalletUpdated, ) } else { Command::none() @@ -313,40 +309,61 @@ async fn register_wallet( hw: std::sync::Arc, fingerprint: Fingerprint, wallet: Arc, -) -> Result { + daemon: Arc, +) -> Result, Error> { let hmac = hw .register_wallet(&wallet.name, &wallet.main_descriptor.to_string()) .await .map_err(Error::from)?; if let Some(hmac) = hmac { - let mut settings = settings::Settings::from_file(data_dir.clone(), network)?; - let checksum = wallet.descriptor_checksum(); - if let Some(wallet_setting) = settings - .wallets - .iter_mut() - .find(|w| w.descriptor_checksum == checksum) - { - let kind = hw.device_kind().to_string(); - if let Some(hw_config) = wallet_setting - .hardware_wallets + let kind = hw.device_kind().to_string(); + let hw_cfg = HardwareWalletConfig { + kind: kind.clone(), + token: hex::encode(hmac), + fingerprint, + }; + + if daemon.backend() != DaemonBackend::RemoteBackend { + let mut settings = settings::Settings::from_file(data_dir.clone(), network)?; + let checksum = wallet.descriptor_checksum(); + + if let Some(wallet_setting) = settings + .wallets .iter_mut() - .find(|cfg| cfg.kind == kind && cfg.fingerprint == fingerprint) + .find(|w| w.descriptor_checksum == checksum) { - hw_config.token = hex::encode(hmac); - } else { - wallet_setting.hardware_wallets.push(HardwareWalletConfig { - kind, - token: hex::encode(hmac), - fingerprint, - }) + if let Some(hw_config) = wallet_setting + .hardware_wallets + .iter_mut() + .find(|cfg| cfg.kind == kind && cfg.fingerprint == fingerprint) + { + *hw_config = hw_cfg.clone(); + } else { + wallet_setting.hardware_wallets.push(hw_cfg.clone()) + } } + + settings.to_file(data_dir, network)?; } - settings.to_file(data_dir, network)?; + let mut wallet = wallet.as_ref().clone(); + if let Some(hw_config) = wallet + .hardware_wallets + .iter_mut() + .find(|cfg| cfg.kind == kind && cfg.fingerprint == fingerprint) + { + *hw_config = hw_cfg.clone(); + } else { + wallet.hardware_wallets.push(hw_cfg) + } + daemon + .update_wallet_metadata(&wallet.keys_aliases, &wallet.hardware_wallets) + .await?; + return Ok(Arc::new(wallet)); } - Ok(fingerprint) + Ok(wallet) } async fn update_keys_aliases( @@ -354,24 +371,34 @@ async fn update_keys_aliases( network: Network, wallet: Arc, keys_aliases: Vec<(Fingerprint, String)>, -) -> Result<(), Error> { - let mut settings = settings::Settings::from_file(data_dir.clone(), network)?; - let checksum = wallet.descriptor_checksum(); - if let Some(wallet_setting) = settings - .wallets - .iter_mut() - .find(|w| w.descriptor_checksum == checksum) - { - wallet_setting.keys = keys_aliases - .into_iter() - .map(|(master_fingerprint, name)| settings::KeySetting { - master_fingerprint, - name, - }) - .collect(); + daemon: Arc, +) -> Result, Error> { + if daemon.backend() != DaemonBackend::RemoteBackend { + let mut settings = settings::Settings::from_file(data_dir.clone(), network)?; + let checksum = wallet.descriptor_checksum(); + if let Some(wallet_setting) = settings + .wallets + .iter_mut() + .find(|w| w.descriptor_checksum == checksum) + { + wallet_setting.keys = keys_aliases + .iter() + .map(|(master_fingerprint, name)| settings::KeySetting { + master_fingerprint: *master_fingerprint, + name: name.clone(), + }) + .collect(); + } + + settings.to_file(data_dir, network)?; } - settings.to_file(data_dir, network)?; + let mut wallet = wallet.as_ref().clone(); + wallet.keys_aliases = keys_aliases.into_iter().collect(); - Ok(()) + daemon + .update_wallet_metadata(&wallet.keys_aliases, &wallet.hardware_wallets) + .await?; + + Ok(Arc::new(wallet)) } diff --git a/gui/src/app/view/warning.rs b/gui/src/app/view/warning.rs index 1ec4dfd2..1496646a 100644 --- a/gui/src/app/view/warning.rs +++ b/gui/src/app/view/warning.rs @@ -25,12 +25,16 @@ impl From<&Error> for WarningMessage { WarningMessage("Internal error".to_string()) } } + DaemonError::Http(Some(code), error) => { + WarningMessage(format!("HTTP error {}: {}", code, error)) + } + DaemonError::Http(None, error) => WarningMessage(format!("HTTP error: {}", error)), DaemonError::Unexpected(_) => WarningMessage("Unknown error".to_string()), DaemonError::Start(_) => WarningMessage("Daemon failed to start".to_string()), DaemonError::ClientNotSupported => { WarningMessage("Daemon client is not supported".to_string()) } - DaemonError::NoAnswer | DaemonError::Transport(..) => { + DaemonError::NoAnswer | DaemonError::RpcSocket(..) => { WarningMessage("Communication with Daemon failed".to_string()) } DaemonError::DaemonStopped => WarningMessage("Daemon stopped".to_string()), diff --git a/gui/src/app/wallet.rs b/gui/src/app/wallet.rs index d6b4405d..2fb83226 100644 --- a/gui/src/app/wallet.rs +++ b/gui/src/app/wallet.rs @@ -1,5 +1,6 @@ use std::collections::{HashMap, HashSet}; use std::path::Path; +use std::sync::Arc; use crate::{app::settings, hw::HardwareWalletConfig, signer::Signer}; @@ -24,13 +25,13 @@ pub fn wallet_name(main_descriptor: &LianaDescriptor) -> String { ) } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Wallet { pub name: String, pub main_descriptor: LianaDescriptor, pub keys_aliases: HashMap, pub hardware_wallets: Vec, - pub signer: Option, + pub signer: Option>, } impl Wallet { @@ -60,7 +61,7 @@ impl Wallet { } pub fn with_signer(mut self, signer: Signer) -> Self { - self.signer = Some(signer); + self.signer = Some(Arc::new(signer)); self } @@ -87,12 +88,12 @@ impl Wallet { .to_string() } - pub fn load_settings( + pub fn load_from_settings( self, datadir_path: &Path, network: bitcoin::Network, ) -> Result { - let mut wallet = match settings::Settings::from_file(datadir_path.to_path_buf(), network) { + let wallet = match settings::Settings::from_file(datadir_path.to_path_buf(), network) { Ok(settings) => { if let Some(wallet_setting) = settings.wallets.first() { self.with_name(wallet_setting.name.clone()) @@ -114,6 +115,14 @@ impl Wallet { Err(e) => return Err(e.into()), }; + Ok(wallet) + } + + pub fn load_hotsigners( + self, + datadir_path: &Path, + network: bitcoin::Network, + ) -> Result { let hot_signers = match HotSigner::from_datadir(datadir_path, network) { Ok(signers) => signers, Err(e) => match e { @@ -129,15 +138,15 @@ impl Wallet { }; let curve = bitcoin::secp256k1::Secp256k1::signing_only(); - let keys = wallet.descriptor_keys(); + let keys = self.descriptor_keys(); if let Some(hot_signer) = hot_signers .into_iter() .find(|s| keys.contains(&s.fingerprint(&curve))) { - wallet = wallet.with_signer(Signer::new(hot_signer)); + Ok(self.with_signer(Signer::new(hot_signer))) + } else { + Ok(self) } - - Ok(wallet) } } diff --git a/gui/src/daemon/client/jsonrpc.rs b/gui/src/daemon/client/jsonrpc.rs index 1d9fd302..3eeb12dd 100644 --- a/gui/src/daemon/client/jsonrpc.rs +++ b/gui/src/daemon/client/jsonrpc.rs @@ -240,13 +240,13 @@ impl error::Error for Error { impl From for super::DaemonError { fn from(e: Error) -> super::DaemonError { match e { - Error::Io(e) => super::DaemonError::Transport(Some(e.kind()), format!("io: {:?}", e)), - Error::Json(e) => super::DaemonError::Transport(None, format!("json decode: {}", e)), + Error::Io(e) => super::DaemonError::RpcSocket(Some(e.kind()), format!("io: {:?}", e)), + Error::Json(e) => super::DaemonError::RpcSocket(None, format!("json decode: {}", e)), Error::NonceMismatch => { - super::DaemonError::Transport(None, format!("transport: {}", e)) + super::DaemonError::RpcSocket(None, format!("transport: {}", e)) } Error::VersionMismatch => { - super::DaemonError::Transport(None, format!("transport: {}", e)) + super::DaemonError::RpcSocket(None, format!("transport: {}", e)) } Error::NoErrorOrResult => super::DaemonError::NoAnswer, Error::NotSupported => super::DaemonError::ClientNotSupported, diff --git a/gui/src/daemon/client/mod.rs b/gui/src/daemon/client/mod.rs index 57cd637b..8f546f18 100644 --- a/gui/src/daemon/client/mod.rs +++ b/gui/src/daemon/client/mod.rs @@ -18,7 +18,7 @@ use liana::{ miniscript::bitcoin::{address, psbt::Psbt, Address, OutPoint, Txid}, }; -use super::{model::*, Daemon, DaemonError}; +use super::{model::*, Daemon, DaemonBackend, DaemonError}; pub trait Client { type Error: Into + Debug; @@ -55,8 +55,8 @@ impl Lianad { #[async_trait] impl Daemon for Lianad { - fn is_external(&self) -> bool { - true + fn backend(&self) -> DaemonBackend { + DaemonBackend::ExternalLianad } fn config(&self) -> Option<&Config> { diff --git a/gui/src/daemon/embedded.rs b/gui/src/daemon/embedded.rs index 98e63037..f9ee842c 100644 --- a/gui/src/daemon/embedded.rs +++ b/gui/src/daemon/embedded.rs @@ -1,7 +1,7 @@ use std::collections::{HashMap, HashSet}; use tokio::sync::Mutex; -use super::{model::*, Daemon, DaemonError}; +use super::{model::*, Daemon, DaemonBackend, DaemonError}; use async_trait::async_trait; use liana::{ commands::{CoinStatus, LabelItem}, @@ -49,8 +49,8 @@ impl std::fmt::Debug for EmbeddedDaemon { #[async_trait] impl Daemon for EmbeddedDaemon { - fn is_external(&self) -> bool { - false + fn backend(&self) -> DaemonBackend { + DaemonBackend::EmbeddedLianad } fn config(&self) -> Option<&Config> { diff --git a/gui/src/daemon/mod.rs b/gui/src/daemon/mod.rs index 0a462a6c..43398ce6 100644 --- a/gui/src/daemon/mod.rs +++ b/gui/src/daemon/mod.rs @@ -13,16 +13,22 @@ use async_trait::async_trait; use liana::{ commands::{CoinStatus, LabelItem, TransactionInfo}, config::Config, - miniscript::bitcoin::{address, psbt::Psbt, secp256k1, Address, OutPoint, Txid}, + miniscript::bitcoin::{ + address, bip32::Fingerprint, psbt::Psbt, secp256k1, Address, OutPoint, Txid, + }, StartupError, }; +use crate::hw::HardwareWalletConfig; + #[derive(Debug)] pub enum DaemonError { /// Something was wrong with the request. Rpc(i32, String), - /// Something was wrong with the communication. - Transport(Option, String), + /// Something was wrong with the rpc socket communication. + RpcSocket(Option, String), + /// Something was wrong with the http communication. + Http(Option, String), /// Something unexpected happened. Unexpected(String), /// No response. @@ -43,7 +49,8 @@ impl std::fmt::Display for DaemonError { Self::Rpc(code, e) => write!(f, "Daemon error rpc call: [{:?}] {}", code, e), Self::NoAnswer => write!(f, "Daemon returned no answer"), Self::DaemonStopped => write!(f, "Daemon stopped"), - Self::Transport(kind, e) => write!(f, "Daemon transport error: [{:?}] {}", kind, e), + Self::RpcSocket(kind, e) => write!(f, "Daemon transport error: [{:?}] {}", kind, e), + Self::Http(kind, e) => write!(f, "Http error: [{:?}] {}", kind, e), Self::Unexpected(e) => write!(f, "Daemon unexpected error: {}", e), Self::Start(e) => write!(f, "Daemon did not start: {}", e), Self::ClientNotSupported => write!(f, "Daemon communication is not supported"), @@ -52,9 +59,16 @@ impl std::fmt::Display for DaemonError { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DaemonBackend { + EmbeddedLianad, + ExternalLianad, + RemoteBackend, +} + #[async_trait] pub trait Daemon: Debug { - fn is_external(&self) -> bool; + fn backend(&self) -> DaemonBackend; fn config(&self) -> Option<&Config>; async fn is_alive(&self) -> Result<(), DaemonError>; async fn stop(&self) -> Result<(), DaemonError>; @@ -263,6 +277,10 @@ pub trait Daemon: Debug { } } + if txids.is_empty() { + return Ok(Vec::new()); + } + let txs = self.list_txs(&txids).await?.transactions; let mut txs = txs .into_iter() @@ -295,6 +313,14 @@ pub trait Daemon: Debug { load_labels(self, &mut txs).await?; Ok(txs) } + /// Implemented by LianaLite backend + async fn update_wallet_metadata( + &self, + _fingerprint_aliases: &HashMap, + _hws: &[HardwareWalletConfig], + ) -> Result<(), DaemonError> { + Ok(()) + } } async fn load_labels( diff --git a/gui/src/lianalite/client/backend/mod.rs b/gui/src/lianalite/client/backend/mod.rs index 4eed47d9..6bff0861 100644 --- a/gui/src/lianalite/client/backend/mod.rs +++ b/gui/src/lianalite/client/backend/mod.rs @@ -2,10 +2,7 @@ pub mod api; use std::{ collections::{HashMap, HashSet}, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, RwLock, - }, + sync::Arc, }; use async_trait::async_trait; @@ -17,6 +14,7 @@ use liana::{ miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid}, }; use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response}; +use tokio::sync::RwLock; use crate::{ daemon::{model::*, Daemon, DaemonBackend, DaemonError}, @@ -53,11 +51,10 @@ fn request( req } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct BackendClient { auth: Arc>, auth_client: auth::AuthClient, - auth_refreshing: AtomicBool, url: String, network: Network, @@ -90,7 +87,6 @@ impl BackendClient { Ok(Self { auth: Arc::new(RwLock::new(credentials)), auth_client, - auth_refreshing: AtomicBool::new(false), network: Network::Signet, url, user_id, @@ -112,18 +108,15 @@ impl BackendClient { )) } - fn request(&self, method: Method, url: U) -> Result { - let access_token = &self - .auth - .read() - .map_err(|e| DaemonError::Unexpected(e.to_string()))? - .access_token; - Ok(request(&self.http, method, url, access_token)) + async fn request(&self, method: Method, url: U) -> RequestBuilder { + let access_token = &self.auth.read().await.access_token; + request(&self.http, method, url, access_token) } pub async fn list_wallets(&self) -> Result, DaemonError> { let response = self - .request(Method::GET, &format!("{}/v1/wallets", self.url))? + .request(Method::GET, &format!("{}/v1/wallets", self.url)) + .await .send() .await?; if !response.status().is_success() { @@ -138,7 +131,7 @@ impl BackendClient { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct BackendWalletClient { inner: BackendClient, wallet_uuid: String, @@ -177,7 +170,8 @@ impl BackendWalletClient { .request( Method::GET, &format!("{}/v1/wallets/{}/psbts", self.inner.url, self.wallet_uuid), - )? + ) + .await .query(&query) .send() .await?; @@ -219,7 +213,8 @@ impl BackendWalletClient { "{}/v1/wallets/{}/transactions", self.inner.url, self.wallet_uuid ), - )? + ) + .await .query(&query) .send() .await?; @@ -254,7 +249,8 @@ impl BackendWalletClient { "{}/v1/wallets/{}/transactions", self.inner.url, self.wallet_uuid ), - )? + ) + .await .query(&query) .send() .await?; @@ -300,7 +296,8 @@ impl BackendWalletClient { .request( Method::GET, &format!("{}/v1/wallets/{}/coins", self.inner.url, self.wallet_uuid), - )? + ) + .await .query(&query) .send() .await?; @@ -316,13 +313,8 @@ impl BackendWalletClient { Ok(res) } - fn auth(&self) -> Result { - Ok(self - .inner - .auth - .read() - .map_err(|e| DaemonError::Unexpected(e.to_string()))? - .clone()) + async fn auth(&self) -> AccessTokenResponse { + self.inner.auth.read().await.clone() } } @@ -337,27 +329,25 @@ impl Daemon for BackendWalletClient { } /// refresh the token if close to expiration. - /// auth_refreshing enforce that no other thread will try to refresh the - /// access credentials in the same time. async fn is_alive(&self) -> Result<(), DaemonError> { - let auth = self.auth()?; - if auth.expires_at < Utc::now().timestamp() + 60 - && !self.inner.auth_refreshing.load(Ordering::Relaxed) - { - self.inner.auth_refreshing.store(true, Ordering::Relaxed); - let new = self - .inner - .auth_client - .refresh_token(&auth.refresh_token) - .await?; - let mut old = self - .inner - .auth - .write() - .map_err(|e| DaemonError::Unexpected(e.to_string()))?; - *old = new; - tracing::info!("Liana backend access was refreshed"); - self.inner.auth_refreshing.store(false, Ordering::Relaxed); + let auth = self.auth().await; + if auth.expires_at < Utc::now().timestamp() + 60 { + match self.inner.auth.try_write() { + Err(_) => { + // something is using the lock, we will try next time. + return Ok(()); + } + Ok(mut old) => { + let new = self + .inner + .auth_client + .refresh_token(&auth.refresh_token) + .await?; + + *old = new; + tracing::info!("Liana backend access was refreshed"); + } + } } Ok(()) } @@ -391,7 +381,8 @@ impl Daemon for BackendWalletClient { "{}/v1/wallets/{}/addresses", self.inner.url, self.wallet_uuid ), - )? + ) + .await .send() .await?; @@ -527,7 +518,8 @@ impl Daemon for BackendWalletClient { "{}/v1/wallets/{}/psbts/generate", self.inner.url, self.wallet_uuid ), - )? + ) + .await .json(&api::payload::GeneratePsbt { save: false, feerate: feerate_vb, @@ -563,7 +555,8 @@ impl Daemon for BackendWalletClient { "{}/v1/wallets/{}/psbts/rbf", self.inner.url, self.wallet_uuid ), - )? + ) + .await .json(&api::payload::GenerateRbfPsbt { txid: *txid, is_cancel, @@ -591,7 +584,8 @@ impl Daemon for BackendWalletClient { .request( Method::POST, &format!("{}/v1/wallets/{}/psbts", self.inner.url, self.wallet_uuid), - )? + ) + .await .json(&api::payload::ImportPsbt { psbt: psbt.to_string(), }) @@ -625,7 +619,8 @@ impl Daemon for BackendWalletClient { .request( Method::DELETE, &format!("{}/v1/psbts/{}", self.inner.url, psbt.uuid), - )? + ) + .await .send() .await?; @@ -653,7 +648,8 @@ impl Daemon for BackendWalletClient { .request( Method::POST, &format!("{}/v1/psbts/{}/broadcast", self.inner.url, psbt.uuid), - )? + ) + .await .send() .await?; @@ -685,7 +681,8 @@ impl Daemon for BackendWalletClient { "{}/v1/wallets/{}/psbts/recovery", self.inner.url, self.wallet_uuid ), - )? + ) + .await .json(&api::payload::GenerateRecoveryPsbt { save: false, feerate: feerate_vb, @@ -715,7 +712,8 @@ impl Daemon for BackendWalletClient { .request( Method::GET, &format!("{}/v1/wallets/{}/labels", self.inner.url, self.wallet_uuid), - )? + ) + .await .query(&[("items", chunk.join(","))]) .send() .await?; @@ -743,7 +741,8 @@ impl Daemon for BackendWalletClient { .request( Method::POST, &format!("{}/v1/wallets/{}/labels", self.inner.url, self.wallet_uuid), - )? + ) + .await .json(&api::payload::Labels { labels: items .iter() @@ -858,7 +857,7 @@ impl Daemon for BackendWalletClient { hws: &[HardwareWalletConfig], ) -> Result<(), DaemonError> { let wallet = self.get_wallet().await?; - let ledger_kinds = vec![ + let ledger_kinds = [ async_hwi::DeviceKind::Ledger.to_string(), async_hwi::DeviceKind::LedgerSimulator.to_string(), ]; @@ -873,7 +872,8 @@ impl Daemon for BackendWalletClient { .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(), @@ -908,7 +908,8 @@ impl Daemon for BackendWalletClient { .request( Method::PATCH, &format!("{}/v1/wallets/{}", self.inner.url, self.wallet_uuid), - )? + ) + .await .json(&api::payload::UpdateWallet { ledger_hmac: None, fingerprint_aliases: Some( diff --git a/gui/src/loader.rs b/gui/src/loader.rs index ec09d898..47e22ac3 100644 --- a/gui/src/loader.rs +++ b/gui/src/loader.rs @@ -22,6 +22,7 @@ use liana_ui::{ widget::*, }; +use crate::daemon::DaemonBackend; use crate::{ app::{ cache::Cache, @@ -128,8 +129,8 @@ impl Loader { self.step = Step::Error(Box::new(e)); } Error::Daemon(DaemonError::ClientNotSupported) - | Error::Daemon(DaemonError::Transport(Some(ErrorKind::ConnectionRefused), _)) - | Error::Daemon(DaemonError::Transport(Some(ErrorKind::NotFound), _)) => { + | Error::Daemon(DaemonError::RpcSocket(Some(ErrorKind::ConnectionRefused), _)) + | Error::Daemon(DaemonError::RpcSocket(Some(ErrorKind::NotFound), _)) => { if let Some(daemon_config_path) = self.gui_config.daemon_config_path.clone() { self.step = Step::StartingDaemon; self.daemon_started = true; @@ -226,7 +227,7 @@ impl Loader { pub fn stop(&mut self) { info!("Close requested"); if let Step::Syncing { daemon, .. } = &mut self.step { - if !daemon.is_external() { + if daemon.backend() == DaemonBackend::EmbeddedLianad { info!("Stopping internal daemon..."); if let Err(e) = Handle::current().block_on(async { daemon.stop().await }) { warn!("Internal daemon failed to stop: {}", e); @@ -366,7 +367,9 @@ pub async fn load_application( ), Error, > { - let wallet = Wallet::new(info.descriptors.main).load_settings(&datadir_path, network)?; + let wallet = Wallet::new(info.descriptors.main) + .load_from_settings(&datadir_path, network)? + .load_hotsigners(&datadir_path, network)?; let coins = daemon .list_coins(&[CoinStatus::Unconfirmed, CoinStatus::Confirmed], &[]) diff --git a/gui/src/main.rs b/gui/src/main.rs index ba344fc5..10be8488 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -1,5 +1,9 @@ #![windows_subsystem = "windows"] +use std::{ + collections::HashMap, error::Error, io::Write, path::PathBuf, process, str::FromStr, sync::Arc, +}; + use iced::{ event::{self, Event}, executor, keyboard, @@ -7,7 +11,6 @@ use iced::{ window::settings::PlatformSpecific, Application, Command, Settings, Size, Subscription, }; -use std::{error::Error, io::Write, path::PathBuf, process, str::FromStr}; use tracing::{error, info}; use tracing_subscriber::filter::LevelFilter; extern crate serde; @@ -19,11 +22,15 @@ use liana_ui::{component::text, font, image, theme, widget::Element}; use liana_gui::{ app::{ self, + cache::Cache, config::{default_datadir, ConfigError}, + wallet::Wallet, App, }, + hw::HardwareWalletConfig, installer::{self, Installer}, launcher::{self, Launcher}, + lianalite::client::{auth::AuthClient, backend::BackendClient, get_service_config}, loader::{self, Loader}, logger::Logger, VERSION, @@ -34,6 +41,8 @@ enum Arg { ConfigPath(PathBuf), DatadirPath(PathBuf), Network(bitcoin::Network), + Email(String), + RefreshToken(String), } fn parse_args(args: Vec) -> Result, Box> { @@ -57,6 +66,18 @@ fn parse_args(args: Vec) -> Result, Box> { } 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)); @@ -164,6 +185,99 @@ 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 = 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 = 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 { @@ -361,6 +475,7 @@ pub enum Config { Run(PathBuf, app::Config, bitcoin::Network), Launcher(PathBuf), Install(PathBuf, bitcoin::Network), + RunWithRemoteBackend(String, Option), } impl Config { @@ -390,6 +505,12 @@ fn main() -> Result<(), Box> { 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))