wallet: config backend for validators
This commit is contained in:
Generated
+1
@@ -623,6 +623,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"handlebars",
|
||||
"humantime-serde",
|
||||
"log",
|
||||
"network-defaults",
|
||||
"serde",
|
||||
"toml",
|
||||
|
||||
@@ -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))) => {
|
||||
|
||||
@@ -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" }
|
||||
network-defaults = { path = "../network-defaults" }
|
||||
|
||||
@@ -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<Self> {
|
||||
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))
|
||||
|
||||
Generated
+1
@@ -219,6 +219,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"handlebars",
|
||||
"humantime-serde",
|
||||
"log",
|
||||
"network-defaults",
|
||||
"serde",
|
||||
"toml",
|
||||
|
||||
Generated
+1
@@ -568,6 +568,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"handlebars",
|
||||
"humantime-serde",
|
||||
"log",
|
||||
"network-defaults",
|
||||
"serde",
|
||||
"toml",
|
||||
|
||||
@@ -1,39 +1,60 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<GlobalConfig>,
|
||||
|
||||
// One configuration file per network
|
||||
networks: HashMap<String, NetworkConfig>,
|
||||
}
|
||||
|
||||
#[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<Url>,
|
||||
selected_api_url: Option<Url>,
|
||||
|
||||
// Additional user provided validators
|
||||
validator_urls: Option<Vec<ValidatorUrl>>,
|
||||
}
|
||||
|
||||
impl NetworkConfig {
|
||||
fn validators(&self) -> impl Iterator<Item = &ValidatorUrl> {
|
||||
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<WalletNetwork>) -> 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<PathBuf>) -> 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::<GlobalConfig>(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::<NetworkConfig>(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<Item = ValidatorUrl> + '_ {
|
||||
// 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<Item = ValidatorUrl> + '_ {
|
||||
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<Item = Url> + '_ {
|
||||
self.get_validators(network).into_iter().map(|v| v.nymd_url)
|
||||
}
|
||||
|
||||
pub fn get_api_urls(&self, network: WalletNetwork) -> impl Iterator<Item = Url> + '_ {
|
||||
pub fn get_configured_validators(
|
||||
&self,
|
||||
network: WalletNetwork,
|
||||
) -> impl Iterator<Item = ValidatorUrl> + '_ {
|
||||
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<cosmrs::AccountId> {
|
||||
@@ -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<Url> {
|
||||
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<Url> {
|
||||
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<T>(file: PathBuf) -> Result<T, io::Error>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
fs::read_to_string(file).and_then(|contents| {
|
||||
toml::from_str::<T>(&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<Vec<ValidatorUrl>>,
|
||||
@@ -224,7 +334,7 @@ struct OptionalValidators {
|
||||
}
|
||||
|
||||
impl OptionalValidators {
|
||||
fn validators(&self, network: WalletNetwork) -> impl Iterator<Item = &ValidatorUrl> {
|
||||
pub fn validators(&self, network: WalletNetwork) -> impl Iterator<Item = &ValidatorUrl> {
|
||||
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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<RwLock<State>>>,
|
||||
) -> Result<Account, BackendError> {
|
||||
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::<HashMap<_, _>>();
|
||||
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<WalletNetwork, Url> = untested_nymd_urls
|
||||
.iter()
|
||||
.map(|(network, urls)| (*network, urls.iter().next().unwrap().clone()))
|
||||
.collect();
|
||||
let default_api_urls: HashMap<WalletNetwork, Url> = 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, Vec<(Url, bool)>>,
|
||||
network: WalletNetwork,
|
||||
async fn run_connection_test(
|
||||
untested_nymd_urls: HashMap<WalletNetwork, Vec<Url>>,
|
||||
untested_api_urls: HashMap<WalletNetwork, Vec<Url>>,
|
||||
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<Network, Vec<(Url, bool)>>,
|
||||
HashMap<Network, Vec<(Url, bool)>>,
|
||||
) {
|
||||
let mixnet_contract_address = WalletNetwork::iter()
|
||||
.map(|network| (network.into(), config.get_mixnet_contract_address(network)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
fn select_first_responding_api_url(
|
||||
api_urls: &HashMap<Network, Vec<(Url, bool)>>,
|
||||
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<Network, Vec<(Url, bool)>>,
|
||||
api_urls: &HashMap<Network, Vec<(Url, bool)>>,
|
||||
mnemonic: &Mnemonic,
|
||||
default_nymd_urls: &HashMap<WalletNetwork, Url>,
|
||||
default_api_urls: &HashMap<WalletNetwork, Url>,
|
||||
config: &Config,
|
||||
mnemonic: &Mnemonic,
|
||||
) -> Result<Vec<Client<SigningNymdClient>>, 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, Vec<(Url, bool)>>,
|
||||
network: WalletNetwork,
|
||||
) -> Option<Url> {
|
||||
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, Vec<(Url, bool)>>,
|
||||
network: WalletNetwork,
|
||||
//config: &Config,
|
||||
) -> Option<Url> {
|
||||
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<bool, BackendError> {
|
||||
log::info!("Checking wallet file");
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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<Network, Client<SigningNymdClient>>,
|
||||
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<SigningNymdClient>) {
|
||||
@@ -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<Item = ValidatorUrl> + '_ {
|
||||
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<Item = Url> + '_ {
|
||||
self.get_validators(network).into_iter().map(|v| v.nymd_url)
|
||||
}
|
||||
|
||||
pub fn get_api_urls(&self, network: Network) -> impl Iterator<Item = Url> + '_ {
|
||||
self
|
||||
.get_validators(network)
|
||||
.into_iter()
|
||||
.filter_map(|v| v.api_url)
|
||||
}
|
||||
|
||||
pub fn get_all_nymd_urls(&self) -> HashMap<Network, Vec<Url>> {
|
||||
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, Vec<Url>> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
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<_>>(),
|
||||
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<_>>(),
|
||||
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(),
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user