wallet: try validators one by one if available (#1143)

This commit is contained in:
Jon Häggblad
2022-03-10 07:38:39 +01:00
committed by GitHub
parent 9fea869bbc
commit 160328a08e
11 changed files with 204 additions and 97 deletions
@@ -92,7 +92,7 @@ impl Config {
#[cfg(feature = "nymd-client")]
pub struct Client<C> {
network: network_defaults::all::Network,
pub network: network_defaults::all::Network,
mixnet_contract_address: Option<cosmrs::AccountId>,
vesting_contract_address: Option<cosmrs::AccountId>,
erc20_bridge_contract_address: Option<cosmrs::AccountId>,
@@ -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<U>(
pub fn connect_with_signer<U: Clone>(
endpoint: U,
signer: DirectSecp256k1HdWallet,
gas_price: GasPrice,
@@ -641,7 +641,7 @@ pub struct Client {
}
impl Client {
pub fn connect_with_signer<U>(
pub fn connect_with_signer<U: Clone>(
endpoint: U,
signer: DirectSecp256k1HdWallet,
gas_price: GasPrice,
@@ -83,7 +83,7 @@ impl NymdClient<QueryNymdClient> {
impl NymdClient<SigningNymdClient> {
// maybe the wallet could be made into a generic, but for now, let's just have this one implementation
pub fn connect_with_signer<U>(
pub fn connect_with_signer<U: Clone>(
network: config::defaults::all::Network,
endpoint: U,
mixnet_contract_address: Option<AccountId>,
@@ -114,7 +114,7 @@ impl NymdClient<SigningNymdClient> {
})
}
pub fn connect_with_mnemonic<U>(
pub fn connect_with_mnemonic<U: Clone>(
network: config::defaults::all::Network,
endpoint: U,
mixnet_contract_address: Option<AccountId>,
+7 -15
View File
@@ -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<Network>) -> 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> {
+1
View File
@@ -2816,6 +2816,7 @@ dependencies = [
"credentials",
"dirs",
"eyre",
"futures",
"mixnet-contract-common",
"rand 0.6.5",
"serde",
+1
View File
@@ -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"
+17 -22
View File
@@ -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<Url> {
// TODO: for now we pick the first one
pub fn get_validators(
&self,
network: WalletNetwork,
) -> impl Iterator<Item = &ValidatorDetails> + '_ {
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<Url> {
// 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<cosmrs::AccountId> {
@@ -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(),
+16 -1
View File
@@ -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<ValidatorClientError> for BackendError {
fn from(e: ValidatorClientError) -> Self {
match e {
ValidatorClientError::ValidatorAPIError { source } => source.into(),
ValidatorClientError::MalformedUrlProvided(e) => e.into(),
ValidatorClientError::NymdError(e) => e.into(),
}
}
}
+20 -18
View File
@@ -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<ConfigNetwork> 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<ConfigNetwork> for Network {
fn from(network: ConfigNetwork) -> Self {
match network {
ConfigNetwork::QA => Network::QA,
ConfigNetwork::SANDBOX => Network::SANDBOX,
ConfigNetwork::MAINNET => Network::MAINNET,
}
}
}
impl From<Network> for ConfigNetwork {
fn from(network: Network) -> Self {
match network {
Network::QA => ConfigNetwork::QA,
Network::SANDBOX => ConfigNetwork::SANDBOX,
Network::MAINNET => ConfigNetwork::MAINNET,
}
}
}
impl TryFrom<ConfigNetwork> for Network {
type Error = BackendError;
fn try_from(value: ConfigNetwork) -> Result<Self, Self::Error> {
match value {
ConfigNetwork::QA => Ok(Network::QA),
ConfigNetwork::SANDBOX => Ok(Network::SANDBOX),
ConfigNetwork::MAINNET => Ok(Network::MAINNET),
}
}
}
@@ -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<RwLock<State>>>,
) -> Result<Account, BackendError> {
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<HashMap<Network, ValidatorDetails>, 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::<Result<Vec<_>, _>>()?;
// Filter out networks we were not able to connect to
Ok(validators.into_iter().flatten().collect::<HashMap<_, _>>())
}
async fn try_connect_to_validators(
validators: impl Iterator<Item = &ValidatorDetails>,
config: &Config,
network: Network,
mnemonic: Mnemonic,
) -> Result<Option<(Network, ValidatorDetails)>, 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<Option<(Network, ValidatorDetails)>, 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<SigningNymdClient>) -> bool {
match client.get_mixnet_contract_version().await {
Err(NymdError::TendermintError(_)) => false,
Err(_) | Ok(_) => true,
}
}