diff --git a/src/fedimint.rs b/src/fedimint.rs index 0d1a1b0..e5998cc 100644 --- a/src/fedimint.rs +++ b/src/fedimint.rs @@ -112,7 +112,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. @@ -150,9 +150,12 @@ impl Wallet { Ok(()) } - pub async fn join_federation(&self, invite_code: InviteCode) -> anyhow::Result<()> { + 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(); @@ -163,7 +166,7 @@ impl Wallet { // Short-circuit if we're already connected to this federation. if federation_data_dir.is_dir() { - return Ok(()); + return Ok(Self::get_current_state(clients).await); } let db: Database = RocksDb::open(federation_data_dir)?.into(); @@ -172,7 +175,44 @@ impl Wallet { clients.insert(federation_id, client); - Ok(()) + Ok(Self::get_current_state(clients).await) + } + + // 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)?; + } + } + + Ok(Self::get_current_state(clients).await) } async fn get_current_state( diff --git a/src/routes/bitcoin_wallet.rs b/src/routes/bitcoin_wallet.rs index b6bbac0..7e81e21 100644 --- a/src/routes/bitcoin_wallet.rs +++ b/src/routes/bitcoin_wallet.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeMap, str::FromStr}; +use std::{collections::BTreeMap, str::FromStr, sync::Arc}; use fedimint_core::{ config::{ClientConfig, FederationId, META_FEDERATION_NAME_KEY}, @@ -15,7 +15,7 @@ use iced::{ use crate::{ app, fedimint::FederationView, - ui_components::{icon_button, PaletteColor, SvgIcon}, + ui_components::{icon_button, PaletteColor, SvgIcon, Toast, ToastStatus}, util::{format_amount, lighten, truncate_text}, }; @@ -42,6 +42,10 @@ pub enum Message { JoinFederation(InviteCode), ConnectedToFederation, + LeaveFederation(FederationId), + LeftFederation((FederationId, BTreeMap)), + FailedToLeaveFederation((FederationId, Arc)), + Send(send::Message), Receive(receive::Message), @@ -160,17 +164,57 @@ impl Page { let wallet = self.connected_state.wallet.clone(); Task::future(async move { - wallet.join_federation(invite_code).await.unwrap(); - app::Message::Routes(super::Message::BitcoinWalletPage( - Message::ConnectedToFederation, - )) + let federation_views = wallet.join_federation(invite_code).await.unwrap(); + app::Message::UpdateFederationViews(federation_views) }) + .chain(Task::done(app::Message::Routes( + super::Message::BitcoinWalletPage(Message::ConnectedToFederation), + ))) + .chain(Task::done(app::Message::AddToast(Toast { + title: "Joined federation".to_string(), + body: "You have successfully joined the federation.".to_string(), + status: ToastStatus::Good, + }))) } Message::ConnectedToFederation => { // TODO: Do something here, or remove `ConnectedToFederation` message variant. Task::none() } + Message::LeaveFederation(federation_id) => { + let wallet = self.connected_state.wallet.clone(); + + Task::future(async move { + match wallet.leave_federation(federation_id).await { + Ok(federation_views) => { + app::Message::Routes(super::Message::BitcoinWalletPage( + Message::LeftFederation((federation_id, federation_views)), + )) + } + Err(err) => app::Message::Routes(super::Message::BitcoinWalletPage( + Message::FailedToLeaveFederation((federation_id, Arc::from(err))), + )), + } + }) + } + Message::LeftFederation((_federation_id, federation_views)) => { + Task::done(app::Message::UpdateFederationViews(federation_views)) + .chain(Task::done(app::Message::Routes(super::Message::Navigate( + RouteName::BitcoinWallet(SubrouteName::List), + )))) + .chain(Task::done(app::Message::AddToast(Toast { + title: "Left federation".to_owned(), + body: "You have successfully left the federation.".to_owned(), + status: ToastStatus::Good, + }))) + } + Message::FailedToLeaveFederation((_federation_id, _err)) => { + Task::done(app::Message::AddToast(Toast { + title: "Failed to leave federation".to_owned(), + body: "Could not leave federation.".to_owned(), + status: ToastStatus::Bad, + })) + } Message::Send(send_message) => { if let Subroute::Send(send_page) = &mut self.subroute { send_page.update(send_message) @@ -418,6 +462,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(