Add liana remote backend

This commit is contained in:
edouardparis 2024-05-28 15:32:05 +02:00
parent ca662eea6a
commit 5e2fe3b6c1
14 changed files with 381 additions and 187 deletions

View File

@ -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),

View File

@ -23,8 +23,7 @@ pub enum Message {
View(view::Message),
LoadDaemonConfig(Box<DaemonConfig>),
DaemonConfigLoaded(Result<(), Error>),
LoadWallet,
WalletLoaded(Result<Arc<Wallet>, Error>),
LoadWallet(Wallet),
Info(Result<GetInfoResult, Error>),
ReceiveAddress(Result<(Address, ChildNumber), Error>),
Coins(Result<Vec<Coin>, Error>),
@ -34,7 +33,7 @@ pub enum Message {
RbfPsbt(Result<Txid, Error>),
Recovery(Result<SpendTx, Error>),
Signed(Fingerprint, Result<Psbt, Error>),
WalletRegistered(Result<Fingerprint, Error>),
WalletUpdated(Result<Arc<Wallet>, Error>),
Updated(Result<(), Error>),
Saved(Result<(), Error>),
Verified(Fingerprint, Result<(), Error>),

View File

@ -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<Wallet>,
@ -135,17 +134,11 @@ impl App {
data_dir: PathBuf,
internal_bitcoind: Option<Bitcoind>,
) -> (App, Command<Message>) {
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<Message> {
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<Arc<Wallet>, 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<Message> {
let content = self.panels.current().view(&self.cache).map(Message::View);
if self.cache.network != bitcoin::Network::Bitcoin {

View File

@ -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(),

View File

@ -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<Message> {
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<dyn Daemon + Sync + Send>,
daemon: Arc<dyn Daemon + Sync + Send>,
cache: &Cache,
message: Message,
) -> Command<Message> {
@ -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<dyn async_hwi::HWI + Send + Sync>,
fingerprint: Fingerprint,
wallet: Arc<Wallet>,
) -> Result<Fingerprint, Error> {
daemon: Arc<dyn Daemon + Sync + Send>,
) -> Result<Arc<Wallet>, 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<Wallet>,
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<dyn Daemon + Sync + Send>,
) -> Result<Arc<Wallet>, 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))
}

View File

@ -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()),

View File

@ -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<Fingerprint, String>,
pub hardware_wallets: Vec<HardwareWalletConfig>,
pub signer: Option<Signer>,
pub signer: Option<Arc<Signer>>,
}
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<Self, WalletError> {
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<Self, WalletError> {
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)
}
}

View File

@ -240,13 +240,13 @@ impl error::Error for Error {
impl From<Error> 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,

View File

@ -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<DaemonError> + Debug;
@ -55,8 +55,8 @@ impl<C: Client> Lianad<C> {
#[async_trait]
impl<C: Client + Send + Sync + Debug> Daemon for Lianad<C> {
fn is_external(&self) -> bool {
true
fn backend(&self) -> DaemonBackend {
DaemonBackend::ExternalLianad
}
fn config(&self) -> Option<&Config> {

View File

@ -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> {

View File

@ -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<ErrorKind>, String),
/// Something was wrong with the rpc socket communication.
RpcSocket(Option<ErrorKind>, String),
/// Something was wrong with the http communication.
Http(Option<u16>, 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<Fingerprint, String>,
_hws: &[HardwareWalletConfig],
) -> Result<(), DaemonError> {
Ok(())
}
}
async fn load_labels<T: model::Labelled, D: Daemon + ?Sized>(

View File

@ -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<U: IntoUrl>(
req
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct BackendClient {
auth: Arc<RwLock<auth::AccessTokenResponse>>,
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<U: IntoUrl>(&self, method: Method, url: U) -> Result<RequestBuilder, DaemonError> {
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<U: IntoUrl>(&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<Vec<api::Wallet>, 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<AccessTokenResponse, DaemonError> {
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(

View File

@ -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], &[])

View File

@ -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<String>) -> Result<Vec<Arg>, Box<dyn Error>> {
@ -57,6 +66,18 @@ fn parse_args(args: Vec<String>) -> Result<Vec<Arg>, Box<dyn Error>> {
} 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<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 {
@ -361,6 +475,7 @@ pub enum Config {
Run(PathBuf, app::Config, bitcoin::Network),
Launcher(PathBuf),
Install(PathBuf, bitcoin::Network),
RunWithRemoteBackend(String, Option<String>),
}
impl Config {
@ -390,6 +505,12 @@ 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))