Merge #1683: Add connect cache file

fe14c7807e60a080052ebc94c1e0d160c8db8ed6 pin email in liana connect login (edouardparis)
1746314e7d8ef4e06cc05951dc620d88d280d26e Add liana-connect cache file (edouardparis)

Pull request description:

  This PR introduces a new file in the network directory: `connect.json` where the access tokens per email accounts are stored. Race conditions and potential conflicts to update the file are handle by the crate `async-fd-lock`.

  If the cache is missing (like for a previous liana generated setup), then the user is asked to authenticate again.
  The change does not remove totally the legacy `refresh_token` field in the settings file in order to be backward compatible with previous liana-gui versions.

ACKs for top commit:
  edouardparis:
    Self-ACK fe14c7807e60a080052ebc94c1e0d160c8db8ed6

Tree-SHA512: a4ce6c78e8edd8378f227ae464a5ff0ebd88ee16b519a26f78eb95c9ab5752d14a50ea559902ce5edb9a00452e77b2552c2743a75182a9de825846b5c6f4d06c
This commit is contained in:
edouardparis 2025-05-02 09:36:11 +02:00
commit 6fe88ad4df
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
9 changed files with 424 additions and 313 deletions

16
Cargo.lock generated
View File

@ -254,6 +254,21 @@ dependencies = [
"slab",
]
[[package]]
name = "async-fd-lock"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7569377d7062165f6f7834d9cb3051974a2d141433cc201c2f94c149e993cccf"
dependencies = [
"async-trait",
"cfg-if",
"pin-project",
"rustix",
"thiserror 1.0.69",
"tokio",
"windows-sys 0.52.0",
]
[[package]]
name = "async-fs"
version = "2.1.2"
@ -2993,6 +3008,7 @@ dependencies = [
name = "liana-gui"
version = "10.0.0"
dependencies = [
"async-fd-lock",
"async-hwi",
"async-trait",
"backtrace",

View File

@ -29,6 +29,7 @@ iced_runtime = "0.13.1"
email_address = "0.2.7"
tokio = {version = "1.21.0", features = ["signal"]}
async-fd-lock = "0.2.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

View File

@ -66,7 +66,21 @@ impl Settings {
pub struct AuthConfig {
pub email: String,
pub wallet_id: String,
pub refresh_token: String,
// legacy field, refresh_token is now stored in the connect cache file
// Keep it in case, user want to open the wallet with a previous Liana-GUI version.
// Field cannot be ignored as the settings file is override during settings update.
// TODO: remove later after multiple versions.
pub refresh_token: Option<String>,
}
impl AuthConfig {
pub fn new(email: String, wallet_id: String) -> Self {
Self {
email,
wallet_id,
refresh_token: None,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
@ -241,7 +255,6 @@ pub enum SettingsError {
WritingFile(String),
Unexpected(String),
}
impl std::fmt::Display for SettingsError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {

View File

@ -13,6 +13,7 @@ use liana_ui::{
widget::{Column, Element},
};
use lianad::config::Config;
use std::ops::Deref;
use tracing::{error, info, warn};
use std::io::Write;
@ -37,6 +38,7 @@ use crate::{
api::payload::{Provider, ProviderKey},
BackendClient, BackendWalletClient,
},
cache::update_connect_cache,
},
},
signer::Signer,
@ -550,6 +552,22 @@ pub async fn create_remote_wallet(
info!("Settings file created");
let backend = remote_backend.inner_client();
if let Err(e) = update_connect_cache(
&network_datadir,
backend.auth.read().await.deref(),
backend.auth_client(),
false,
)
.await
{
// this error is not critical, the liana-connect backend stored the wallet
// and user can reauthenticate.
tracing::error!("Failed to update Liana-Connect cache: {}", e);
} else {
info!("Liana-Connect cache updated");
};
Ok(gui_config_path)
}
@ -599,6 +617,22 @@ pub async fn import_remote_wallet(
info!("Gui configuration file created");
let backend = backend.inner_client();
if let Err(e) = update_connect_cache(
&network_datadir,
backend.auth.read().await.deref(),
backend.auth_client(),
false,
)
.await
{
// this error is not critical, the liana-connect backend stored the wallet
// and user can reauthenticate.
tracing::error!("Failed to update Liana-Connect cache: {}", e);
} else {
info!("Liana-Connect cache updated");
};
Ok(gui_config_path)
}
@ -631,19 +665,16 @@ pub async fn extract_remote_gui_settings(ctx: &Context, backend: &BackendWalletC
.expect("LianaDescriptor.to_string() always include the checksum")
.to_string();
let auth = backend.inner_client().auth.read().await;
Settings {
wallets: vec![WalletSetting {
name: wallet_name(descriptor),
descriptor_checksum,
keys: Vec::new(),
hardware_wallets: Vec::new(),
remote_backend_auth: Some(AuthConfig {
email: backend.user_email().to_string(),
wallet_id: backend.wallet_id(),
refresh_token: auth.refresh_token.clone(),
}),
remote_backend_auth: Some(AuthConfig::new(
backend.user_email().to_string(),
backend.wallet_id(),
)),
}],
}
}

View File

@ -223,14 +223,13 @@ impl GUI {
);
let network_dir = datadir_path.network_directory(network);
if let Ok(settings) = app::settings::Settings::from_file(&network_dir) {
if settings
if let Some(setting) = settings
.wallets
.first()
.map(|w| w.remote_backend_auth.is_some())
== Some(true)
.into_iter()
.find_map(|w| w.remote_backend_auth)
{
let (login, command) =
login::LianaLiteLogin::new(datadir_path, network, settings);
login::LianaLiteLogin::new(datadir_path, network, setting);
self.state = State::Login(Box::new(login));
command.map(|msg| Message::Login(Box::new(msg)))
} else {
@ -296,14 +295,13 @@ impl GUI {
let network_dir = i.datadir.network_directory(i.network);
let settings = app::settings::Settings::from_file(&network_dir)
.expect("A settings file was created");
if settings
if let Some(setting) = settings
.wallets
.first()
.map(|w| w.remote_backend_auth.is_some())
== Some(true)
.into_iter()
.find_map(|w| w.remote_backend_auth)
{
let (login, command) =
login::LianaLiteLogin::new(i.datadir.clone(), i.network, settings);
login::LianaLiteLogin::new(i.datadir.clone(), i.network, setting);
self.state = State::Login(Box::new(login));
command.map(|msg| Message::Login(Box::new(msg)))
} else {

View File

@ -20,7 +20,6 @@ use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response};
use tokio::sync::RwLock;
use crate::{
app::settings::{AuthConfig, Settings},
daemon::{model::*, Daemon, DaemonBackend, DaemonError},
dir::LianaDirectory,
hw::HardwareWalletConfig,
@ -28,7 +27,10 @@ use crate::{
use self::api::{UTXOKind, DEFAULT_OUTPOINTS_LIMIT};
use super::auth::{self, AccessTokenResponse, AuthError};
use super::{
auth::{self, AccessTokenResponse, AuthError},
cache::update_connect_cache,
};
impl From<Error> for DaemonError {
fn from(value: Error) -> Self {
@ -101,6 +103,10 @@ impl BackendClient {
})
}
pub fn auth_client(&self) -> &auth::AuthClient {
&self.auth_client
}
pub fn user_email(&self) -> &str {
&self.auth_client.email
}
@ -532,45 +538,20 @@ impl Daemon for BackendWalletClient {
return Ok(());
}
Ok(mut old) => {
let new = self
.inner
.auth_client
.refresh_token(&auth.refresh_token)
.await?;
let network_dir = datadir.network_directory(network);
let mut settings = Settings::from_file(&network_dir).map_err(|e| {
DaemonError::Unexpected(format!(
"Cannot access to settings.json file: {}",
e
))
})?;
if let Some(wallet_settings) = settings.wallets.iter_mut().find(|w| {
if let Some(auth) = &w.remote_backend_auth {
auth.wallet_id == self.wallet_uuid
} else {
false
}
}) {
wallet_settings.remote_backend_auth = Some(AuthConfig {
email: self.inner.auth_client.email.clone(),
wallet_id: self.wallet_id(),
refresh_token: new.refresh_token.clone(),
});
} else {
tracing::info!("Wallet id was not found in the settings");
}
settings.to_file(&network_dir).map_err(|e| {
DaemonError::Unexpected(format!(
"Cannot access to settings.json file: {}",
e
))
let new = update_connect_cache(
&network_dir,
&old,
&self.inner.auth_client,
true, // refresh the token
)
.await
.map_err(|e| {
DaemonError::Unexpected(format!("Cannot update Liana-connect cache: {}", e))
})?;
*old = new;
tracing::info!("Liana backend access was refreshed");
}
}
}

View File

@ -0,0 +1,159 @@
use crate::dir::NetworkDirectory;
use async_fd_lock::LockWrite;
use serde::{Deserialize, Serialize};
use std::io::SeekFrom;
use tokio::fs::OpenOptions;
use tokio::io::AsyncSeekExt;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use super::auth::{AccessTokenResponse, AuthClient, AuthError};
pub const CONNECT_CACHE_FILENAME: &str = "connect.json";
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct ConnectCache {
pub accounts: Vec<Account>,
}
impl ConnectCache {
fn upsert_credential(&mut self, email: &str, tokens: AccessTokenResponse) {
if let Some(c) = self.accounts.iter_mut().find(|c| c.email == email) {
c.tokens = tokens;
} else {
self.accounts.push(Account {
email: email.to_string(),
tokens,
})
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Account {
pub email: String,
pub tokens: AccessTokenResponse,
}
impl Account {
pub fn from_cache(
network_dir: &NetworkDirectory,
email: &str,
) -> Result<Option<Self>, ConnectCacheError> {
let mut path = network_dir.path().to_path_buf();
path.push(CONNECT_CACHE_FILENAME);
std::fs::read(path)
.map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => ConnectCacheError::NotFound,
_ => ConnectCacheError::ReadingFile(format!("Reading settings file: {}", e)),
})
.and_then(|file_content| {
serde_json::from_slice::<ConnectCache>(&file_content).map_err(|e| {
ConnectCacheError::ReadingFile(format!("Parsing settings file: {}", e))
})
})
.map(|cache| cache.accounts.into_iter().find(|c| c.email == email))
}
}
pub async fn update_connect_cache(
network_dir: &NetworkDirectory,
current_tokens: &AccessTokenResponse,
client: &AuthClient,
refresh: bool,
) -> Result<AccessTokenResponse, ConnectCacheError> {
let email = &client.email;
let mut path = network_dir.path().to_path_buf();
path.push(CONNECT_CACHE_FILENAME);
let file_exists = tokio::fs::try_exists(&path).await.unwrap_or(false);
let mut file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.await
.map_err(|e| ConnectCacheError::ReadingFile(format!("Opening file: {}", e)))?
.lock_write()
.await
.map_err(|e| ConnectCacheError::ReadingFile(format!("Locking file: {:?}", e)))?;
let mut cache = if file_exists {
let mut file_content = Vec::new();
file.read_to_end(&mut file_content)
.await
.map_err(|e| ConnectCacheError::ReadingFile(format!("Reading file content: {}", e)))?;
match serde_json::from_slice::<ConnectCache>(&file_content) {
Ok(cache) => cache,
Err(e) => {
tracing::warn!("Something wrong with Liana-Connect cache file: {:?}", e);
tracing::warn!("Liana-Connect cache file is reset");
ConnectCache::default()
}
}
} else {
ConnectCache::default()
};
if let Some(c) = cache.accounts.iter().find(|cred| cred.email == *email) {
// An other process updated the tokens
if current_tokens.expires_at < c.tokens.expires_at {
tracing::debug!("Liana-Connect authentication tokens are up to date, nothing to do");
return Ok(c.tokens.clone());
}
}
let tokens = if refresh {
client
.refresh_token(&current_tokens.refresh_token)
.await
.map_err(ConnectCacheError::Updating)?
} else {
current_tokens.clone()
};
cache.upsert_credential(email, tokens.clone());
let content = serde_json::to_vec_pretty(&cache).map_err(|e| {
ConnectCacheError::WritingFile(format!("Failed to serialize settings: {}", e))
})?;
file.seek(SeekFrom::Start(0)).await.map_err(|e| {
ConnectCacheError::WritingFile(format!("Failed to seek to start of file: {}", e))
})?;
file.write_all(&content).await.map_err(|e| {
tracing::warn!("failed to write to file: {:?}", e);
ConnectCacheError::WritingFile(e.to_string())
})?;
file.inner_mut()
.set_len(content.len() as u64)
.await
.map_err(|e| ConnectCacheError::WritingFile(format!("Failed to truncate file: {}", e)))?;
Ok(tokens)
}
#[derive(Debug, Clone)]
pub enum ConnectCacheError {
NotFound,
ReadingFile(String),
WritingFile(String),
Unexpected(String),
Updating(AuthError),
}
impl std::fmt::Display for ConnectCacheError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::NotFound => write!(f, "ConnectCache 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),
Self::Updating(e) => write!(f, "Error while updating cache file: {}", e),
}
}
}

View File

@ -1,5 +1,6 @@
pub mod auth;
pub mod backend;
pub mod cache;
use liana::miniscript::bitcoin;

View File

@ -13,7 +13,7 @@ use lianad::commands::ListCoinsResult;
use crate::{
app::{
cache::coins_to_cache,
settings::{AuthConfig, Settings, SettingsError, WalletSetting},
settings::{AuthConfig, SettingsError},
},
daemon::DaemonError,
dir::LianaDirectory,
@ -22,15 +22,18 @@ use crate::{
use super::client::{
auth::{AuthClient, AuthError},
backend::{api, BackendClient, BackendWalletClient},
cache::{self, update_connect_cache, ConnectCacheError},
};
#[derive(Debug, Clone)]
pub enum Error {
Auth(AuthError),
CredentialsMissing,
// DaemonError does not implement Clone.
// TODO: maybe Arc is overkill
Backend(Arc<DaemonError>),
Settings(SettingsError),
Cache(cache::ConnectCacheError),
Unexpected(String),
}
@ -38,8 +41,10 @@ impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Auth(e) => write!(f, "Authentication error: {}", e),
Self::CredentialsMissing => write!(f, "credentials missing"),
Self::Backend(e) => write!(f, "Remote backend error: {}", e),
Self::Settings(e) => write!(f, "Settings file error: {}", e),
Self::Cache(e) => write!(f, "Connect cache file error: {}", e),
Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
}
}
@ -63,6 +68,12 @@ impl From<SettingsError> for Error {
}
}
impl From<ConnectCacheError> for Error {
fn from(value: ConnectCacheError) -> Self {
Self::Cache(value)
}
}
#[derive(Debug, Clone)]
pub enum Message {
View(ViewMessage),
@ -88,8 +99,6 @@ pub enum Message {
#[derive(Debug, Clone)]
pub enum ViewMessage {
RequestOTP,
EditEmail,
EmailEdited(String),
OTPEdited(String),
BackToLauncher(Network),
}
@ -105,6 +114,7 @@ pub struct LianaLiteLogin {
pub network: Network,
wallet_id: String,
email: String,
processing: bool,
step: ConnectionStep,
@ -117,13 +127,10 @@ pub struct LianaLiteLogin {
pub enum ConnectionStep {
CheckingAuthFile,
EnterEmail {
email: form::Value<String>,
},
CheckEmail,
EnterOtp {
client: AuthClient,
backend_api_url: String,
email: String,
otp: form::Value<String>,
},
}
@ -132,63 +139,41 @@ impl LianaLiteLogin {
pub fn new(
datadir: LianaDirectory,
network: Network,
settings: Settings,
setting: AuthConfig,
) -> (Self, Task<Message>) {
match settings
.wallets
.first()
.cloned()
.and_then(|w| w.remote_backend_auth)
.ok_or(Error::Unexpected(
"Missing auth configuration in settings.json".to_string(),
)) {
Err(e) => (
Self {
network,
datadir,
step: ConnectionStep::EnterEmail {
email: form::Value::default(),
},
wallet_id: String::new(),
connection_error: Some(e),
auth_error: None,
processing: true,
},
Task::none(),
),
Ok(auth_config) => (
Self {
network,
datadir,
step: ConnectionStep::CheckingAuthFile,
connection_error: None,
wallet_id: auth_config.wallet_id.clone(),
auth_error: None,
processing: true,
},
Task::perform(
async move {
let service_config = super::client::get_service_config(network)
.await
.map_err(|e| Error::Unexpected(e.to_string()))?;
let client = AuthClient::new(
service_config.auth_api_url,
service_config.auth_api_public_key,
auth_config.email,
);
connect_with_refresh_token(
client,
auth_config.refresh_token,
auth_config.wallet_id,
service_config.backend_api_url,
network,
)
(
Self {
network,
datadir: datadir.clone(),
step: ConnectionStep::CheckingAuthFile,
connection_error: None,
wallet_id: setting.wallet_id.clone(),
email: setting.email.clone(),
auth_error: None,
processing: true,
},
Task::perform(
async move {
let service_config = super::client::get_service_config(network)
.await
},
Message::Connected,
),
.map_err(|e| Error::Unexpected(e.to_string()))?;
let client = AuthClient::new(
service_config.auth_api_url,
service_config.auth_api_public_key,
setting.email,
);
connect_with_credentials(
client,
setting.wallet_id,
service_config.backend_api_url,
network,
datadir,
)
.await
},
Message::Connected,
),
}
)
}
pub fn update(&mut self, message: Message) -> Task<Message> {
@ -207,36 +192,27 @@ impl LianaLiteLogin {
);
}
Err(e) => {
self.connection_error = Some(e);
self.step = ConnectionStep::EnterEmail {
email: form::Value::default(),
};
// Do not display error, if the Liana-Connect cache does not exist,
// simply ask user to do the authentication steps.
if !matches!(e, Error::CredentialsMissing) {
self.connection_error = Some(e);
}
self.step = ConnectionStep::CheckEmail;
}
}
}
}
ConnectionStep::EnterEmail { email } => match message {
Message::View(ViewMessage::EmailEdited(value)) => {
email.valid = value.is_empty()
|| email_address::EmailAddress::parse_with_options(
&value,
email_address::Options::default().with_required_tld(),
)
.is_ok();
email.value = value;
}
ConnectionStep::CheckEmail => match message {
Message::View(ViewMessage::RequestOTP) => {
if email.value.is_empty() {
email.valid = false;
} else if email.valid {
let email = email.value.clone();
let network = self.network;
self.processing = true;
self.connection_error = None;
self.auth_error = None;
return Task::perform(
async move {
let config = super::client::get_service_config(network)
let email = self.email.clone();
let network = self.network;
self.processing = true;
self.connection_error = None;
self.auth_error = None;
return Task::perform(
async move {
let config =
super::client::get_service_config(network)
.await
.map_err(|e| {
if e.status() == Some(reqwest::StatusCode::NOT_FOUND) {
@ -247,24 +223,22 @@ impl LianaLiteLogin {
Error::Unexpected(e.to_string())
}
})?;
let client = AuthClient::new(
config.auth_api_url,
config.auth_api_public_key,
email,
);
client.sign_in_otp().await?;
Ok((client, config.backend_api_url))
},
Message::OTPRequested,
);
}
let client = AuthClient::new(
config.auth_api_url,
config.auth_api_public_key,
email,
);
client.sign_in_otp().await?;
Ok((client, config.backend_api_url))
},
Message::OTPRequested,
);
}
Message::OTPRequested(res) => {
self.processing = false;
match res {
Ok((client, backend_api_url)) => {
self.step = ConnectionStep::EnterOtp {
email: email.value.to_owned(),
otp: form::Value::default(),
client,
backend_api_url,
@ -279,18 +253,9 @@ impl LianaLiteLogin {
},
ConnectionStep::EnterOtp {
client,
email,
otp,
backend_api_url,
} => match message {
Message::View(ViewMessage::EditEmail) => {
self.step = ConnectionStep::EnterEmail {
email: form::Value {
value: email.clone(),
valid: true,
},
};
}
Message::View(ViewMessage::RequestOTP) => {
*otp = form::Value::default();
let client = client.clone();
@ -327,9 +292,11 @@ impl LianaLiteLogin {
self.auth_error = None;
let wallet_id = self.wallet_id.clone();
let network = self.network;
let datadir = self.datadir.clone();
return Task::perform(
async move {
connect(client, otp, wallet_id, backend_api_url, network).await
connect(client, otp, wallet_id, backend_api_url, network, datadir)
.await
},
Message::Connected,
);
@ -343,21 +310,8 @@ impl LianaLiteLogin {
return Task::perform(async move { Some(client) }, Message::Install);
}
Ok(BackendState::WalletExists(client, wallet, coins)) => {
let datadir = self.datadir.clone();
let network = self.network;
return Task::perform(
async move {
update_wallet_auth_settings(
datadir,
network,
wallet.clone(),
client.user_email().to_string(),
client.auth().await.refresh_token,
)
.await?;
Ok((client, wallet, coins))
},
async move { Ok((client, wallet, coins)) },
Message::Run,
);
}
@ -390,88 +344,66 @@ impl LianaLiteLogin {
pub fn view(&self) -> Element<Message> {
let content = Into::<Element<ViewMessage>>::into(
Container::new(
Column::new()
.spacing(100)
.align_x(Alignment::Center)
.push(
Column::new()
.align_x(Alignment::Center)
.spacing(20)
.width(Length::Fill)
.push(h2("Liana Connect"))
.push(
Column::new()
.max_width(500)
.spacing(20)
.push(match &self.step {
ConnectionStep::CheckingAuthFile => Column::new(),
ConnectionStep::EnterEmail { email } => Column::new()
.spacing(20)
.push_maybe(
self.auth_error
.map(|e| text(e).style(theme::text::warning)),
)
.push(
form::Form::new_trimmed("email", email, |msg| {
ViewMessage::EmailEdited(msg)
})
.size(P1_SIZE)
.padding(10)
.warning("Email is not valid"),
)
.push(button::secondary(None, "Next").on_press_maybe(
if self.processing {
Column::new().spacing(100).align_x(Alignment::Center).push(
Column::new()
.align_x(Alignment::Center)
.spacing(20)
.width(Length::Fill)
.push(h2("Liana Connect"))
.push(
Column::new()
.max_width(500)
.spacing(20)
.push(match &self.step {
ConnectionStep::CheckingAuthFile => Column::new(),
ConnectionStep::CheckEmail => Column::new()
.spacing(20)
.align_x(Alignment::Center)
.push_maybe(
self.auth_error
.map(|e| text(e).style(theme::text::warning)),
)
.push(text(&self.email))
.push(
button::secondary(None, "Login")
.width(Length::Fixed(200.0))
.on_press_maybe(if self.processing {
None
} else {
Some(ViewMessage::RequestOTP)
},
)),
ConnectionStep::EnterOtp { otp, .. } => Column::new()
.push(text("An authentication was send to your email"))
.push_maybe(
self.auth_error
.map(|e| text(e).style(theme::text::warning)),
)
.spacing(20)
.push(
form::Form::new_trimmed("Token", otp, |msg| {
ViewMessage::OTPEdited(msg)
})
.size(P1_SIZE)
.padding(10)
.warning("Token is not valid"),
)
.push(
Row::new()
.spacing(10)
.push(
button::secondary(
Some(icon::previous_icon()),
"Change email",
)
.on_press(ViewMessage::EditEmail),
)
.push(
button::secondary(None, "Resend token")
.on_press_maybe(if self.processing {
None
} else {
Some(ViewMessage::RequestOTP)
}),
),
}),
),
ConnectionStep::EnterOtp { otp, .. } => Column::new()
.spacing(20)
.align_x(Alignment::Center)
.push(text("An authentication was sent to your email:"))
.push(text(&self.email))
.push_maybe(
self.auth_error
.map(|e| text(e).style(theme::text::warning)),
)
.push(
form::Form::new_trimmed("Token", otp, |msg| {
ViewMessage::OTPEdited(msg)
})
.size(P1_SIZE)
.padding(10)
.warning("Token is not valid"),
)
.push(
Row::new().spacing(10).push(
button::secondary(None, "Resend token")
.width(Length::Fixed(200.0))
.on_press_maybe(if self.processing {
None
} else {
Some(ViewMessage::RequestOTP)
}),
),
}),
),
)
.push_maybe(if !matches!(self.step, ConnectionStep::CheckingAuthFile) {
Some(
button::secondary(Some(icon::previous_icon()), "Change network")
.width(Length::Fixed(200.0))
.on_press(ViewMessage::BackToLauncher(self.network)),
)
} else {
None
}),
),
}),
),
),
)
.padding(50)
.center_x(Length::Fill)
@ -490,61 +422,21 @@ impl LianaLiteLogin {
);
}
col.push(content).into()
}
}
async fn update_wallet_auth_settings(
datadir: LianaDirectory,
network: Network,
wallet: api::Wallet,
email: String,
refresh_token: String,
) -> Result<(), Error> {
let network_dir = datadir.network_directory(network);
let mut settings = Settings::from_file(&network_dir)?;
let descriptor_checksum = wallet
.descriptor
.to_string()
.split_once('#')
.map(|(_, checksum)| checksum)
.expect("Failed to get checksum from a valid LianaDescriptor")
.to_string();
let remote_backend_auth = Some(AuthConfig {
email,
wallet_id: wallet.id.clone(),
refresh_token,
});
if let Some(wallet_settings) = settings.wallets.iter_mut().find(|w| {
if let Some(auth) = &w.remote_backend_auth {
auth.wallet_id == wallet.id
col.push_maybe(if !matches!(self.step, ConnectionStep::CheckingAuthFile) {
Some(
Container::new(
button::secondary(Some(icon::previous_icon()), "Go back")
.width(Length::Fixed(200.0))
.on_press(Message::View(ViewMessage::BackToLauncher(self.network))),
)
.padding(20),
)
} else {
false
}
}) {
wallet_settings.remote_backend_auth = remote_backend_auth;
} else {
tracing::info!("Wallet id was not found in the settings, adding now the wallet settings to the settings.json file");
settings.wallets.insert(
0,
WalletSetting {
name: wallet.name,
descriptor_checksum,
keys: Vec::new(),
hardware_wallets: Vec::new(),
remote_backend_auth,
},
);
None
})
.push(content)
.into()
}
settings.to_file(&network_dir).map_err(|e| {
DaemonError::Unexpected(format!("Cannot access to settings.json file: {}", e))
})?;
Ok(())
}
pub async fn connect(
@ -553,9 +445,14 @@ pub async fn connect(
wallet_id: String,
backend_api_url: String,
network: Network,
liana_directory: LianaDirectory,
) -> Result<BackendState, Error> {
let network_dir = liana_directory.network_directory(network);
let access = auth.verify_otp(token.trim_end()).await?;
let client = BackendClient::connect(auth, backend_api_url, access.clone(), network).await?;
let client =
BackendClient::connect(auth.clone(), backend_api_url, access.clone(), network).await?;
update_connect_cache(&network_dir, &access, &auth, false).await?;
let wallets = client.list_wallets().await?;
if wallets.is_empty() {
@ -566,25 +463,39 @@ pub async fn connect(
let first = wallets.first().cloned().ok_or(DaemonError::NoAnswer)?;
let (wallet_client, wallet) = client.connect_wallet(first);
let coins = coins_to_cache(Arc::new(wallet_client.clone())).await?;
Ok(BackendState::WalletExists(wallet_client, wallet, coins))
} else if let Some(wallet) = wallets.into_iter().find(|w| w.id == wallet_id) {
let (wallet_client, wallet) = client.connect_wallet(wallet);
let coins = coins_to_cache(Arc::new(wallet_client.clone())).await?;
Ok(BackendState::WalletExists(wallet_client, wallet, coins))
} else {
Ok(BackendState::NoWallet(client))
}
}
pub async fn connect_with_refresh_token(
pub async fn connect_with_credentials(
auth: AuthClient,
refresh_token: String,
wallet_id: String,
backend_api_url: String,
network: Network,
liana_directory: LianaDirectory,
) -> Result<BackendState, Error> {
let access = auth.refresh_token(&refresh_token).await?;
let client = BackendClient::connect(auth, backend_api_url, access.clone(), network).await?;
let network_dir = liana_directory.network_directory(network);
let mut tokens = cache::Account::from_cache(&network_dir, &auth.email)
.map_err(|e| match e {
ConnectCacheError::NotFound => Error::CredentialsMissing,
_ => e.into(),
})?
.ok_or(Error::CredentialsMissing)?
.tokens;
if tokens.expires_at < chrono::Utc::now().timestamp() {
tokens = cache::update_connect_cache(&network_dir, &tokens, &auth, true).await?;
}
let client = BackendClient::connect(auth, backend_api_url, tokens, network).await?;
if let Some(wallet) = client
.list_wallets()