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:
commit
0a7ff2b0ea
@ -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"
|
||||
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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>),
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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()),
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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>(
|
||||
|
||||
176
gui/src/lianalite/client/auth.rs
Normal file
176
gui/src/lianalite/client/auth.rs
Normal 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?)
|
||||
}
|
||||
}
|
||||
416
gui/src/lianalite/client/backend/api.rs
Normal file
416
gui/src/lianalite/client/backend/api.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
1043
gui/src/lianalite/client/backend/mod.rs
Normal file
1043
gui/src/lianalite/client/backend/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
32
gui/src/lianalite/client/mod.rs
Normal file
32
gui/src/lianalite/client/mod.rs
Normal 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
86
gui/src/lianalite/mod.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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], &[])
|
||||
|
||||
123
gui/src/main.rs
123
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<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))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user