From 4116aa18a8b76eaad84ccb28c37313a2644cd479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Fri, 1 Apr 2022 11:22:26 +0200 Subject: [PATCH] wallet: config backend for validators --- Cargo.lock | 1 + .../validator-client/src/connection_tester.rs | 6 +- common/config/Cargo.toml | 3 +- common/config/src/lib.rs | 8 +- contracts/Cargo.lock | 1 + nym-wallet/Cargo.lock | 1 + nym-wallet/src-tauri/src/config/mod.rs | 370 ++++++++++++------ nym-wallet/src-tauri/src/network.rs | 4 + .../src/operations/mixnet/account.rs | 195 +++++---- .../src-tauri/src/platform_constants.rs | 8 + nym-wallet/src-tauri/src/state.rs | 175 ++++++++- 11 files changed, 566 insertions(+), 206 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2853c9d980..e417e0bc27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -623,6 +623,7 @@ version = "0.1.0" dependencies = [ "handlebars", "humantime-serde", + "log", "network-defaults", "serde", "toml", diff --git a/common/client-libs/validator-client/src/connection_tester.rs b/common/client-libs/validator-client/src/connection_tester.rs index ebf156d7ca..1a8154cd80 100644 --- a/common/client-libs/validator-client/src/connection_tester.rs +++ b/common/client-libs/validator-client/src/connection_tester.rs @@ -93,7 +93,11 @@ async fn test_nymd_connection( { Ok(Err(NymdError::TendermintError(e))) => { // If we get a tendermint-rpc error, we classify the node as not contactable - log::debug!("Checking: nymd_url: {network}: {url}: failed: {}", e); + log::debug!( + "Checking: nymd_url: {network}: {url}: {}: {}", + "failed".red(), + e + ); false } Ok(Err(NymdError::AbciError(code, log))) => { diff --git a/common/config/Cargo.toml b/common/config/Cargo.toml index 2455b32f8a..bbdc13e620 100644 --- a/common/config/Cargo.toml +++ b/common/config/Cargo.toml @@ -9,8 +9,9 @@ edition = "2021" [dependencies] handlebars = "3.0.1" humantime-serde = "1.0" +log = "0.4" serde = { version = "1.0", features = ["derive"] } toml = "0.5.6" url = "2.2" -network-defaults = { path = "../network-defaults" } \ No newline at end of file +network-defaults = { path = "../network-defaults" } diff --git a/common/config/src/lib.rs b/common/config/src/lib.rs index 3d00921eef..61e95e9ca9 100644 --- a/common/config/src/lib.rs +++ b/common/config/src/lib.rs @@ -13,6 +13,7 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned { fn template() -> &'static str; fn config_file_name() -> String { + log::trace!("NymdConfig::config_file_name"); "config.toml".to_string() } @@ -20,6 +21,7 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned { // default, most probable, implementations; can be easily overridden where required fn default_config_directory(id: Option<&str>) -> PathBuf { + log::trace!("NymdConfig::default_config_directory"); if let Some(id) = id { Self::default_root_directory().join(id).join("config") } else { @@ -28,6 +30,7 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned { } fn default_data_directory(id: Option<&str>) -> PathBuf { + log::trace!("NymdConfig::default_data_path"); if let Some(id) = id { Self::default_root_directory().join(id).join("data") } else { @@ -36,6 +39,7 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned { } fn default_config_file_path(id: Option<&str>) -> PathBuf { + log::trace!("NymdConfig::default_config_file_path"); Self::default_config_directory(id).join(Self::config_file_name()) } @@ -68,7 +72,9 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned { } fn load_from_file(id: Option<&str>) -> io::Result { - let config_contents = fs::read_to_string(Self::default_config_file_path(id))?; + let file = Self::default_config_file_path(id); + log::trace!("Loading from file: {:#?}", file); + let config_contents = fs::read_to_string(file)?; toml::from_str(&config_contents) .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err)) diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 9e5ee6baee..0efb879812 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -219,6 +219,7 @@ version = "0.1.0" dependencies = [ "handlebars", "humantime-serde", + "log", "network-defaults", "serde", "toml", diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 3f5a26d260..fd97a2b32b 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -568,6 +568,7 @@ version = "0.1.0" dependencies = [ "handlebars", "humantime-serde", + "log", "network-defaults", "serde", "toml", diff --git a/nym-wallet/src-tauri/src/config/mod.rs b/nym-wallet/src-tauri/src/config/mod.rs index c35c80cb10..c0972a77fa 100644 --- a/nym-wallet/src-tauri/src/config/mod.rs +++ b/nym-wallet/src-tauri/src/config/mod.rs @@ -1,39 +1,60 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::platform_constants::{CONFIG_DIR_NAME, CONFIG_FILENAME}; use crate::{error::BackendError, network::Network as WalletNetwork}; +use config::defaults::all::Network; use config::defaults::{all::SupportedNetworks, ValidatorDetails}; -use config::NymConfig; use core::fmt; use itertools::Itertools; +use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; -use std::time::Duration; +use std::collections::HashMap; +use std::str::FromStr; use std::{fs, io, path::PathBuf}; use strum::IntoEnumIterator; use url::Url; -const REMOTE_SOURCE_OF_VALIDATOR_URLS: &str = +pub const REMOTE_SOURCE_OF_VALIDATOR_URLS: &str = "https://nymtech.net/.wellknown/wallet/validators.json"; -#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] -#[serde(deny_unknown_fields)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Config { // Base configuration is not part of the configuration file as it's not intended to be changed. - #[serde(skip)] base: Base, - // Network level configuration - network: OptionalValidators, + // Global configuration file + global: Option, + + // One configuration file per network + networks: HashMap, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] -#[serde(deny_unknown_fields)] struct Base { /// Information on all the networks that the wallet connects to. networks: SupportedNetworks, +} - /// Validators that have been fetched dynamically, probably during startup. - fetched_validators: OptionalValidators, +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct GlobalConfig { + // TODO: there are no global settings (yet) +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct NetworkConfig { + // User selected urls + selected_nymd_url: Option, + selected_api_url: Option, + + // Additional user provided validators + validator_urls: Option>, +} + +impl NetworkConfig { + fn validators(&self) -> impl Iterator { + self.validator_urls.iter().flat_map(|v| v.iter()) + } } impl Default for Base { @@ -41,91 +62,125 @@ impl Default for Base { let networks = WalletNetwork::iter().map(Into::into).collect(); Base { networks: SupportedNetworks::new(networks), - fetched_validators: OptionalValidators::default(), } } } -impl NymConfig for Config { - fn template() -> &'static str { - // For now we're not using a template - unimplemented!(); +impl Config { + fn root_directory() -> PathBuf { + tauri::api::path::config_dir().expect("Failed to get config directory") } - fn default_root_directory() -> PathBuf { - dirs::home_dir() - .expect("Failed to evaluate $HOME value") - .join(".nym") - .join("wallet") + fn config_directory() -> PathBuf { + Self::root_directory().join(CONFIG_DIR_NAME) } - fn root_directory(&self) -> PathBuf { - Self::default_root_directory() + fn config_file_path(network: Option) -> PathBuf { + if let Some(network) = network { + let network_filename = format!("{}.toml", network.as_key()); + Self::config_directory().join(network_filename) + } else { + Self::config_directory().join(CONFIG_FILENAME) + } } - fn config_directory(&self) -> PathBuf { - self.root_directory().join("config") - } - - fn data_directory(&self) -> PathBuf { - self.root_directory().join("data") - } - - fn save_to_file(&self, custom_location: Option) -> io::Result<()> { - let config_toml = toml::to_string_pretty(&self) - .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err))?; + pub fn save_to_files(&self) -> io::Result<()> { + log::trace!("Config::save_to_file"); // Make sure the whole directory structure actually exists - match custom_location.clone() { - Some(loc) => { - if let Some(parent_dir) = loc.parent() { - fs::create_dir_all(parent_dir) - } else { - Ok(()) + fs::create_dir_all(Self::config_directory())?; + + // Global config + if let Some(global) = &self.global { + let location = Self::config_file_path(None); + + match toml::to_string_pretty(&global) + .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err)) + .map(|toml| fs::write(location.clone(), toml)) + { + Ok(_) => log::debug!("Writing to: {:#?}", location), + Err(err) => log::warn!("Failed to write to {:#?}: {err}", location), + } + } + + // One file per network + for (network, config) in &self.networks { + let network = match Network::from_str(network).map(Into::into) { + Ok(network) => network, + Err(err) => { + log::warn!("Unexpected name for network configuration, not saving: {err}"); + break; + } + }; + + let location = Self::config_file_path(Some(network)); + match toml::to_string_pretty(config) + .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err)) + .map(|toml| fs::write(location.clone(), toml)) + { + Ok(_) => log::debug!("Writing to: {:#?}", location), + Err(err) => log::warn!("Failed to write to {:#?}: {err}", location), + } + } + Ok(()) + } + + pub fn load_from_files() -> Self { + // Global + let global = { + let file = Self::config_file_path(None); + match load_from_file::(file.clone()) { + Ok(global) => { + log::debug!("Loaded from file {:#?}", file); + Some(global) + } + Err(err) => { + log::trace!("Not loading {:#?}: {}", file, err); + None } } - None => fs::create_dir_all(self.config_directory()), - }?; + }; - fs::write( - custom_location.unwrap_or_else(|| self.config_directory().join(Self::config_file_name())), - config_toml, - ) + // One file per network + let mut networks = HashMap::new(); + for network in WalletNetwork::iter() { + let file = Self::config_file_path(Some(network)); + match load_from_file::(file.clone()) { + Ok(config) => { + log::trace!("Loaded from file {:#?}", file); + networks.insert(network.as_key(), config); + } + Err(err) => log::trace!("Not loading {:#?}: {}", file, err), + }; + } + + Self { + base: Base::default(), + global, + networks, + } } -} -impl Config { - /// Get the available validators in the order - /// 1. from the configuration file - /// 2. provided remotely - /// 3. hardcoded fallback - pub fn get_validators(&self, network: WalletNetwork) -> impl Iterator + '_ { - // The base validators are (currently) stored as strings - let base_validators = self.base.networks.validators(network.into()).map(|v| { + pub fn get_base_validators( + &self, + network: WalletNetwork, + ) -> impl Iterator + '_ { + self.base.networks.validators(network.into()).map(|v| { v.clone() .try_into() .expect("The hardcoded validators are assumed to be valid urls") - }); - - self - .base - .fetched_validators - .validators(network) - .chain(self.network.validators(network)) - .cloned() - .chain(base_validators) - .unique() + }) } - pub fn get_nymd_urls(&self, network: WalletNetwork) -> impl Iterator + '_ { - self.get_validators(network).into_iter().map(|v| v.nymd_url) - } - - pub fn get_api_urls(&self, network: WalletNetwork) -> impl Iterator + '_ { + pub fn get_configured_validators( + &self, + network: WalletNetwork, + ) -> impl Iterator + '_ { self - .get_validators(network) + .networks + .get(&network.as_key()) .into_iter() - .filter_map(|v| v.api_url) + .flat_map(|c| c.validators().cloned()) } pub fn get_mixnet_contract_address(&self, network: WalletNetwork) -> Option { @@ -161,25 +216,80 @@ impl Config { .ok() } - pub async fn fetch_updated_validator_urls(&mut self) -> Result<(), BackendError> { - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(3)) - .build()?; - log::debug!( - "Fetching validator urls from: {}", - REMOTE_SOURCE_OF_VALIDATOR_URLS - ); - let response = client - .get(REMOTE_SOURCE_OF_VALIDATOR_URLS.to_string()) - .send() - .await?; - self.base.fetched_validators = serde_json::from_str(&response.text().await?)?; - log::debug!( - "Received validator urls: \n{}", - self.base.fetched_validators - ); - Ok(()) + pub fn select_validator_nymd_url(&mut self, nymd_url: Url, network: WalletNetwork) { + if let Some(net) = self.networks.get_mut(&network.as_key()) { + net.selected_nymd_url = Some(nymd_url); + } else { + self.networks.insert( + network.as_key(), + NetworkConfig { + selected_nymd_url: Some(nymd_url), + ..NetworkConfig::default() + }, + ); + } } + + pub fn select_validator_api_url(&mut self, api_url: Url, network: WalletNetwork) { + if let Some(net) = self.networks.get_mut(&network.as_key()) { + net.selected_api_url = Some(api_url); + } else { + self.networks.insert( + network.as_key(), + NetworkConfig { + selected_nymd_url: Some(api_url), + ..NetworkConfig::default() + }, + ); + } + } + + pub fn get_selected_validator_nymd_url(&self, network: &WalletNetwork) -> Option { + self + .networks + .get(&network.as_key()) + .and_then(|config| config.selected_nymd_url.clone()) + } + + pub fn get_selected_validator_api_url(&self, network: &WalletNetwork) -> Option { + self + .networks + .get(&network.as_key()) + .and_then(|config| config.selected_api_url.clone()) + } + + pub fn add_validator_url(&mut self, url: ValidatorUrl, network: WalletNetwork) { + if let Some(net) = self.networks.get_mut(&network.as_key()) { + if let Some(ref mut urls) = net.validator_urls { + urls.push(url); + } else { + net.validator_urls = Some(vec![url]); + } + } else { + self.networks.insert( + network.as_key(), + NetworkConfig { + validator_urls: Some(vec![url]), + ..NetworkConfig::default() + }, + ); + } + } + + #[allow(unused)] + pub fn remove_validator_url(&mut self, _url: ValidatorUrl, _network: WalletNetwork) { + todo!(); + } +} + +fn load_from_file(file: PathBuf) -> Result +where + T: DeserializeOwned, +{ + fs::read_to_string(file).and_then(|contents| { + toml::from_str::(&contents) + .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err)) + }) } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] @@ -215,7 +325,7 @@ impl fmt::Display for ValidatorUrl { #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde(deny_unknown_fields)] -struct OptionalValidators { +pub struct OptionalValidators { // User supplied additional validator urls in addition to the hardcoded ones. // These are separate fields, rather than a map, to force the serialization order. mainnet: Option>, @@ -224,7 +334,7 @@ struct OptionalValidators { } impl OptionalValidators { - fn validators(&self, network: WalletNetwork) -> impl Iterator { + pub fn validators(&self, network: WalletNetwork) -> impl Iterator { match network { WalletNetwork::MAINNET => self.mainnet.as_ref(), WalletNetwork::SANDBOX => self.sandbox.as_ref(), @@ -261,53 +371,63 @@ mod tests { use super::*; fn test_config() -> Config { - Config { - base: Base::default(), - network: OptionalValidators { - mainnet: Some(vec![ - ValidatorDetails { - nymd_url: "https://foo".to_string(), - api_url: None, - } - .try_into() - .unwrap(), - ValidatorUrl { - nymd_url: "https://baz".parse().unwrap(), - api_url: Some("https://baz/api".parse().unwrap()), - }, - ]), - sandbox: Some(vec![ValidatorUrl { + let netconfig = NetworkConfig { + selected_nymd_url: None, + selected_api_url: Some("https://my_api_url.com".parse().unwrap()), + + validator_urls: Some(vec![ + ValidatorUrl { + nymd_url: "https://foo".parse().unwrap(), + api_url: None, + }, + ValidatorUrl { nymd_url: "https://bar".parse().unwrap(), api_url: Some("https://bar/api".parse().unwrap()), - }]), - qa: None, - }, + }, + ValidatorUrl { + nymd_url: "https://baz".parse().unwrap(), + api_url: Some("https://baz/api".parse().unwrap()), + }, + ]), + }; + + Config { + base: Base::default(), + global: Some(GlobalConfig::default()), + networks: [(WalletNetwork::MAINNET.as_key(), netconfig)] + .into_iter() + .collect(), } } #[test] fn serialize_to_toml() { + let config = test_config(); + let netconfig = &config.networks[&WalletNetwork::MAINNET.as_key()]; assert_eq!( - toml::to_string_pretty(&test_config()).unwrap(), - r#"[[network.mainnet]] + toml::to_string_pretty(netconfig).unwrap(), + r#"selected_api_url = 'https://my_api_url.com/' + +[[validator_urls]] nymd_url = 'https://foo/' -[[network.mainnet]] -nymd_url = 'https://baz/' -api_url = 'https://baz/api' - -[[network.sandbox]] +[[validator_urls]] nymd_url = 'https://bar/' api_url = 'https://bar/api' + +[[validator_urls]] +nymd_url = 'https://baz/' +api_url = 'https://baz/api' "# ); } #[test] fn serialize_and_deserialize_to_toml() { let config = test_config(); - let config_str = toml::to_string_pretty(&config).unwrap(); - let config_from_toml = toml::from_str(&config_str).unwrap(); - assert_eq!(config, config_from_toml); + let netconfig = &config.networks[&WalletNetwork::MAINNET.as_key()]; + let config_str = toml::to_string_pretty(netconfig).unwrap(); + let config_from_toml: NetworkConfig = toml::from_str(&config_str).unwrap(); + assert_eq!(netconfig, &config_from_toml); } #[test] @@ -315,7 +435,7 @@ api_url = 'https://bar/api' let config = test_config(); let nymd_url = config - .get_validators(WalletNetwork::MAINNET) + .get_configured_validators(WalletNetwork::MAINNET) .next() .map(|v| v.nymd_url) .unwrap(); @@ -323,7 +443,7 @@ api_url = 'https://bar/api' // The first entry is missing an API URL let api_url = config - .get_validators(WalletNetwork::MAINNET) + .get_configured_validators(WalletNetwork::MAINNET) .next() .and_then(|v| v.api_url); assert_eq!(api_url, None); @@ -334,14 +454,14 @@ api_url = 'https://bar/api' let config = Config::default(); let nymd_url = config - .get_validators(WalletNetwork::MAINNET) + .get_base_validators(WalletNetwork::MAINNET) .next() .map(|v| v.nymd_url) .unwrap(); assert_eq!(nymd_url.as_ref(), "https://rpc.nyx.nodes.guru/"); let api_url = config - .get_validators(WalletNetwork::MAINNET) + .get_base_validators(WalletNetwork::MAINNET) .next() .and_then(|v| v.api_url) .unwrap(); diff --git a/nym-wallet/src-tauri/src/network.rs b/nym-wallet/src-tauri/src/network.rs index 5202b70d52..59574c744e 100644 --- a/nym-wallet/src-tauri/src/network.rs +++ b/nym-wallet/src-tauri/src/network.rs @@ -21,6 +21,10 @@ pub enum Network { } impl Network { + pub fn as_key(&self) -> String { + self.to_string().to_lowercase() + } + pub fn denom(&self) -> Denom { match self { // network defaults should be correctly formatted diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index 6429024bd4..3fc7d1380e 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -21,9 +21,7 @@ use strum::IntoEnumIterator; use tokio::sync::RwLock; use url::Url; -use validator_client::{ - connection_tester::run_validator_connection_test, nymd::SigningNymdClient, Client, -}; +use validator_client::{nymd::SigningNymdClient, Client}; #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr(test, ts(export, export_to = "../src/types/rust/account.ts"))] @@ -167,36 +165,54 @@ async fn _connect_with_mnemonic( mnemonic: Mnemonic, state: tauri::State<'_, Arc>>, ) -> Result { - update_validator_urls(state.clone()).await?; - let config = state.read().await.config(); - - for network in WalletNetwork::iter() { - log::debug!( - "List of validators for {network}: [\n{}\n]", - config.get_validators(network).format(",\n") - ); + { + let mut w_state = state.write().await; + w_state.load_config_files(); } - // Run connection tests on all nymd and validator-api endpoints - let (nymd_urls, api_urls) = { - let mixnet_contract_address = WalletNetwork::iter() - .map(|network| (network.into(), config.get_mixnet_contract_address(network))) - .collect::>(); - let nymd_urls = WalletNetwork::iter().flat_map(|network| { - config - .get_nymd_urls(network) - .map(move |url| (network.into(), url)) - }); - let api_urls = WalletNetwork::iter().flat_map(|network| { - config - .get_api_urls(network) - .map(move |url| (network.into(), url)) - }); + update_validator_urls(state.clone()).await?; - run_validator_connection_test(nymd_urls, api_urls, mixnet_contract_address).await + let config = { + let state = state.read().await; + + // Take the oppertunity to list all the known validators while we have the state. + for network in WalletNetwork::iter() { + log::debug!( + "List of validators for {network}: [\n{}\n]", + state.get_validators(network).format(",\n") + ); + } + + state.config().clone() }; - let clients = create_clients(&nymd_urls, &api_urls, &mnemonic, &config)?; + // Get all the urls needed for the connection test + let (untested_nymd_urls, untested_api_urls) = { + let state = state.read().await; + (state.get_all_nymd_urls(), state.get_all_api_urls()) + }; + let default_nymd_urls: HashMap = untested_nymd_urls + .iter() + .map(|(network, urls)| (*network, urls.iter().next().unwrap().clone())) + .collect(); + let default_api_urls: HashMap = untested_api_urls + .iter() + .map(|(network, urls)| (*network, urls.iter().next().unwrap().clone())) + .collect(); + + // Run connection tests on all nymd and validator-api endpoints + let (nymd_urls, api_urls) = + run_connection_test(untested_nymd_urls, untested_api_urls, &config).await; + + // Create clients for all networks + let clients = create_clients( + &nymd_urls, + &api_urls, + &default_nymd_urls, + &default_api_urls, + &config, + &mnemonic, + )?; // Set the default account let default_network: WalletNetwork = config::defaults::DEFAULT_NETWORK.into(); @@ -224,63 +240,69 @@ async fn _connect_with_mnemonic( account_for_default_network } -fn select_random_responding_nymd_url( - nymd_urls: &HashMap>, - network: WalletNetwork, +async fn run_connection_test( + untested_nymd_urls: HashMap>, + untested_api_urls: HashMap>, config: &Config, -) -> Url { - // We pick a randon responding nymd url, and if not, fall back on the first one in the list. - nymd_urls - .get(&network.into()) - .and_then(|urls| { - let nymd_urls: Vec<_> = urls - .iter() - .filter_map(|(url, result)| if *result { Some(url.clone()) } else { None }) - .collect(); - nymd_urls.choose(&mut rand::thread_rng()).cloned() - }) - .unwrap_or_else(|| { - log::debug!("No passing nymd_urls for {network}: using default"); - config - .get_nymd_urls(network) - .next() - .expect("Expected at least one hardcoded nymd url") - }) -} +) -> ( + HashMap>, + HashMap>, +) { + let mixnet_contract_address = WalletNetwork::iter() + .map(|network| (network.into(), config.get_mixnet_contract_address(network))) + .collect::>(); -fn select_first_responding_api_url( - api_urls: &HashMap>, - network: WalletNetwork, - config: &Config, -) -> Url { - // We pick the first API url among the responding ones. If none exists, fall back on the first - // one in the list. - api_urls - .get(&network.into()) - .and_then(|urls| { - urls - .iter() - .find_map(|(url, result)| if *result { Some(url.clone()) } else { None }) - }) - .unwrap_or_else(|| { - log::debug!("No passing api_urls for {network}: using default"); - config - .get_api_urls(network) - .next() - .expect("Expected at least one hardcoded api url") - }) + let untested_nymd_urls = untested_nymd_urls + .into_iter() + .flat_map(|(net, urls)| urls.into_iter().map(move |url| (net.into(), url))); + + let untested_api_urls = untested_api_urls + .into_iter() + .flat_map(|(net, urls)| urls.into_iter().map(move |url| (net.into(), url))); + + validator_client::connection_tester::run_validator_connection_test( + untested_nymd_urls, + untested_api_urls, + mixnet_contract_address, + ) + .await } fn create_clients( nymd_urls: &HashMap>, api_urls: &HashMap>, - mnemonic: &Mnemonic, + default_nymd_urls: &HashMap, + default_api_urls: &HashMap, config: &Config, + mnemonic: &Mnemonic, ) -> Result>, BackendError> { let mut clients = Vec::new(); for network in WalletNetwork::iter() { - let nymd_url = select_random_responding_nymd_url(nymd_urls, network, config); - let api_url = select_first_responding_api_url(api_urls, network, config); + let nymd_url = if let Some(url) = config.get_selected_validator_nymd_url(&network) { + log::debug!("Using selected nymd_url for {network}: {url}"); + url.clone() + } else { + let default_nymd_url = default_nymd_urls + .get(&network) + .expect("Expected at least one nymd_url"); + select_random_responding_url(nymd_urls, network).unwrap_or_else(|| { + log::debug!("No successful nymd_urls for {network}: using default: {default_nymd_url}"); + default_nymd_url.clone() + }) + }; + + let api_url = if let Some(url) = config.get_selected_validator_api_url(&network) { + log::debug!("Using selected api_url for {network}: {url}"); + url.clone() + } else { + let default_api_url = default_api_urls + .get(&network) + .expect("Expected at least one api url"); + select_first_responding_url(api_urls, network).unwrap_or_else(|| { + log::debug!("No passing api_urls for {network}: using default: {default_api_url}"); + default_api_url.clone() + }) + }; log::info!("Connecting to: nymd_url: {nymd_url} for {network}"); log::info!("Connecting to: api_url: {api_url} for {network}"); @@ -301,6 +323,31 @@ fn create_clients( Ok(clients) } +fn select_random_responding_url( + urls: &HashMap>, + network: WalletNetwork, +) -> Option { + urls.get(&network.into()).and_then(|urls| { + let urls: Vec<_> = urls + .iter() + .filter_map(|(url, result)| if *result { Some(url.clone()) } else { None }) + .collect(); + urls.choose(&mut rand::thread_rng()).cloned() + }) +} + +fn select_first_responding_url( + urls: &HashMap>, + network: WalletNetwork, + //config: &Config, +) -> Option { + urls.get(&network.into()).and_then(|urls| { + urls + .iter() + .find_map(|(url, result)| if *result { Some(url.clone()) } else { None }) + }) +} + #[tauri::command] pub fn does_password_file_exist() -> Result { log::info!("Checking wallet file"); diff --git a/nym-wallet/src-tauri/src/platform_constants.rs b/nym-wallet/src-tauri/src/platform_constants.rs index 309a5fee7c..583c00e7d3 100644 --- a/nym-wallet/src-tauri/src/platform_constants.rs +++ b/nym-wallet/src-tauri/src/platform_constants.rs @@ -6,16 +6,24 @@ cfg_if::cfg_if! { if #[cfg(target_os = "linux")] { + pub const CONFIG_DIR_NAME: &str = "nym-wallet"; + pub const CONFIG_FILENAME: &str = "config.toml"; pub const STORAGE_DIR_NAME: &str = "nym-wallet"; pub const WALLET_INFO_FILENAME: &str = "saved-wallet.json"; } else if #[cfg(taret_os = "macos")] { + pub const CONFIG_DIR_NAME: &str = "nym-wallet"; + pub const CONFIG_FILENAME: &str = "config.toml"; pub const STORAGE_DIR_NAME: &str = "nym-wallet"; pub const WALLET_INFO_FILENAME: &str = "saved-wallet.json"; } else if #[cfg(taret_os = "windows")] { + pub const CONFIG_DIR_NAME: &str = "NymWallet"; + pub const CONFIG_FILENAME: &str = "Config.toml"; pub const STORAGE_DIR_NAME: &str = "NymWallet"; pub const WALLET_INFO_FILENAME: &str = "saved_wallet.json"; } else { // This case is likely to be a unix-y system + pub const CONFIG_DIR_NAME: &str = "nym-wallet"; + pub const CONFIG_FILENAME: &str = "config.toml"; pub const STORAGE_DIR_NAME: &str = "nym-wallet"; pub const WALLET_INFO_FILENAME: &str = "saved-wallet.json"; } diff --git a/nym-wallet/src-tauri/src/state.rs b/nym-wallet/src-tauri/src/state.rs index 82da416be5..63e5a961c5 100644 --- a/nym-wallet/src-tauri/src/state.rs +++ b/nym-wallet/src-tauri/src/state.rs @@ -1,16 +1,25 @@ -use crate::config::Config; +use crate::config::{Config, OptionalValidators, ValidatorUrl}; use crate::error::BackendError; use crate::network::Network; + +use strum::IntoEnumIterator; use validator_client::nymd::SigningNymdClient; use validator_client::Client; +use itertools::Itertools; +use url::Url; + use std::collections::HashMap; +use std::time::Duration; #[derive(Default)] pub struct State { config: Config, signing_clients: HashMap>, current_network: Network, + + /// Validators that have been fetched dynamically, probably during startup. + fetched_validators: OptionalValidators, } impl State { @@ -28,8 +37,18 @@ impl State { .ok_or(BackendError::ClientNotInitialized) } - pub fn config(&self) -> Config { - self.config.clone() + pub fn config(&self) -> &Config { + &self.config + } + + /// Load configuration from files. If unsuccessful we just log it and move on. + pub fn load_config_files(&mut self) { + self.config = Config::load_from_files(); + } + + #[allow(unused)] + pub fn save_config_files(&self) -> Result<(), BackendError> { + Ok(self.config.save_to_files()?) } pub fn add_client(&mut self, network: Network, client: Client) { @@ -48,8 +67,91 @@ impl State { self.signing_clients = HashMap::new(); } + /// Get the available validators in the order + /// 1. from the configuration file + /// 2. provided remotely + /// 3. hardcoded fallback + pub fn get_validators(&self, network: Network) -> impl Iterator + '_ { + let validators_in_config = self.config.get_configured_validators(network); + let fetched_validators = self.fetched_validators.validators(network).cloned(); + let default_validators = self.config.get_base_validators(network); + + validators_in_config + .chain(fetched_validators) + .chain(default_validators) + .unique() + } + + pub fn get_nymd_urls(&self, network: Network) -> impl Iterator + '_ { + self.get_validators(network).into_iter().map(|v| v.nymd_url) + } + + pub fn get_api_urls(&self, network: Network) -> impl Iterator + '_ { + self + .get_validators(network) + .into_iter() + .filter_map(|v| v.api_url) + } + + pub fn get_all_nymd_urls(&self) -> HashMap> { + Network::iter() + .flat_map(|network| self.get_nymd_urls(network).map(move |url| (network, url))) + .into_group_map() + } + + pub fn get_all_api_urls(&self) -> HashMap> { + Network::iter() + .flat_map(|network| self.get_api_urls(network).map(move |url| (network, url))) + .into_group_map() + } + + /// Fetch validator urls remotely. These are used to in addition to the base ones, and the user + /// configured ones. pub async fn fetch_updated_validator_urls(&mut self) -> Result<(), BackendError> { - self.config.fetch_updated_validator_urls().await + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(3)) + .build()?; + log::debug!( + "Fetching validator urls from: {}", + crate::config::REMOTE_SOURCE_OF_VALIDATOR_URLS + ); + let response = client + .get(crate::config::REMOTE_SOURCE_OF_VALIDATOR_URLS.to_string()) + .send() + .await?; + self.fetched_validators = serde_json::from_str(&response.text().await?)?; + log::debug!("Received validator urls: \n{}", self.fetched_validators); + Ok(()) + } + + #[allow(unused)] + pub fn select_validator_nymd_url( + &mut self, + url: &str, + network: Network, + ) -> Result<(), BackendError> { + self.config.select_validator_nymd_url(url.parse()?, network); + Ok(()) + } + + #[allow(unused)] + pub fn select_validator_api_url( + &mut self, + url: &str, + network: Network, + ) -> Result<(), BackendError> { + self.config.select_validator_api_url(url.parse()?, network); + Ok(()) + } + + #[allow(unused)] + pub fn add_validator_url(&mut self, url: ValidatorUrl, network: Network) { + self.config.add_validator_url(url, network); + } + + #[allow(unused)] + pub fn remove_validator_url(&mut self, url: ValidatorUrl, network: Network) { + self.config.remove_validator_url(url, network) } } @@ -73,3 +175,68 @@ macro_rules! api_client { $state.read().await.current_client()?.validator_api }; } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn adding_validators_urls_prepends() { + let mut state = State::default(); + let _api_urls = state.get_api_urls(Network::MAINNET).collect::>(); + + state.add_validator_url( + ValidatorUrl { + nymd_url: "http://nymd_url.com".parse().unwrap(), + api_url: Some("http://nymd_url.com/api".parse().unwrap()), + }, + Network::MAINNET, + ); + + state.add_validator_url( + ValidatorUrl { + nymd_url: "http://foo.com".parse().unwrap(), + api_url: None, + }, + Network::MAINNET, + ); + + state.add_validator_url( + ValidatorUrl { + nymd_url: "http://bar.com".parse().unwrap(), + api_url: None, + }, + Network::MAINNET, + ); + + assert_eq!( + state.get_nymd_urls(Network::MAINNET).collect::>(), + vec![ + "http://nymd_url.com/".parse().unwrap(), + "http://foo.com".parse().unwrap(), + "http://bar.com".parse().unwrap(), + "https://rpc.nyx.nodes.guru".parse().unwrap(), + ], + ); + assert_eq!( + state.get_api_urls(Network::MAINNET).collect::>(), + vec![ + "http://nymd_url.com/api".parse().unwrap(), + "https://api.nyx.nodes.guru".parse().unwrap(), + ], + ); + assert_eq!( + state + .get_all_nymd_urls() + .get(&Network::MAINNET) + .unwrap() + .clone(), + vec![ + "http://nymd_url.com/".parse().unwrap(), + "http://foo.com".parse().unwrap(), + "http://bar.com".parse().unwrap(), + "https://rpc.nyx.nodes.guru".parse().unwrap(), + ], + ) + } +}