Merge #1086: GUI: Stateless remote backend integration

3b60a57bd4e30f40d3bd3efae52a8e488bc060da Remove bitcoind from settings panel if remote backend (edouardparis)
5e2fe3b6c1e9099dc2f087b122bef1c398ebec62 Add liana remote backend (edouardparis)
ca662eea6a40919e1f51275ef1915c892f13e7ce Add lianalite module (edouardparis)

Pull request description:

  Implements the Daemon Trait for a http client consuming signet.lianalite.com API (**only signet for now**).
  Everything is **stateless**, it will not create configuration files or modify settings.json files, but only communicate with the remote backend.

  In order to test it, you need an account and wallet created on `signet.lianalite.com`, then run:
  ```
  cargo run -- --email <email>
  ```
  the cli will then ask you for the otp token present in the authentification email.

ACKs for top commit:
  edouardparis:
    Self-ACK 3b60a57bd4e30f40d3bd3efae52a8e488bc060da

Tree-SHA512: 39b0436e3cd484b2a4b90115994ffd7ba1c3bc578549c5e992b7ee3a77625f821fbab9c6325b44ebd6f1058fc25ac2f5eee73962c8364845a2e85b26471e71ca
This commit is contained in:
edouardparis 2024-07-17 11:50:55 +02:00
commit 0a7ff2b0ea
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
21 changed files with 2108 additions and 142 deletions

View File

