diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index b2255e3824..8090788f49 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -739,6 +739,7 @@ dependencies = [ name = "credentials" version = "0.1.0" dependencies = [ + "bls12_381", "coconut-interface", "crypto", "network-defaults", @@ -2819,6 +2820,7 @@ dependencies = [ "futures", "mixnet-contract-common", "rand 0.6.5", + "reqwest", "serde", "serde_json", "strum 0.23.0", diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 34f1b6f598..b123a1844d 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -24,6 +24,7 @@ dirs = "4.0" eyre = "0.6.5" futures = "0.3.15" rand = "0.6.5" +reqwest = "0.11.9" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" strum = { version = "0.23", features = ["derive"] } diff --git a/nym-wallet/src-tauri/src/config/mod.rs b/nym-wallet/src-tauri/src/config/mod.rs index 7b22614822..11e0531243 100644 --- a/nym-wallet/src-tauri/src/config/mod.rs +++ b/nym-wallet/src-tauri/src/config/mod.rs @@ -1,12 +1,17 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::network::Network as WalletNetwork; +use crate::{error::BackendError, network::Network as WalletNetwork}; use config::defaults::{all::SupportedNetworks, ValidatorDetails}; use config::NymConfig; use serde::{Deserialize, Serialize}; +use std::time::Duration; use std::{fs, io, path::PathBuf}; use strum::IntoEnumIterator; +use url::Url; + +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)] @@ -16,7 +21,7 @@ pub struct Config { base: Base, // Network level configuration - network: Network, + network: OptionalValidators, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] @@ -24,15 +29,17 @@ pub struct Config { 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, } impl Default for Base { fn default() -> Self { - let networks = WalletNetwork::iter() - .map(|network| network.into()) - .collect(); + let networks = WalletNetwork::iter().map(Into::into).collect(); Base { networks: SupportedNetworks::new(networks), + fetched_validators: OptionalValidators::default(), } } } @@ -86,16 +93,31 @@ impl NymConfig for Config { } 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 + '_ { self - .network + .base + .fetched_validators .validators(network) + .chain(self.network.validators(network)) .chain(self.base.networks.validators(network.into())) } + pub fn get_validators_with_api_endpoint( + &self, + network: WalletNetwork, + ) -> impl Iterator + '_ { + self + .get_validators(network) + .filter_map(|validator| ValidatorWithApiEndpoint::try_from(validator.clone()).ok()) + } + pub fn get_mixnet_contract_address(&self, network: WalletNetwork) -> Option { self .base @@ -128,19 +150,53 @@ impl Config { .parse() .ok() } + + pub async fn fetch_updated_validator_urls(&mut self) -> Result<(), BackendError> { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(3)) + .build()?; + let response = client + .get(REMOTE_SOURCE_OF_VALIDATOR_URLS.to_string()) + .send() + .await?; + self.base.fetched_validators = serde_json::from_str(&response.text().await?)?; + Ok(()) + } +} + +// Unlike `ValidatorDetails` this represents validators which are always supposed to have a +// validator-api endpoint running. +#[derive(Clone)] +pub struct ValidatorWithApiEndpoint { + pub nymd_url: Url, + pub api_url: Url, +} + +impl TryFrom for ValidatorWithApiEndpoint { + type Error = BackendError; + + fn try_from(validator: ValidatorDetails) -> Result { + match validator.api_url() { + Some(api_url) => Ok(ValidatorWithApiEndpoint { + nymd_url: validator.nymd_url(), + api_url, + }), + None => Err(BackendError::NoValidatorApiUrlConfigured), + } + } } #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde(deny_unknown_fields)] -struct Network { +struct OptionalValidators { // User supplied additional validator urls in addition to the hardcoded ones. - // NOTE: these are separate fields, rather than a map, to force the serialization order. + // These are separate fields, rather than a map, to force the serialization order. mainnet: Option>, sandbox: Option>, qa: Option>, } -impl Network { +impl OptionalValidators { fn validators(&self, network: WalletNetwork) -> impl Iterator { match network { WalletNetwork::MAINNET => self.mainnet.as_ref(), @@ -159,7 +215,7 @@ mod tests { fn test_config() -> Config { Config { base: Base::default(), - network: Network { + network: OptionalValidators { mainnet: Some(vec![ ValidatorDetails { nymd_url: "https://foo".to_string(), diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index adfd88c37c..0f3aa9bd10 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -56,6 +56,11 @@ pub enum BackendError { #[from] source: url::ParseError, }, + #[error("{source}")] + ReqwestError { + #[from] + source: reqwest::Error, + }, #[error("failed to encrypt the given data with the provided password")] EncryptionError, #[error("failed to decrypt the given data with the provided password")] diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index daf7cbfe6b..ca063a6c0d 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -37,6 +37,7 @@ fn main() { mixnet::account::get_balance, mixnet::account::logout, mixnet::account::switch_network, + mixnet::account::update_validator_urls, mixnet::admin::get_contract_settings, mixnet::admin::update_contract_settings, mixnet::bond::bond_gateway, diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index a1970ddf48..334136f6f2 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -1,12 +1,11 @@ use crate::coin::{Coin, Denom}; -use crate::config::Config; +use crate::config::{Config, ValidatorWithApiEndpoint}; use crate::error::BackendError; use crate::network::Network; use crate::nymd_client; use crate::state::State; use bip39::{Language, Mnemonic}; -use config::defaults::ValidatorDetails; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::convert::TryInto; @@ -135,55 +134,28 @@ fn random_mnemonic() -> Mnemonic { Mnemonic::generate_in_with(&mut rng, Language::English, 24).unwrap() } +#[tauri::command] +pub async fn update_validator_urls( + state: tauri::State<'_, Arc>>, +) -> Result<(), BackendError> { + // Update the list of validators by fecthing additional ones remotely. If it fails, just ignore. + let mut w_state = state.write().await; + let _r = w_state.fetch_updated_validator_urls().await; + Ok(()) +} + async fn _connect_with_mnemonic( mnemonic: Mnemonic, state: tauri::State<'_, Arc>>, ) -> Result { - let default_network: Network = config::defaults::DEFAULT_NETWORK.into(); + update_validator_urls(state.clone()).await?; + let validators = choose_validators(mnemonic.clone(), &state).await?; + let config = state.read().await.config(); - - // Try to connect to validators on all networks - let mut validators = select_validators(&config, &mnemonic).await?; - - // If we didn't manage to connect to any validators, just go ahead and try with the first in the - // list - for network in Network::iter() { - validators - .entry(network) - // We always have at least one hardcoded defalt validator - .or_insert_with(|| { - let default_validator = config.get_validators(network).next().unwrap().clone(); - println!( - "Using default for {network}: {}, {}", - default_validator.nymd_url(), - default_validator - .api_url() - .map_or_else(|| "empty".to_string(), |url| url.to_string()), - ); - default_validator - }); - } - - // Now we are ready to create the clients that we will use - let mut clients = Vec::new(); - for network in Network::iter() { - let client = validator_client::Client::new_signing( - validator_client::Config::new( - network.into(), - validators[&network].nymd_url(), - validators[&network] - .api_url() - .ok_or(BackendError::NoValidatorApiUrlConfigured)?, - config.get_mixnet_contract_address(network), - config.get_vesting_contract_address(network), - config.get_bandwidth_claim_contract_address(network), - ), - mnemonic.clone(), - )?; - clients.push(client); - } + let clients = create_clients(&validators, &mnemonic, &config)?; // Set the default account + let default_network: Network = config::defaults::DEFAULT_NETWORK.into(); let client_for_default_network = clients .iter() .find(|client| Network::from(client.network) == default_network); @@ -208,18 +180,69 @@ async fn _connect_with_mnemonic( account_for_default_network } +fn create_clients( + validators: &HashMap, + mnemonic: &Mnemonic, + config: &Config, +) -> Result>, BackendError> { + let mut clients = Vec::new(); + for network in Network::iter() { + let client = validator_client::Client::new_signing( + validator_client::Config::new( + network.into(), + validators[&network].nymd_url.clone(), + validators[&network].api_url.clone(), + config.get_mixnet_contract_address(network), + config.get_vesting_contract_address(network), + config.get_bandwidth_claim_contract_address(network), + ), + mnemonic.clone(), + )?; + clients.push(client); + } + Ok(clients) +} + +async fn choose_validators( + mnemonic: Mnemonic, + state: &tauri::State<'_, Arc>>, +) -> Result, BackendError> { + let config = state.read().await.config(); + + // Try to connect to validators on all networks + let mut validators = select_responding_validators(&config, &mnemonic).await?; + + // If for a network we didn't manage to connect to any validators, just go ahead and try with the + // first in the list + for network in Network::iter() { + validators.entry(network).or_insert_with(|| { + let default_validator = config + .get_validators_with_api_endpoint(network) + .next() + // We always have at least one hardcoded default validator + .unwrap(); + println!( + "Using default for {network}: {}, {}", + default_validator.nymd_url, default_validator.api_url, + ); + default_validator + }); + } + Ok(validators) +} + // For each network, try the list of available validators one by one and use the first responding // one. -async fn select_validators( +async fn select_responding_validators( config: &Config, mnemonic: &Mnemonic, -) -> Result, BackendError> { +) -> Result, BackendError> { use tokio::time::timeout; let validators = futures::future::join_all(Network::iter().map(|network| { timeout( Duration::from_millis(3000), try_connect_to_validators( - config.get_validators(network), + config.get_validators_with_api_endpoint(network), config, network, mnemonic.clone(), @@ -228,29 +251,27 @@ async fn select_validators( })) .await; - let validators = validators - .into_iter() - // Filter out networks that timed out. - .filter_map(Result::ok) - // Rewrap so that the Result is outermost, so we can return any errors encountered - .collect::, _>>()? - .into_iter() - // Filter out networks where we exhausted all listed validators. - .flatten() - .collect::>(); + // Drop networks that failed the global timeout + let validators = validators.into_iter().filter_map(Result::ok); - Ok(validators) + // Rewrap to return any errors during client creation + let validators = validators.collect::, _>>()?; + + // Filter out networks where we exhausted all listed validators + let validators = validators.into_iter().flatten(); + + Ok(validators.collect::>()) } async fn try_connect_to_validators( - validators: impl Iterator, + validators: impl Iterator, config: &Config, network: Network, mnemonic: Mnemonic, -) -> Result, BackendError> { +) -> Result, BackendError> { for validator in validators { if let Some(responding_validator) = - try_connect_to_validator(validator, config, network, mnemonic.clone()).await? + try_connect_to_validator(&validator, config, network, mnemonic.clone()).await? { // Pick the first successful one return Ok(Some(responding_validator)); @@ -260,22 +281,16 @@ async fn try_connect_to_validators( } async fn try_connect_to_validator( - validator: &ValidatorDetails, + validator: &ValidatorWithApiEndpoint, config: &Config, network: Network, mnemonic: Mnemonic, -) -> Result, BackendError> { - let nymd_url = validator.nymd_url(); - let api_url = match validator.api_url() { - Some(url) => url, - None => return Ok(None), - }; - +) -> Result, BackendError> { let client = validator_client::Client::new_signing( validator_client::Config::new( network.into(), - nymd_url.clone(), - api_url.clone(), + validator.nymd_url.clone(), + validator.api_url.clone(), config.get_mixnet_contract_address(network), config.get_vesting_contract_address(network), config.get_bandwidth_claim_contract_address(network), @@ -284,7 +299,10 @@ async fn try_connect_to_validator( )?; if is_validator_connection_ok(&client).await { - println!("Connection ok for {network}: {nymd_url}, {api_url}"); + println!( + "Connection ok for {network}: {}, {}", + validator.nymd_url, validator.api_url + ); Ok(Some((network, validator.clone()))) } else { Ok(None) diff --git a/nym-wallet/src-tauri/src/state.rs b/nym-wallet/src-tauri/src/state.rs index 61c70d95a0..82da416be5 100644 --- a/nym-wallet/src-tauri/src/state.rs +++ b/nym-wallet/src-tauri/src/state.rs @@ -47,6 +47,10 @@ impl State { pub fn logout(&mut self) { self.signing_clients = HashMap::new(); } + + pub async fn fetch_updated_validator_urls(&mut self) -> Result<(), BackendError> { + self.config.fetch_updated_validator_urls().await + } } #[macro_export]