diff --git a/src/routes/bitcoin_wallet.rs b/src/routes/bitcoin_wallet.rs index 08eff05..b6f7846 100644 --- a/src/routes/bitcoin_wallet.rs +++ b/src/routes/bitcoin_wallet.rs @@ -1,14 +1,6 @@ -use std::str::FromStr; - -use fedimint_core::{ - config::{ClientConfig, FederationId, META_FEDERATION_NAME_KEY}, - invite_code::InviteCode, - Amount, -}; +use fedimint_core::{config::FederationId, Amount}; use iced::{ - widget::{ - column, container::Style, horizontal_space, row, text_input, Column, Container, Space, Text, - }, + widget::{column, container::Style, horizontal_space, row, Column, Container, Space, Text}, Border, Length, Shadow, Task, Theme, }; @@ -21,30 +13,16 @@ use crate::{ use super::{container, ConnectedState, Loadable, RouteName}; +mod add; mod receive; mod send; #[derive(Debug, Clone)] pub enum Message { - JoinFederationInviteCodeInputChanged(String), - - LoadedFederationConfigFromInviteCode { - // The invite code that was used to load the federation config. - config_invite_code: InviteCode, - // The loaded federation config. - config: ClientConfig, - }, - FailedToLoadFederationConfigFromInviteCode { - // The invite code that was used to attempt to load the federation config. - config_invite_code: InviteCode, - }, - - JoinFederation(InviteCode), - JoinedFederation(InviteCode), - LeaveFederation(FederationId), LeftFederation(FederationId), + Add(add::Message), Send(send::Message), Receive(receive::Message), @@ -57,148 +35,8 @@ pub struct Page { } impl Page { - // TODO: Remove this clippy allow. - #[allow(clippy::too_many_lines)] pub fn update(&mut self, msg: Message) -> Task { match msg { - Message::JoinFederationInviteCodeInputChanged(new_federation_invite_code) => { - let Subroute::Add(Add { - federation_invite_code, - parsed_federation_invite_code_state_or, - }) = &mut self.subroute - else { - return Task::none(); - }; - - *federation_invite_code = new_federation_invite_code; - - if let Ok(invite_code) = InviteCode::from_str(federation_invite_code) { - *parsed_federation_invite_code_state_or = - Some(ParsedFederationInviteCodeState { - invite_code: invite_code.clone(), - loadable_federation_config: Loadable::Loading, - }); - - Task::perform( - async move { - match fedimint_api_client::download_from_invite_code(&invite_code).await - { - Ok(config) => { - app::Message::Routes(super::Message::BitcoinWalletPage( - Message::LoadedFederationConfigFromInviteCode { - config_invite_code: invite_code, - config, - }, - )) - } - // TODO: Include error in message and display it in the UI. - Err(_err) => { - app::Message::Routes(super::Message::BitcoinWalletPage( - Message::FailedToLoadFederationConfigFromInviteCode { - config_invite_code: invite_code, - }, - )) - } - } - }, - |msg| msg, - ) - } else { - *parsed_federation_invite_code_state_or = None; - - Task::none() - } - } - Message::LoadedFederationConfigFromInviteCode { - config_invite_code, - config, - } => { - let Subroute::Add(Add { - parsed_federation_invite_code_state_or, - .. - }) = &mut self.subroute - else { - return Task::none(); - }; - - if let Some(ParsedFederationInviteCodeState { - invite_code, - loadable_federation_config: maybe_loading_federation_config, - }) = parsed_federation_invite_code_state_or - { - // If the invite code has changed since the request was made, ignore the response. - if &config_invite_code == invite_code { - *maybe_loading_federation_config = Loadable::Loaded(config); - } - } - - Task::none() - } - Message::FailedToLoadFederationConfigFromInviteCode { config_invite_code } => { - let Subroute::Add(Add { - parsed_federation_invite_code_state_or, - .. - }) = &mut self.subroute - else { - return Task::none(); - }; - - if let Some(ParsedFederationInviteCodeState { - invite_code, - loadable_federation_config: maybe_loading_federation_config, - }) = parsed_federation_invite_code_state_or - { - // If the invite code has changed since the request was made, ignore the response. - // Also only update the state if the config hasn't already been loaded. - if &config_invite_code == invite_code - && matches!(maybe_loading_federation_config, Loadable::Loading) - { - *maybe_loading_federation_config = Loadable::Failed; - } - } - - Task::none() - } - Message::JoinFederation(invite_code) => { - let wallet = self.connected_state.wallet.clone(); - - Task::stream(async_stream::stream! { - match wallet.join_federation(invite_code.clone()).await { - Ok(()) => { - yield app::Message::AddToast(Toast { - title: "Joined federation".to_string(), - body: "You have successfully joined the federation.".to_string(), - status: ToastStatus::Good, - }); - - yield app::Message::Routes(super::Message::BitcoinWalletPage( - Message::JoinedFederation(invite_code) - )); - } - Err(err) => { - yield app::Message::AddToast(Toast { - title: "Failed to join federation".to_string(), - body: format!("Failed to join the federation: {err}"), - status: ToastStatus::Bad, - }); - } - } - }) - } - Message::JoinedFederation(invite_code) => { - // A verbose way of saying "if the user is currently on the Add page and the invite code matches the one that was just joined, navigate back to the List page". - if let Subroute::Add(add) = &self.subroute { - if let Some(invite_code_state) = &add.parsed_federation_invite_code_state_or { - if invite_code_state.invite_code == invite_code { - return Task::done(app::Message::Routes(super::Message::Navigate( - RouteName::BitcoinWallet(SubrouteName::List), - ))); - } - } - } - - Task::none() - } Message::LeaveFederation(federation_id) => { let wallet = self.connected_state.wallet.clone(); @@ -237,6 +75,13 @@ impl Page { Task::none() } + Message::Add(add_message) => { + if let Subroute::Add(add_page) = &mut self.subroute { + add_page.update(add_message) + } else { + Task::none() + } + } Message::Send(send_message) => { if let Subroute::Send(send_page) = &mut self.subroute { send_page.update(send_message) @@ -292,10 +137,7 @@ impl SubrouteName { view: federation_view.clone(), }) } - Self::Add => Subroute::Add(Add { - federation_invite_code: String::new(), - parsed_federation_invite_code_state_or: None, - }), + Self::Add => Subroute::Add(add::Page::new(connected_state)), Self::Send => Subroute::Send(send::Page::new(connected_state)), Self::Receive => Subroute::Receive(receive::Page::new(connected_state)), } @@ -305,7 +147,7 @@ impl SubrouteName { pub enum Subroute { List(List), FederationDetails(FederationDetails), - Add(Add), + Add(add::Page), Send(send::Page), Receive(receive::Page), } @@ -517,100 +359,3 @@ impl FederationDetails { container } } - -pub struct Add { - federation_invite_code: String, - parsed_federation_invite_code_state_or: Option, -} - -pub struct ParsedFederationInviteCodeState { - invite_code: InviteCode, - loadable_federation_config: Loadable, -} - -impl Add { - fn view<'a>(&self) -> Column<'a, app::Message> { - let mut container = container("Join Federation") - .push( - text_input("Federation Invite Code", &self.federation_invite_code) - .on_input(|input| { - app::Message::Routes(super::Message::BitcoinWalletPage( - Message::JoinFederationInviteCodeInputChanged(input), - )) - }) - .padding(10) - .size(30), - ) - .push( - icon_button("Join Federation", SvgIcon::Groups, PaletteColor::Primary) - .on_press_maybe(self.parsed_federation_invite_code_state_or.as_ref().map( - |parsed_federation_invite_code_state| { - app::Message::Routes(super::Message::BitcoinWalletPage( - Message::JoinFederation( - parsed_federation_invite_code_state.invite_code.clone(), - ), - )) - }, - )), - ); - - if let Some(parsed_federation_invite_code_state) = - &self.parsed_federation_invite_code_state_or - { - container = container - .push(Text::new("Federation ID").size(25)) - .push(Text::new(truncate_text( - &parsed_federation_invite_code_state - .invite_code - .federation_id() - .to_string(), - 21, - true, - ))); - - match &parsed_federation_invite_code_state.loadable_federation_config { - Loadable::Loading => { - container = container.push(Text::new("Loading...")); - } - Loadable::Loaded(client_config) => { - container = container - .push(Text::new("Federation Name").size(25)) - .push(Text::new( - client_config - .meta::(META_FEDERATION_NAME_KEY) - .ok() - .flatten() - .unwrap_or_default(), - )) - .push(Text::new("Modules").size(25)) - .push(Text::new( - client_config - .modules - .values() - .map(|module| module.kind().to_string()) - .collect::>() - .join(", "), - )) - .push(Text::new("Guardians").size(25)); - for peer_url in client_config.global.api_endpoints.values() { - container = container - .push(Text::new(format!("{} ({})", peer_url.name, peer_url.url))); - } - } - Loadable::Failed => { - container = container.push(Text::new("Failed to load client config")); - } - } - } - - container = container.push( - icon_button("Back", SvgIcon::ArrowBack, PaletteColor::Background).on_press( - app::Message::Routes(super::Message::Navigate(RouteName::BitcoinWallet( - SubrouteName::List, - ))), - ), - ); - - container - } -} diff --git a/src/routes/bitcoin_wallet/add.rs b/src/routes/bitcoin_wallet/add.rs new file mode 100644 index 0000000..13a3ba8 --- /dev/null +++ b/src/routes/bitcoin_wallet/add.rs @@ -0,0 +1,252 @@ +use std::str::FromStr; +use std::sync::Arc; + +use fedimint_core::{ + config::{ClientConfig, META_FEDERATION_NAME_KEY}, + invite_code::InviteCode, +}; +use iced::{ + widget::{text_input, Column, Text}, + Task, +}; + +use crate::{ + app, + fedimint::Wallet, + routes::{self, container, ConnectedState, Loadable}, + ui_components::{icon_button, PaletteColor, SvgIcon, Toast, ToastStatus}, + util::truncate_text, +}; + +#[derive(Debug, Clone)] +pub enum Message { + InviteCodeInputChanged(String), + + LoadedFederationConfigFromInviteCode { + // The invite code that was used to load the federation config. + invite_code: InviteCode, + // The loaded federation config from the federation that the invite code belongs to. + config: ClientConfig, + }, + FailedToLoadFederationConfigFromInviteCode { + // The invite code that was used to attempt to load the federation config. + invite_code: InviteCode, + }, + + JoinFederation(InviteCode), + JoinedFederation(InviteCode), +} + +pub struct Page { + wallet: Arc, + invite_code_input: String, + parsed_invite_code_state_or: Option, +} + +struct ParsedInviteCodeState { + invite_code: InviteCode, + loadable_federation_config: Loadable, +} + +impl Page { + pub fn new(connected_state: &ConnectedState) -> Self { + Self { + wallet: connected_state.wallet.clone(), + invite_code_input: String::new(), + parsed_invite_code_state_or: None, + } + } + + // TODO: Remove this clippy allow. + #[allow(clippy::too_many_lines)] + pub fn update(&mut self, msg: Message) -> Task { + match msg { + Message::InviteCodeInputChanged(new_invite_code_input) => { + self.invite_code_input = new_invite_code_input; + + let Ok(invite_code) = InviteCode::from_str(&self.invite_code_input) else { + self.parsed_invite_code_state_or = None; + + return Task::none(); + }; + + self.parsed_invite_code_state_or = Some(ParsedInviteCodeState { + invite_code: invite_code.clone(), + loadable_federation_config: Loadable::Loading, + }); + + Task::future(async { + match fedimint_api_client::download_from_invite_code(&invite_code).await { + Ok(config) => app::Message::Routes(routes::Message::BitcoinWalletPage( + super::Message::Add(Message::LoadedFederationConfigFromInviteCode { + invite_code, + config, + }), + )), + Err(_err) => app::Message::Routes(routes::Message::BitcoinWalletPage( + super::Message::Add( + Message::FailedToLoadFederationConfigFromInviteCode { invite_code }, + ), + )), + } + }) + } + Message::LoadedFederationConfigFromInviteCode { + invite_code, + config, + } => { + if let Some(ParsedInviteCodeState { + invite_code: parsed_invite_code, + loadable_federation_config, + }) = &mut self.parsed_invite_code_state_or + { + // If the invite code has changed since the request was made, ignore the response. + if &invite_code == parsed_invite_code { + *loadable_federation_config = Loadable::Loaded(config); + } + } + + Task::none() + } + Message::FailedToLoadFederationConfigFromInviteCode { invite_code } => { + if let Some(ParsedInviteCodeState { + invite_code: parsed_invite_code, + loadable_federation_config, + }) = &mut self.parsed_invite_code_state_or + { + // If the invite code has changed since the request was made, ignore the response. + // Also only update the state if the user attempted to load the config. + if &invite_code == parsed_invite_code + && matches!(loadable_federation_config, Loadable::Loading) + { + *loadable_federation_config = Loadable::Failed; + } + } + + // TODO: Show toast instead of returning an empty task. + Task::none() + } + Message::JoinFederation(invite_code) => { + let wallet = self.wallet.clone(); + + Task::stream(async_stream::stream! { + match wallet.join_federation(invite_code.clone()).await { + Ok(()) => { + yield app::Message::AddToast(Toast { + title: "Joined federation".to_string(), + body: "You have successfully joined the federation.".to_string(), + status: ToastStatus::Good, + }); + + yield app::Message::Routes(routes::Message::BitcoinWalletPage(super::Message::Add( + Message::JoinedFederation(invite_code) + ))); + } + Err(err) => { + yield app::Message::AddToast(Toast { + title: "Failed to join federation".to_string(), + body: format!("Failed to join the federation: {err}"), + status: ToastStatus::Bad, + }); + } + } + }) + } + Message::JoinedFederation(invite_code) => { + // If the invite code matches the one that was just joined, navigate back to the `List` page. + if let Some(invite_code_state) = &self.parsed_invite_code_state_or { + if invite_code_state.invite_code == invite_code { + return Task::done(app::Message::Routes(routes::Message::Navigate( + routes::RouteName::BitcoinWallet(super::SubrouteName::List), + ))); + } + } + + Task::none() + } + } + } + + pub fn view<'a>(&self) -> Column<'a, app::Message> { + let mut container = container("Join Federation") + .push( + text_input("Federation Invite Code", &self.invite_code_input) + .on_input(|input| { + app::Message::Routes(routes::Message::BitcoinWalletPage( + super::Message::Add(Message::InviteCodeInputChanged(input)), + )) + }) + .padding(10) + .size(30), + ) + .push( + icon_button("Join Federation", SvgIcon::Groups, PaletteColor::Primary) + .on_press_maybe(self.parsed_invite_code_state_or.as_ref().map( + |parsed_federation_invite_code_state| { + app::Message::Routes(routes::Message::BitcoinWalletPage( + super::Message::Add(Message::JoinFederation( + parsed_federation_invite_code_state.invite_code.clone(), + )), + )) + }, + )), + ); + + if let Some(parsed_federation_invite_code_state) = &self.parsed_invite_code_state_or { + container = container + .push(Text::new("Federation ID").size(25)) + .push(Text::new(truncate_text( + &parsed_federation_invite_code_state + .invite_code + .federation_id() + .to_string(), + 21, + true, + ))); + + match &parsed_federation_invite_code_state.loadable_federation_config { + Loadable::Loading => { + container = container.push(Text::new("Loading...")); + } + Loadable::Loaded(client_config) => { + container = container + .push(Text::new("Federation Name").size(25)) + .push(Text::new( + client_config + .meta::(META_FEDERATION_NAME_KEY) + .ok() + .flatten() + .unwrap_or_default(), + )) + .push(Text::new("Modules").size(25)) + .push(Text::new( + client_config + .modules + .values() + .map(|module| module.kind().to_string()) + .collect::>() + .join(", "), + )) + .push(Text::new("Guardians").size(25)); + for peer_url in client_config.global.api_endpoints.values() { + container = container + .push(Text::new(format!("{} ({})", peer_url.name, peer_url.url))); + } + } + Loadable::Failed => { + container = container.push(Text::new("Failed to load client config")); + } + } + } + + container = container.push( + icon_button("Back", SvgIcon::ArrowBack, PaletteColor::Background).on_press( + app::Message::Routes(routes::Message::Navigate(super::RouteName::BitcoinWallet( + super::SubrouteName::List, + ))), + ), + ); + + container + } +}