wallet: fetch validators url remotely if available (#1146)
This commit is contained in:
Generated
+2
@@ -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",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<Item = &ValidatorDetails> + '_ {
|
||||
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<Item = ValidatorWithApiEndpoint> + '_ {
|
||||
self
|
||||
.get_validators(network)
|
||||
.filter_map(|validator| ValidatorWithApiEndpoint::try_from(validator.clone()).ok())
|
||||
}
|
||||
|
||||
pub fn get_mixnet_contract_address(&self, network: WalletNetwork) -> Option<cosmrs::AccountId> {
|
||||
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<ValidatorDetails> for ValidatorWithApiEndpoint {
|
||||
type Error = BackendError;
|
||||
|
||||
fn try_from(validator: ValidatorDetails) -> Result<Self, Self::Error> {
|
||||
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<Vec<ValidatorDetails>>,
|
||||
sandbox: Option<Vec<ValidatorDetails>>,
|
||||
qa: Option<Vec<ValidatorDetails>>,
|
||||
}
|
||||
|
||||
impl Network {
|
||||
impl OptionalValidators {
|
||||
fn validators(&self, network: WalletNetwork) -> impl Iterator<Item = &ValidatorDetails> {
|
||||
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(),
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<RwLock<State>>>,
|
||||
) -> 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<RwLock<State>>>,
|
||||
) -> Result<Account, BackendError> {
|
||||
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<Network, ValidatorWithApiEndpoint>,
|
||||
mnemonic: &Mnemonic,
|
||||
config: &Config,
|
||||
) -> Result<Vec<Client<SigningNymdClient>>, 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<RwLock<State>>>,
|
||||
) -> Result<HashMap<Network, ValidatorWithApiEndpoint>, 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<HashMap<Network, ValidatorDetails>, BackendError> {
|
||||
) -> Result<HashMap<Network, ValidatorWithApiEndpoint>, 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::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
// Filter out networks where we exhausted all listed validators.
|
||||
.flatten()
|
||||
.collect::<HashMap<_, _>>();
|
||||
// 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::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// Filter out networks where we exhausted all listed validators
|
||||
let validators = validators.into_iter().flatten();
|
||||
|
||||
Ok(validators.collect::<HashMap<_, _>>())
|
||||
}
|
||||
|
||||
async fn try_connect_to_validators(
|
||||
validators: impl Iterator<Item = &ValidatorDetails>,
|
||||
validators: impl Iterator<Item = ValidatorWithApiEndpoint>,
|
||||
config: &Config,
|
||||
network: Network,
|
||||
mnemonic: Mnemonic,
|
||||
) -> Result<Option<(Network, ValidatorDetails)>, BackendError> {
|
||||
) -> Result<Option<(Network, ValidatorWithApiEndpoint)>, 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<Option<(Network, ValidatorDetails)>, BackendError> {
|
||||
let nymd_url = validator.nymd_url();
|
||||
let api_url = match validator.api_url() {
|
||||
Some(url) => url,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
) -> Result<Option<(Network, ValidatorWithApiEndpoint)>, 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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user