From 392cdc818324562aa33396826088f12079136a69 Mon Sep 17 00:00:00 2001 From: edouard Date: Wed, 16 Feb 2022 22:55:01 +0100 Subject: [PATCH 1/2] Make config editable in settings panel close #322 --- Cargo.lock | 2 +- src/app/context.rs | 48 +- src/app/message.rs | 20 +- src/app/mod.rs | 9 +- src/app/state/settings.rs | 699 ++++++++++++++++++++++++++++- src/app/view/mod.rs | 2 +- src/app/view/settings.rs | 775 +++++++++++++++++++++++++++++++++ src/app/view/settings/boxes.rs | 385 ---------------- src/app/view/settings/mod.rs | 69 --- tests/app.rs | 8 +- tests/app_revault.rs | 6 +- tests/app_stakeholder.rs | 8 +- ui/src/icon.rs | 4 + 13 files changed, 1542 insertions(+), 493 deletions(-) create mode 100644 src/app/view/settings.rs delete mode 100644 src/app/view/settings/boxes.rs delete mode 100644 src/app/view/settings/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 4ea41cf5..e892b5bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2596,7 +2596,7 @@ dependencies = [ [[package]] name = "revaultd" version = "0.3.1" -source = "git+https://github.com/revault/revaultd?branch=master#843e513e13c8fe4f6d90fa735e7c92f51315f1b4" +source = "git+https://github.com/revault/revaultd?branch=master#b08ee27076e2dfdb355bbf03934b6910909317af" dependencies = [ "backtrace", "base64", diff --git a/src/app/context.rs b/src/app/context.rs index 500ef62a..a84cb65f 100644 --- a/src/app/context.rs +++ b/src/app/context.rs @@ -1,16 +1,23 @@ +use std::fs::OpenOptions; use std::future::Future; +use std::io::Write; use std::pin::Pin; use std::sync::Arc; use bitcoin::util::psbt::PartiallySignedTransaction as Psbt; -use super::menu::Menu; -use crate::{app::config, conversion::Converter, daemon::Daemon, revault::Role}; use revaultd::config::Config as DaemonConfig; use revaultd::revault_tx::miniscript::DescriptorPublicKey; use revault_hwi::{app::revault::RevaultHWI, HWIError}; +use crate::{ + app::{config, error::Error, menu::Menu}, + conversion::Converter, + daemon::{embedded::EmbeddedDaemon, Daemon}, + revault::Role, +}; + pub type HardwareWallet = Box, HWIError>> + Send + Sync>; @@ -113,6 +120,43 @@ impl Context { false } } + + pub fn load_daemon_config(&mut self, cfg: DaemonConfig) -> Result<(), Error> { + let mut daemon = EmbeddedDaemon::new(); + daemon.start(cfg.clone())?; + + let mut old_daemon = self.revaultd.clone(); + self.revaultd = Arc::new(daemon); + + self.config.daemon = cfg; + + let mut daemon_config_file = OpenOptions::new() + .write(true) + .open(&self.config.gui.revaultd_config_path) + .map_err(|e| Error::ConfigError(e.to_string()))?; + + let content = + toml::to_string(&self.config.daemon).map_err(|e| Error::ConfigError(e.to_string()))?; + + daemon_config_file + .write_all(content.as_bytes()) + .map_err(|e| { + log::warn!("failed to write to file: {:?}", e); + Error::ConfigError(e.to_string()) + })?; + + loop { + match Arc::get_mut(&mut old_daemon) { + None => {} + Some(old) => { + old.stop()?; + break; + } + } + } + + Ok(()) + } } pub struct ConfigContext { diff --git a/src/app/message.rs b/src/app/message.rs index 4a1018a2..34e6e155 100644 --- a/src/app/message.rs +++ b/src/app/message.rs @@ -1,8 +1,11 @@ -use bitcoin::{util::psbt::PartiallySignedTransaction as Psbt, OutPoint}; -use revault_hwi::{app::revault::RevaultHWI, HWIError}; use std::sync::Arc; + +use bitcoin::{util::psbt::PartiallySignedTransaction as Psbt, OutPoint}; use tokio::sync::Mutex; +use revault_hwi::{app::revault::RevaultHWI, HWIError}; +use revaultd::config::Config as DaemonConfig; + use crate::{ app::{error::Error, menu::Menu}, daemon::{ @@ -58,6 +61,19 @@ pub enum Message { Close, Revault, Revaulted(Result<(), RevaultDError>), + Settings(usize, SettingsMessage), + AddWatchtower, + LoadDaemonConfig(DaemonConfig), + DaemonConfigLoaded(Result<(), Error>), +} + +#[derive(Debug, Clone)] +pub enum SettingsMessage { + Remove, + Edit, + FieldEdited(&'static str, String), + CancelEdit, + ConfirmEdit, } #[derive(Debug, Clone)] diff --git a/src/app/mod.rs b/src/app/mod.rs index 60df1c07..fd7cd55f 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -14,7 +14,7 @@ use iced::{time, Clipboard, Command, Element, Subscription}; use iced_native::{window, Event}; pub use config::Config; -pub use message::Message; +pub use message::{Message, SettingsMessage}; use menu::Menu; use state::{ @@ -31,14 +31,13 @@ pub struct App { context: Context, } -#[allow(unreachable_patterns)] pub fn new_state(context: &Context) -> Box { match (context.role, &context.menu) { (_, Menu::Deposit) => DepositState::new().into(), (_, Menu::History) => HistoryState::new().into(), (_, Menu::Vaults(menu)) => VaultsState::new(menu).into(), (_, Menu::RevaultVaults) => RevaultVaultsState::default().into(), - (_, Menu::Settings) => SettingsState::new(context.config.gui.clone()).into(), + (_, Menu::Settings) => SettingsState::new(context).into(), (Role::Stakeholder, Menu::Home) => StakeholderHomeState::new().into(), (Role::Stakeholder, Menu::CreateVaults) => StakeholderCreateVaultsState::new().into(), (Role::Stakeholder, Menu::DelegateFunds) => StakeholderDelegateVaultsState::new().into(), @@ -107,6 +106,10 @@ impl App { } Command::none() } + Message::LoadDaemonConfig(cfg) => { + let res = self.context.load_daemon_config(cfg); + self.update(Message::DaemonConfigLoaded(res), clipboard) + } Message::ChangeRole(role) => { self.context.role = role; self.state = new_state(&self.context); diff --git a/src/app/state/settings.rs b/src/app/state/settings.rs index f1148f6b..1d021d76 100644 --- a/src/app/state/settings.rs +++ b/src/app/state/settings.rs @@ -1,57 +1,165 @@ use std::convert::From; +use std::net::SocketAddr; +use std::path::PathBuf; +use std::str::FromStr; use iced::{Command, Element}; -use super::State; +use bitcoin::hashes::hex::{FromHex, ToHex}; +use revault_ui::component::form; +use revaultd::{config::WatchtowerConfig, revault_net::noise::PublicKey as NoisePubkey}; -use crate::app::config::Config; - -use crate::app::{ - context::Context, error::Error, message::Message, state::cmd::get_server_status, - view::SettingsView, +use crate::{ + app::{ + context::Context, + error::Error, + message::{Message, SettingsMessage}, + state::cmd::get_server_status, + state::State, + view::settings::*, + }, + daemon::model::ServersStatuses, + revault::Role, }; -use crate::daemon::model::ServersStatuses; +trait Setting: std::fmt::Debug { + fn edited(&mut self, success: bool); + fn update(&mut self, ctx: &Context, message: SettingsMessage) -> Command; + fn view( + &mut self, + ctx: &Context, + statuses: &Option, + can_edit: bool, + ) -> Element; +} #[derive(Debug)] pub struct SettingsState { - _config: Config, warning: Option, view: SettingsView, - server_status: Option, + server_statuses: Option, + config_updated: bool, + + settings: Vec>, + current: Option, } impl SettingsState { - pub fn new(config: Config) -> Self { + pub fn new(ctx: &Context) -> Self { + let mut settings = vec![ + BitcoindSettings::default().into(), + CoordinatorSettings::default().into(), + ]; + + if ctx.role == Role::Stakeholder { + if let Some(cfg) = &ctx.config.daemon.stakeholder_config { + for i in 0..cfg.watchtowers.len() { + settings.push(WatchtowerSettings::new(i).into()); + } + } + } else { + if let Some(cfg) = &ctx.config.daemon.manager_config { + for i in 0..cfg.cosigners.len() { + settings.push(CosignerSettings::new(i).into()); + } + } + } SettingsState { - _config: config, view: SettingsView::new(), warning: None, - server_status: None, + server_statuses: None, + config_updated: false, + settings, + current: None, } } } impl State for SettingsState { - fn update(&mut self, _ctx: &Context, message: Message) -> Command { + fn update(&mut self, ctx: &Context, message: Message) -> Command { match message { Message::ServerStatus(s) => { match s { - Ok(server_status) => self.server_status = Some(server_status), + Ok(statuses) => self.server_statuses = Some(statuses), Err(e) => self.warning = Error::from(e).into(), }; - Command::none() } - _ => Command::none(), - } + Message::DaemonConfigLoaded(res) => match res { + Ok(()) => { + self.config_updated = true; + if let Some(current) = self.current { + if let Some(setting) = self.settings.get_mut(current) { + setting.edited(true) + } + } + self.current = None; + return Command::perform( + get_server_status(ctx.revaultd.clone()), + Message::ServerStatus, + ); + } + Err(e) => { + self.config_updated = false; + self.warning = Some(e); + if let Some(current) = self.current { + if let Some(setting) = self.settings.get_mut(current) { + setting.edited(false); + } + } + } + }, + Message::Settings(i, SettingsMessage::Remove) => { + if Some(i) == self.current { + self.current = None; + } + self.settings.remove(i); + } + Message::Settings(i, msg) => { + if let Some(setting) = self.settings.get_mut(i) { + match msg { + SettingsMessage::Edit => self.current = Some(i), + SettingsMessage::CancelEdit => self.current = None, + _ => {} + }; + return setting.update(ctx, msg); + } + } + Message::AddWatchtower => { + if ctx.role == Role::Stakeholder { + self.settings.push( + WatchtowerSettings::Edit { + index: self.settings.len() - 2, + processing: false, + key: form::Value::default(), + host: form::Value::default(), + view: WatchtowerSettingsEditView::default(), + } + .into(), + ); + self.current = Some(self.settings.len() - 1); + } + } + _ => {} + }; + Command::none() } fn view(&mut self, ctx: &Context) -> Element { + let server_statuses = &self.server_statuses; + let can_edit = self.current.is_none() && !ctx.revaultd.is_external(); self.view.view( ctx, self.warning.as_ref(), - &ctx.config.daemon, - self.server_status.clone(), + can_edit, + self.settings + .iter_mut() + .enumerate() + .map(|(i, setting)| { + setting + .view(ctx, server_statuses, can_edit) + .map(move |msg| Message::Settings(i, msg)) + }) + .collect(), ) } @@ -68,3 +176,556 @@ impl From for Box { Box::new(s) } } + +#[derive(Debug)] +pub enum BitcoindSettings { + Display(BitcoindSettingsView), + Edit { + processing: bool, + cookie_path: form::Value, + addr: form::Value, + view: BitcoindSettingsEditView, + }, +} + +impl Default for BitcoindSettings { + fn default() -> Self { + Self::Display(BitcoindSettingsView::default()) + } +} + +impl From for Box { + fn from(s: BitcoindSettings) -> Box { + Box::new(s) + } +} + +impl Setting for BitcoindSettings { + fn edited(&mut self, success: bool) { + if success { + *self = Self::default(); + } else { + if let Self::Edit { processing, .. } = self { + *processing = false; + } + } + } + + fn update(&mut self, ctx: &Context, message: SettingsMessage) -> Command { + if matches!(message, SettingsMessage::Edit) { + *self = Self::Edit { + processing: false, + cookie_path: form::Value { + valid: true, + value: ctx + .config + .daemon + .bitcoind_config + .cookie_path + .to_str() + .unwrap() + .to_string(), + }, + addr: form::Value { + valid: true, + value: ctx.config.daemon.bitcoind_config.addr.to_string(), + }, + view: BitcoindSettingsEditView::default(), + }; + } + if let Self::Edit { + addr, + cookie_path, + processing, + .. + } = self + { + match message { + SettingsMessage::Edit | SettingsMessage::Remove => {} + SettingsMessage::CancelEdit => { + if !*processing { + *self = Self::default(); + } + } + SettingsMessage::FieldEdited(field, value) => { + if !*processing { + match field { + "socket_address" => addr.value = value, + "cookie_file_path" => cookie_path.value = value, + _ => {} + } + } + } + SettingsMessage::ConfirmEdit => { + let new_addr = SocketAddr::from_str(&addr.value); + addr.valid = new_addr.is_ok(); + let new_path = PathBuf::from_str(&cookie_path.value); + cookie_path.valid = new_path.is_ok(); + + if addr.valid & cookie_path.valid { + let mut daemon_config = ctx.config.daemon.clone(); + daemon_config.bitcoind_config.cookie_path = new_path.unwrap(); + daemon_config.bitcoind_config.addr = new_addr.unwrap(); + *processing = true; + return Command::perform(async move { daemon_config }, |cfg| { + Message::LoadDaemonConfig(cfg) + }); + } + } + }; + } + Command::none() + } + + fn view( + &mut self, + ctx: &Context, + _statuses: &Option, + can_edit: bool, + ) -> Element { + match self { + Self::Display(v) => v.view( + &ctx.config.daemon.bitcoind_config, + ctx.blockheight, + Some(ctx.blockheight != 0), + can_edit, + ), + Self::Edit { + view, + addr, + cookie_path, + processing, + } => view.view( + &ctx.config.daemon.bitcoind_config, + ctx.blockheight, + &addr, + &cookie_path, + *processing, + ), + } + } +} + +#[derive(Debug)] +pub enum CoordinatorSettings { + Display(CoordinatorSettingsView), + Edit { + processing: bool, + host: form::Value, + key: form::Value, + view: CoordinatorSettingsEditView, + }, +} + +impl Default for CoordinatorSettings { + fn default() -> Self { + Self::Display(CoordinatorSettingsView::default()) + } +} + +impl From for Box { + fn from(s: CoordinatorSettings) -> Box { + Box::new(s) + } +} + +impl Setting for CoordinatorSettings { + fn edited(&mut self, success: bool) { + if success { + *self = Self::default(); + } else { + if let Self::Edit { processing, .. } = self { + *processing = false; + } + } + } + + fn update(&mut self, ctx: &Context, message: SettingsMessage) -> Command { + if matches!(message, SettingsMessage::Edit) { + *self = Self::Edit { + processing: false, + key: form::Value { + valid: true, + value: ctx.config.daemon.coordinator_noise_key.as_ref().to_hex(), + }, + host: form::Value { + valid: true, + value: ctx.config.daemon.coordinator_host.to_string(), + }, + view: CoordinatorSettingsEditView::default(), + }; + } + if let Self::Edit { + host, + key, + processing, + .. + } = self + { + match message { + SettingsMessage::Edit | SettingsMessage::Remove => {} + SettingsMessage::CancelEdit => { + if !*processing { + *self = Self::default(); + } + } + SettingsMessage::FieldEdited(field, value) => { + if !*processing { + match field { + "host" => host.value = value, + "key" => key.value = value, + _ => {} + } + } + } + SettingsMessage::ConfirmEdit => { + let new_host = SocketAddr::from_str(&host.value); + host.valid = new_host.is_ok(); + let new_key: Option = + FromHex::from_hex(&key.value).map(NoisePubkey).ok(); + key.valid = new_key.is_some(); + + if host.valid & key.valid { + let mut daemon_config = ctx.config.daemon.clone(); + daemon_config.coordinator_host = new_host.unwrap(); + daemon_config.coordinator_noise_key = new_key.unwrap(); + *processing = true; + return Command::perform(async move { daemon_config }, |cfg| { + Message::LoadDaemonConfig(cfg) + }); + } + } + }; + } + Command::none() + } + + fn view( + &mut self, + ctx: &Context, + statuses: &Option, + can_edit: bool, + ) -> Element { + match self { + Self::Display(v) => v.view( + &ctx.config.daemon.coordinator_host.to_string(), + &ctx.config.daemon.coordinator_noise_key.as_ref().to_hex(), + statuses.as_ref().map(|s| s.coordinator.reachable), + can_edit, + ), + Self::Edit { + view, + host, + key, + processing, + } => view.view(&host, &key, *processing), + } + } +} + +#[derive(Debug)] +pub enum WatchtowerSettings { + Display(usize, WatchtowerSettingsView), + Edit { + index: usize, + processing: bool, + host: form::Value, + key: form::Value, + view: WatchtowerSettingsEditView, + }, +} + +impl WatchtowerSettings { + fn new(index: usize) -> Self { + Self::Display(index, WatchtowerSettingsView::default()) + } + + fn index(&self) -> usize { + match self { + Self::Display(i, _) => *i, + Self::Edit { index, .. } => *index, + } + } +} + +impl Setting for WatchtowerSettings { + fn edited(&mut self, success: bool) { + if success { + *self = Self::new(self.index()); + } else { + if let Self::Edit { processing, .. } = self { + *processing = false; + } + } + } + + fn update(&mut self, ctx: &Context, message: SettingsMessage) -> Command { + if matches!(message, SettingsMessage::Edit) { + *self = Self::Edit { + index: self.index(), + processing: false, + key: form::Value { + valid: true, + value: ctx.config.daemon.coordinator_noise_key.as_ref().to_hex(), + }, + host: form::Value { + valid: true, + value: ctx.config.daemon.coordinator_host.to_string(), + }, + view: WatchtowerSettingsEditView::default(), + }; + } + if let Self::Edit { + host, + key, + processing, + index, + .. + } = self + { + match message { + SettingsMessage::Edit | SettingsMessage::Remove => {} + SettingsMessage::CancelEdit => { + if !*processing { + *self = Self::new(self.index()); + } + } + SettingsMessage::FieldEdited(field, value) => { + if !*processing { + match field { + "host" => host.value = value, + "key" => key.value = value, + _ => {} + } + } + } + SettingsMessage::ConfirmEdit => { + let new_host = SocketAddr::from_str(&host.value); + host.valid = new_host.is_ok(); + let new_key: Option = + FromHex::from_hex(&key.value).map(NoisePubkey).ok(); + key.valid = new_key.is_some(); + + if host.valid & key.valid { + let mut stakeholder_config = + ctx.config.daemon.stakeholder_config.clone().unwrap(); + if let Some(wt) = stakeholder_config.watchtowers.get_mut(*index) { + wt.host = new_host.unwrap(); + wt.noise_key = new_key.unwrap(); + } else { + stakeholder_config.watchtowers.push(WatchtowerConfig { + host: new_host.unwrap(), + noise_key: new_key.unwrap(), + }) + } + let mut daemon_config = ctx.config.daemon.clone(); + daemon_config.stakeholder_config = Some(stakeholder_config); + *processing = true; + return Command::perform(async move { daemon_config }, |cfg| { + Message::LoadDaemonConfig(cfg) + }); + } + } + }; + } + Command::none() + } + + fn view( + &mut self, + ctx: &Context, + statuses: &Option, + can_edit: bool, + ) -> Element { + match self { + Self::Display(i, v) => { + let wt = ctx + .config + .daemon + .stakeholder_config + .as_ref() + .unwrap() + .watchtowers + .get(*i) + .unwrap(); + v.view( + &wt.host.to_string(), + &wt.noise_key.as_ref().to_hex(), + statuses + .as_ref() + .map(|s| s.watchtowers.get(*i).map(|r| r.reachable).unwrap_or(false)), + can_edit, + ) + } + Self::Edit { + view, + host, + key, + processing, + index, + } => { + let not_saved = ctx + .config + .daemon + .stakeholder_config + .as_ref() + .unwrap() + .watchtowers + .get(*index) + .is_none(); + view.view(not_saved, &host, &key, *processing) + } + } + } +} + +impl From for Box { + fn from(s: WatchtowerSettings) -> Box { + Box::new(s) + } +} + +#[derive(Debug)] +pub enum CosignerSettings { + Display(usize, CosignerSettingsView), + Edit { + index: usize, + processing: bool, + host: form::Value, + key: form::Value, + view: CosignerSettingsEditView, + }, +} + +impl CosignerSettings { + fn new(index: usize) -> Self { + Self::Display(index, CosignerSettingsView::default()) + } + + fn index(&self) -> usize { + match self { + Self::Display(i, _) => *i, + Self::Edit { index, .. } => *index, + } + } +} + +impl Setting for CosignerSettings { + fn edited(&mut self, success: bool) { + if success { + *self = Self::new(self.index()); + } else { + if let Self::Edit { processing, .. } = self { + *processing = false; + } + } + } + + fn update(&mut self, ctx: &Context, message: SettingsMessage) -> Command { + if matches!(message, SettingsMessage::Edit) { + *self = Self::Edit { + index: self.index(), + processing: false, + key: form::Value { + valid: true, + value: ctx.config.daemon.coordinator_noise_key.as_ref().to_hex(), + }, + host: form::Value { + valid: true, + value: ctx.config.daemon.coordinator_host.to_string(), + }, + view: CosignerSettingsEditView::default(), + }; + } + if let Self::Edit { + host, + key, + processing, + index, + .. + } = self + { + match message { + SettingsMessage::Edit | SettingsMessage::Remove => {} + SettingsMessage::CancelEdit => { + if !*processing { + *self = Self::new(self.index()); + } + } + SettingsMessage::FieldEdited(field, value) => { + if !*processing { + match field { + "host" => host.value = value, + "key" => key.value = value, + _ => {} + } + } + } + SettingsMessage::ConfirmEdit => { + let new_host = SocketAddr::from_str(&host.value); + host.valid = new_host.is_ok(); + let new_key: Option = + FromHex::from_hex(&key.value).map(NoisePubkey).ok(); + key.valid = new_key.is_some(); + + if host.valid & key.valid { + let mut manager_config = ctx.config.daemon.manager_config.clone().unwrap(); + if let Some(cs) = manager_config.cosigners.get_mut(*index) { + cs.host = new_host.unwrap(); + cs.noise_key = new_key.unwrap(); + } + let mut daemon_config = ctx.config.daemon.clone(); + daemon_config.manager_config = Some(manager_config); + *processing = true; + return Command::perform(async move { daemon_config }, |cfg| { + Message::LoadDaemonConfig(cfg) + }); + } + } + }; + } + Command::none() + } + + fn view( + &mut self, + ctx: &Context, + statuses: &Option, + can_edit: bool, + ) -> Element { + match self { + Self::Display(i, v) => { + let cs = ctx + .config + .daemon + .manager_config + .as_ref() + .unwrap() + .cosigners + .get(*i) + .unwrap(); + v.view( + &cs.host.to_string(), + &cs.noise_key.as_ref().to_hex(), + statuses + .as_ref() + .map(|s| s.cosigners.get(*i).map(|r| r.reachable).unwrap_or(false)), + can_edit, + ) + } + Self::Edit { + view, + host, + key, + processing, + .. + } => view.view(&host, &key, *processing), + } + } +} + +impl From for Box { + fn from(s: CosignerSettings) -> Box { + Box::new(s) + } +} diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 3764fff6..c313c718 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -5,7 +5,7 @@ mod home; mod layout; pub mod manager; mod revault; -mod settings; +pub mod settings; mod sidebar; pub mod sign; pub mod spend_transaction; diff --git a/src/app/view/settings.rs b/src/app/view/settings.rs new file mode 100644 index 00000000..35d1ce79 --- /dev/null +++ b/src/app/view/settings.rs @@ -0,0 +1,775 @@ +use iced::{text_input, Align, Column, Container, Element, Length, Row}; + +use revault_ui::{ + color, + component::{badge, button, card, form, separation, text::Text}, + icon, +}; + +use crate::{ + app::{ + context::Context, + error::Error, + message::{Message, SettingsMessage}, + view::layout, + }, + revault::Role, +}; + +#[derive(Debug)] +pub struct SettingsView { + dashboard: layout::Dashboard, + add_watchtower_button: iced::button::State, +} + +impl SettingsView { + pub fn new() -> Self { + SettingsView { + dashboard: layout::Dashboard::new(), + add_watchtower_button: iced::button::State::default(), + } + } + + pub fn view<'a>( + &'a mut self, + ctx: &Context, + warning: Option<&Error>, + can_edit: bool, + settings: Vec>, + ) -> Element<'a, Message> { + let mut col = Column::with_children(settings).spacing(20); + if can_edit && ctx.role == Role::Stakeholder { + col = col.push( + Container::new( + button::important( + &mut self.add_watchtower_button, + button::button_content(Some(icon::plus_icon()), "Add watchtower"), + ) + .on_press(Message::AddWatchtower), + ) + .width(Length::Fill) + .align_x(Align::End), + ); + } + self.dashboard.view(ctx, warning, col) + } +} + +#[derive(Debug, Default)] +pub struct BitcoindSettingsEditView { + cancel_button: iced::button::State, + confirm_button: iced::button::State, + + addr_input: text_input::State, + cookie_path_input: text_input::State, +} + +impl BitcoindSettingsEditView { + pub fn view<'a>( + &'a mut self, + config: &revaultd::config::BitcoindConfig, + blockheight: i32, + addr: &form::Value, + cookie_path: &form::Value, + processing: bool, + ) -> Element<'a, SettingsMessage> { + let mut col = Column::new().spacing(20); + if blockheight != 0 { + col = col + .push( + Row::new() + .push( + Row::new() + .push(badge::network()) + .push( + Column::new() + .push(Text::new("Network:")) + .push(Text::new(&config.network.to_string()).bold()), + ) + .spacing(10) + .width(Length::FillPortion(1)), + ) + .push( + Row::new() + .push(badge::block()) + .push( + Column::new() + .push(Text::new("Block Height:")) + .push(Text::new(&blockheight.to_string()).bold()), + ) + .spacing(10) + .width(Length::FillPortion(1)), + ), + ) + .push(separation().width(Length::Fill)); + } + + col = col + .push( + Column::new() + .push(Text::new("Cookie file path:").bold().small()) + .push( + form::Form::new( + &mut self.cookie_path_input, + "Cookie file path", + cookie_path, + |value| SettingsMessage::FieldEdited("cookie_file_path", value), + ) + .warning("Please enter a valid filesystem path") + .size(20) + .padding(5) + .render(), + ) + .spacing(5), + ) + .push( + Column::new() + .push(Text::new("Socket address:").bold().small()) + .push( + form::Form::new(&mut self.addr_input, "Socket address:", addr, |value| { + SettingsMessage::FieldEdited("socket_address", value) + }) + .warning("Please enter a valid address") + .size(20) + .padding(5) + .render(), + ) + .spacing(5), + ); + + let mut cancel_button = button::cancel( + &mut self.cancel_button, + Container::new(Text::new(" Cancel ")).padding(5), + ); + let mut confirm_button = button::primary( + &mut self.confirm_button, + Container::new(Text::new(" Save ")).padding(5), + ); + if !processing { + cancel_button = cancel_button.on_press(SettingsMessage::CancelEdit); + confirm_button = confirm_button.on_press(SettingsMessage::ConfirmEdit); + } + + card::simple(Container::new( + Column::new() + .push( + Row::new() + .push(badge::bitcoin_core()) + .push(Text::new("Bitcoind")) + .padding(10) + .spacing(20) + .align_items(Align::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(Align::Center), + ) + .width(Length::Fill) + .align_x(Align::End), + ) + .spacing(20), + )) + .width(Length::Fill) + .into() + } +} + +#[derive(Debug, Default)] +pub struct BitcoindSettingsView { + edit_button: iced::button::State, +} + +impl BitcoindSettingsView { + pub fn view<'a>( + &'a mut self, + config: &revaultd::config::BitcoindConfig, + blockheight: i32, + is_running: Option, + can_edit: bool, + ) -> Element<'a, SettingsMessage> { + let mut col = Column::new().spacing(20); + if blockheight != 0 { + col = col + .push( + Row::new() + .push( + Row::new() + .push(badge::network()) + .push( + Column::new() + .push(Text::new("Network:")) + .push(Text::new(&config.network.to_string()).bold()), + ) + .spacing(10) + .width(Length::FillPortion(1)), + ) + .push( + Row::new() + .push(badge::block()) + .push( + Column::new() + .push(Text::new("Block Height:")) + .push(Text::new(&blockheight.to_string()).bold()), + ) + .spacing(10) + .width(Length::FillPortion(1)), + ), + ) + .push(separation().width(Length::Fill)); + } + + let rows = vec![ + ( + "Cookie file path:", + config.cookie_path.to_str().unwrap().to_string(), + ), + ("Socket address:", config.addr.to_string()), + ]; + + let mut column = Column::new(); + for (k, v) in rows { + column = column.push( + Row::new() + .push(Container::new(Text::new(k).bold().small()).width(Length::Fill)) + .push(Text::new(&v).small()), + ); + } + + card::simple(Container::new( + Column::new() + .push( + Row::new() + .push( + Row::new() + .push(badge::bitcoin_core()) + .push(Text::new("Bitcoind")) + .push(is_running_label(is_running)) + .spacing(20) + .align_items(Align::Center) + .width(Length::Fill), + ) + .push(if can_edit { + button::white_card_button( + &mut self.edit_button, + Container::new(icon::pencil_icon()), + ) + .on_press(SettingsMessage::Edit) + } else { + button::white_card_button( + &mut self.edit_button, + Container::new(icon::pencil_icon()), + ) + }) + .align_items(Align::Center), + ) + .push(separation().width(Length::Fill)) + .push(col.push(column)) + .spacing(20), + )) + .width(Length::Fill) + .into() + } +} + +#[derive(Debug, Default)] +pub struct CoordinatorSettingsEditView { + cancel_button: iced::button::State, + confirm_button: iced::button::State, + + host_input: text_input::State, + key_input: text_input::State, +} + +impl CoordinatorSettingsEditView { + pub fn view<'a>( + &'a mut self, + host: &form::Value, + key: &form::Value, + processing: bool, + ) -> Element<'a, SettingsMessage> { + let mut col = Column::new().spacing(20); + col = col + .push( + Column::new() + .push(Text::new("Host:").bold().small()) + .push( + form::Form::new(&mut self.host_input, "Host", host, |value| { + SettingsMessage::FieldEdited("host", value) + }) + .warning("Please enter a valid socket address") + .size(20) + .padding(5) + .render(), + ) + .spacing(5), + ) + .push( + Column::new() + .push(Text::new("Public key:").bold().small()) + .push( + form::Form::new(&mut self.key_input, "Key", key, |value| { + SettingsMessage::FieldEdited("key", value) + }) + .warning("Please enter a valid public noise key") + .size(20) + .padding(5) + .render(), + ) + .spacing(5), + ); + + let mut cancel_button = button::cancel( + &mut self.cancel_button, + Container::new(Text::new(" Cancel ")).padding(5), + ); + let mut confirm_button = button::primary( + &mut self.confirm_button, + Container::new(Text::new(" Save ")).padding(5), + ); + if !processing { + cancel_button = cancel_button.on_press(SettingsMessage::CancelEdit); + confirm_button = confirm_button.on_press(SettingsMessage::ConfirmEdit); + } + + card::simple(Container::new( + Column::new() + .push( + Row::new() + .push(Text::new("Coordinator")) + .padding(10) + .spacing(20) + .align_items(Align::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(Align::Center), + ) + .width(Length::Fill) + .align_x(Align::End), + ) + .spacing(20), + )) + .width(Length::Fill) + .into() + } +} + +#[derive(Debug, Default)] +pub struct CoordinatorSettingsView { + edit_button: iced::button::State, +} + +impl CoordinatorSettingsView { + pub fn view<'a>( + &'a mut self, + host: &str, + key: &str, + is_running: Option, + can_edit: bool, + ) -> Element<'a, SettingsMessage> { + let rows = vec![("Host:", host), ("Public key:", key)]; + + let mut column = Column::new(); + for (k, v) in rows { + column = column.push( + Row::new() + .push(Container::new(Text::new(k).bold().small()).width(Length::Fill)) + .push(Text::new(&v).small()), + ); + } + + card::simple(Container::new( + Column::new() + .push( + Row::new() + .push( + Row::new() + .push(Text::new("Coordinator")) + .push(is_running_label(is_running)) + .spacing(20) + .align_items(Align::Center) + .width(Length::Fill), + ) + .push(if can_edit { + button::white_card_button( + &mut self.edit_button, + Container::new(icon::pencil_icon()), + ) + .on_press(SettingsMessage::Edit) + } else { + button::white_card_button( + &mut self.edit_button, + Container::new(icon::pencil_icon()), + ) + }) + .align_items(Align::Center), + ) + .push(separation().width(Length::Fill)) + .push(column) + .spacing(20), + )) + .width(Length::Fill) + .into() + } +} + +#[derive(Debug, Default)] +pub struct WatchtowerSettingsEditView { + cancel_button: iced::button::State, + confirm_button: iced::button::State, + remove_button: iced::button::State, + + host_input: text_input::State, + key_input: text_input::State, +} + +impl WatchtowerSettingsEditView { + pub fn view<'a>( + &'a mut self, + not_saved: bool, + host: &form::Value, + key: &form::Value, + processing: bool, + ) -> Element<'a, SettingsMessage> { + let mut col = Column::new().spacing(20); + col = col + .push( + Column::new() + .push(Text::new("Host:").bold().small()) + .push( + form::Form::new(&mut self.host_input, "Host", host, |value| { + SettingsMessage::FieldEdited("host", value) + }) + .warning("Please enter a valid socket address") + .size(20) + .padding(5) + .render(), + ) + .spacing(5), + ) + .push( + Column::new() + .push(Text::new("Public key:").bold().small()) + .push( + form::Form::new(&mut self.key_input, "Key", key, |value| { + SettingsMessage::FieldEdited("key", value) + }) + .warning("Please enter a valid public noise key") + .size(20) + .padding(5) + .render(), + ) + .spacing(5), + ); + + let mut cancel_button = button::cancel( + &mut self.cancel_button, + Container::new(Text::new(" Cancel ")).padding(5), + ); + let mut confirm_button = button::primary( + &mut self.confirm_button, + Container::new(Text::new(" Save ")).padding(5), + ); + if !processing { + if not_saved { + cancel_button = cancel_button.on_press(SettingsMessage::Remove); + } else { + cancel_button = cancel_button.on_press(SettingsMessage::CancelEdit); + } + confirm_button = confirm_button.on_press(SettingsMessage::ConfirmEdit); + } + + card::simple(Container::new( + Column::new() + .push( + Row::new() + .push(Text::new("Watchtower")) + .padding(10) + .spacing(20) + .align_items(Align::Center) + .width(Length::Fill), + ) + .push(separation().width(Length::Fill)) + .push(col) + .push( + Container::new( + Row::new() + .push( + Row::new() + .push( + button::transparent( + &mut self.remove_button, + Container::new( + Row::new() + .push(icon::trash_icon().color(color::ALERT)) + .push(Text::new("Remove").color(color::ALERT)) + .spacing(10) + .align_items(Align::Center), + ), + ) + .on_press(SettingsMessage::Remove), + ) + .width(Length::Fill), + ) + .push( + Row::new() + .push(cancel_button) + .push(confirm_button) + .align_items(Align::Center) + .spacing(10), + ) + .align_items(Align::Center), + ) + .width(Length::Fill) + .align_x(Align::End), + ) + .spacing(20), + )) + .width(Length::Fill) + .into() + } +} + +#[derive(Debug, Default)] +pub struct WatchtowerSettingsView { + edit_button: iced::button::State, +} + +impl WatchtowerSettingsView { + pub fn view<'a>( + &'a mut self, + host: &str, + key: &str, + is_running: Option, + can_edit: bool, + ) -> Element<'a, SettingsMessage> { + let rows = vec![("Host:", host), ("Public key:", key)]; + + let mut column = Column::new(); + for (k, v) in rows { + column = column.push( + Row::new() + .push(Container::new(Text::new(k).bold().small()).width(Length::Fill)) + .push(Text::new(&v).small()), + ); + } + + card::simple(Container::new( + Column::new() + .push( + Row::new() + .push( + Row::new() + .push(Text::new("Watchtower")) + .push(is_running_label(is_running)) + .spacing(20) + .align_items(Align::Center) + .width(Length::Fill), + ) + .push(if can_edit { + button::white_card_button( + &mut self.edit_button, + Container::new(icon::pencil_icon()), + ) + .on_press(SettingsMessage::Edit) + } else { + button::white_card_button( + &mut self.edit_button, + Container::new(icon::pencil_icon()), + ) + }) + .align_items(Align::Center), + ) + .push(separation().width(Length::Fill)) + .push(column) + .spacing(20), + )) + .width(Length::Fill) + .into() + } +} + +#[derive(Debug, Default)] +pub struct CosignerSettingsEditView { + cancel_button: iced::button::State, + confirm_button: iced::button::State, + + host_input: text_input::State, + key_input: text_input::State, +} + +impl CosignerSettingsEditView { + pub fn view<'a>( + &'a mut self, + host: &form::Value, + key: &form::Value, + processing: bool, + ) -> Element<'a, SettingsMessage> { + let mut col = Column::new().spacing(20); + col = col + .push( + Column::new() + .push(Text::new("Host:").bold().small()) + .push( + form::Form::new(&mut self.host_input, "Host", host, |value| { + SettingsMessage::FieldEdited("host", value) + }) + .warning("Please enter a valid socket address") + .size(20) + .padding(5) + .render(), + ) + .spacing(5), + ) + .push( + Column::new() + .push(Text::new("Public key:").bold().small()) + .push( + form::Form::new(&mut self.key_input, "Key", key, |value| { + SettingsMessage::FieldEdited("key", value) + }) + .warning("Please enter a valid public noise key") + .size(20) + .padding(5) + .render(), + ) + .spacing(5), + ); + + let mut cancel_button = button::cancel( + &mut self.cancel_button, + Container::new(Text::new(" Cancel ")).padding(5), + ); + let mut confirm_button = button::primary( + &mut self.confirm_button, + Container::new(Text::new(" Save ")).padding(5), + ); + if !processing { + cancel_button = cancel_button.on_press(SettingsMessage::CancelEdit); + confirm_button = confirm_button.on_press(SettingsMessage::ConfirmEdit); + } + + card::simple(Container::new( + Column::new() + .push( + Row::new() + .push(Text::new("Cosigner")) + .padding(10) + .spacing(20) + .align_items(Align::Center) + .width(Length::Fill), + ) + .push(separation().width(Length::Fill)) + .push(col) + .push( + Container::new( + Row::new() + .push(cancel_button) + .push(confirm_button) + .align_items(Align::Center) + .spacing(10), + ) + .width(Length::Fill) + .align_x(Align::End), + ) + .spacing(20), + )) + .width(Length::Fill) + .into() + } +} + +#[derive(Debug, Default)] +pub struct CosignerSettingsView { + edit_button: iced::button::State, +} + +impl CosignerSettingsView { + pub fn view<'a>( + &'a mut self, + host: &str, + key: &str, + is_running: Option, + can_edit: bool, + ) -> Element<'a, SettingsMessage> { + let rows = vec![("Host:", host), ("Public key:", key)]; + + let mut column = Column::new(); + for (k, v) in rows { + column = column.push( + Row::new() + .push(Container::new(Text::new(k).bold().small()).width(Length::Fill)) + .push(Text::new(&v).small()), + ); + } + + card::simple(Container::new( + Column::new() + .push( + Row::new() + .push( + Row::new() + .push(Text::new("Cosigner")) + .push(is_running_label(is_running)) + .spacing(20) + .align_items(Align::Center) + .width(Length::Fill), + ) + .push(if can_edit { + button::white_card_button( + &mut self.edit_button, + Container::new(icon::pencil_icon()), + ) + .on_press(SettingsMessage::Edit) + } else { + button::white_card_button( + &mut self.edit_button, + Container::new(icon::pencil_icon()), + ) + }) + .align_items(Align::Center), + ) + .push(separation().width(Length::Fill)) + .push(column) + .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 { + Container::new( + Row::new() + .push(icon::dot_icon().size(5).color(color::SUCCESS)) + .push(Text::new("Running").small().color(color::SUCCESS)) + .align_items(iced::Align::Center), + ) + } else { + Container::new( + Row::new() + .push(icon::dot_icon().size(5).color(color::ALERT)) + .push(Text::new("Not running").small().color(color::ALERT)) + .align_items(iced::Align::Center), + ) + } + } else { + Container::new(Column::new()) + } +} diff --git a/src/app/view/settings/boxes.rs b/src/app/view/settings/boxes.rs deleted file mode 100644 index fad156eb..00000000 --- a/src/app/view/settings/boxes.rs +++ /dev/null @@ -1,385 +0,0 @@ -use iced::{Align, Column, Container, Length, Row}; - -use revault_ui::{ - color, - component::{badge, card, separation, text::Text}, - icon::dot_icon, -}; -use revaultd::{config::Config, revault_tx::bitcoin::hashes::hex::ToHex}; - -use crate::app::message::Message; - -use crate::daemon::model::ServersStatuses; - -pub trait SettingsBox { - fn title(&self) -> &'static str; - fn description(&self) -> &'static str; - fn badge<'a>(&self) -> Option>; - /// Some(true) means it's running, Some(false) means it's not, None means - /// it's not supposed to show the "Running" badge - fn running(&self) -> Option; - fn body<'a>(&self, config: &Config) -> Column<'a, Message>; - fn display<'a>(&self, config: &Config) -> Container<'a, Message> { - let mut title_row = Row::new(); - if let Some(badge) = self.badge() { - title_row = title_row.push(badge); - } - title_row = title_row - .push( - Row::new() - .push(Text::new(self.title()).bold()) - .width(Length::Fill), - ) - .spacing(10) - .align_items(iced::Align::Center); - - if let Some(running) = self.running() { - let running_container = if running { - Container::new( - Row::new() - .push(dot_icon().size(5).color(color::SUCCESS)) - .push(Text::new("Running").small().color(color::SUCCESS)) - .align_items(iced::Align::Center), - ) - } else { - Container::new( - Row::new() - .push(dot_icon().size(5).color(color::ALERT)) - .push(Text::new("Not running").small().color(color::ALERT)) - .align_items(iced::Align::Center), - ) - }; - - title_row = title_row.push(running_container).width(Length::Shrink); - } - - card::simple(Container::new( - Column::new() - .push( - Row::new() - .push( - Container::new(Row::new().push(title_row).spacing(20)) - .width(Length::Fill), - ) - .spacing(20) - .align_items(Align::Center), - ) - .push(separation().width(Length::Fill)) - .push(self.body(config)) - .spacing(20), - )) - .width(Length::Fill) - } -} - -#[derive(Debug, Clone, Default)] -pub struct SettingsBoxes { - pub bitcoin: BitcoinCoreBox, - pub coordinator: CoordinatorBox, - pub cosigners: Vec, - pub watchtowers: Vec, - pub advanced: AdvancedBox, -} - -impl SettingsBoxes { - pub fn new(bitcoin_blockheight: i32, server_status: ServersStatuses) -> Self { - let mut cosigners: Vec<_> = server_status - .cosigners - .iter() - .map(|c| CosignerBox { - running: c.reachable, - host: c.host.clone(), - }) - .collect(); - cosigners.sort_by(|a, b| a.host.partial_cmp(&b.host).unwrap()); - - let mut watchtowers: Vec<_> = server_status - .watchtowers - .iter() - .map(|w| WatchtowerBox { - running: w.reachable, - host: w.host.clone(), - }) - .collect(); - watchtowers.sort_by(|a, b| a.host.partial_cmp(&b.host).unwrap()); - - SettingsBoxes { - bitcoin: BitcoinCoreBox { - blockheight: bitcoin_blockheight, - }, - coordinator: CoordinatorBox { - running: server_status.coordinator.reachable, - }, - cosigners, - watchtowers, - ..Default::default() - } - } -} - -#[derive(Debug, Clone, Default)] -pub struct AdvancedBox {} - -impl SettingsBox for AdvancedBox { - fn title(&self) -> &'static str { - "Advanced" - } - - fn description(&self) -> &'static str { - "" - } - - fn badge<'a>(&self) -> Option> { - None - } - - fn running(&self) -> Option { - None - } - - fn body<'a>(&self, config: &Config) -> Column<'a, Message> { - let rows = vec![ - ( - "Data dir", - config - .data_dir - .clone() - .map(|d| format!("{:?}", d)) - .unwrap_or_else(|| "Not set".to_string()), - ), - ( - "Daemon", - config - .daemon - .map(|d| d.to_string()) - .unwrap_or_else(|| "Not set".to_string()), - ), - ("Log level", config.log_level.to_string()), - ]; - let mut column = Column::new(); - for (k, v) in rows { - column = column.push( - Row::new() - .push(Container::new(Text::new(k).small()).width(Length::Fill)) - .push(Text::new(&v).small()), - ); - } - column - } -} - -#[derive(Debug, Clone, Default)] -pub struct BitcoinCoreBox { - blockheight: i32, -} - -impl SettingsBox for BitcoinCoreBox { - fn title(&self) -> &'static str { - "Bitcoin Core" - } - - fn description(&self) -> &'static str { - "" - } - - fn badge<'a>(&self) -> Option> { - Some(badge::bitcoin_core()) - } - - fn running(&self) -> Option { - Some(true) - } - - fn body<'a>(&self, config: &Config) -> Column<'a, Message> { - let mut col = Column::new().spacing(20); - if self.blockheight != 0 { - col = col.push( - Row::new() - .push( - Row::new() - .push(badge::network()) - .push(Column::new().push(Text::new("Network:")).push( - Text::new(&config.bitcoind_config.network.to_string()).bold(), - )) - .spacing(10) - .width(Length::FillPortion(1)), - ) - .push( - Row::new() - .push(badge::block()) - .push( - Column::new() - .push(Text::new("Block Height:")) - .push(Text::new(&self.blockheight.to_string()).bold()), - ) - .spacing(10) - .width(Length::FillPortion(1)), - ), - ); - } - - let config = &config.bitcoind_config; - let rows = vec![ - ( - "Cookie file path", - config.cookie_path.to_str().unwrap().to_string(), - ), - ("Socket address", config.addr.to_string()), - ( - "Poll interval", - format!("{} seconds", config.poll_interval_secs.as_secs()), - ), - ]; - - let mut column = Column::new(); - for (k, v) in rows { - column = column.push( - Row::new() - .push(Container::new(Text::new(k).small()).width(Length::Fill)) - .push(Text::new(&v).small()), - ); - } - - col.push(column) - } -} - -#[derive(Debug, Clone, Default)] -pub struct CoordinatorBox { - running: bool, -} - -impl SettingsBox for CoordinatorBox { - fn title(&self) -> &'static str { - "Coordinator" - } - - fn description(&self) -> &'static str { - "" - } - - fn badge<'a>(&self) -> Option> { - None - } - - fn running(&self) -> Option { - Some(self.running) - } - - fn body<'a>(&self, config: &Config) -> Column<'a, Message> { - let rows = vec![ - ("Host", config.coordinator_host.to_string()), - ("Noise key", config.coordinator_noise_key.0.to_hex()), - ( - "Poll interval", - format!("{} seconds", config.coordinator_poll_seconds.as_secs()), - ), - ]; - let mut column = Column::new(); - for (k, v) in rows { - column = column.push( - Row::new() - .push(Container::new(Text::new(k).small()).width(Length::Fill)) - .push(Text::new(&v).small()), - ); - } - column - } -} - -#[derive(Debug, Clone, Default)] -pub struct CosignerBox { - running: bool, - host: String, -} - -impl SettingsBox for CosignerBox { - fn title(&self) -> &'static str { - "Cosigner" - } - - fn description(&self) -> &'static str { - "" - } - - fn badge<'a>(&self) -> Option> { - None - } - - fn running(&self) -> Option { - Some(self.running) - } - - fn body<'a>(&self, config: &Config) -> Column<'a, Message> { - let cosigner_config = config - .manager_config - .as_ref() - .unwrap() - .cosigners - .iter() - .find(|c| c.host.to_string() == self.host) - .unwrap(); - let rows = vec![ - ("Host", cosigner_config.host.to_string()), - ("Noise key", cosigner_config.noise_key.0.to_hex()), - // TODO maybe having the bitcoin public key here? - ]; - let mut column = Column::new(); - for (k, v) in rows { - column = column.push( - Row::new() - .push(Container::new(Text::new(k).small()).width(Length::Fill)) - .push(Text::new(&v).small()), - ); - } - column - } -} - -#[derive(Debug, Clone, Default)] -pub struct WatchtowerBox { - running: bool, - host: String, -} - -impl SettingsBox for WatchtowerBox { - fn title(&self) -> &'static str { - "Watchtower" - } - - fn description(&self) -> &'static str { - "" - } - - fn badge<'a>(&self) -> Option> { - None - } - - fn running(&self) -> Option { - Some(self.running) - } - - fn body<'a>(&self, config: &Config) -> Column<'a, Message> { - let watchtower_config = config - .stakeholder_config - .as_ref() - .unwrap() - .watchtowers - .iter() - .find(|c| c.host.to_string() == self.host) - .unwrap(); - let rows = vec![ - ("Host", watchtower_config.host.to_string()), - ("Noise key", watchtower_config.noise_key.0.to_hex()), - ]; - let mut column = Column::new(); - for (k, v) in rows { - column = column.push( - Row::new() - .push(Container::new(Text::new(k).small()).width(Length::Fill)) - .push(Text::new(&v).small()), - ); - } - column - } -} diff --git a/src/app/view/settings/mod.rs b/src/app/view/settings/mod.rs deleted file mode 100644 index a35ec448..00000000 --- a/src/app/view/settings/mod.rs +++ /dev/null @@ -1,69 +0,0 @@ -use iced::{Column, Element}; - -use revaultd::config::Config; - -use crate::{ - app::{context::Context, error::Error, message::Message, view::layout}, - daemon::model::ServersStatuses, - revault::Role, -}; - -mod boxes; -use boxes::*; - -#[derive(Debug)] -pub struct SettingsView { - dashboard: layout::Dashboard, -} - -impl SettingsView { - pub fn new() -> Self { - SettingsView { - dashboard: layout::Dashboard::new(), - } - } - - pub fn view<'a>( - &'a mut self, - ctx: &Context, - warning: Option<&Error>, - config: &Config, - server_status: Option, - ) -> Element<'a, Message> { - self.dashboard.view( - ctx, - warning, - SettingsView::display_boxes(&ctx, server_status, &config).spacing(8), - ) - } - - pub fn display_boxes<'a>( - ctx: &Context, - server_status: Option, - config: &Config, - ) -> Column<'a, Message> { - if let Some(server_status) = server_status { - let boxes = SettingsBoxes::new(ctx.blockheight, server_status); - let mut column = Column::new() - .push(boxes.bitcoin.display(config)) - .push(boxes.coordinator.display(config)); - - match ctx.role { - Role::Manager => { - for c in boxes.cosigners { - column = column.push(c.display(config)); - } - } - Role::Stakeholder => { - for w in boxes.watchtowers { - column = column.push(w.display(config)); - } - } - }; - - column.push(boxes.advanced.display(config)).spacing(20) - } else { - Column::new() - } - } -} diff --git a/tests/app.rs b/tests/app.rs index b903c0e5..b57c5629 100644 --- a/tests/app.rs +++ b/tests/app.rs @@ -101,7 +101,7 @@ async fn test_emergency_state() { ) .unwrap(), vout: 1, - blockheight: 1, + blockheight: Some(1), delegated_at: None, secured_at: Some(1), funded_at: Some(1), @@ -120,7 +120,7 @@ async fn test_emergency_state() { ) .unwrap(), vout: 1, - blockheight: 1, + blockheight: Some(1), delegated_at: None, secured_at: Some(1), funded_at: Some(1), @@ -198,7 +198,7 @@ async fn test_vaults_state() { ) .unwrap(), vout: 0, - blockheight: 1, + blockheight: Some(1), delegated_at: None, secured_at: Some(1), funded_at: Some(1), @@ -217,7 +217,7 @@ async fn test_vaults_state() { ) .unwrap(), vout: 1, - blockheight: 1, + blockheight: Some(1), delegated_at: None, secured_at: Some(1), funded_at: Some(1), diff --git a/tests/app_revault.rs b/tests/app_revault.rs index a4b3105b..d1969af3 100644 --- a/tests/app_revault.rs +++ b/tests/app_revault.rs @@ -48,7 +48,7 @@ async fn test_revault_state() { ) .unwrap(), vout: 0, - blockheight: 1, + blockheight: Some(1), delegated_at: None, secured_at: Some(1), funded_at: Some(1), @@ -67,7 +67,7 @@ async fn test_revault_state() { ) .unwrap(), vout: 1, - blockheight: 1, + blockheight: Some(1), delegated_at: None, secured_at: Some(1), funded_at: Some(1), @@ -86,7 +86,7 @@ async fn test_revault_state() { ) .unwrap(), vout: 2, - blockheight: 1, + blockheight: Some(1), delegated_at: None, secured_at: Some(1), funded_at: Some(1), diff --git a/tests/app_stakeholder.rs b/tests/app_stakeholder.rs index 7e59e3d4..067fa720 100644 --- a/tests/app_stakeholder.rs +++ b/tests/app_stakeholder.rs @@ -61,7 +61,7 @@ async fn test_stakeholder_delegate_state() { ) .unwrap(), vout: 0, - blockheight: 1, + blockheight: Some(1), delegated_at: None, secured_at: Some(1), funded_at: Some(1), @@ -80,7 +80,7 @@ async fn test_stakeholder_delegate_state() { ) .unwrap(), vout: 1, - blockheight: 1, + blockheight: Some(1), delegated_at: None, secured_at: Some(1), funded_at: Some(1), @@ -99,7 +99,7 @@ async fn test_stakeholder_delegate_state() { ) .unwrap(), vout: 2, - blockheight: 1, + blockheight: Some(1), delegated_at: None, secured_at: Some(1), funded_at: Some(1), @@ -118,7 +118,7 @@ async fn test_stakeholder_delegate_state() { ) .unwrap(), vout: 3, - blockheight: 1, + blockheight: Some(1), delegated_at: None, secured_at: Some(1), funded_at: Some(1), diff --git a/ui/src/icon.rs b/ui/src/icon.rs index 7cc8568b..7e318776 100644 --- a/ui/src/icon.rs +++ b/ui/src/icon.rs @@ -137,6 +137,10 @@ pub fn cross_icon() -> Text { icon('\u{F62A}') } +pub fn pencil_icon() -> Text { + icon('\u{F4CB}') +} + #[allow(dead_code)] pub fn stakeholder_icon() -> Text { icon('\u{F4AE}') From 5408a5ac89adf510a280c1984b7e716c4566f53a Mon Sep 17 00:00:00 2001 From: edouard Date: Wed, 23 Mar 2022 15:35:17 +0100 Subject: [PATCH 2/2] Add load_config to Daemon trait --- src/app/context.rs | 25 +++++++------------------ src/daemon/embedded.rs | 18 ++++++++++++++++++ src/daemon/mod.rs | 13 +++++-------- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/app/context.rs b/src/app/context.rs index a84cb65f..ded854a3 100644 --- a/src/app/context.rs +++ b/src/app/context.rs @@ -14,7 +14,7 @@ use revault_hwi::{app::revault::RevaultHWI, HWIError}; use crate::{ app::{config, error::Error, menu::Menu}, conversion::Converter, - daemon::{embedded::EmbeddedDaemon, Daemon}, + daemon::Daemon, revault::Role, }; @@ -122,13 +122,12 @@ impl Context { } pub fn load_daemon_config(&mut self, cfg: DaemonConfig) -> Result<(), Error> { - let mut daemon = EmbeddedDaemon::new(); - daemon.start(cfg.clone())?; - - let mut old_daemon = self.revaultd.clone(); - self.revaultd = Arc::new(daemon); - - self.config.daemon = cfg; + loop { + if let Some(daemon) = Arc::get_mut(&mut self.revaultd) { + daemon.load_config(cfg.clone())?; + break; + } + } let mut daemon_config_file = OpenOptions::new() .write(true) @@ -145,16 +144,6 @@ impl Context { Error::ConfigError(e.to_string()) })?; - loop { - match Arc::get_mut(&mut old_daemon) { - None => {} - Some(old) => { - old.stop()?; - break; - } - } - } - Ok(()) } } diff --git a/src/daemon/embedded.rs b/src/daemon/embedded.rs index fd253b62..ef3fd2f8 100644 --- a/src/daemon/embedded.rs +++ b/src/daemon/embedded.rs @@ -5,6 +5,7 @@ use bitcoin::{consensus::encode, util::psbt::PartiallySignedTransaction as Psbt, use super::{model::*, Daemon, RevaultDError}; use revaultd::{ + commands::CommandError, config::Config, revault_tx::transactions::{ CancelTransaction, EmergencyTransaction, RevaultTransaction, SpendTransaction, @@ -13,6 +14,12 @@ use revaultd::{ DaemonHandle, }; +impl From for RevaultDError { + fn from(error: CommandError) -> Self { + RevaultDError::Rpc(error.code() as i32, error.to_string()) + } +} + pub struct EmbeddedDaemon { handle: Option>, } @@ -41,6 +48,17 @@ impl Daemon for EmbeddedDaemon { false } + fn load_config(&mut self, cfg: Config) -> Result<(), RevaultDError> { + if self.handle.is_none() { + return Ok(()); + } + + let next = DaemonHandle::start(cfg).map_err(|e| RevaultDError::Start(e.to_string()))?; + self.handle.take().unwrap().into_inner().unwrap().shutdown(); + self.handle = Some(Mutex::new(next)); + Ok(()) + } + fn stop(&mut self) -> Result<(), RevaultDError> { if let Some(h) = self.handle.take() { let handle = h.into_inner().unwrap(); diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index a9a13e08..2e68df37 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -3,12 +3,11 @@ pub mod embedded; pub mod model; use std::collections::BTreeMap; -use std::convert::From; use std::fmt::Debug; use std::io::ErrorKind; use bitcoin::{util::psbt::PartiallySignedTransaction as Psbt, OutPoint, Txid}; -use revaultd::commands::CommandError; +use revaultd::config::Config; use model::*; @@ -38,15 +37,13 @@ impl std::fmt::Display for RevaultDError { } } -impl From for RevaultDError { - fn from(error: CommandError) -> Self { - RevaultDError::Rpc(error.code() as i32, error.to_string()) - } -} - pub trait Daemon: Debug { fn is_external(&self) -> bool; + fn load_config(&mut self, _cfg: Config) -> Result<(), RevaultDError> { + return Ok(()); + } + fn stop(&mut self) -> Result<(), RevaultDError>; fn get_deposit_address(&self) -> Result;