From 0125dd9b50318ba7728fd55a1495f2ff51327b60 Mon Sep 17 00:00:00 2001 From: Tommy Volk Date: Sun, 29 Sep 2024 14:59:48 -0500 Subject: [PATCH] chore: users can leave federations with zero balance --- src/fedimint.rs | 40 +++++++++++++++++++++-- src/routes/bitcoin_wallet.rs | 62 +++++++++++++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/src/fedimint.rs b/src/fedimint.rs index 7064488..02fb744 100644 --- a/src/fedimint.rs +++ b/src/fedimint.rs @@ -175,7 +175,7 @@ impl Wallet { pub async fn connect_to_joined_federations(&self) -> anyhow::Result<()> { // Note: We're intentionally locking the clients mutex earlier than - // necessary so that the lock is held while we're reading the data directory. + // necessary so that the lock is held while we're accessing the data directory. let mut clients = self.clients.lock().await; // List all files in the data directory. @@ -217,7 +217,7 @@ impl Wallet { pub async fn join_federation(&self, invite_code: InviteCode) -> anyhow::Result<()> { // Note: We're intentionally locking the clients mutex earlier than - // necessary so that the lock is held while we're reading the data directory. + // necessary so that the lock is held while we're accessing the data directory. let mut clients = self.clients.lock().await; let federation_id = invite_code.federation_id(); @@ -242,6 +242,42 @@ impl Wallet { Ok(()) } + // TODO: Call `ClientModule::leave()` for every module. + // https://docs.rs/fedimint-client/0.4.2/fedimint_client/module/trait.ClientModule.html#method.leave + // Currently it isn't implemented for the `LightningClientModule`, so for now we're just checking + // that the client has a zero balance. + pub async fn leave_federation(&self, federation_id: FederationId) -> anyhow::Result<()> { + // Note: We're intentionally locking the clients mutex earlier than + // necessary so that the lock is held while we're accessing the data directory. + let mut clients = self.clients.lock().await; + + if let Some(client) = clients.remove(&federation_id) { + if client.get_balance().await.msats != 0 { + // Re-insert the client back into the clients map. + clients.insert(federation_id, client); + + return Err(anyhow::anyhow!( + "Cannot leave federation with non-zero balance: {}", + federation_id + )); + } + + client.shutdown().await; + + let federation_data_dir = self + .fedimint_clients_data_dir + .join(federation_id.to_string()); + + if federation_data_dir.is_dir() { + std::fs::remove_dir_all(federation_data_dir)?; + } + } + + self.force_update_view(clients).await; + + Ok(()) + } + /// Constructs the current view of the wallet. /// SHOULD ONLY BE CALLED FROM THE `view_update_task`. /// This way, `view_update_task` can only yield values diff --git a/src/routes/bitcoin_wallet.rs b/src/routes/bitcoin_wallet.rs index 08ce6d4..b8284f9 100644 --- a/src/routes/bitcoin_wallet.rs +++ b/src/routes/bitcoin_wallet.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use fedimint_core::{ - config::{ClientConfig, META_FEDERATION_NAME_KEY}, + config::{ClientConfig, FederationId, META_FEDERATION_NAME_KEY}, invite_code::InviteCode, Amount, }; @@ -42,6 +42,9 @@ pub enum Message { JoinFederation(InviteCode), JoinedFederation(InviteCode), + LeaveFederation(FederationId), + LeftFederation(FederationId), + Send(send::Message), Receive(receive::Message), @@ -196,6 +199,44 @@ impl Page { Task::none() } + Message::LeaveFederation(federation_id) => { + let wallet = self.connected_state.wallet.clone(); + + Task::stream(async_stream::stream! { + match wallet.leave_federation(federation_id).await { + Ok(()) => { + yield app::Message::AddToast(Toast { + title: "Left federation".to_string(), + body: "You have successfully left the federation.".to_string(), + status: ToastStatus::Good, + }); + + yield app::Message::Routes(super::Message::BitcoinWalletPage( + Message::LeftFederation(federation_id) + )); + } + Err(err) => { + yield app::Message::AddToast(Toast { + title: "Failed to leave federation".to_string(), + body: format!("Failed to leave the federation: {err}"), + status: ToastStatus::Bad, + }); + } + } + }) + } + Message::LeftFederation(federation_id) => { + // A verbose way of saying "if the user is currently on the FederationDetails page and the federation ID matches the one that was just left, navigate back to the List page". + if let Subroute::FederationDetails(federation_details) = &self.subroute { + if federation_details.view.federation_id == federation_id { + return Task::done(app::Message::Routes(super::Message::Navigate( + RouteName::BitcoinWallet(SubrouteName::List), + ))); + } + } + + Task::none() + } Message::Send(send_message) => { if let Subroute::Send(send_page) = &mut self.subroute { send_page.update(send_message) @@ -444,6 +485,25 @@ impl FederationDetails { ); } + let has_zero_balance = self.view.balance.msats == 0; + + if !has_zero_balance { + container = container.push( + Text::new("Must have a zero balance in this federation in order to leave.") + .size(20), + ); + } + + container = container.push( + icon_button("Leave Federation", SvgIcon::Delete, PaletteColor::Danger).on_press_maybe( + has_zero_balance.then(|| { + app::Message::Routes(super::Message::BitcoinWalletPage( + Message::LeaveFederation(self.view.federation_id), + )) + }), + ), + ); + container = container.push( icon_button("Back", SvgIcon::ArrowBack, PaletteColor::Background).on_press( app::Message::Routes(super::Message::Navigate(RouteName::BitcoinWallet(