Merge #1775: Delete wallet on Liana-Connect
f62d9429581371d830ef455feb1158024199739f Add check of user membership to delete wallet modal (edouardparis)
1ca02a48979a7a36440ac57d8d83f988241af92e Delete wallet on Liana-Connect (edouardparis)
Pull request description:
ACKs for top commit:
edouardparis:
Self-ACK f62d9429581371d830ef455feb1158024199739f
Tree-SHA512: d624955703c83b8308378231a4563c575ed13c7e419377f3f4d8636bdb2b1a7fc157f8ae75f47ccabbf4c9a44ae3f449314987052dbe8337e8e44f8a145700d2
This commit is contained in:
commit
0e7a87ea80
@ -1,16 +1,25 @@
|
||||
use liana::miniscript::bitcoin::Network;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{
|
||||
app::settings::{self, SettingsError, WalletId},
|
||||
app::settings::{self, SettingsError, WalletSettings},
|
||||
dir::NetworkDirectory,
|
||||
services::connect::client::cache::{self, ConnectCacheError},
|
||||
services::connect::{
|
||||
client::{
|
||||
auth::AuthClient,
|
||||
cache::{self, ConnectCacheError},
|
||||
get_service_config,
|
||||
},
|
||||
login::{connect_with_credentials, BackendState},
|
||||
},
|
||||
signer,
|
||||
};
|
||||
|
||||
pub enum DeleteError {
|
||||
Io(std::io::Error),
|
||||
Settings(SettingsError),
|
||||
Connect(ConnectCacheError),
|
||||
ConnectCache(ConnectCacheError),
|
||||
Connect(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DeleteError {
|
||||
@ -18,6 +27,7 @@ impl std::fmt::Display for DeleteError {
|
||||
match self {
|
||||
Self::Io(e) => write!(f, "{}", e),
|
||||
Self::Settings(e) => write!(f, "{}", e),
|
||||
Self::ConnectCache(e) => write!(f, "{}", e),
|
||||
Self::Connect(e) => write!(f, "{}", e),
|
||||
}
|
||||
}
|
||||
@ -37,11 +47,12 @@ fn ignore_not_found<T>(result: std::io::Result<T>) -> std::io::Result<Option<T>>
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_wallet(
|
||||
pub async fn delete_failed_install(
|
||||
network_dir: &NetworkDirectory,
|
||||
wallet_id: &WalletId,
|
||||
wallet_id: &settings::WalletId,
|
||||
) -> Result<(), DeleteError> {
|
||||
let lianad_directory = network_dir.lianad_data_directory(wallet_id);
|
||||
|
||||
if !wallet_id.is_legacy() {
|
||||
ignore_not_found(tokio::fs::remove_dir_all(lianad_directory.path()).await)?;
|
||||
} else {
|
||||
@ -77,7 +88,95 @@ pub async fn delete_wallet(
|
||||
|
||||
cache::filter_connect_cache(network_dir, &remaining_accounts)
|
||||
.await
|
||||
.map_err(DeleteError::Connect)?;
|
||||
.map_err(DeleteError::ConnectCache)?;
|
||||
|
||||
signer::delete_wallet_mnemonics(
|
||||
network_dir,
|
||||
&wallet_id.descriptor_checksum,
|
||||
wallet_id.timestamp,
|
||||
)
|
||||
.map_err(DeleteError::Io)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_wallet(
|
||||
network: Network,
|
||||
network_dir: &NetworkDirectory,
|
||||
wallet: &WalletSettings,
|
||||
delete_liana_connect: bool,
|
||||
) -> Result<(), DeleteError> {
|
||||
let wallet_id = wallet.wallet_id();
|
||||
let lianad_directory = network_dir.lianad_data_directory(&wallet_id);
|
||||
|
||||
if !wallet_id.is_legacy() {
|
||||
ignore_not_found(tokio::fs::remove_dir_all(lianad_directory.path()).await)?;
|
||||
} else {
|
||||
// if this is a legacy wallet, then it is the only wallet in the network directory.
|
||||
ignore_not_found(tokio::fs::remove_file(lianad_directory.sqlite_db_file_path()).await)?;
|
||||
ignore_not_found(
|
||||
tokio::fs::remove_dir_all(lianad_directory.lianad_watchonly_wallet_path()).await,
|
||||
)?;
|
||||
ignore_not_found(
|
||||
tokio::fs::remove_file(lianad_directory.path().join("daemon.toml")).await,
|
||||
)?;
|
||||
}
|
||||
|
||||
if delete_liana_connect {
|
||||
if let Some(auth) = &wallet.remote_backend_auth {
|
||||
let service_config = get_service_config(network)
|
||||
.await
|
||||
.map_err(|e| DeleteError::Connect(e.to_string()))?;
|
||||
|
||||
let client = AuthClient::new(
|
||||
service_config.auth_api_url,
|
||||
service_config.auth_api_public_key,
|
||||
auth.email.to_string(),
|
||||
);
|
||||
if let BackendState::WalletExists(client, _, _) = connect_with_credentials(
|
||||
client,
|
||||
auth.wallet_id.clone(),
|
||||
service_config.backend_api_url,
|
||||
network,
|
||||
network_dir,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| DeleteError::Connect(e.to_string()))?
|
||||
{
|
||||
tracing::info!("Deleting wallet on Liana-Connect {} backend", network);
|
||||
client
|
||||
.delete_wallet()
|
||||
.await
|
||||
.map_err(|e| DeleteError::Connect(e.to_string()))?;
|
||||
} else {
|
||||
tracing::warn!("Wallet not found on the platform");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut remaining_accounts = HashSet::<String>::new();
|
||||
settings::update_settings_file(network_dir, |mut settings| {
|
||||
settings
|
||||
.wallets
|
||||
.retain(|settings| settings.wallet_id() != wallet_id);
|
||||
remaining_accounts = settings
|
||||
.wallets
|
||||
.iter()
|
||||
.filter_map(|settings| {
|
||||
settings
|
||||
.remote_backend_auth
|
||||
.as_ref()
|
||||
.map(|auth| auth.email.clone())
|
||||
})
|
||||
.collect();
|
||||
settings
|
||||
})
|
||||
.await
|
||||
.map_err(DeleteError::Settings)?;
|
||||
|
||||
cache::filter_connect_cache(network_dir, &remaining_accounts)
|
||||
.await
|
||||
.map_err(DeleteError::ConnectCache)?;
|
||||
|
||||
signer::delete_wallet_mnemonics(
|
||||
network_dir,
|
||||
|
||||
@ -299,9 +299,10 @@ impl Installer {
|
||||
// In case of failure during install, block the thread to
|
||||
// deleted the data_dir/network directory in order to start clean again.
|
||||
warn!("Installation failed. Cleaning up the network directory.");
|
||||
if let Err(e) = Handle::current()
|
||||
.block_on(delete::delete_wallet(&network_directory, &wallet_id))
|
||||
{
|
||||
if let Err(e) = Handle::current().block_on(delete::delete_failed_install(
|
||||
&network_directory,
|
||||
&wallet_id,
|
||||
)) {
|
||||
error!(
|
||||
"Failed to completely clean the network directory (path: '{}'): {}",
|
||||
network_directory.path().to_string_lossy(),
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use iced::{
|
||||
alignment::Horizontal,
|
||||
widget::{pick_list, scrollable, Button, Space},
|
||||
widget::{checkbox, pick_list, scrollable, Button, Space},
|
||||
Alignment, Length, Subscription, Task,
|
||||
};
|
||||
|
||||
@ -16,11 +16,15 @@ use tokio::runtime::Handle;
|
||||
use crate::{
|
||||
app::{
|
||||
self,
|
||||
settings::{self, WalletId, WalletSettings},
|
||||
settings::{self, AuthConfig, WalletId, WalletSettings},
|
||||
},
|
||||
delete::{delete_wallet, DeleteError},
|
||||
dir::{LianaDirectory, NetworkDirectory},
|
||||
installer::UserFlow,
|
||||
services::connect::{
|
||||
client::{auth::AuthClient, backend::api::UserRole, get_service_config},
|
||||
login::{connect_with_credentials, BackendState},
|
||||
},
|
||||
};
|
||||
|
||||
const NETWORKS: [Network; 4] = [
|
||||
@ -124,6 +128,7 @@ impl Launcher {
|
||||
None
|
||||
};
|
||||
self.delete_wallet_modal = Some(DeleteWalletModal::new(
|
||||
self.network,
|
||||
wallet_datadir,
|
||||
wallets[i].clone(),
|
||||
internal_bitcoind,
|
||||
@ -425,52 +430,83 @@ pub enum DeleteWalletMessage {
|
||||
ShowModal(usize),
|
||||
CloseModal,
|
||||
Confirm(WalletId),
|
||||
DeleteLianaConnect(bool),
|
||||
Deleted,
|
||||
}
|
||||
|
||||
struct DeleteWalletModal {
|
||||
network: Network,
|
||||
network_directory: NetworkDirectory,
|
||||
wallet_settings: WalletSettings,
|
||||
warning: Option<DeleteError>,
|
||||
deleted: bool,
|
||||
delete_liana_connect: bool,
|
||||
user_role: Option<UserRole>,
|
||||
// `None` means we were not able to determine whether wallet uses internal bitcoind.
|
||||
internal_bitcoind: Option<bool>,
|
||||
}
|
||||
|
||||
impl DeleteWalletModal {
|
||||
fn new(
|
||||
network: Network,
|
||||
network_directory: NetworkDirectory,
|
||||
wallet_settings: WalletSettings,
|
||||
internal_bitcoind: Option<bool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
let mut modal = Self {
|
||||
network,
|
||||
wallet_settings,
|
||||
network_directory,
|
||||
warning: None,
|
||||
deleted: false,
|
||||
delete_liana_connect: false,
|
||||
internal_bitcoind,
|
||||
user_role: None,
|
||||
};
|
||||
if let Some(auth) = &modal.wallet_settings.remote_backend_auth {
|
||||
match Handle::current().block_on(check_membership(
|
||||
modal.network,
|
||||
&modal.network_directory,
|
||||
auth,
|
||||
)) {
|
||||
Err(e) => {
|
||||
modal.warning = Some(e);
|
||||
}
|
||||
Ok(user_role) => {
|
||||
modal.user_role = user_role;
|
||||
}
|
||||
}
|
||||
}
|
||||
modal
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
if let Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::Confirm(wallet_id))) =
|
||||
message
|
||||
{
|
||||
if wallet_id != self.wallet_settings.wallet_id() {
|
||||
return Task::none();
|
||||
match message {
|
||||
Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::Confirm(wallet_id))) => {
|
||||
if wallet_id != self.wallet_settings.wallet_id() {
|
||||
return Task::none();
|
||||
}
|
||||
self.warning = None;
|
||||
if let Err(e) = Handle::current().block_on(delete_wallet(
|
||||
self.network,
|
||||
&self.network_directory,
|
||||
&self.wallet_settings,
|
||||
self.delete_liana_connect,
|
||||
)) {
|
||||
self.warning = Some(e);
|
||||
} else {
|
||||
self.deleted = true;
|
||||
return Task::perform(async {}, |_| {
|
||||
Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::Deleted))
|
||||
});
|
||||
};
|
||||
}
|
||||
self.warning = None;
|
||||
if let Err(e) = Handle::current().block_on(delete_wallet(
|
||||
&self.network_directory,
|
||||
&self.wallet_settings.wallet_id(),
|
||||
)) {
|
||||
self.warning = Some(e);
|
||||
} else {
|
||||
self.deleted = true;
|
||||
return Task::perform(async {}, |_| {
|
||||
Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::Deleted))
|
||||
});
|
||||
};
|
||||
Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::DeleteLianaConnect(
|
||||
delete,
|
||||
))) => {
|
||||
self.delete_liana_connect = delete;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
@ -485,18 +521,22 @@ impl DeleteWalletModal {
|
||||
));
|
||||
}
|
||||
// Use separate `Row`s for help text in order to have better spacing.
|
||||
let help_text_1 = if let Some(alias) = &self.wallet_settings.alias {
|
||||
format!(
|
||||
"Are you sure you want to delete the configuration and all associated data for the wallet {} (Liana-{})?",
|
||||
alias,
|
||||
self.wallet_settings.descriptor_checksum
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Are you sure you want to delete the configuration and all associated data for the wallet Liana-{}?",
|
||||
&self.wallet_settings.descriptor_checksum,
|
||||
)
|
||||
};
|
||||
let help_text_1 = format!(
|
||||
"Are you sure you want to {} for the wallet {}",
|
||||
if self.wallet_settings.remote_backend_auth.is_some() {
|
||||
"delete locally the configuration"
|
||||
} else {
|
||||
"delete the configuration and all associated data"
|
||||
},
|
||||
if let Some(alias) = &self.wallet_settings.alias {
|
||||
format!(
|
||||
"{} (Liana-{})?",
|
||||
alias, self.wallet_settings.descriptor_checksum
|
||||
)
|
||||
} else {
|
||||
format!("Liana-{}?", &self.wallet_settings.descriptor_checksum)
|
||||
}
|
||||
);
|
||||
let help_text_2 = match self.internal_bitcoind {
|
||||
Some(true) => Some("(The Liana-managed Bitcoin node for this network will not be affected by this action.)"),
|
||||
Some(false) => None,
|
||||
@ -529,6 +569,22 @@ impl DeleteWalletModal {
|
||||
.map(|t| Row::new().push(p1_regular(t).style(theme::text::secondary))),
|
||||
)
|
||||
.push(Row::new())
|
||||
.push_maybe(self.wallet_settings.remote_backend_auth.as_ref().map(|a| {
|
||||
checkbox(
|
||||
match self.user_role {
|
||||
Some(UserRole::Owner) | None => "Also permanently delete this wallet from Liana Connect (for all members).".to_string(),
|
||||
Some(UserRole::Member) => format!("Also disassociate {} from this Liana Connect wallet.", a.email),
|
||||
},
|
||||
self.delete_liana_connect,
|
||||
)
|
||||
.on_toggle_maybe(if !self.deleted {
|
||||
Some(|v| {
|
||||
ViewMessage::DeleteWallet(DeleteWalletMessage::DeleteLianaConnect(v))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}))
|
||||
.push(Row::new().push(text(help_text_3)))
|
||||
.push_maybe(self.warning.as_ref().map(|w| {
|
||||
notification::warning(w.to_string(), w.to_string()).width(Length::Fill)
|
||||
@ -554,6 +610,40 @@ impl DeleteWalletModal {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn check_membership(
|
||||
network: Network,
|
||||
network_dir: &NetworkDirectory,
|
||||
auth: &AuthConfig,
|
||||
) -> Result<Option<UserRole>, DeleteError> {
|
||||
let service_config = get_service_config(network)
|
||||
.await
|
||||
.map_err(|e| DeleteError::Connect(e.to_string()))?;
|
||||
|
||||
if let BackendState::WalletExists(client, _, _) = connect_with_credentials(
|
||||
AuthClient::new(
|
||||
service_config.auth_api_url,
|
||||
service_config.auth_api_public_key,
|
||||
auth.email.to_string(),
|
||||
),
|
||||
auth.wallet_id.clone(),
|
||||
service_config.backend_api_url,
|
||||
network,
|
||||
network_dir,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| DeleteError::Connect(e.to_string()))?
|
||||
{
|
||||
Ok(Some(
|
||||
client
|
||||
.user_wallet_membership()
|
||||
.await
|
||||
.map_err(|e| DeleteError::Connect(e.to_string()))?,
|
||||
))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_network_datadir(path: NetworkDirectory) -> Result<State, String> {
|
||||
let mut config_path = path.clone().path().to_path_buf();
|
||||
config_path.push(app::config::DEFAULT_FILE_NAME);
|
||||
|
||||
@ -122,6 +122,24 @@ pub struct ListWallets {
|
||||
pub wallets: Vec<Wallet>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum UserRole {
|
||||
Owner,
|
||||
Member,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ListWalletMembers {
|
||||
pub members: Vec<Member>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Member {
|
||||
pub user_id: String,
|
||||
pub role: UserRole,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Provider {
|
||||
pub uuid: String,
|
||||
|
||||
@ -468,9 +468,45 @@ impl BackendWalletClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn user_wallet_membership(&self) -> Result<api::UserRole, DaemonError> {
|
||||
let list: api::ListWalletMembers = self
|
||||
.inner
|
||||
.request(
|
||||
Method::GET,
|
||||
&format!("{}/v1/wallets/{}/members", self.inner.url, self.wallet_uuid),
|
||||
)
|
||||
.await
|
||||
.send()
|
||||
.await?
|
||||
.check_success()
|
||||
.await?
|
||||
.json_or_error()
|
||||
.await?;
|
||||
list.members
|
||||
.into_iter()
|
||||
.find(|m| m.user_id == self.user_id())
|
||||
.map(|m| m.role)
|
||||
.ok_or(DaemonError::Unexpected("Membership not found".to_string()))
|
||||
}
|
||||
|
||||
pub async fn auth(&self) -> AccessTokenResponse {
|
||||
self.inner.auth.read().await.clone()
|
||||
}
|
||||
|
||||
pub async fn delete_wallet(&self) -> Result<(), DaemonError> {
|
||||
self.inner
|
||||
.request(
|
||||
Method::DELETE,
|
||||
&format!("{}/v1/wallets/{}", self.inner.url, self.wallet_uuid),
|
||||
)
|
||||
.await
|
||||
.send()
|
||||
.await?
|
||||
.check_success()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@ -16,7 +16,7 @@ use crate::{
|
||||
settings::{SettingsError, WalletSettings},
|
||||
},
|
||||
daemon::DaemonError,
|
||||
dir::LianaDirectory,
|
||||
dir::{LianaDirectory, NetworkDirectory},
|
||||
};
|
||||
|
||||
use super::client::{
|
||||
@ -170,7 +170,7 @@ impl LianaLiteLogin {
|
||||
auth.wallet_id,
|
||||
service_config.backend_api_url,
|
||||
network,
|
||||
datadir,
|
||||
&datadir.network_directory(network),
|
||||
)
|
||||
.await
|
||||
},
|
||||
@ -483,10 +483,9 @@ pub async fn connect_with_credentials(
|
||||
wallet_id: String,
|
||||
backend_api_url: String,
|
||||
network: Network,
|
||||
liana_directory: LianaDirectory,
|
||||
network_dir: &NetworkDirectory,
|
||||
) -> Result<BackendState, Error> {
|
||||
let network_dir = liana_directory.network_directory(network);
|
||||
let mut tokens = cache::Account::from_cache(&network_dir, &auth.email)
|
||||
let mut tokens = cache::Account::from_cache(network_dir, &auth.email)
|
||||
.map_err(|e| match e {
|
||||
ConnectCacheError::NotFound => Error::CredentialsMissing,
|
||||
_ => e.into(),
|
||||
@ -495,7 +494,7 @@ pub async fn connect_with_credentials(
|
||||
.tokens;
|
||||
|
||||
if tokens.expires_at < chrono::Utc::now().timestamp() {
|
||||
tokens = cache::update_connect_cache(&network_dir, &tokens, &auth, true).await?;
|
||||
tokens = cache::update_connect_cache(network_dir, &tokens, &auth, true).await?;
|
||||
}
|
||||
|
||||
let client = BackendClient::connect(auth, backend_api_url, tokens, network).await?;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user