diff --git a/gui/src/app/state/settings/bitcoind.rs b/gui/src/app/state/settings/bitcoind.rs index 03f0a2a7..b2f02c9c 100644 --- a/gui/src/app/state/settings/bitcoind.rs +++ b/gui/src/app/state/settings/bitcoind.rs @@ -9,7 +9,9 @@ use iced::Command; use tracing::info; use liana::{ - config::{BitcoinBackend, BitcoinConfig, BitcoindConfig, BitcoindRpcAuth, Config}, + config::{ + BitcoinBackend, BitcoinConfig, BitcoindConfig, BitcoindRpcAuth, Config, ElectrumConfig, + }, miniscript::bitcoin::Network, }; @@ -27,6 +29,7 @@ pub struct BitcoindSettingsState { config_updated: bool, node_settings: Option, + electrum_settings: Option, rescan_settings: RescanSetting, } @@ -37,19 +40,19 @@ impl BitcoindSettingsState { daemon_is_external: bool, bitcoind_is_internal: bool, ) -> Self { - let bitcoind_config = if let Some(BitcoinBackend::Bitcoind(bitcoind_config)) = - config.clone().and_then(|c| c.bitcoin_backend) - { - Some(bitcoind_config) - } else { - None - }; + let (bitcoind_config, electrum_config) = + match config.clone().and_then(|c| c.bitcoin_backend) { + Some(BitcoinBackend::Bitcoind(bitcoind_config)) => (Some(bitcoind_config), None), + Some(BitcoinBackend::Electrum(electrum_config)) => (None, Some(electrum_config)), + _ => (None, None), + }; BitcoindSettingsState { warning: None, config_updated: false, node_settings: bitcoind_config.map(|bitcoind_config| { BitcoindSettings::new( config + .clone() .expect("config must exist if bitcoind_config exists") .bitcoin_config, bitcoind_config, @@ -57,6 +60,15 @@ impl BitcoindSettingsState { bitcoind_is_internal, ) }), + electrum_settings: electrum_config.map(|electrum_config| { + ElectrumSettings::new( + config + .expect("config must exist if electrum_config exists") + .bitcoin_config, + electrum_config, + daemon_is_external, + ) + }), rescan_settings: RescanSetting::new(cache.rescan_progress), } } @@ -82,6 +94,14 @@ impl State for BitcoindSettingsState { )) }); } + if let Some(settings) = &mut self.electrum_settings { + settings.edited(true); + return Command::perform(async {}, |_| { + Message::View(view::Message::Settings( + view::SettingsMessage::EditBitcoindSettings, + )) + }); + } } Err(e) => { self.config_updated = false; @@ -110,6 +130,13 @@ impl State for BitcoindSettingsState { return settings.update(daemon, cache, msg); } } + Message::View(view::Message::Settings(view::SettingsMessage::ElectrumSettings( + msg, + ))) => { + if let Some(settings) = &mut self.electrum_settings { + return settings.update(daemon, cache, msg); + } + } Message::View(view::Message::Settings(view::SettingsMessage::RescanSettings(msg))) => { return self.rescan_settings.update(daemon, cache, msg); } @@ -121,8 +148,17 @@ impl State for BitcoindSettingsState { fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> { let can_edit_bitcoind_settings = self.node_settings.is_some() && !self.rescan_settings.processing; - let can_do_rescan = !self.rescan_settings.processing - && self.node_settings.as_ref().map(|settings| settings.edit) == Some(false); + let can_edit_electrum_settings = + self.electrum_settings.is_some() && !self.rescan_settings.processing; + let settings_edit = self + .node_settings + .as_ref() + .map(|settings| settings.edit) + .or(self + .electrum_settings + .as_ref() + .map(|settings| settings.edit)); + let can_do_rescan = !self.rescan_settings.processing && settings_edit == Some(false); view::settings::bitcoind_settings( cache, self.warning.as_ref(), @@ -139,6 +175,19 @@ impl State for BitcoindSettingsState { view::Message::Settings(view::SettingsMessage::RescanSettings(msg)) }), ] + } else if let Some(settings) = &self.electrum_settings { + vec![ + settings + .view(cache, can_edit_electrum_settings) + .map(move |msg| { + view::Message::Settings(view::SettingsMessage::ElectrumSettings(msg)) + }), + self.rescan_settings + .view(cache, can_do_rescan) + .map(move |msg| { + view::Message::Settings(view::SettingsMessage::RescanSettings(msg)) + }), + ] } else { vec![self .rescan_settings @@ -321,6 +370,105 @@ impl BitcoindSettings { } } +#[derive(Debug)] +pub struct ElectrumSettings { + electrum_config: ElectrumConfig, + bitcoin_config: BitcoinConfig, + edit: bool, + processing: bool, + addr: form::Value, + daemon_is_external: bool, +} + +impl ElectrumSettings { + fn new( + bitcoin_config: BitcoinConfig, + electrum_config: ElectrumConfig, + daemon_is_external: bool, + ) -> ElectrumSettings { + let addr = electrum_config.addr.to_string(); + ElectrumSettings { + daemon_is_external, + electrum_config, + bitcoin_config, + edit: false, + processing: false, + addr: form::Value { + valid: true, + value: addr, + }, + } + } +} + +impl ElectrumSettings { + fn edited(&mut self, success: bool) { + self.processing = false; + if success { + self.edit = false; + } + } + + fn update( + &mut self, + daemon: Arc, + _cache: &Cache, + message: view::SettingsEditMessage, + ) -> Command { + match message { + view::SettingsEditMessage::Select => { + if !self.processing { + self.edit = true; + } + } + view::SettingsEditMessage::Cancel => { + if !self.processing { + self.edit = false; + } + } + view::SettingsEditMessage::FieldEdited(field, value) => { + if !self.processing && field == "address" { + self.addr.value = value + } + } + view::SettingsEditMessage::Confirm => { + if self.addr.valid { + let mut daemon_config = daemon.config().cloned().unwrap(); + daemon_config.bitcoin_backend = + Some(liana::config::BitcoinBackend::Electrum(ElectrumConfig { + addr: self.addr.value.clone(), + })); + self.processing = true; + return Command::perform(async move { daemon_config }, |cfg| { + Message::LoadDaemonConfig(Box::new(cfg)) + }); + } + } + _ => {} + }; + Command::none() + } + + fn view<'a>(&self, cache: &'a Cache, can_edit: bool) -> Element<'a, view::SettingsEditMessage> { + if self.edit { + view::settings::electrum_edit( + self.bitcoin_config.network, + cache.blockheight, + &self.addr, + self.processing, + ) + } else { + view::settings::electrum( + self.bitcoin_config.network, + &self.electrum_config, + cache.blockheight, + Some(cache.blockheight != 0), + can_edit && !self.daemon_is_external, + ) + } + } +} + #[derive(Debug, Default)] pub struct RescanSetting { processing: bool, diff --git a/gui/src/app/view/message.rs b/gui/src/app/view/message.rs index 78118ff8..c726b5fc 100644 --- a/gui/src/app/view/message.rs +++ b/gui/src/app/view/message.rs @@ -67,6 +67,7 @@ pub enum SpendTxMessage { pub enum SettingsMessage { EditBitcoindSettings, BitcoindSettings(SettingsEditMessage), + ElectrumSettings(SettingsEditMessage), RescanSettings(SettingsEditMessage), EditRemoteBackendSettings, RemoteBackendSettings(RemoteBackendSettingsMessage), diff --git a/gui/src/app/view/settings.rs b/gui/src/app/view/settings.rs index 893d4077..b6b310b5 100644 --- a/gui/src/app/view/settings.rs +++ b/gui/src/app/view/settings.rs @@ -532,6 +532,173 @@ pub fn bitcoind<'a>( .into() } +pub fn electrum_edit<'a>( + network: Network, + blockheight: i32, + addr: &form::Value, + processing: bool, +) -> Element<'a, SettingsEditMessage> { + let mut col = Column::new().spacing(20); + if blockheight != 0 { + col = col + .push( + Row::new() + .push( + Row::new() + .push(badge::Badge::new(icon::network_icon())) + .push( + Column::new() + .push(text("Network:")) + .push(text(network.to_string()).bold()), + ) + .spacing(10) + .width(Length::FillPortion(1)), + ) + .push( + Row::new() + .push(badge::Badge::new(icon::block_icon())) + .push( + Column::new() + .push(text("Block Height:")) + .push(text(blockheight.to_string()).bold()), + ) + .spacing(10) + .width(Length::FillPortion(1)), + ), + ) + .push(separation().width(Length::Fill)); + } + + col = col.push( + Column::new() + .push(text("Address:").bold().small()) + .push( + form::Form::new_trimmed("127:0.0.1:50001", addr, |value| { + SettingsEditMessage::FieldEdited("address", value) + }) + .warning("Please enter a valid address") + .size(P1_SIZE) + .padding(5), + ) + .spacing(5), + ); + + let mut cancel_button = button::transparent(None, " Cancel ").padding(5); + let mut confirm_button = button::primary(None, " Save ").padding(5); + if !processing { + cancel_button = cancel_button.on_press(SettingsEditMessage::Cancel); + confirm_button = confirm_button.on_press(SettingsEditMessage::Confirm); + } + + card::simple(Container::new( + Column::new() + .push( + Row::new() + .push(badge::Badge::new(icon::bitcoin_icon())) + .push(text("Electrum").bold()) + .padding(10) + .spacing(20) + .align_items(Alignment::Center) + .width(Length::Fill), + ) + .push(separation().width(Length::Fill)) + .push(col) + .push( + Container::new( + Row::new() + .push(cancel_button) + .push(confirm_button) + .spacing(10) + .align_items(Alignment::Center), + ) + .width(Length::Fill) + .align_x(alignment::Horizontal::Right), + ) + .spacing(20), + )) + .width(Length::Fill) + .into() +} + +pub fn electrum<'a>( + network: Network, + config: &liana::config::ElectrumConfig, + blockheight: i32, + is_running: Option, + can_edit: bool, +) -> Element<'a, SettingsEditMessage> { + let mut col = Column::new().spacing(20); + if blockheight != 0 { + col = col + .push( + Row::new() + .push( + Row::new() + .push(badge::Badge::new(icon::network_icon())) + .push( + Column::new() + .push(text("Network:")) + .push(text(network.to_string()).bold()), + ) + .spacing(10) + .width(Length::FillPortion(1)), + ) + .push( + Row::new() + .push(badge::Badge::new(icon::block_icon())) + .push( + Column::new() + .push(text("Block Height:")) + .push(text(blockheight.to_string()).bold()), + ) + .spacing(10) + .width(Length::FillPortion(1)), + ), + ) + .push(separation().width(Length::Fill)); + } + + let rows = vec![("Address:", config.addr.to_string())]; + + let mut col_fields = Column::new(); + for (k, v) in rows { + col_fields = col_fields.push( + Row::new() + .push(Container::new(text(k).bold().small()).width(Length::Fill)) + .push(text(v).small()), + ); + } + + card::simple(Container::new( + Column::new() + .push( + Row::new() + .push( + Row::new() + .push(badge::Badge::new(icon::bitcoin_icon())) + .push(text("Electrum").bold()) + .push(is_running_label(is_running)) + .spacing(20) + .align_items(Alignment::Center) + .width(Length::Fill), + ) + .push(if can_edit { + Button::new(icon::pencil_icon()) + .style(theme::Button::TransparentBorder) + .on_press(SettingsEditMessage::Select) + } else { + Button::new(icon::pencil_icon()).style(theme::Button::TransparentBorder) + }) + .align_items(Alignment::Center), + ) + .push(separation().width(Length::Fill)) + .push(col.push(col_fields)) + .spacing(20), + )) + .width(Length::Fill) + .into() +} + pub fn is_running_label<'a, T: 'a>(is_running: Option) -> Container<'a, T> { if let Some(running) = is_running { if running {