wallet: add backend support for validator name (#1262)

* wallet: add support for validator nymd name

* changelog: add entry for wallt validator name

* rustfmt

* wallet: keep nymd_name entirely on wallet side

* wallet: lint fixes
This commit is contained in:
Jon Häggblad
2022-05-10 11:26:49 +02:00
committed by GitHub
parent 0f6f47c5ac
commit 04eef83c15
11 changed files with 297 additions and 65 deletions
+1
View File
@@ -4,6 +4,7 @@
### Added
- wallet: the wallet backend learned how to keep track of validator name, either hardcoded or by querying the status endpoint.
- mixnet-contract: Replace all naked `-` with `saturating_sub`.
- validator-api: add Swagger to document the REST API ([#1249]).
- all: added network compilation target to `--help` (or `--version`) commands ([#1256]).
+7
View File
@@ -101,6 +101,13 @@ impl ValidatorDetails {
}
}
pub fn new_with_name(nymd_url: &str, api_url: Option<&str>) -> Self {
ValidatorDetails {
nymd_url: nymd_url.to_string(),
api_url: api_url.map(ToString::to_string),
}
}
pub fn nymd_url(&self) -> Url {
self.nymd_url
.parse()
+1 -1
View File
@@ -21,6 +21,6 @@ pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hh
pub(crate) fn validators() -> Vec<ValidatorDetails> {
vec![ValidatorDetails::new(
"https://rpc.nyx.nodes.guru/",
Some("https://validator.nymtech.net/api"),
Some("https://validator.nymtech.net/api/"),
)]
}
+1
View File
@@ -2965,6 +2965,7 @@ dependencies = [
"itertools",
"log",
"mixnet-contract-common",
"once_cell",
"pretty_env_logger",
"rand 0.6.5",
"reqwest",
+1
View File
@@ -28,6 +28,7 @@ eyre = "0.6.5"
futures = "0.3.15"
itertools = "0.10"
log = "0.4"
once_cell = "1.7.2"
pretty_env_logger = "0.4"
rand = "0.6.5"
reqwest = "0.11.9"
+69 -22
View File
@@ -57,7 +57,7 @@ pub struct NetworkConfig {
// Additional user provided validators.
// It is an option for the purpuse of file serialization.
validator_urls: Option<Vec<ValidatorUrl>>,
validator_urls: Option<Vec<ValidatorConfigEntry>>,
}
impl Default for Base {
@@ -89,7 +89,7 @@ impl Default for NetworkConfig {
}
impl NetworkConfig {
fn validators(&self) -> impl Iterator<Item = &ValidatorUrl> {
fn validators(&self) -> impl Iterator<Item = &ValidatorConfigEntry> {
self.validator_urls.iter().flat_map(|v| v.iter())
}
}
@@ -192,7 +192,7 @@ impl Config {
pub fn get_base_validators(
&self,
network: WalletNetwork,
) -> impl Iterator<Item = ValidatorUrl> + '_ {
) -> impl Iterator<Item = ValidatorConfigEntry> + '_ {
self.base.networks.validators(network.into()).map(|v| {
v.clone()
.try_into()
@@ -203,7 +203,7 @@ impl Config {
pub fn get_configured_validators(
&self,
network: WalletNetwork,
) -> impl Iterator<Item = ValidatorUrl> + '_ {
) -> impl Iterator<Item = ValidatorConfigEntry> + '_ {
self
.networks
.get(&network.as_key())
@@ -272,7 +272,7 @@ impl Config {
}
}
pub fn get_selected_validator_nymd_url(&self, network: &WalletNetwork) -> Option<Url> {
pub fn get_selected_validator_nymd_url(&self, network: WalletNetwork) -> Option<Url> {
self
.networks
.get(&network.as_key())
@@ -286,7 +286,7 @@ impl Config {
.and_then(|config| config.selected_api_url.clone())
}
pub fn add_validator_url(&mut self, url: ValidatorUrl, network: WalletNetwork) {
pub fn add_validator_url(&mut self, url: ValidatorConfigEntry, network: WalletNetwork) {
if let Some(network_config) = self.networks.get_mut(&network.as_key()) {
if let Some(ref mut urls) = network_config.validator_urls {
urls.push(url);
@@ -304,7 +304,7 @@ impl Config {
}
}
pub fn remove_validator_url(&mut self, url: ValidatorUrl, network: WalletNetwork) {
pub fn remove_validator_url(&mut self, url: ValidatorConfigEntry, network: WalletNetwork) {
if let Some(network_config) = self.networks.get_mut(&network.as_key()) {
if let Some(ref mut urls) = network_config.validator_urls {
// Removes duplicates too if there are any
@@ -325,17 +325,19 @@ where
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct ValidatorUrl {
pub struct ValidatorConfigEntry {
pub nymd_url: Url,
pub nymd_name: Option<String>,
pub api_url: Option<Url>,
}
impl TryFrom<ValidatorDetails> for ValidatorUrl {
impl TryFrom<ValidatorDetails> for ValidatorConfigEntry {
type Error = BackendError;
fn try_from(validator: ValidatorDetails) -> Result<Self, Self::Error> {
Ok(ValidatorUrl {
Ok(ValidatorConfigEntry {
nymd_url: validator.nymd_url.parse()?,
nymd_name: None,
api_url: match &validator.api_url {
Some(url) => Some(url.parse()?),
None => None,
@@ -344,12 +346,13 @@ impl TryFrom<ValidatorDetails> for ValidatorUrl {
}
}
impl TryFrom<network_config::Validator> for ValidatorUrl {
impl TryFrom<network_config::Validator> for ValidatorConfigEntry {
type Error = BackendError;
fn try_from(validator: network_config::Validator) -> Result<Self, Self::Error> {
Ok(ValidatorUrl {
Ok(ValidatorConfigEntry {
nymd_url: validator.nymd_url.parse()?,
nymd_name: validator.nymd_name,
api_url: match &validator.api_url {
Some(url) => Some(url.parse()?),
None => None,
@@ -358,14 +361,21 @@ impl TryFrom<network_config::Validator> for ValidatorUrl {
}
}
impl fmt::Display for ValidatorUrl {
impl fmt::Display for ValidatorConfigEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s1 = format!("nymd_url: {}", self.nymd_url);
let name = self.nymd_name.as_ref().map(|name| format!(" ({})", name));
let s2 = self
.api_url
.as_ref()
.map(|url| format!(", api_url: {}", url));
write!(f, " {}{},", s1, s2.unwrap_or_default())
write!(
f,
" {}{}{},",
s1,
name.unwrap_or_default(),
s2.unwrap_or_default()
)
}
}
@@ -374,13 +384,13 @@ impl fmt::Display for ValidatorUrl {
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>>,
sandbox: Option<Vec<ValidatorUrl>>,
qa: Option<Vec<ValidatorUrl>>,
mainnet: Option<Vec<ValidatorConfigEntry>>,
sandbox: Option<Vec<ValidatorConfigEntry>>,
qa: Option<Vec<ValidatorConfigEntry>>,
}
impl OptionalValidators {
pub fn validators(&self, network: WalletNetwork) -> impl Iterator<Item = &ValidatorUrl> {
pub fn validators(&self, network: WalletNetwork) -> impl Iterator<Item = &ValidatorConfigEntry> {
match network {
WalletNetwork::MAINNET => self.mainnet.as_ref(),
WalletNetwork::SANDBOX => self.sandbox.as_ref(),
@@ -422,16 +432,19 @@ mod tests {
selected_api_url: Some("https://my_api_url.com".parse().unwrap()),
validator_urls: Some(vec![
ValidatorUrl {
ValidatorConfigEntry {
nymd_url: "https://foo".parse().unwrap(),
nymd_name: Some("FooName".to_string()),
api_url: None,
},
ValidatorUrl {
ValidatorConfigEntry {
nymd_url: "https://bar".parse().unwrap(),
nymd_name: None,
api_url: Some("https://bar/api".parse().unwrap()),
},
ValidatorUrl {
ValidatorConfigEntry {
nymd_url: "https://baz".parse().unwrap(),
nymd_name: None,
api_url: Some("https://baz/api".parse().unwrap()),
},
]),
@@ -458,6 +471,7 @@ selected_api_url = 'https://my_api_url.com/'
[[validator_urls]]
nymd_url = 'https://foo/'
nymd_name = 'FooName'
[[validator_urls]]
nymd_url = 'https://bar/'
@@ -469,6 +483,39 @@ api_url = 'https://baz/api'
"#
);
}
#[test]
fn serialize_to_json() {
let config = test_config();
let netconfig = &config.networks[&WalletNetwork::MAINNET.as_key()];
println!("{}", serde_json::to_string_pretty(netconfig).unwrap());
assert_eq!(
serde_json::to_string_pretty(netconfig).unwrap(),
r#"{
"version": 1,
"selected_nymd_url": null,
"selected_api_url": "https://my_api_url.com/",
"validator_urls": [
{
"nymd_url": "https://foo/",
"nymd_name": "FooName",
"api_url": null
},
{
"nymd_url": "https://bar/",
"nymd_name": null,
"api_url": "https://bar/api"
},
{
"nymd_url": "https://baz/",
"nymd_name": null,
"api_url": "https://baz/api"
}
]
}"#
);
}
#[test]
fn serialize_and_deserialize_to_toml() {
let config = test_config();
@@ -513,6 +560,6 @@ api_url = 'https://baz/api'
.next()
.and_then(|v| v.api_url)
.unwrap();
assert_eq!(api_url.as_ref(), "https://validator.nymtech.net/api",);
assert_eq!(api_url.as_ref(), "https://validator.nymtech.net/api/",);
}
}
+16 -10
View File
@@ -9,18 +9,30 @@ use serde::{Deserialize, Serialize};
use std::{fmt, sync::Arc};
use tokio::sync::RwLock;
// When the UI queries validator urls we use this type
#[cfg_attr(test, derive(ts_rs::TS))]
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/validatorurls.ts"))]
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct ValidatorUrls {
pub urls: Vec<String>,
pub urls: Vec<ValidatorUrl>,
}
#[cfg_attr(test, derive(ts_rs::TS))]
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/validatorurl.ts"))]
#[derive(Debug, Serialize, Deserialize)]
pub struct ValidatorUrl {
pub url: String,
pub name: Option<String>,
}
// The type used when adding or removing validators, effectively the input.
// NOTE: we should consider if we want to split this up
#[cfg_attr(test, derive(ts_rs::TS))]
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/validatorurls.ts"))]
#[derive(Debug, Serialize, Deserialize)]
pub struct Validator {
pub nymd_url: String,
pub nymd_name: Option<String>,
pub api_url: Option<String>,
}
@@ -42,10 +54,7 @@ pub async fn get_validator_nymd_urls(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<ValidatorUrls, BackendError> {
let state = state.read().await;
let urls: Vec<String> = state
.get_nymd_urls(network)
.map(|url| url.to_string())
.collect();
let urls: Vec<ValidatorUrl> = state.get_nymd_urls(network).collect();
Ok(ValidatorUrls { urls })
}
@@ -55,10 +64,7 @@ pub async fn get_validator_api_urls(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<ValidatorUrls, BackendError> {
let state = state.read().await;
let urls: Vec<String> = state
.get_api_urls(network)
.map(|url| url.to_string())
.collect();
let urls: Vec<ValidatorUrl> = state.get_api_urls(network).collect();
Ok(ValidatorUrls { urls })
}
@@ -169,7 +169,7 @@ async fn _connect_with_mnemonic(
for network in WalletNetwork::iter() {
log::debug!(
"List of validators for {network}: [\n{}\n]",
state.get_validators(network).format(",\n")
state.get_config_validator_entries(network).format(",\n")
);
}
@@ -268,7 +268,7 @@ fn create_clients(
) -> Result<Vec<Client<SigningNymdClient>>, BackendError> {
let mut clients = Vec::new();
for network in WalletNetwork::iter() {
let nymd_url = if let Some(url) = config.get_selected_validator_nymd_url(&network) {
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 {
+192 -24
View File
@@ -1,12 +1,13 @@
use crate::config::{Config, OptionalValidators, ValidatorUrl};
use crate::error::BackendError;
use crate::network::Network;
use crate::{config, network_config};
use strum::IntoEnumIterator;
use validator_client::nymd::SigningNymdClient;
use validator_client::Client;
use itertools::Itertools;
use once_cell::sync::Lazy;
use tokio::sync::RwLock;
use url::Url;
@@ -14,6 +15,16 @@ use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
// Some hardcoded metadata overrides
static METADATA_OVERRIDES: Lazy<Vec<(Url, ValidatorMetadata)>> = Lazy::new(|| {
vec![(
"https://rpc.nyx.nodes.guru/".parse().unwrap(),
ValidatorMetadata {
name: Some("Nodes.Guru".to_string()),
},
)]
});
#[tauri::command]
pub async fn load_config_from_files(
state: tauri::State<'_, Arc<RwLock<State>>>,
@@ -31,12 +42,15 @@ pub async fn save_config_to_files(
#[derive(Default)]
pub struct State {
config: Config,
config: config::Config,
signing_clients: HashMap<Network, Client<SigningNymdClient>>,
current_network: Network,
/// Validators that have been fetched dynamically, probably during startup.
fetched_validators: OptionalValidators,
fetched_validators: config::OptionalValidators,
/// We fetch (and cache) some metadata, such as names, when available
validator_metadata: HashMap<Url, ValidatorMetadata>,
}
impl State {
@@ -72,13 +86,13 @@ impl State {
.ok_or(BackendError::ClientNotInitialized)
}
pub fn config(&self) -> &Config {
pub fn config(&self) -> &config::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();
self.config = config::Config::load_from_files();
}
#[allow(unused)]
@@ -106,37 +120,98 @@ impl State {
/// 1. from the configuration file
/// 2. provided remotely
/// 3. hardcoded fallback
pub fn get_validators(&self, network: Network) -> impl Iterator<Item = ValidatorUrl> + '_ {
/// The format is the config backend format, which is flat due to serialization preference.
pub fn get_config_validator_entries(
&self,
network: Network,
) -> impl Iterator<Item = config::ValidatorConfigEntry> + '_ {
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
// All the validators, in decending list of priority
let validators = validators_in_config
.chain(fetched_validators)
.chain(default_validators)
.unique()
.unique_by(|v| (v.nymd_url.clone(), v.api_url.clone()));
// Annotate with dynamic metadata
validators.map(|v| {
let metadata = self.validator_metadata.get(&v.nymd_url);
let name = v
.nymd_name
.or_else(|| metadata.and_then(|m| m.name.clone()));
config::ValidatorConfigEntry {
nymd_url: v.nymd_url,
nymd_name: name,
api_url: v.api_url,
}
})
}
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> + '_ {
pub fn get_nymd_urls_only(&self, network: Network) -> impl Iterator<Item = Url> + '_ {
self
.get_validators(network)
.get_config_validator_entries(network)
.into_iter()
.map(|v| v.nymd_url)
}
pub fn get_api_urls_only(&self, network: Network) -> impl Iterator<Item = Url> + '_ {
self
.get_config_validator_entries(network)
.into_iter()
.filter_map(|v| v.api_url)
}
/// Get the list of validator nymd urls in the network config format, suitable for passing on to
/// the UI
pub fn get_nymd_urls(
&self,
network: Network,
) -> impl Iterator<Item = network_config::ValidatorUrl> + '_ {
self
.get_config_validator_entries(network)
.into_iter()
.map(|v| network_config::ValidatorUrl {
url: v.nymd_url.to_string(),
name: v.nymd_name,
})
}
/// Get the list of validator-api urls in the network config format, suitable for passing on to
/// the UI
pub fn get_api_urls(
&self,
network: Network,
) -> impl Iterator<Item = network_config::ValidatorUrl> + '_ {
self
.get_config_validator_entries(network)
.into_iter()
.filter_map(|v| {
v.api_url.map(|u| network_config::ValidatorUrl {
url: u.to_string(),
name: None,
})
})
}
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)))
.flat_map(|network| {
self
.get_nymd_urls_only(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)))
.flat_map(|network| {
self
.get_api_urls_only(network)
.map(move |url| (network, url))
})
.into_group_map()
}
@@ -154,11 +229,71 @@ impl State {
.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);
self.refresh_validator_status().await?;
Ok(())
}
pub async fn refresh_validator_status(&mut self) -> Result<(), BackendError> {
log::debug!("Refreshing validator status");
// All urls for all networks
let nymd_urls = self
.get_all_nymd_urls()
.into_iter()
.flat_map(|(_, urls)| urls.into_iter());
// Fetch status for all urls
let responses = fetch_status_for_urls(nymd_urls).await?;
// Update the stored metadata
self.apply_responses(responses)?;
// Override some overrides for usability
self.apply_metadata_override(METADATA_OVERRIDES.to_vec());
Ok(())
}
fn apply_responses(
&mut self,
responses: Vec<Result<(Url, String), reqwest::Error>>,
) -> Result<(), BackendError> {
for response in responses.into_iter().flatten() {
let json: serde_json::Value = serde_json::from_str(&response.1)?;
let moniker = &json["result"]["node_info"]["moniker"];
log::debug!("Fetched moniker for: {}: {}", response.0, moniker);
// Insert into metadata map
if let Some(ref mut m) = self.validator_metadata.get_mut(&response.0) {
m.name = Some(moniker.to_string());
} else {
self.validator_metadata.insert(
response.0,
ValidatorMetadata {
name: Some(moniker.to_string()),
},
);
}
}
Ok(())
}
fn apply_metadata_override(&mut self, metadata_overrides: Vec<(Url, ValidatorMetadata)>) {
for (url, metadata) in metadata_overrides {
log::debug!("Overriding (some) metadata for: {url}");
if let Some(m) = self.validator_metadata.get_mut(&url) {
m.name = metadata.name;
} else {
self.validator_metadata.insert(url, metadata);
}
}
}
pub fn select_validator_nymd_url(
&mut self,
url: &str,
@@ -183,15 +318,41 @@ impl State {
Ok(())
}
pub fn add_validator_url(&mut self, url: ValidatorUrl, network: Network) {
pub fn add_validator_url(&mut self, url: config::ValidatorConfigEntry, network: Network) {
self.config.add_validator_url(url, network);
}
pub fn remove_validator_url(&mut self, url: ValidatorUrl, network: Network) {
pub fn remove_validator_url(&mut self, url: config::ValidatorConfigEntry, network: Network) {
self.config.remove_validator_url(url, network)
}
}
async fn fetch_status_for_urls(
nymd_urls: impl Iterator<Item = Url>,
) -> Result<Vec<Result<(Url, String), reqwest::Error>>, BackendError> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(3))
.build()?;
let responses = futures::future::join_all(nymd_urls.into_iter().map(|url| {
let client = &client;
let status_url = url.join("status").unwrap_or_else(|_| url.clone());
async move {
let resp = client.get(status_url).send().await?;
resp.text().await.map(|text| (url, text))
}
}))
.await;
Ok(responses)
}
// Validator metadata that can by dynamically populated
#[derive(Clone, Debug)]
pub struct ValidatorMetadata {
pub name: Option<String>,
}
#[macro_export]
macro_rules! client {
($state:ident) => {
@@ -223,31 +384,36 @@ mod tests {
let _api_urls = state.get_api_urls(Network::MAINNET).collect::<Vec<_>>();
state.add_validator_url(
ValidatorUrl {
config::ValidatorConfigEntry {
nymd_url: "http://nymd_url.com".parse().unwrap(),
nymd_name: Some("NymdUrl".to_string()),
api_url: Some("http://nymd_url.com/api".parse().unwrap()),
},
Network::MAINNET,
);
state.add_validator_url(
ValidatorUrl {
config::ValidatorConfigEntry {
nymd_url: "http://foo.com".parse().unwrap(),
nymd_name: None,
api_url: None,
},
Network::MAINNET,
);
state.add_validator_url(
ValidatorUrl {
config::ValidatorConfigEntry {
nymd_url: "http://bar.com".parse().unwrap(),
nymd_name: None,
api_url: None,
},
Network::MAINNET,
);
assert_eq!(
state.get_nymd_urls(Network::MAINNET).collect::<Vec<_>>(),
state
.get_nymd_urls_only(Network::MAINNET)
.collect::<Vec<_>>(),
vec![
"http://nymd_url.com/".parse().unwrap(),
"http://foo.com".parse().unwrap(),
@@ -256,10 +422,12 @@ mod tests {
],
);
assert_eq!(
state.get_api_urls(Network::MAINNET).collect::<Vec<_>>(),
state
.get_api_urls_only(Network::MAINNET)
.collect::<Vec<_>>(),
vec![
"http://nymd_url.com/api".parse().unwrap(),
"https://validator.nymtech.net/api".parse().unwrap(),
"https://validator.nymtech.net/api/".parse().unwrap(),
],
);
assert_eq!(
@@ -0,0 +1,4 @@
export interface ValidatorUrl {
url: string;
name: string | null;
}
+3 -6
View File
@@ -1,8 +1,5 @@
export interface ValidatorUrls {
urls: Array<string>;
}
import type { ValidatorUrl } from './validatorurl';
export interface Validator {
nymd_url: string;
api_url: string | null;
export interface ValidatorUrls {
urls: Array<ValidatorUrl>;
}