@ -44,7 +44,7 @@ chrono = "0.4.38"
# Used for managing internal bitcoind
base64 = "0.21"
bitcoin_hashes = "0.12"
reqwest = { version = "0.11", default-features=false, features = ["rustls-tls"] }
reqwest = { version = "0.11", default-features=false, features = ["json", "rustls-tls"] }
rust-ini = "0.19.0"

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;
@ -58,6 +58,7 @@ impl Panels {
cache: &Cache,
wallet: Arc<Wallet>,
data_dir: PathBuf,
daemon_backend: DaemonBackend,
internal_bitcoind: Option<&Bitcoind>,
) -> Panels {
Self {
@ -77,6 +78,7 @@ impl Panels {
settings: state::SettingsState::new(
data_dir,
wallet.clone(),
daemon_backend,
internal_bitcoind.is_some(),
),
}
@ -116,7 +118,6 @@ impl Panels {
}
pub struct App {
data_dir: PathBuf,
cache: Cache,
config: Config,
wallet: Arc<Wallet>,
@ -138,14 +139,14 @@ impl App {
let mut panels = Panels::new(
&cache,
wallet.clone(),
data_dir.clone(),
data_dir,
daemon.backend(),
internal_bitcoind.as_ref(),
);
let cmd = panels.home.reload(daemon.clone(), wallet.clone());
(
Self {
panels,
data_dir,
cache,
config,
daemon,
@ -218,14 +219,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 +258,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 +290,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 +331,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,22 +14,29 @@ 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 {
data_dir: PathBuf,
wallet: Arc<Wallet>,
setting: Option<Box<dyn State>>,
daemon_backend: DaemonBackend,
internal_bitcoind: bool,
}
impl SettingsState {
pub fn new(data_dir: PathBuf, wallet: Arc<Wallet>, internal_bitcoind: bool) -> Self {
pub fn new(
data_dir: PathBuf,
wallet: Arc<Wallet>,
daemon_backend: DaemonBackend,
internal_bitcoind: bool,
) -> Self {
Self {
data_dir,
wallet,
setting: None,
daemon_backend,
internal_bitcoind,
}
}
@ -48,7 +55,7 @@ impl State for SettingsState {
BitcoindSettingsState::new(
daemon.config().cloned(),
cache,
daemon.is_external(),
daemon.backend() != DaemonBackend::EmbeddedLianad,
self.internal_bitcoind,
)
.into(),
@ -97,7 +104,7 @@ impl State for SettingsState {
if let Some(setting) = &self.setting {
setting.view(cache)
} else {
view::settings::list(cache)
view::settings::list(cache, self.daemon_backend == DaemonBackend::RemoteBackend)
}
}

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

@ -32,7 +32,7 @@ use crate::{
hw::HardwareWallet,
};
pub fn list(cache: &Cache) -> Element<Message> {
pub fn list(cache: &Cache, is_remote_backend: bool) -> Element<Message> {
dashboard(
&Menu::Settings,
cache,
@ -44,23 +44,27 @@ pub fn list(cache: &Cache) -> Element<Message> {
Button::new(text("Settings").size(30).bold())
.style(theme::Button::Transparent)
.on_press(Message::Menu(Menu::Settings)))
.push(
Container::new(
Button::new(
Row::new()
.push(badge::Badge::new(icon::bitcoin_icon()))
.push(text("Bitcoin Core").bold())
.padding(10)
.spacing(20)
.align_items(Alignment::Center)
.width(Length::Fill),
.push_maybe(
if !is_remote_backend {
Some(Container::new(
Button::new(
Row::new()
.push(badge::Badge::new(icon::bitcoin_icon()))
.push(text("Bitcoin Core").bold())
.padding(10)
.spacing(20)
.align_items(Alignment::Center)
.width(Length::Fill),
)
.width(Length::Fill)
.style(theme::Button::TransparentBorder)
.on_press(Message::Settings(SettingsMessage::EditBitcoindSettings))
)
.width(Length::Fill)
.style(theme::Button::TransparentBorder)
.on_press(Message::Settings(SettingsMessage::EditBitcoindSettings))
)
.width(Length::Fill)
.style(theme::Container::Card(theme::Card::Simple))
.style(theme::Container::Card(theme::Card::Simple)))
} else {
None
}
)
.push(
Container::new(

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

@ -0,0 +1,176 @@
use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct SignInOtp<'a> {
email: &'a str,
create_user: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VerifyOtp<'a, 'b> {
email: &'a str,
token: &'b str,
#[serde(rename = "type")]
kind: &'static str,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ResendOtp<'a> {
email: &'a str,
#[serde(rename = "type")]
kind: &'static str,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RefreshToken<'a> {
refresh_token: &'a str,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessTokenResponse {
pub access_token: String,
pub expires_at: i64,
pub refresh_token: String,
}
#[derive(Debug, Clone)]
pub struct AuthClient {
http: reqwest::Client,
url: String,
api_public_key: String,
}
#[derive(Debug, Clone)]
pub struct AuthError {
pub http_status: Option<u16>,
pub error: String,
}
impl std::fmt::Display for AuthError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if let Some(status) = self.http_status {
write!(f, "{}: {}", status, self.error)
} else {
write!(f, "{}", self.error)
}
}
}
impl From<Error> for AuthError {
fn from(value: Error) -> Self {
AuthError {
http_status: None,
error: value.to_string(),
}
}
}
impl AuthClient {
pub fn new(url: String, api_public_key: String) -> Self {
AuthClient {
http: reqwest::Client::new(),
url,
api_public_key,
}
}
fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
let req = self
.http
.request(method, url)
.header("apikey", &self.api_public_key)
.header("Content-Type", "application/json");
tracing::debug!("Sending http request: {:?}", req);
req
}
pub async fn sign_in_otp(&self, email: &str) -> Result<(), AuthError> {
let response: Response = self
.request(Method::POST, &format!("{}/auth/v1/otp", self.url))
.json(&SignInOtp {
email,
create_user: true,
})
.send()
.await?;
if !response.status().is_success() {
return Err(AuthError {
http_status: Some(response.status().into()),
error: response.text().await?,
});
}
Ok(())
}
pub async fn resend_otp(&self, email: &str) -> Result<Response, AuthError> {
let response: Response = self
.request(Method::POST, &format!("{}/auth/v1/resend", self.url))
.json(&ResendOtp {
email,
kind: "email",
})
.send()
.await?;
if !response.status().is_success() {
return Err(AuthError {
http_status: Some(response.status().into()),
error: response.text().await?,
});
}
Ok(response)
}
pub async fn verify_otp(
&self,
email: &str,
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,
token,
kind: "email",
})
.send()
.await?;
if !response.status().is_success() {
return Err(AuthError {
http_status: Some(response.status().into()),
error: response.text().await?,
});
}
Ok(response.json().await?)
}
pub async fn refresh_token(
&self,
refresh_token: &str,
) -> Result<AccessTokenResponse, AuthError> {
let response: Response = self
.http
.post(&format!(
"{}/auth/v1/token?grant_type=refresh_token",
self.url
))
.header("apikey", &self.api_public_key)
.header("Content-Type", "application/json")
.json(&RefreshToken { refresh_token })
.send()
.await?;
if !response.status().is_success() {
return Err(AuthError {
http_status: Some(response.status().into()),
error: response.text().await?,
});
}
Ok(response.json().await?)
}
}

View File

@ -0,0 +1,416 @@
use std::collections::HashMap;
use std::str::FromStr;
use liana::{
descriptors::LianaDescriptor,
miniscript::bitcoin::{self, bip32, consensus, hashes::hex::FromHex, Amount, OutPoint, Txid},
};
use serde::{de, Deserialize, Deserializer};
pub fn deser_fromstr<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: FromStr,
<T as FromStr>::Err: std::fmt::Display,
{
let string = String::deserialize(deserializer)?;
T::from_str(&string).map_err(de::Error::custom)
}
/// Deserialize an address from string, assuming the network was checked.
pub fn deser_addr_assume_checked<'de, D>(deserializer: D) -> Result<bitcoin::Address, D::Error>
where
D: Deserializer<'de>,
{
let string = String::deserialize(deserializer)?;
bitcoin::Address::from_str(&string)
.map(|addr| addr.assume_checked())
.map_err(de::Error::custom)
}
/// Deserialize an amount from sats
pub fn deser_amount_from_sats<'de, D>(deserializer: D) -> Result<bitcoin::Amount, D::Error>
where
D: Deserializer<'de>,
{
let a = u64::deserialize(deserializer)?;
Ok(bitcoin::Amount::from_sat(a))
}
pub fn deser_hex<'de, D, T>(d: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: consensus::Decodable,
{
let s = String::deserialize(d)?;
let s = Vec::from_hex(&s).map_err(de::Error::custom)?;
consensus::deserialize(&s).map_err(de::Error::custom)
}
/// The maximum number of item to return.
pub const DEFAULT_LIMIT: usize = 20;
/// The maximum number of outpoints that can be provided as a filter.
pub const DEFAULT_OUTPOINTS_LIMIT: usize = 50;
/// The maximum number of items that can be provided as a filter.
pub const DEFAULT_LABEL_ITEMS_LIMIT: usize = 50;
#[derive(Deserialize)]
pub struct Claims {
pub sub: String,
}
#[derive(Deserialize)]
pub struct NetworkInfo {
pub feerate: Feerate,
pub rates: HashMap<String, f32>,
}
#[derive(Deserialize)]
pub struct Feerate {
pub low: Option<i32>,
pub high: Option<i32>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WalletBalance {
/// Total of funds that present in a block.
pub confirmed: u64,
/// Total of funds that is not yet in a block.
pub unconfirmed: u64,
/// Total of funds that are mined but not yet available
pub immature: u64,
/// Total of funds that are unconfirmed but are coming from
/// the wallet
pub unconfirmed_change: u64,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum WalletStatus {
Normal,
Recovering,
Recovered,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RecoveryPath {
pub sequence: u16,
pub available_balance: u64,
pub total_coins: usize,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Wallet {
pub id: String,
pub name: String,
#[serde(deserialize_with = "deser_fromstr")]
pub descriptor: LianaDescriptor,
pub recovery_paths: Vec<RecoveryPath>,
pub biggest_remaining_sequence: Option<u32>,
pub smallest_remaining_sequence: Option<u32>,
pub metadata: WalletMetadata,
pub created_at: i64,
pub balance: WalletBalance,
pub status: WalletStatus,
pub tip_height: Option<i32>,
}
#[derive(Deserialize)]
pub struct ListWallets {
pub wallets: Vec<Wallet>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WalletMetadata {
pub ledger_hmacs: Vec<LedgerHmac>,
pub fingerprint_aliases: Vec<FingerprintAlias>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct LedgerHmac {
#[serde(deserialize_with = "deser_fromstr")]
pub fingerprint: bip32::Fingerprint,
pub user_id: String,
pub hmac: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct FingerprintAlias {
#[serde(deserialize_with = "deser_fromstr")]
pub fingerprint: bip32::Fingerprint,
pub user_id: String,
pub alias: String,
}
#[derive(Deserialize)]
pub struct WalletLabels {
pub labels: HashMap<String, String>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PaymentKind {
Outgoing,
Incoming,
}
#[derive(Deserialize)]
pub struct Payment {
pub txuuid: String,
pub txid: String,
pub vout: u32,
pub amount: u64,
pub block_height: Option<i32>,
pub confirmed_at: Option<i64>,
pub label: Option<String>,
pub address_label: Option<String>,
pub transaction_label: Option<String>,
pub kind: PaymentKind,
pub is_single: bool,
}
#[derive(Deserialize)]
pub struct ListPayments {
pub payments: Vec<Payment>,
}
#[derive(Clone, Deserialize)]
pub struct Coin {
#[serde(deserialize_with = "deser_addr_assume_checked")]
pub address: bitcoin::Address,
#[serde(deserialize_with = "deser_amount_from_sats")]
pub amount: Amount,
pub derivation_index: bip32::ChildNumber,
pub outpoint: OutPoint,
pub block_height: Option<i32>,
pub spend_info: Option<CoinSpendInfo>,
pub is_immature: bool,
pub is_change_address: bool,
}
#[derive(Clone, Deserialize)]
pub struct CoinSpendInfo {
pub txid: Txid,
pub height: Option<i32>,
}
#[derive(Deserialize)]
pub struct ListCoins {
pub coins: Vec<Coin>,
}
#[derive(Clone, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum UTXOKind {
Deposit,
Change,
External,
}
#[derive(Clone, Deserialize)]
pub struct Transaction {
pub uuid: String,
pub txid: String,
pub fee: u64,
pub fee_rate: u64,
pub block_height: Option<i32>,
pub confirmed_at: Option<i64>,
pub label: Option<String>,
#[serde(deserialize_with = "deser_hex")]
pub raw: bitcoin::Transaction,
pub inputs: Vec<Input>,
pub outputs: Vec<Output>,
/// If the transaction has multiple incoming or ougoing payment.
pub is_batch: bool,
}
#[derive(Deserialize)]
pub struct ListTransactions {
pub transactions: Vec<Transaction>,
}
#[derive(Clone, Deserialize)]
pub struct Output {
pub address: Option<String>,
pub label: Option<String>,
pub address_label: Option<String>,
pub amount: u64,
pub kind: UTXOKind,
pub coin: Option<Coin>,
}
#[derive(Clone, Deserialize)]
pub struct Input {
pub txid: String,
pub vout: usize,
pub amount: Option<u64>,
pub label: Option<String>,
pub kind: UTXOKind,
pub coin: Option<Coin>,
}
#[derive(Clone, Deserialize)]
pub struct Psbt {
pub uuid: String,
pub txid: Txid,
pub fee: Option<u64>,
pub fee_rate: Option<u64>,
pub label: Option<String>,
#[serde(deserialize_with = "deser_fromstr")]
pub raw: bitcoin::Psbt,
pub inputs: Vec<Input>,
pub outputs: Vec<Output>,
pub is_batch: bool,
pub updated_at: i64,
}
#[derive(Clone, Deserialize)]
#[serde(untagged)]
pub enum DraftPsbtResult {
Success(DraftPsbt),
InsufficientFunds(InsufficientFundsInfo),
}
#[derive(Clone, Deserialize)]
pub struct InsufficientFundsInfo {
pub missing: u64,
}
#[derive(Clone, Deserialize)]
pub struct DraftPsbt {
pub uuid: Option<String>,
pub txid: Txid,
pub fee: u64,
pub fee_rate: u64,
pub label: Option<String>,
#[serde(deserialize_with = "deser_fromstr")]
pub raw: bitcoin::Psbt,
pub inputs: Vec<Input>,
pub outputs: Vec<Output>,
pub warnings: Vec<String>,
}
#[derive(Deserialize)]
pub struct ListPsbts {
pub psbts: Vec<Psbt>,
}
#[derive(Deserialize)]
pub struct Address {
#[serde(deserialize_with = "deser_addr_assume_checked")]
pub address: bitcoin::Address,
pub derivation_index: bip32::ChildNumber,
}
pub mod payload {
use liana::miniscript::bitcoin;
use serde::{Serialize, Serializer};
pub fn ser_to_string<T: std::fmt::Display, S: Serializer>(
field: T,
s: S,
) -> Result<S::Ok, S::Error> {
s.serialize_str(&field.to_string())
}
#[derive(Serialize)]
pub struct ImportPsbt {
pub psbt: String,
}
#[derive(Serialize)]
pub struct Recipient {
/// Recipient cannot have an empty amount and is_max set to false
/// Amount cannot be less that the DUST limit.
pub amount: Option<u64>,
pub address: bitcoin::Address<bitcoin::address::NetworkUnchecked>,
/// If is_max is set to true, API will calculate the remaining funds and
/// use it for psbt output amount.
/// Only one recipient can have is_max set to true
pub is_max: bool,
}
#[derive(Serialize)]
pub struct GeneratePsbt<'a> {
pub recipients: Vec<Recipient>,
/// The outpoints of coins to use as transaction inputs. If empty,
/// coins will be selected automatically from the set of confirmed coins
/// and those unconfirmed coins at a change address, excluding immature
/// coins.
pub inputs: &'a [bitcoin::OutPoint],
// The feerate to use for this transaction.
pub feerate: u64,
/// If save is set to true, API will save in database the generated psbt
/// and store the generated change address.
pub save: bool,
}
#[derive(Serialize)]
pub struct GenerateRecoveryPsbt {
/// The address to sweep funds to.
pub address: bitcoin::Address<bitcoin::address::NetworkUnchecked>,
// The feerate to use for this transaction.
pub feerate: u64,
/// Timelock of the recovery path to use.
pub timelock: u16,
/// If save is set to true, API will save in database the generated psbt
/// and store the generated change address.
pub save: bool,
}
#[derive(Serialize)]
pub struct Labels {
pub labels: Vec<Label>,
}
#[derive(Serialize)]
pub struct Label {
pub item: String,
pub value: Option<String>,
}
#[derive(Serialize)]
pub struct GenerateRbfPsbt {
/// ID of the transaction to be replaced.
#[serde(serialize_with = "ser_to_string")]
pub txid: bitcoin::Txid,
/// The target feerate (sat/vb) to use for the replacement transaction
/// in order to bump the fee of the transaction being replaced.
///
/// Must be provided if and only if `is_cancel` is `false`.
pub feerate: Option<u64>,
/// Whether to cancel the transaction.
///
/// If `true`, the feerate of the replacement transaction will be set
/// automatically to the lowest possible feerate that satisfies all
/// RBF policies.
///
/// If `false`, the transaction will be replaced by another at the target
/// `feerate` in order to bump its fee.
pub is_cancel: bool,
/// If save is set to true, API will save in database the generated psbt
/// and, if a new change address is generated for the replacement, store
/// this also. Note that if the transaction being replaced has a change
/// output, then its corresponding change address will be reused in the
/// replacement.
pub save: bool,
}
#[derive(Serialize)]
pub struct UpdateWallet {
pub ledger_hmac: Option<UpdateLedgerHmac>,
pub fingerprint_aliases: Option<Vec<UpdateFingerprintAlias>>,
}
#[derive(Serialize)]
pub struct UpdateLedgerHmac {
pub fingerprint: String,
pub hmac: String,
}
#[derive(Serialize)]
pub struct UpdateFingerprintAlias {
pub fingerprint: String,
pub alias: String,
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
pub mod auth;
pub mod backend;
use liana::miniscript::bitcoin;
use serde::Deserialize;
const LIANALITE_SIGNET_URL: &str = "https://signet.lianalite.com";
const LIANALITE_MAINNET_URL: &str = "https://lianalite.com";
#[derive(Debug, Clone, Deserialize)]
pub struct ServiceConfig {
pub auth_api_url: String,
pub auth_api_public_key: String,
pub backend_api_url: String,
}
pub async fn get_service_config(
network: bitcoin::Network,
) -> Result<ServiceConfig, reqwest::Error> {
reqwest::get(format!(
"{}/api/env",
if network == bitcoin::Network::Bitcoin {
LIANALITE_MAINNET_URL
} else {
LIANALITE_SIGNET_URL
}
))
.await?
.json()
.await
}

86
gui/src/lianalite/mod.rs Normal file
View File

@ -0,0 +1,86 @@
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),
}
}
}

View File

@ -5,6 +5,7 @@ pub mod download;
pub mod hw;
pub mod installer;
pub mod launcher;
pub mod lianalite;
pub mod loader;
pub mod logger;
pub mod signer;

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>> {
@ -76,6 +85,18 @@ 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));
@ -183,6 +204,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 {
@ -380,6 +494,7 @@ pub enum Config {
Run(PathBuf, app::Config, bitcoin::Network),
Launcher(PathBuf),
Install(PathBuf, bitcoin::Network),
RunWithRemoteBackend(String, Option<String>),
}
impl Config {
@ -409,6 +524,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))