From 160328a08ea8270f70e5b18ecd203a840daa5a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 10 Mar 2022 07:38:39 +0100 Subject: [PATCH] wallet: try validators one by one if available (#1143) --- .../validator-client/src/client.rs | 2 +- .../src/nymd/cosmwasm_client/mod.rs | 2 +- .../nymd/cosmwasm_client/signing_client.rs | 2 +- .../validator-client/src/nymd/mod.rs | 4 +- common/network-defaults/src/all.rs | 22 +-- nym-wallet/Cargo.lock | 1 + nym-wallet/src-tauri/Cargo.toml | 1 + nym-wallet/src-tauri/src/config/mod.rs | 39 ++-- nym-wallet/src-tauri/src/error.rs | 17 +- nym-wallet/src-tauri/src/network.rs | 38 ++-- .../src/operations/mixnet/account.rs | 173 ++++++++++++++---- 11 files changed, 204 insertions(+), 97 deletions(-) diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index dd45439d7a..69fb9f132b 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -92,7 +92,7 @@ impl Config { #[cfg(feature = "nymd-client")] pub struct Client { - network: network_defaults::all::Network, + pub network: network_defaults::all::Network, mixnet_contract_address: Option, vesting_contract_address: Option, erc20_bridge_contract_address: Option, diff --git a/common/client-libs/validator-client/src/nymd/cosmwasm_client/mod.rs b/common/client-libs/validator-client/src/nymd/cosmwasm_client/mod.rs index fcc92bb893..2af7b52704 100644 --- a/common/client-libs/validator-client/src/nymd/cosmwasm_client/mod.rs +++ b/common/client-libs/validator-client/src/nymd/cosmwasm_client/mod.rs @@ -21,7 +21,7 @@ where } // maybe the wallet could be made into a generic, but for now, let's just have this one implementation -pub fn connect_with_signer( +pub fn connect_with_signer( endpoint: U, signer: DirectSecp256k1HdWallet, gas_price: GasPrice, diff --git a/common/client-libs/validator-client/src/nymd/cosmwasm_client/signing_client.rs b/common/client-libs/validator-client/src/nymd/cosmwasm_client/signing_client.rs index 42d62971a9..d3163aae6e 100644 --- a/common/client-libs/validator-client/src/nymd/cosmwasm_client/signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/cosmwasm_client/signing_client.rs @@ -641,7 +641,7 @@ pub struct Client { } impl Client { - pub fn connect_with_signer( + pub fn connect_with_signer( endpoint: U, signer: DirectSecp256k1HdWallet, gas_price: GasPrice, diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index 1a3e49090a..df5079d2c4 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -83,7 +83,7 @@ impl NymdClient { impl NymdClient { // maybe the wallet could be made into a generic, but for now, let's just have this one implementation - pub fn connect_with_signer( + pub fn connect_with_signer( network: config::defaults::all::Network, endpoint: U, mixnet_contract_address: Option, @@ -114,7 +114,7 @@ impl NymdClient { }) } - pub fn connect_with_mnemonic( + pub fn connect_with_mnemonic( network: config::defaults::all::Network, endpoint: U, mixnet_contract_address: Option, diff --git a/common/network-defaults/src/all.rs b/common/network-defaults/src/all.rs index 5c7b0952c4..032142f30d 100644 --- a/common/network-defaults/src/all.rs +++ b/common/network-defaults/src/all.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fmt, ops::Deref, str::FromStr}; +use std::{collections::HashMap, fmt, str::FromStr}; use crate::{ DefaultNetworkDetails, ValidatorDetails, MAINNET_DEFAULTS, QA_DEFAULTS, SANDBOX_DEFAULTS, @@ -98,7 +98,7 @@ pub struct NetworkDetails { } impl From<&DefaultNetworkDetails<'_>> for NetworkDetails { - fn from(details: &DefaultNetworkDetails) -> Self { + fn from(details: &DefaultNetworkDetails<'_>) -> Self { NetworkDetails { bech32_prefix: details.bech32_prefix.into(), denom: details.denom.into(), @@ -118,20 +118,12 @@ pub struct SupportedNetworks { impl SupportedNetworks { pub fn new(support: Vec) -> Self { - let mut networks = HashMap::new(); - - for network in support { - match network { - Network::MAINNET => { - networks.insert(Network::MAINNET, MAINNET_DEFAULTS.deref().into()) - } - Network::SANDBOX => { - networks.insert(Network::SANDBOX, SANDBOX_DEFAULTS.deref().into()) - } - Network::QA => networks.insert(Network::QA, QA_DEFAULTS.deref().into()), - }; + SupportedNetworks { + networks: support + .into_iter() + .map(|n| (n, n.details().into())) + .collect(), } - SupportedNetworks { networks } } pub fn bech32_prefix(&self, network: Network) -> Option<&str> { diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 6d32667dee..b2255e3824 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -2816,6 +2816,7 @@ dependencies = [ "credentials", "dirs", "eyre", + "futures", "mixnet-contract-common", "rand 0.6.5", "serde", diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 2c2d13521d..255d35c9d8 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -22,6 +22,7 @@ tauri-macros = "=1.0.0-rc.1" bip39 = "1.0" dirs = "4.0" eyre = "0.6.5" +futures = "0.3.15" rand = "0.6.5" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/nym-wallet/src-tauri/src/config/mod.rs b/nym-wallet/src-tauri/src/config/mod.rs index a15d8bc0d1..7b22614822 100644 --- a/nym-wallet/src-tauri/src/config/mod.rs +++ b/nym-wallet/src-tauri/src/config/mod.rs @@ -7,7 +7,6 @@ use config::NymConfig; use serde::{Deserialize, Serialize}; use std::{fs, io, path::PathBuf}; use strum::IntoEnumIterator; -use url::Url; #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde(deny_unknown_fields)] @@ -87,27 +86,14 @@ impl NymConfig for Config { } impl Config { - pub fn get_nymd_validator_url(&self, network: WalletNetwork) -> Option { - // TODO: for now we pick the first one + pub fn get_validators( + &self, + network: WalletNetwork, + ) -> impl Iterator + '_ { self .network .validators(network) .chain(self.base.networks.validators(network.into())) - .map(|x| x.nymd_url()) - .next() - } - - pub fn get_validator_api_url(&self, network: WalletNetwork) -> Option { - // TODO: for now we pick the first one - // NOTE: we are explictly picking the API URL for the first configured validator, not the first - // API URL out of all validators. - self - .network - .validators(network) - .chain(self.base.networks.validators(network.into())) - .map(|x| x.api_url()) - .next() - .flatten() } pub fn get_mixnet_contract_address(&self, network: WalletNetwork) -> Option { @@ -223,12 +209,17 @@ api_url = 'https://bar/api' let config = test_config(); let nymd_url = config - .get_nymd_validator_url(WalletNetwork::MAINNET) + .get_validators(WalletNetwork::MAINNET) + .next() + .map(ValidatorDetails::nymd_url) .unwrap(); assert_eq!(nymd_url.to_string(), "https://foo/".to_string()); // The first entry is missing an API URL - let api_url = config.get_validator_api_url(WalletNetwork::MAINNET); + let api_url = config + .get_validators(WalletNetwork::MAINNET) + .next() + .and_then(ValidatorDetails::api_url); assert_eq!(api_url, None); } @@ -237,7 +228,9 @@ api_url = 'https://bar/api' let config = Config::default(); let nymd_url = config - .get_nymd_validator_url(WalletNetwork::MAINNET) + .get_validators(WalletNetwork::MAINNET) + .next() + .map(ValidatorDetails::nymd_url) .unwrap(); assert_eq!( nymd_url.to_string(), @@ -245,7 +238,9 @@ api_url = 'https://bar/api' ); let api_url = config - .get_validator_api_url(WalletNetwork::MAINNET) + .get_validators(WalletNetwork::MAINNET) + .next() + .and_then(ValidatorDetails::api_url) .unwrap(); assert_eq!( api_url.to_string(), diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index 9dd15a603e..adfd88c37c 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -1,8 +1,8 @@ use serde::{Serialize, Serializer}; use std::io; use thiserror::Error; -use validator_client::nymd::error::NymdError; use validator_client::validator_api::error::ValidatorAPIError; +use validator_client::{nymd::error::NymdError, ValidatorClientError}; #[derive(Error, Debug)] pub enum BackendError { @@ -51,6 +51,11 @@ pub enum BackendError { #[from] source: serde_json::Error, }, + #[error("{source}")] + MalformedUrlProvided { + #[from] + source: url::ParseError, + }, #[error("failed to encrypt the given data with the provided password")] EncryptionError, #[error("failed to decrypt the given data with the provided password")] @@ -81,3 +86,13 @@ impl Serialize for BackendError { serializer.collect_str(self) } } + +impl From for BackendError { + fn from(e: ValidatorClientError) -> Self { + match e { + ValidatorClientError::ValidatorAPIError { source } => source.into(), + ValidatorClientError::MalformedUrlProvided(e) => e.into(), + ValidatorClientError::NymdError(e) => e.into(), + } + } +} diff --git a/nym-wallet/src-tauri/src/network.rs b/nym-wallet/src-tauri/src/network.rs index d6a9bc8bd0..5202b70d52 100644 --- a/nym-wallet/src-tauri/src/network.rs +++ b/nym-wallet/src-tauri/src/network.rs @@ -3,11 +3,10 @@ use cosmrs::Denom; use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; +use std::fmt; use std::str::FromStr; use strum::EnumIter; -use crate::error::BackendError; use config::defaults::all::Network as ConfigNetwork; use config::defaults::{mainnet, qa, sandbox}; @@ -38,25 +37,28 @@ impl Default for Network { } } -#[allow(clippy::from_over_into)] -impl Into for Network { - fn into(self) -> ConfigNetwork { - match self { +impl fmt::Display for Network { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl From for Network { + fn from(network: ConfigNetwork) -> Self { + match network { + ConfigNetwork::QA => Network::QA, + ConfigNetwork::SANDBOX => Network::SANDBOX, + ConfigNetwork::MAINNET => Network::MAINNET, + } + } +} + +impl From for ConfigNetwork { + fn from(network: Network) -> Self { + match network { Network::QA => ConfigNetwork::QA, Network::SANDBOX => ConfigNetwork::SANDBOX, Network::MAINNET => ConfigNetwork::MAINNET, } } } - -impl TryFrom for Network { - type Error = BackendError; - - fn try_from(value: ConfigNetwork) -> Result { - match value { - ConfigNetwork::QA => Ok(Network::QA), - ConfigNetwork::SANDBOX => Ok(Network::SANDBOX), - ConfigNetwork::MAINNET => Ok(Network::MAINNET), - } - } -} diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index c8d06b0fe2..a90f486486 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -1,16 +1,22 @@ use crate::coin::{Coin, Denom}; +use crate::config::Config; 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::convert::{TryFrom, TryInto}; +use std::collections::HashMap; +use std::convert::TryInto; use std::str::FromStr; use std::sync::Arc; use strum::IntoEnumIterator; use tokio::sync::RwLock; +use validator_client::nymd::error::NymdError; +use validator_client::nymd::SigningNymdClient; +use validator_client::Client; #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr(test, ts(export, export_to = "../src/types/rust/account.ts"))] @@ -132,46 +138,141 @@ async fn _connect_with_mnemonic( mnemonic: Mnemonic, state: tauri::State<'_, Arc>>, ) -> Result { - let default_network = Network::try_from(config::defaults::DEFAULT_NETWORK)?; - let mut default_account = None; + let default_network: Network = config::defaults::DEFAULT_NETWORK.into(); + 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() { - let client = { - let config = state.read().await.config(); - let nymd_url = config - .get_nymd_validator_url(network) - .ok_or(BackendError::NoNymdValidatorConfigured)?; - let api_url = config - .get_validator_api_url(network) - .ok_or(BackendError::NoValidatorApiUrlConfigured)?; - match validator_client::Client::new_signing( - validator_client::Config::new( - network.into(), - nymd_url, - api_url, - config.get_mixnet_contract_address(network), - config.get_vesting_contract_address(network), - config.get_bandwidth_claim_contract_address(network), - ), - mnemonic.clone(), - ) { - Ok(client) => client, - Err(e) => panic!("{}", e), - } - }; + validators + .entry(network) + // We always have at least one hardcoded defalt validator + .or_insert_with(|| config.get_validators(network).next().unwrap().clone()); + } - if network == default_network { - default_account = Some(Account::new( - client.nymd.mixnet_contract_address()?.to_string(), - client.nymd.address().to_string(), - network.denom().try_into()?, - )); - } + // 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); + } + // Set the default account + let client_for_default_network = clients + .iter() + .find(|client| Network::from(client.network) == default_network); + let account_for_default_network = match client_for_default_network { + Some(client) => Ok(Account::new( + client.nymd.mixnet_contract_address()?.to_string(), + client.nymd.address().to_string(), + default_network.denom().try_into()?, + )), + None => Err(BackendError::NetworkNotSupported( + config::defaults::DEFAULT_NETWORK, + )), + }; + + // Register all the clients + for client in clients { + let network: Network = client.network.into(); let mut w_state = state.write().await; w_state.add_client(network, client); } - default_account.ok_or(BackendError::NetworkNotSupported( - config::defaults::DEFAULT_NETWORK, - )) + account_for_default_network +} + +// For each network, try the list of available validators one by one and use the first responding +// one. +async fn select_validators( + config: &Config, + mnemonic: &Mnemonic, +) -> Result, BackendError> { + let validators = futures::future::join_all(Network::iter().map(|network| { + try_connect_to_validators( + config.get_validators(network), + config, + network, + mnemonic.clone(), + ) + })) + .await; + + // Collect for the purpose of returning any errors encountered + let validators = validators.into_iter().collect::, _>>()?; + + // Filter out networks we were not able to connect to + Ok(validators.into_iter().flatten().collect::>()) +} + +async fn try_connect_to_validators( + validators: impl Iterator, + config: &Config, + network: Network, + mnemonic: Mnemonic, +) -> Result, BackendError> { + for validator in validators { + if let Some(responding_validator) = + try_connect_to_validator(validator, config, network, mnemonic.clone()).await? + { + // Pick the first successful one + return Ok(Some(responding_validator)); + } + } + Ok(None) +} + +async fn try_connect_to_validator( + validator: &ValidatorDetails, + 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), + }; + + let client = validator_client::Client::new_signing( + validator_client::Config::new( + network.into(), + nymd_url.clone(), + api_url.clone(), + config.get_mixnet_contract_address(network), + config.get_vesting_contract_address(network), + config.get_bandwidth_claim_contract_address(network), + ), + mnemonic, + )?; + + if is_validator_connection_ok(&client).await { + println!("Connection ok for {network}: {nymd_url}, {api_url}"); + Ok(Some((network, validator.clone()))) + } else { + Ok(None) + } +} + +// The criteria used to determina if a validator endpoint is to be used +async fn is_validator_connection_ok(client: &Client) -> bool { + match client.get_mixnet_contract_version().await { + Err(NymdError::TendermintError(_)) => false, + Err(_) | Ok(_) => true, + } }