Compare commits

...

21 Commits

Author SHA1 Message Date
Bogdan-Ștefan Neacșu c835ab24fd Bump some versions for helios integration 2023-03-02 17:53:09 +02:00
farbanas 36fb0eba29 Update versions as part of release/v1.1.11 2023-02-28 13:53:14 +01:00
farbanas 1b37e85418 Update changelogs as part of release/v1.1.11 2023-02-28 13:43:32 +01:00
Bogdan-Ștefan Neacşu 6a93497c8f Extend public key submission in case no dealer registered (#3106) 2023-02-28 12:58:01 +01:00
Tommy Verrall ffbd76539a Merge pull request #3108 from nymtech/feature/nym-connect-select-sp
Feature/nym connect select sp
2023-02-27 17:48:31 +02:00
fmtabbara bdcc19e86a PR update 2023-02-27 15:33:54 +00:00
fmtabbara 7929bac685 turn off user defined SP address is input it empty 2023-02-27 14:20:16 +00:00
Jon Häggblad f590aad42c nym-api: uptime rework (#3053)
* nym-api: cache updates as node performance

* nym-api: update get mixnode avg_uptime endpoint

* nym-api: mixnode report to use cached data

* nym-api: annotate gateway bond with node performance

* nym-api: gateway report to use cached data

* wip

* Add get_gateway_avg_uptime

* Add comment

* update NR gateways to include node_performance on frontend

* use node_performance values on frontend

* fixup select gateway from list

* fix up lint errors

---------

Co-authored-by: fmtabbara <fmtabbara@hotmail.co.uk>
2023-02-27 12:40:00 +01:00
fmtabbara ec23f3dcb7 disable input when connected
fix lint errors
2023-02-27 11:28:38 +00:00
fmtabbara 9644eb4329 pick service provider 2023-02-27 11:28:38 +00:00
fmtabbara 7a4c6e4ed4 storage type
update types
2023-02-27 11:28:36 +00:00
fmtabbara d5ad504104 reuseable storage functions 2023-02-27 11:28:36 +00:00
Tommy Verrall d684f6d7ae Merge pull request #3099 from nymtech/feature/nym-connect-select-sp
NymConnect Select a service provider
2023-02-27 12:26:00 +02:00
Bogdan-Ștefan Neacşu f0d9703587 Feature/fix db name collision (#3103)
* Fix db naming collision

* Increase gateway timeout to accommodate bandwidth spending time
2023-02-24 16:52:56 +02:00
fmtabbara c08efef8ed add autofocus to IdentityKeyComponent 2023-02-23 22:58:59 +00:00
fmtabbara 6d29774744 style tweaks 2023-02-23 22:57:21 +00:00
fmtabbara af6bab7703 fix lint error 2023-02-23 17:40:39 +00:00
fmtabbara 7033f92d82 add UI for picking service provider 2023-02-23 17:33:03 +00:00
Jędrzej Stuczyński 128dfa6d81 Feature/latency based gateway selection (#3081)
* wip

* new option to select gateways based on latency

* further changes for wasm-compatibility

* post rebase fixes + clippy

I know, I should have probably included them properly during rebasing ¯\_(ツ)_/¯

* android change

* wasm: the gift that keeps on giving
2023-02-23 17:09:22 +00:00
Bogdan-Ștefan Neacşu 0b0ec075bb Use saturating_sub with an additional 1 second buffer (#3095) 2023-02-23 13:44:54 +01:00
Tommy Verrall cca4d21e7c Merge pull request #3097 from nymtech/feature/update-checker-to-use-master
Feature/update checker to use master
2023-02-23 14:24:59 +02:00
73 changed files with 872 additions and 265 deletions
+20
View File
@@ -4,6 +4,26 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [v1.0.11] (2023-02-28)
- Fix empty dealer set loop ([#3105])
- The nym-api db.sqlite is broken when trying to run against it it in `enabled-credentials-mode true` there is an ordering issue with migrations when using the credential binary to purchase bandwidth ([#3100])
- Feature/latency based gateway selection ([#3081])
- Fix the credential binary to handle transactions to sleep when in non-inProgress epochs ([#3057])
- Publish mixnet contract to crates.io ([#1919])
- Publish vesting contract to crates.io ([#1920])
- Feature/update checker to use master ([#3097])
- Feature/improve binary checks ([#3094])
[#3105]: https://github.com/nymtech/nym/issues/3105
[#3100]: https://github.com/nymtech/nym/issues/3100
[#3081]: https://github.com/nymtech/nym/pull/3081
[#3057]: https://github.com/nymtech/nym/issues/3057
[#1919]: https://github.com/nymtech/nym/issues/1919
[#1920]: https://github.com/nymtech/nym/issues/1920
[#3097]: https://github.com/nymtech/nym/pull/3097
[#3094]: https://github.com/nymtech/nym/pull/3094
## [v1.1.10] (2023-02-21)
- Verloc listener causing mixnode unexpected shutdown ([#3038])
Generated
+12 -10
View File
@@ -677,7 +677,7 @@ dependencies = [
[[package]]
name = "client-core"
version = "1.1.10"
version = "1.1.11"
dependencies = [
"async-trait",
"dashmap 5.4.0",
@@ -705,6 +705,8 @@ dependencies = [
"time 0.3.17",
"tokio",
"tokio-stream",
"tokio-tungstenite 0.14.0",
"tungstenite 0.13.0",
"url",
"validator-client",
"wasm-bindgen",
@@ -1800,7 +1802,7 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "explorer-api"
version = "1.1.10"
version = "1.1.11"
dependencies = [
"chrono",
"clap 4.1.4",
@@ -3279,7 +3281,7 @@ dependencies = [
[[package]]
name = "nym-api"
version = "1.1.11"
version = "1.1.12"
dependencies = [
"anyhow",
"async-trait",
@@ -3389,7 +3391,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.1.10"
version = "1.1.11"
dependencies = [
"anyhow",
"base64 0.13.1",
@@ -3447,7 +3449,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.1.10"
version = "1.1.11"
dependencies = [
"clap 4.1.4",
"client-core",
@@ -3541,7 +3543,7 @@ dependencies = [
[[package]]
name = "nym-gateway"
version = "1.1.10"
version = "1.1.11"
dependencies = [
"anyhow",
"async-trait",
@@ -3618,7 +3620,7 @@ dependencies = [
[[package]]
name = "nym-mixnode"
version = "1.1.11"
version = "1.1.12"
dependencies = [
"anyhow",
"atty",
@@ -3673,7 +3675,7 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "1.1.10"
version = "1.1.11"
dependencies = [
"async-trait",
"clap 4.1.4",
@@ -3711,7 +3713,7 @@ dependencies = [
[[package]]
name = "nym-network-statistics"
version = "1.1.10"
version = "1.1.11"
dependencies = [
"dirs",
"log",
@@ -3786,7 +3788,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.1.10"
version = "1.1.11"
dependencies = [
"clap 4.1.4",
"client-core",
+6 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "client-core"
version = "1.1.10"
version = "1.1.11"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
rust-version = "1.66"
@@ -20,6 +20,7 @@ serde_json = "1.0.89"
tap = "1.0.1"
thiserror = "1.0.34"
url = { version ="2.2", features = ["serde"] }
tungstenite = { version = "0.13.0", default-features = false }
tokio = { version = "1.24.1", features = ["macros"]}
time = "0.3.17"
@@ -44,6 +45,9 @@ features = ["time"]
version = "1.24.1"
features = ["time"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-tungstenite]
version = "0.14"
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
version = "0.6.2"
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
@@ -65,6 +69,7 @@ features = ["futures"]
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-utils]
path = "../../common/wasm-utils"
features = ["websocket"]
[target."cfg(target_arch = \"wasm32\")".dependencies.time]
version = "0.3.17"
@@ -304,7 +304,7 @@ where
}
let gateway_address = self.gateway_config.gateway_listener.clone();
if gateway_address.is_empty() {
return Err(ClientCoreError::GatwayAddressUnknown);
return Err(ClientCoreError::GatewayAddressUnknown);
}
let gateway_identity = identity::PublicKey::from_base58_string(gateway_id)
+2 -2
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use nym_config::defaults::NymNetworkDetails;
use nym_config::{NymConfig, OptionalSet, DB_FILE_NAME};
use nym_config::{NymConfig, OptionalSet, CRED_DB_FILE_NAME};
use nym_sphinx::params::PacketSize;
use serde::{Deserialize, Serialize};
use std::marker::PhantomData;
@@ -579,7 +579,7 @@ impl<T: NymConfig> Client<T> {
}
fn default_database_path(id: &str) -> PathBuf {
T::default_data_directory(id).join(DB_FILE_NAME)
T::default_data_directory(id).join(CRED_DB_FILE_NAME)
}
}
+27 -1
View File
@@ -3,6 +3,7 @@
use gateway_client::error::GatewayClientError;
use nym_crypto::asymmetric::identity::Ed25519RecoveryError;
use nym_topology::gateway::GatewayConversionError;
use nym_topology::NymTopologyError;
use validator_client::ValidatorClientError;
@@ -53,7 +54,32 @@ pub enum ClientCoreError {
GatewayOwnerUnknown,
#[error("The address of the gateway is unknown - did you run init?")]
GatwayAddressUnknown,
GatewayAddressUnknown,
#[error("The gateway is malformed: {source}")]
MalformedGateway {
#[from]
source: GatewayConversionError,
},
#[error("failed to establish connection to gateway: {source}")]
GatewayConnectionFailure {
#[from]
source: tungstenite::Error,
},
#[cfg(target_arch = "wasm32")]
#[error("failed to establish gateway connection (wasm)")]
GatewayJsConnectionFailure,
#[error("Gateway connection was abruptly closed")]
GatewayConnectionAbruptlyClosed,
#[error("Timed out while trying to establish gateway connection")]
GatewayConnectionTimeout,
#[error("No ping measurements for the gateway ({identity}) performed")]
NoGatewayMeasurements { identity: String },
#[error("failed to register receiver for reconstructed mixnet messages")]
FailedToRegisterReceiver,
+195 -24
View File
@@ -6,52 +6,223 @@ use crate::{
config::{persistence::key_pathfinder::ClientKeyPathfinder, Config},
error::ClientCoreError,
};
#[cfg(target_arch = "wasm32")]
use gateway_client::wasm_mockups::SigningNyxdClient;
use futures::{SinkExt, StreamExt};
use gateway_client::GatewayClient;
use gateway_requests::registration::handshake::SharedKeys;
use log::{debug, info, trace, warn};
use nym_config::NymConfig;
use nym_crypto::asymmetric::identity;
use nym_topology::{filter::VersionFilterable, gateway};
use rand::{seq::SliceRandom, thread_rng};
use rand::{seq::SliceRandom, thread_rng, Rng};
use std::{sync::Arc, time::Duration};
use tap::TapFallible;
use tungstenite::Message;
use url::Url;
#[cfg(not(target_arch = "wasm32"))]
use tokio::net::TcpStream;
#[cfg(not(target_arch = "wasm32"))]
use tokio::time::Instant;
#[cfg(not(target_arch = "wasm32"))]
use tokio_tungstenite::connect_async;
#[cfg(not(target_arch = "wasm32"))]
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
#[cfg(not(target_arch = "wasm32"))]
use validator_client::nyxd::SigningNyxdClient;
pub(super) async fn query_gateway_details(
validator_servers: Vec<Url>,
chosen_gateway_id: Option<identity::PublicKey>,
) -> Result<gateway::Node, ClientCoreError> {
let nym_api = validator_servers
.choose(&mut thread_rng())
#[cfg(not(target_arch = "wasm32"))]
type WsConn = WebSocketStream<MaybeTlsStream<TcpStream>>;
#[cfg(target_arch = "wasm32")]
use gateway_client::wasm_mockups::SigningNyxdClient;
#[cfg(target_arch = "wasm32")]
use wasm_timer::Instant;
#[cfg(target_arch = "wasm32")]
use wasm_utils::websocket::JSWebsocket;
#[cfg(target_arch = "wasm32")]
type WsConn = JSWebsocket;
const MEASUREMENTS: usize = 3;
#[cfg(not(target_arch = "wasm32"))]
const CONN_TIMEOUT: Duration = Duration::from_millis(1500);
const PING_TIMEOUT: Duration = Duration::from_millis(1000);
struct GatewayWithLatency {
gateway: gateway::Node,
latency: Duration,
}
impl GatewayWithLatency {
fn new(gateway: gateway::Node, latency: Duration) -> Self {
GatewayWithLatency { gateway, latency }
}
}
async fn current_gateways<R: Rng>(
rng: &mut R,
nym_apis: Vec<Url>,
) -> Result<Vec<gateway::Node>, ClientCoreError> {
let nym_api = nym_apis
.choose(rng)
.ok_or(ClientCoreError::ListOfNymApisIsEmpty)?;
let validator_client = validator_client::client::NymApiClient::new(nym_api.clone());
let client = validator_client::client::NymApiClient::new(nym_api.clone());
log::trace!("Fetching list of gateways from: {}", nym_api);
let gateways = validator_client.get_cached_gateways().await?;
let gateways = client.get_cached_gateways().await?;
let valid_gateways = gateways
.into_iter()
.filter_map(|gateway| gateway.try_into().ok())
.collect::<Vec<gateway::Node>>();
// we were always filtering by version so I'm not removing that 'feature'
let filtered_gateways = valid_gateways.filter_by_version(env!("CARGO_PKG_VERSION"));
Ok(filtered_gateways)
}
// if we have chosen particular gateway - use it, otherwise choose a random one.
// (remember that in active topology all gateways have at least 100 reputation so should
// be working correctly)
if let Some(gateway_id) = chosen_gateway_id {
filtered_gateways
.iter()
.find(|gateway| gateway.identity_key == gateway_id)
.ok_or_else(|| ClientCoreError::NoGatewayWithId(gateway_id.to_string()))
.cloned()
#[cfg(not(target_arch = "wasm32"))]
async fn connect(endpoint: &str) -> Result<WsConn, ClientCoreError> {
match tokio::time::timeout(CONN_TIMEOUT, connect_async(endpoint)).await {
Err(_elapsed) => Err(ClientCoreError::GatewayConnectionTimeout),
Ok(Err(conn_failure)) => Err(conn_failure.into()),
Ok(Ok((stream, _))) => Ok(stream),
}
}
#[cfg(target_arch = "wasm32")]
async fn connect(endpoint: &str) -> Result<WsConn, ClientCoreError> {
JSWebsocket::new(endpoint).map_err(|_| ClientCoreError::GatewayJsConnectionFailure)
}
async fn measure_latency(gateway: gateway::Node) -> Result<GatewayWithLatency, ClientCoreError> {
let addr = gateway.clients_address();
trace!(
"establishing connection to {} ({addr})...",
gateway.identity_key,
);
let mut stream = connect(&addr).await?;
let mut results = Vec::new();
for _ in 0..MEASUREMENTS {
let measurement_future = async {
let ping_content = vec![1, 2, 3];
let start = Instant::now();
stream.send(Message::Ping(ping_content.clone())).await?;
match stream.next().await {
Some(Ok(Message::Pong(content))) => {
if content == ping_content {
let elapsed = Instant::now().duration_since(start);
trace!("current ping time: {elapsed:?}");
results.push(elapsed);
} else {
warn!("received a pong message with different content? wtf.")
}
}
Some(Ok(_)) => warn!("received a message that's not a pong!"),
Some(Err(err)) => return Err(err.into()),
None => return Err(ClientCoreError::GatewayConnectionAbruptlyClosed),
}
Ok::<(), ClientCoreError>(())
};
// thanks to wasm we can't use tokio::time::timeout : (
#[cfg(not(target_arch = "wasm32"))]
let timeout = tokio::time::sleep(PING_TIMEOUT);
#[cfg(not(target_arch = "wasm32"))]
tokio::pin!(timeout);
#[cfg(target_arch = "wasm32")]
let mut timeout = wasm_timer::Delay::new(PING_TIMEOUT);
tokio::select! {
_ = &mut timeout => {
warn!("timed out while trying to perform measurement...")
}
res = measurement_future => res?,
}
}
let count = results.len() as u64;
if count == 0 {
return Err(ClientCoreError::NoGatewayMeasurements {
identity: gateway.identity_key.to_base58_string(),
});
}
let sum: Duration = results.into_iter().sum();
let avg = Duration::from_nanos(sum.as_nanos() as u64 / count);
Ok(GatewayWithLatency::new(gateway, avg))
}
async fn choose_gateway_by_latency<R: Rng>(
rng: &mut R,
gateways: Vec<gateway::Node>,
) -> Result<gateway::Node, ClientCoreError> {
info!("choosing gateway by latency...");
let mut gateways_with_latency = Vec::new();
for gateway in gateways {
let id = *gateway.identity();
trace!("measuring latency to {id}...");
let with_latency = match measure_latency(gateway).await {
Ok(res) => res,
Err(err) => {
warn!("failed to measure {id}: {err}");
continue;
}
};
debug!(
"{id} ({}): {:?}",
with_latency.gateway.location, with_latency.latency
);
gateways_with_latency.push(with_latency)
}
let chosen = gateways_with_latency
.choose_weighted(rng, |item| 1. / item.latency.as_secs_f32())
.expect("invalid selection weight!");
info!(
"chose gateway {} (located at {}) with average latency of {:?}",
chosen.gateway.identity_key, chosen.gateway.location, chosen.latency
);
Ok(chosen.gateway.clone())
}
fn uniformly_random_gateway<R: Rng>(
rng: &mut R,
gateways: Vec<gateway::Node>,
) -> Result<gateway::Node, ClientCoreError> {
gateways
.choose(rng)
.ok_or(ClientCoreError::NoGatewaysOnNetwork)
.cloned()
}
pub(super) async fn query_gateway_details(
validator_servers: Vec<Url>,
chosen_gateway_id: Option<identity::PublicKey>,
by_latency: bool,
) -> Result<gateway::Node, ClientCoreError> {
let mut rng = thread_rng();
let gateways = current_gateways(&mut rng, validator_servers).await?;
// if we set an explicit gateway, use that one and nothing else
if let Some(explicitly_chosen) = chosen_gateway_id {
gateways
.into_iter()
.find(|gateway| gateway.identity_key == explicitly_chosen)
.ok_or_else(|| ClientCoreError::NoGatewayWithId(explicitly_chosen.to_string()))
} else if by_latency {
choose_gateway_by_latency(&mut rng, gateways).await
} else {
filtered_gateways
.choose(&mut rand::thread_rng())
.ok_or(ClientCoreError::NoGatewaysOnNetwork)
.cloned()
uniformly_random_gateway(&mut rng, gateways)
}
}
+10 -4
View File
@@ -77,9 +77,11 @@ pub async fn register_with_gateway(
key_manager: &mut KeyManager,
nym_api_endpoints: Vec<Url>,
chosen_gateway_id: Option<identity::PublicKey>,
by_latency: bool,
) -> Result<GatewayEndpointConfig, ClientCoreError> {
// Get the gateway details of the gateway we will use
let gateway = helpers::query_gateway_details(nym_api_endpoints, chosen_gateway_id).await?;
let gateway =
helpers::query_gateway_details(nym_api_endpoints, chosen_gateway_id, by_latency).await?;
log::debug!("Querying gateway gives: {}", gateway);
let our_identity = key_manager.identity_keypair();
@@ -102,6 +104,7 @@ pub async fn setup_gateway_from_config<C, T>(
register_gateway: bool,
user_chosen_gateway_id: Option<identity::PublicKey>,
config: &Config<T>,
by_latency: bool,
) -> Result<GatewayEndpointConfig, ClientCoreError>
where
C: NymConfig + ClientCoreConfigTrait,
@@ -117,9 +120,12 @@ where
}
// Else, we preceed by querying the nym-api
let gateway =
helpers::query_gateway_details(config.get_nym_api_endpoints(), user_chosen_gateway_id)
.await?;
let gateway = helpers::query_gateway_details(
config.get_nym_api_endpoints(),
user_chosen_gateway_id,
by_latency,
)
.await?;
log::debug!("Querying gateway gives: {}", gateway);
// If we are not registering, just return this and assume the caller has the keys already and
+10 -4
View File
@@ -11,7 +11,7 @@ use commands::*;
use error::Result;
use log::*;
use nym_bin_common::completions::fig_generate;
use nym_config::{DATA_DIR, DB_FILE_NAME};
use nym_config::{CRED_DB_FILE_NAME, DATA_DIR};
use nym_network_defaults::{setup_env, NymNetworkDetails};
use std::process::exit;
use std::time::{Duration, SystemTime};
@@ -51,8 +51,11 @@ async fn block_until_coconut_is_available<C: Clone + CosmWasmClient + Send + Syn
break;
} else {
// Use 20 additional seconds to avoid the exact moment of going into the final epoch state
let secs_until_final = epoch.final_timestamp_secs() + 20 - current_timestamp_secs;
// Use 1 additional second to not start the next iteration immediately and spam get_current_epoch queries
let secs_until_final = epoch
.final_timestamp_secs()
.saturating_sub(current_timestamp_secs)
+ 1;
info!("Approximately {} seconds until coconut is available. Sleeping until then. You can safely kill the process at any moment.", secs_until_final);
std::thread::sleep(Duration::from_secs(secs_until_final));
}
@@ -70,7 +73,10 @@ async fn main() -> Result<()> {
match args.command {
Command::Run(r) => {
let db_path = r.client_home_directory.join(DATA_DIR).join(DB_FILE_NAME);
let db_path = r
.client_home_directory
.join(DATA_DIR)
.join(CRED_DB_FILE_NAME);
let shared_storage = credential_storage::initialise_storage(db_path).await;
let recovery_storage = recovery_storage::RecoveryStorage::new(r.recovery_dir)?;
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.10"
version = "1.1.11"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+6
View File
@@ -25,6 +25,11 @@ pub(crate) struct Init {
#[clap(long)]
gateway: Option<identity::PublicKey>,
/// Specifies whether the new gateway should be determined based by latency as opposed to being chosen
/// uniformly.
#[clap(long, conflicts_with = "gateway")]
latency_based_selection: bool,
/// Force register gateway. WARNING: this will overwrite any existing keys for the given id,
/// potentially causing loss of access.
#[clap(long)]
@@ -143,6 +148,7 @@ pub(crate) async fn execute(args: &Init) -> Result<(), ClientError> {
register_gateway,
user_chosen_gateway_id,
config.get_base(),
args.latency_based_selection,
)
.await
.tap_err(|err| eprintln!("Failed to setup gateway\nError: {err}"))?;
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.10"
version = "1.1.11"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
+6
View File
@@ -37,6 +37,11 @@ pub(crate) struct Init {
#[clap(long)]
gateway: Option<identity::PublicKey>,
/// Specifies whether the new gateway should be determined based by latency as opposed to being chosen
/// uniformly.
#[clap(long, conflicts_with = "gateway")]
latency_based_selection: bool,
/// Force register gateway. WARNING: this will overwrite any existing keys for the given id,
/// potentially causing loss of access.
#[clap(long)]
@@ -149,6 +154,7 @@ pub(crate) async fn execute(args: &Init) -> Result<(), Socks5ClientError> {
register_gateway,
user_chosen_gateway_id,
config.get_base(),
args.latency_based_selection,
)
.await
.tap_err(|err| eprintln!("Failed to setup gateway\nError: {err}"))?;
@@ -36,9 +36,9 @@ nym-api-requests = { path = "../../../nym-api/nym-api-requests" }
# at some point it might be possible to make it wasm-compatible
# perhaps after https://github.com/cosmos/cosmos-rust/pull/97 is resolved (and tendermint-rs is updated)
async-trait = { version = "0.1.51", optional = true }
bip39 = { version = "1", features = ["rand"], optional = true }
bip39 = { version = "2", features = ["rand"], optional = true }
nym-config = { path = "../../config", optional = true }
cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support", features = ["rpc", "bip32", "cosmwasm"], optional = true}
cosmrs = { version = "0.8.0", features = ["rpc", "bip32", "cosmwasm"], optional = true}
cw3 = { version = "0.13.4", optional = true }
cw4 = { version = "0.13.4", optional = true }
prost = { version = "0.10", default-features = false, optional = true }
@@ -11,9 +11,7 @@ use nym_api_requests::models::{
GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse,
RewardEstimationResponse, StakeSaturationResponse,
};
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
use nym_mixnet_contract_common::MixId;
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef};
pub use nym_mixnet_contract_common::{mixnode::MixNodeDetails, GatewayBond, IdentityKeyRef, MixId};
#[cfg(feature = "nyxd-client")]
use crate::nyxd::traits::{DkgQueryClient, MixnetQueryClient, MultisigQueryClient};
@@ -121,7 +121,7 @@ impl GasAdjustable for Gas {
mod sealed {
use cosmrs::tx::{self, Gas};
use cosmrs::Coin as CosmosCoin;
use cosmrs::{AccountId, Decimal as CosmosDecimal, Denom as CosmosDenom};
use cosmrs::{AccountId, Denom as CosmosDenom};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
fn cosmos_denom_inner_getter(val: &CosmosDenom) -> String {
@@ -138,29 +138,11 @@ mod sealed {
}
}
fn cosmos_decimal_inner_getter(val: &CosmosDecimal) -> u64 {
// haha, this code is so disgusting. I'll make a PR on cosmrs to slightly alleviate those issues...
// note: unwrap here is fine as the to_string is just returning a stringified u64 which, well, is a valid u64
val.to_string().parse().unwrap()
}
// at the time of writing it the current cosmrs' Decimal is extremely limited...
#[derive(Serialize, Deserialize)]
#[serde(remote = "CosmosDecimal")]
struct Decimal(#[serde(getter = "cosmos_decimal_inner_getter")] u64);
impl From<Decimal> for CosmosDecimal {
fn from(val: Decimal) -> Self {
val.0.into()
}
}
#[derive(Serialize, Deserialize, Clone)]
struct Coin {
#[serde(with = "Denom")]
denom: CosmosDenom,
#[serde(with = "Decimal")]
amount: CosmosDecimal,
amount: u128,
}
impl From<Coin> for CosmosCoin {
@@ -42,7 +42,7 @@ pub use cosmrs::tendermint::validator::Info as TendermintValidatorInfo;
pub use cosmrs::tendermint::Time as TendermintTime;
pub use cosmrs::tx::{self, Gas};
pub use cosmrs::Coin as CosmosCoin;
pub use cosmrs::{bip32, AccountId, Decimal, Denom};
pub use cosmrs::{bip32, AccountId, Denom};
pub use cosmwasm_std::Coin as CosmWasmCoin;
pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment};
pub use signing_client::Client as SigningNyxdClient;
+1 -1
View File
@@ -25,7 +25,7 @@ toml = "0.5.6"
url = "2.2"
tap = "1"
cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support" }
cosmrs = "0.8.0"
cosmwasm-std = { version = "1.0.0" }
validator-client = { path = "../client-libs/validator-client", features = ["nyxd-client"] }
+1 -1
View File
@@ -18,7 +18,7 @@ pub mod defaults;
pub const CONFIG_DIR: &str = "config";
pub const DATA_DIR: &str = "data";
pub const DB_FILE_NAME: &str = "db.sqlite";
pub const CRED_DB_FILE_NAME: &str = "credentials_database.db";
pub trait NymConfig: Default + Serialize + DeserializeOwned {
fn template() -> &'static str;
+1 -1
View File
@@ -7,7 +7,7 @@ edition = "2021"
[dependencies]
bls12_381 = { version = "0.5", default-features = false, features = ["pairings", "alloc", "experimental"] }
cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support" }
cosmrs = "0.8.0"
thiserror = "1.0"
# I guess temporarily until we get serde support in coconut up and running
+1 -1
View File
@@ -20,7 +20,7 @@ url = "2.2"
ts-rs = "6.1.2"
cosmwasm-std = "1.0.0"
cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support" }
cosmrs = "0.8.0"
validator-client = { path = "../../common/client-libs/validator-client", features = [
"nyxd-client",
@@ -80,7 +80,7 @@ pub(crate) mod tests {
use crate::epoch_state::transactions::advance_epoch_state;
use crate::support::tests::fixtures::dealer_details_fixture;
use crate::support::tests::helpers;
use crate::support::tests::helpers::GROUP_MEMBERS;
use crate::support::tests::helpers::{add_fixture_dealer, GROUP_MEMBERS};
use coconut_dkg_common::types::{InitialReplacementData, TimeConfiguration};
use cosmwasm_std::testing::{mock_env, mock_info};
use cw4::Member;
@@ -147,6 +147,8 @@ pub(crate) mod tests {
.block
.time
.plus_seconds(TimeConfiguration::default().public_key_submission_time_secs);
add_fixture_dealer(deps.as_mut());
advance_epoch_state(deps.as_mut(), env).unwrap();
let ret = try_add_dealer(
@@ -53,6 +53,7 @@ pub(crate) mod tests {
use crate::epoch_state::transactions::advance_epoch_state;
use crate::support::tests::fixtures::{dealer_details_fixture, dealing_bytes_fixture};
use crate::support::tests::helpers;
use crate::support::tests::helpers::add_fixture_dealer;
use coconut_dkg_common::dealer::DealerDetails;
use coconut_dkg_common::types::{InitialReplacementData, TimeConfiguration};
use cosmwasm_std::testing::{mock_env, mock_info};
@@ -80,6 +81,7 @@ pub(crate) mod tests {
.block
.time
.plus_seconds(TimeConfiguration::default().public_key_submission_time_secs);
add_fixture_dealer(deps.as_mut());
advance_epoch_state(deps.as_mut(), env).unwrap();
let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone(), false)
@@ -89,23 +89,29 @@ pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Respons
let current_epoch = CURRENT_EPOCH.load(deps.storage)?;
let next_epoch = if let Some(state) = current_epoch.state.next() {
// We are during DKG process
let mut new_state = state;
if let EpochState::DealingExchange { resharing } = state {
let current_dealers = current_dealers()
.keys(deps.storage, None, None, Order::Ascending)
.collect::<Result<Vec<Addr>, _>>()?;
// note: ceiling in integer division can be achieved via q = (x + y - 1) / y;
let threshold = (2 * current_dealers.len() as u64 + 3 - 1) / 3;
THRESHOLD.save(deps.storage, &threshold)?;
if !resharing {
let replacement_data = InitialReplacementData {
initial_dealers: current_dealers,
initial_height: None,
};
INITIAL_REPLACEMENT_DATA.save(deps.storage, &replacement_data)?;
if current_dealers.is_empty() {
// If no dealer registered yet, we just stay in the same state until there's at least one
new_state = current_epoch.state;
} else {
// note: ceiling in integer division can be achieved via q = (x + y - 1) / y;
let threshold = (2 * current_dealers.len() as u64 + 3 - 1) / 3;
THRESHOLD.save(deps.storage, &threshold)?;
if !resharing {
let replacement_data = InitialReplacementData {
initial_dealers: current_dealers,
initial_height: None,
};
INITIAL_REPLACEMENT_DATA.save(deps.storage, &replacement_data)?;
}
}
}
};
Epoch::new(
state,
new_state,
current_epoch.epoch_id,
current_epoch.time_configuration,
env.block.time,
@@ -392,6 +398,14 @@ pub(crate) mod tests {
EarlyEpochStateAdvancement(1)
);
env.block.time = env.block.time.plus_seconds(1);
advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
assert_eq!(
epoch.state,
EpochState::PublicKeySubmission { resharing: false }
);
// setup dealer details
let all_details: [_; 4] = std::array::from_fn(|i| dealer_details_fixture(i as u64 + 1));
for details in all_details.iter() {
@@ -404,7 +418,7 @@ pub(crate) mod tests {
.may_load(&deps.storage)
.unwrap()
.is_none());
env.block.time = env.block.time.plus_seconds(1);
env.block.time = env.block.time.plus_seconds(epoch.time_configuration.public_key_submission_time_secs);
advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
assert_eq!(
@@ -2,11 +2,13 @@
// SPDX-License-Identifier: Apache-2.0
use crate::contract::instantiate;
use crate::dealers::storage::current_dealers;
use coconut_dkg_common::msg::InstantiateMsg;
use coconut_dkg_common::types::DealerDetails;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier};
use cosmwasm_std::{
from_binary, to_binary, ContractResult, Empty, MemoryStorage, OwnedDeps, QuerierResult,
SystemResult, WasmQuery,
from_binary, to_binary, Addr, ContractResult, DepsMut, Empty, MemoryStorage, OwnedDeps,
QuerierResult, SystemResult, WasmQuery,
};
use cw4::{Cw4QueryMsg, Member, MemberListResponse, MemberResponse};
use lazy_static::lazy_static;
@@ -22,6 +24,22 @@ lazy_static! {
pub static ref GROUP_MEMBERS: Mutex<Vec<(Member, u64)>> = Mutex::new(vec![]);
}
pub fn add_fixture_dealer(deps: DepsMut<'_>) {
let owner = Addr::unchecked("owner");
current_dealers()
.save(
deps.storage,
&owner,
&DealerDetails {
address: owner.clone(),
bte_public_key_with_proof: String::new(),
announce_address: String::new(),
assigned_index: 100,
},
)
.unwrap();
}
fn querier_handler(query: &WasmQuery) -> QuerierResult {
let bin = match query {
WasmQuery::Smart { contract_addr, msg } => {
@@ -91,7 +91,7 @@ mod tests {
use super::*;
use crate::epoch_state::transactions::advance_epoch_state;
use crate::support::tests::helpers;
use crate::support::tests::helpers::MULTISIG_CONTRACT;
use crate::support::tests::helpers::{add_fixture_dealer, MULTISIG_CONTRACT};
use coconut_dkg_common::dealer::DealerDetails;
use coconut_dkg_common::types::{EpochState, TimeConfiguration};
use cosmwasm_std::testing::{mock_env, mock_info};
@@ -104,6 +104,7 @@ mod tests {
let info = mock_info("requester", &[]);
let share = "share".to_string();
add_fixture_dealer(deps.as_mut());
env.block.time = env
.block
.time
@@ -171,6 +172,7 @@ mod tests {
.to_string()
}
);
add_fixture_dealer(deps.as_mut());
env.block.time = env
.block
.time
@@ -247,6 +249,7 @@ mod tests {
}
);
add_fixture_dealer(deps.as_mut());
env.block.time = env
.block
.time
@@ -292,6 +295,7 @@ mod tests {
let share = "share".to_string();
let multisig_info = mock_info(MULTISIG_CONTRACT, &[]);
add_fixture_dealer(deps.as_mut());
env.block.time = env
.block
.time
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "explorer-api"
version = "1.1.10"
version = "1.1.11"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@nym/network-explorer",
"version": "1.0.5",
"version": "1.0.6",
"private": true,
"license": "Apache-2.0",
"dependencies": {
@@ -121,4 +121,4 @@
"last 1 safari version"
]
}
}
}
+2 -3
View File
@@ -29,7 +29,6 @@ import {
GatewayBondAnnotated,
GatewayBond,
} from '../typeDefs/explorer-api';
import { toPercentIntegerString } from '../utils';
function getFromCache(key: string) {
const ts = Number(localStorage.getItem('ts'));
@@ -94,9 +93,9 @@ export class Api {
static fetchGateways = async (): Promise<GatewayBond[]> => {
const res = await fetch(GATEWAYS_API);
const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json();
return gatewaysAnnotated.map(({ gateway_bond, performance }) => ({
return gatewaysAnnotated.map(({ gateway_bond, node_performance }) => ({
...gateway_bond,
performance: toPercentIntegerString(performance),
node_performance,
}));
};
+4 -3
View File
@@ -1,4 +1,5 @@
import { GatewayResponse, GatewayBond, GatewayReportResponse } from '../typeDefs/explorer-api';
import { toPercentIntegerString } from '../utils';
export type GatewayRowType = {
id: string;
@@ -8,7 +9,7 @@ export type GatewayRowType = {
host: string;
location: string;
version: string;
performance: string;
node_performance: string;
};
export type GatewayEnrichedRowType = GatewayRowType & {
@@ -29,7 +30,7 @@ export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowTy
bond: gw.pledge_amount.amount || 0,
host: gw.gateway.host || '',
version: gw.gateway.version || '',
performance: gw.performance,
node_performance: toPercentIntegerString(gw.node_performance.last_24h),
}));
}
@@ -46,6 +47,6 @@ export function gatewayEnrichedToGridRow(gateway: GatewayBond, report: GatewayRe
mixPort: gateway.gateway.mix_port || 0,
routingScore: `${report.most_recent}%`,
avgUptime: `${report.last_day || report.last_hour}%`,
performance: gateway.performance,
node_performance: toPercentIntegerString(gateway.node_performance.most_recent),
};
}
@@ -38,7 +38,7 @@ const CustomPagination = () => {
color="primary"
count={state.pagination.pageCount}
page={state.pagination.page + 1}
onChange={(event, value) => apiRef.current.setPage(value - 1)}
onChange={(_, value) => apiRef.current.setPage(value - 1)}
/>
);
};
+3 -3
View File
@@ -26,7 +26,7 @@ const columns: ColumnsType[] = [
headerAlign: 'left',
},
{
field: 'routingScore',
field: 'node_performance',
title: 'Routing Score',
flex: 1,
headerAlign: 'left',
@@ -130,13 +130,13 @@ const PageGatewayDetailsWithState = ({ selectedGateway }: { selectedGateway: Gat
* Guard component to handle loading and not found states
*/
const PageGatewayDetailGuard: FCWithChildren = () => {
const [selectedGateway, setSelectedGateway] = React.useState<GatewayBond | undefined>();
const [selectedGateway, setSelectedGateway] = React.useState<GatewayBond>();
const { gateways } = useMainContext();
const { id } = useParams<{ id: string | undefined }>();
React.useEffect(() => {
if (gateways?.data) {
setSelectedGateway(gateways.data.find((gateway) => gateway.gateway.identity_key === id));
setSelectedGateway(gateways.data.find((g) => g.gateway.identity_key === id));
}
}, [gateways, id]);
+8 -11
View File
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Link as RRDLink } from 'react-router-dom';
import { Box, Button, Card, Grid, Link as MuiLink } from '@mui/material';
import { Box, Card, Grid, Link as MuiLink } from '@mui/material';
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
import { SelectChangeEvent } from '@mui/material/Select';
@@ -86,7 +86,6 @@ export const PageGateways: FCWithChildren = () => {
const columns: GridColDef[] = [
{
field: 'identity_key',
headerName: 'Identity Key',
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
headerClassName: 'MuiDataGrid-header-override',
width: 380,
@@ -119,7 +118,7 @@ export const PageGateways: FCWithChildren = () => {
<MuiLink
sx={{ ...cellStyles }}
component={RRDLink}
to={`/network-components/gateway/${params.row.identityKey}`}
to={`/network-components/gateway/${params.row.identity_key}`}
data-testid="pledge-amount"
>
{unymToNym(params.value, 6)}
@@ -127,8 +126,7 @@ export const PageGateways: FCWithChildren = () => {
),
},
{
field: 'performance',
headerName: 'Routing Score',
field: 'node_performance',
renderHeader: () => <CustomColumnHeading headingTitle="Routing Score" />,
width: 150,
headerAlign: 'left',
@@ -137,7 +135,7 @@ export const PageGateways: FCWithChildren = () => {
<MuiLink
sx={{ ...cellStyles }}
component={RRDLink}
to={`/network-components/gateway/${params.row.identityKey}`}
to={`/network-components/gateway/${params.row.identity_key}`}
data-testid="pledge-amount"
>
{`${params.value}%`}
@@ -154,7 +152,7 @@ export const PageGateways: FCWithChildren = () => {
<MuiLink
sx={{ ...cellStyles }}
component={RRDLink}
to={`/network-components/gateway/${params.row.identityKey}`}
to={`/network-components/gateway/${params.row.identity_key}`}
data-testid="host"
>
{params.value}
@@ -168,9 +166,9 @@ export const PageGateways: FCWithChildren = () => {
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<Button
<Box
onClick={() => handleSearch(params.value as string)}
sx={{ ...cellStyles, justifyContent: 'flex-start' }}
sx={{ ...cellStyles, justifyContent: 'flex-start', cursor: 'pointer' }}
data-testid="location-button"
>
<Tooltip text={params.value} id="gateway-location-text">
@@ -184,7 +182,7 @@ export const PageGateways: FCWithChildren = () => {
{params.value}
</Box>
</Tooltip>
</Button>
</Box>
),
},
{
@@ -207,7 +205,6 @@ export const PageGateways: FCWithChildren = () => {
},
{
field: 'version',
headerName: 'Version',
renderHeader: () => <CustomColumnHeading headingTitle="Version" />,
width: 150,
headerAlign: 'left',
+8 -2
View File
@@ -114,6 +114,12 @@ export interface StatsResponse {
packets_explicitly_dropped_since_last_update: number;
}
export interface NodePerformance {
most_recent: string;
last_hour: string;
last_24h: string;
}
export type MixNodeHistoryResponse = StatsResponse;
export interface GatewayBond {
@@ -122,12 +128,12 @@ export interface GatewayBond {
total_delegation: Amount;
owner: string;
gateway: Gateway;
performance: string;
node_performance: NodePerformance;
}
export interface GatewayBondAnnotated {
gateway_bond: GatewayBond;
performance: string;
node_performance: NodePerformance;
}
export type GatewayResponse = GatewayBond[];
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-gateway"
version = "1.1.10"
version = "1.1.11"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
+1 -1
View File
@@ -11,7 +11,7 @@ thiserror = "1.0"
k256 = { version = "0.10", features = ["ecdsa", "sha256"] }
eyre = "0.6.5"
cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support" }
cosmrs = "0.8.0"
nym-cli-commands = { path = "../../common/commands" }
validator-client = { path = "../../common/client-libs/validator-client", features = [
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-mixnode"
version = "1.1.11"
version = "1.1.12"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-api"
version = "1.1.11"
version = "1.1.12"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
+1 -1
View File
@@ -7,7 +7,7 @@ edition = "2021"
[dependencies]
bs58 = "0.4.0"
cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support" }
cosmrs = "0.8.0"
cosmwasm-std = { version = "1.0.0", default-features = false }
getset = "0.1.1"
schemars = { version = "0.8", features = ["preserve_order"] }
+40 -1
View File
@@ -1,7 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{Coin, Decimal};
use cosmwasm_std::{Addr, Coin, Decimal};
use nym_mixnet_contract_common::families::FamilyHead;
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams};
@@ -93,12 +93,21 @@ pub struct MixnodeStatusResponse {
pub status: MixnodeStatus,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct NodePerformance {
pub most_recent: Performance,
pub last_hour: Performance,
pub last_24h: Performance,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct MixNodeBondAnnotated {
pub mixnode_details: MixNodeDetails,
pub stake_saturation: StakeSaturation,
pub uncapped_stake_saturation: StakeSaturation,
// NOTE: the performance field is deprecated in favour of node_performance
pub performance: Performance,
pub node_performance: NodePerformance,
pub estimated_operator_apy: Decimal,
pub estimated_delegators_apy: Decimal,
pub family: Option<FamilyHead>,
@@ -112,12 +121,32 @@ impl MixNodeBondAnnotated {
pub fn mix_id(&self) -> MixId {
self.mixnode_details.mix_id()
}
pub fn identity_key(&self) -> &str {
self.mixnode_details.bond_information.identity()
}
pub fn owner(&self) -> &Addr {
self.mixnode_details.bond_information.owner()
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct GatewayBondAnnotated {
pub gateway_bond: GatewayBond,
// NOTE: the performance field is deprecated in favour of node_performance
pub performance: Performance,
pub node_performance: NodePerformance,
}
impl GatewayBondAnnotated {
pub fn identity(&self) -> &String {
self.gateway_bond.identity()
}
pub fn owner(&self) -> &Addr {
self.gateway_bond.owner()
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -147,7 +176,17 @@ pub struct RewardEstimationResponse {
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct UptimeResponse {
pub mix_id: MixId,
// The same as node_performance.last_24h. Legacy
pub avg_uptime: u8,
pub node_performance: NodePerformance,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct GatewayUptimeResponse {
pub identity: String,
// The same as node_performance.last_24h. Legacy
pub avg_uptime: u8,
pub node_performance: NodePerformance,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
+27 -4
View File
@@ -1,6 +1,6 @@
use crate::node_status_api::reward_estimate::{compute_apy_from_reward, compute_reward_estimate};
use crate::support::storage::NymApiStorage;
use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated};
use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodePerformance};
use nym_mixnet_contract_common::families::FamilyHead;
use nym_mixnet_contract_common::{reward_params::Performance, Interval, MixId};
use nym_mixnet_contract_common::{
@@ -99,16 +99,15 @@ pub(super) async fn annotate_nodes_with_details(
.rewarding_details
.uncapped_bond_saturation(&interval_reward_params);
let rewarded_set_status = rewarded_set.get(&mixnode.mix_id()).copied();
// If the performance can't be obtained, because the nym-api was not started with
// the monitoring (and hence, storage), then reward estimates will be all zero
let performance =
get_mixnode_performance_from_storage(storage, mixnode.mix_id(), current_interval)
.await
.unwrap_or_default();
let rewarded_set_status = rewarded_set.get(&mixnode.mix_id()).copied();
let reward_estimate = compute_reward_estimate(
&mixnode,
performance,
@@ -117,6 +116,17 @@ pub(super) async fn annotate_nodes_with_details(
current_interval,
);
let node_performance = if let Some(storage) = storage {
storage
.construct_mixnode_report(mixnode.mix_id())
.await
.map(NodePerformance::from)
.ok()
} else {
None
}
.unwrap_or_default();
let (estimated_operator_apy, estimated_delegators_apy) =
compute_apy_from_reward(&mixnode, reward_estimate, current_interval);
@@ -129,6 +139,7 @@ pub(super) async fn annotate_nodes_with_details(
stake_saturation,
uncapped_stake_saturation,
performance,
node_performance,
estimated_operator_apy,
estimated_delegators_apy,
family,
@@ -152,9 +163,21 @@ pub(crate) async fn annotate_gateways_with_details(
.await
.unwrap_or_default();
let node_performance = if let Some(storage) = storage {
storage
.construct_gateway_report(gateway_bond.identity())
.await
.map(NodePerformance::from)
.ok()
} else {
None
}
.unwrap_or_default();
annotated.push(GatewayBondAnnotated {
gateway_bond,
performance,
node_performance,
});
}
annotated
+115 -37
View File
@@ -8,26 +8,112 @@ use crate::{NodeStatusCache, NymContractCache};
use cosmwasm_std::Decimal;
use nym_api_requests::models::{
AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayBondAnnotated,
InclusionProbabilityResponse, MixNodeBondAnnotated, MixnodeCoreStatusResponse,
MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse,
RewardEstimationResponse, StakeSaturationResponse, UptimeResponse,
GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse,
GatewayUptimeResponse, InclusionProbabilityResponse, MixNodeBondAnnotated,
MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse,
MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse,
UptimeResponse,
};
use nym_mixnet_contract_common::reward_params::Performance;
use nym_mixnet_contract_common::{Interval, MixId, RewardedSetNodeStatus};
use nym_mixnet_contract_common::{MixId, RewardedSetNodeStatus};
use rocket::http::Status;
use rocket::State;
use super::reward_estimate::compute_reward_estimate;
pub(crate) async fn _mixnode_report(
async fn get_gateway_bond_annotated(
cache: &NodeStatusCache,
identity: &str,
) -> Result<GatewayBondAnnotated, ErrorResponse> {
let gateways = cache.gateways_annotated().await.ok_or(ErrorResponse::new(
"no data available",
Status::ServiceUnavailable,
))?;
gateways
.into_inner()
.into_iter()
.find(|gateway| gateway.identity() == identity)
.ok_or(ErrorResponse::new(
"mixnode bond not found",
Status::NotFound,
))
}
async fn get_mixnode_bond_annotated(
cache: &NodeStatusCache,
mix_id: MixId,
) -> Result<MixNodeBondAnnotated, ErrorResponse> {
let mixnodes = cache.mixnodes_annotated().await.ok_or(ErrorResponse::new(
"no data available",
Status::ServiceUnavailable,
))?;
mixnodes
.into_inner()
.into_iter()
.find(|mixnode| mixnode.mix_id() == mix_id)
.ok_or(ErrorResponse::new(
"mixnode bond not found",
Status::NotFound,
))
}
pub(crate) async fn _gateway_report(
cache: &NodeStatusCache,
identity: &str,
) -> Result<GatewayStatusReportResponse, ErrorResponse> {
let gateway = get_gateway_bond_annotated(cache, identity).await?;
Ok(GatewayStatusReportResponse {
identity: gateway.identity().to_owned(),
owner: gateway.owner().to_string(),
most_recent: gateway.node_performance.most_recent.round_to_integer(),
last_hour: gateway.node_performance.last_hour.round_to_integer(),
last_day: gateway.node_performance.last_24h.round_to_integer(),
})
}
pub(crate) async fn _gateway_uptime_history(
storage: &NymApiStorage,
identity: &str,
) -> Result<GatewayUptimeHistoryResponse, ErrorResponse> {
storage
.get_gateway_uptime_history(identity)
.await
.map(GatewayUptimeHistoryResponse::from)
.map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))
}
pub(crate) async fn _gateway_core_status_count(
storage: &State<NymApiStorage>,
identity: &str,
since: Option<i64>,
) -> Result<GatewayCoreStatusResponse, ErrorResponse> {
let count = storage
.get_core_gateway_status_count(identity, since)
.await
.map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))?;
Ok(GatewayCoreStatusResponse {
identity: identity.to_string(),
count,
})
}
pub(crate) async fn _mixnode_report(
cache: &NodeStatusCache,
mix_id: MixId,
) -> Result<MixnodeStatusReportResponse, ErrorResponse> {
storage
.construct_mixnode_report(mix_id)
.await
.map(MixnodeStatusReportResponse::from)
.map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))
let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?;
Ok(MixnodeStatusReportResponse {
mix_id,
identity: mixnode.identity_key().to_owned(),
owner: mixnode.owner().to_string(),
most_recent: mixnode.node_performance.most_recent.round_to_integer(),
last_hour: mixnode.node_performance.last_hour.round_to_integer(),
last_day: mixnode.node_performance.last_24h.round_to_integer(),
})
}
pub(crate) async fn _mixnode_uptime_history(
@@ -103,21 +189,6 @@ pub(crate) async fn _get_mixnode_reward_estimation(
}
}
async fn average_mixnode_performance(
mix_id: MixId,
current_interval: Interval,
storage: &NymApiStorage,
) -> Result<Performance, ErrorResponse> {
storage
.get_average_mixnode_uptime_in_the_last_24hrs(
mix_id,
current_interval.current_epoch_end_unix_timestamp(),
)
.await
.map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))
.map(Into::into)
}
pub(crate) async fn _compute_mixnode_reward_estimation(
user_reward_param: ComputeRewardEstParam,
cache: &NodeStatusCache,
@@ -254,21 +325,28 @@ pub(crate) async fn _get_mixnode_inclusion_probability(
}
pub(crate) async fn _get_mixnode_avg_uptime(
cache: &NymContractCache,
storage: &NymApiStorage,
cache: &NodeStatusCache,
mix_id: MixId,
) -> Result<UptimeResponse, ErrorResponse> {
let current_interval = cache
.current_interval()
.await
.into_inner()
.ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?;
let performance = average_mixnode_performance(mix_id, current_interval, storage).await?;
let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?;
Ok(UptimeResponse {
mix_id,
avg_uptime: performance.round_to_integer(),
avg_uptime: mixnode.node_performance.last_24h.round_to_integer(),
node_performance: mixnode.node_performance,
})
}
pub(crate) async fn _get_gateway_avg_uptime(
cache: &NodeStatusCache,
identity: &str,
) -> Result<GatewayUptimeResponse, ErrorResponse> {
let gateway = get_gateway_bond_annotated(cache, identity).await?;
Ok(GatewayUptimeResponse {
identity: identity.to_string(),
avg_uptime: gateway.node_performance.last_24h.round_to_integer(),
node_performance: gateway.node_performance,
})
}
@@ -288,7 +366,7 @@ pub(crate) async fn _get_mixnode_inclusion_probabilities(
})
} else {
Err(ErrorResponse::new(
"No data available".to_string(),
"No data available",
Status::ServiceUnavailable,
))
}
+1
View File
@@ -45,6 +45,7 @@ pub(crate) fn node_status_routes(
routes::get_mixnode_stake_saturation,
routes::get_mixnode_inclusion_probability,
routes::get_mixnode_avg_uptime,
routes::get_gateway_avg_uptime,
routes::get_mixnode_inclusion_probabilities,
routes::get_mixnodes_detailed,
routes::get_rewarded_set_detailed,
+21 -1
View File
@@ -5,7 +5,7 @@ use crate::node_status_api::utils::NodeUptimes;
use crate::storage::models::NodeStatus;
use nym_api_requests::models::{
GatewayStatusReportResponse, GatewayUptimeHistoryResponse, HistoricalUptimeResponse,
MixnodeStatusReportResponse, MixnodeUptimeHistoryResponse, RequestError,
MixnodeStatusReportResponse, MixnodeUptimeHistoryResponse, NodePerformance, RequestError,
};
use nym_mixnet_contract_common::reward_params::Performance;
use nym_mixnet_contract_common::{IdentityKey, MixId};
@@ -179,6 +179,16 @@ impl From<MixnodeStatusReport> for MixnodeStatusReportResponse {
}
}
impl From<MixnodeStatusReport> for NodePerformance {
fn from(report: MixnodeStatusReport) -> Self {
NodePerformance {
most_recent: report.most_recent.into(),
last_hour: report.last_hour.into(),
last_24h: report.last_day.into(),
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
pub struct GatewayStatusReport {
pub(crate) identity: String,
@@ -228,6 +238,16 @@ impl From<GatewayStatusReport> for GatewayStatusReportResponse {
}
}
impl From<GatewayStatusReport> for NodePerformance {
fn from(report: GatewayStatusReport) -> Self {
NodePerformance {
most_recent: report.most_recent.into(),
last_hour: report.last_hour.into(),
last_24h: report.last_day.into(),
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
pub struct MixnodeUptimeHistory {
pub(crate) mix_id: MixId,
+31 -37
View File
@@ -4,11 +4,13 @@
use super::helpers::_get_gateways_detailed;
use super::NodeStatusCache;
use crate::node_status_api::helpers::{
_compute_mixnode_reward_estimation, _get_active_set_detailed, _get_mixnode_avg_uptime,
_get_mixnode_inclusion_probabilities, _get_mixnode_inclusion_probability,
_get_mixnode_reward_estimation, _get_mixnode_stake_saturation, _get_mixnode_status,
_get_mixnodes_detailed, _get_rewarded_set_detailed, _mixnode_core_status_count,
_mixnode_report, _mixnode_uptime_history,
_compute_mixnode_reward_estimation, _gateway_core_status_count, _gateway_report,
_gateway_uptime_history, _get_active_set_detailed, _get_gateway_avg_uptime,
_get_mixnode_avg_uptime, _get_mixnode_inclusion_probabilities,
_get_mixnode_inclusion_probability, _get_mixnode_reward_estimation,
_get_mixnode_stake_saturation, _get_mixnode_status, _get_mixnodes_detailed,
_get_rewarded_set_detailed, _mixnode_core_status_count, _mixnode_report,
_mixnode_uptime_history,
};
use crate::node_status_api::models::ErrorResponse;
use crate::storage::NymApiStorage;
@@ -16,12 +18,12 @@ use crate::NymContractCache;
use nym_api_requests::models::{
AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayBondAnnotated,
GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse,
InclusionProbabilityResponse, MixNodeBondAnnotated, MixnodeCoreStatusResponse,
MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse,
RewardEstimationResponse, StakeSaturationResponse, UptimeResponse,
GatewayUptimeResponse, InclusionProbabilityResponse, MixNodeBondAnnotated,
MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse,
MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse,
UptimeResponse,
};
use nym_mixnet_contract_common::MixId;
use rocket::http::Status;
use rocket::serde::json::Json;
use rocket::State;
use rocket_okapi::openapi;
@@ -29,15 +31,10 @@ use rocket_okapi::openapi;
#[openapi(tag = "status")]
#[get("/gateway/<identity>/report")]
pub(crate) async fn gateway_report(
storage: &State<NymApiStorage>,
cache: &State<NodeStatusCache>,
identity: &str,
) -> Result<Json<GatewayStatusReportResponse>, ErrorResponse> {
storage
.construct_gateway_report(identity)
.await
.map(GatewayStatusReportResponse::from)
.map(Json)
.map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))
Ok(Json(_gateway_report(cache, identity).await?))
}
#[openapi(tag = "status")]
@@ -46,12 +43,7 @@ pub(crate) async fn gateway_uptime_history(
storage: &State<NymApiStorage>,
identity: &str,
) -> Result<Json<GatewayUptimeHistoryResponse>, ErrorResponse> {
storage
.get_gateway_uptime_history(identity)
.await
.map(GatewayUptimeHistoryResponse::from)
.map(Json)
.map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))
Ok(Json(_gateway_uptime_history(storage, identity).await?))
}
#[openapi(tag = "status")]
@@ -60,25 +52,19 @@ pub(crate) async fn gateway_core_status_count(
storage: &State<NymApiStorage>,
identity: &str,
since: Option<i64>,
) -> Json<GatewayCoreStatusResponse> {
let count = storage
.get_core_gateway_status_count(identity, since)
.await
.unwrap_or_default();
Json(GatewayCoreStatusResponse {
identity: identity.to_string(),
count,
})
) -> Result<Json<GatewayCoreStatusResponse>, ErrorResponse> {
Ok(Json(
_gateway_core_status_count(storage, identity, since).await?,
))
}
#[openapi(tag = "status")]
#[get("/mixnode/<mix_id>/report")]
pub(crate) async fn mixnode_report(
storage: &State<NymApiStorage>,
cache: &State<NodeStatusCache>,
mix_id: MixId,
) -> Result<Json<MixnodeStatusReportResponse>, ErrorResponse> {
Ok(Json(_mixnode_report(storage, mix_id).await?))
Ok(Json(_mixnode_report(cache, mix_id).await?))
}
#[openapi(tag = "status")]
@@ -171,11 +157,19 @@ pub(crate) async fn get_mixnode_inclusion_probability(
#[openapi(tag = "status")]
#[get("/mixnode/<mix_id>/avg_uptime")]
pub(crate) async fn get_mixnode_avg_uptime(
cache: &State<NymContractCache>,
storage: &State<NymApiStorage>,
cache: &State<NodeStatusCache>,
mix_id: MixId,
) -> Result<Json<UptimeResponse>, ErrorResponse> {
Ok(Json(_get_mixnode_avg_uptime(cache, storage, mix_id).await?))
Ok(Json(_get_mixnode_avg_uptime(cache, mix_id).await?))
}
#[openapi(tag = "status")]
#[get("/gateway/<identity>/avg_uptime")]
pub(crate) async fn get_gateway_avg_uptime(
cache: &State<NodeStatusCache>,
identity: &str,
) -> Result<Json<GatewayUptimeResponse>, ErrorResponse> {
Ok(Json(_get_gateway_avg_uptime(cache, identity).await?))
}
#[openapi(tag = "status")]
+2 -1
View File
@@ -26,7 +26,8 @@ const DEFAULT_GATEWAY_PING_INTERVAL: Duration = Duration::from_secs(60);
// bought bandwidth tokens to not have time to be spent; Once we remove the gateway from the
// bandwidth bridging protocol, we can come back to a smaller timeout value
const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
const DEFAULT_GATEWAY_CONNECTION_TIMEOUT: Duration = Duration::from_millis(2_500);
// This timeout value should be big enough to accommodate an initial bandwidth acquirement
const DEFAULT_GATEWAY_CONNECTION_TIMEOUT: Duration = Duration::from_secs(2 * 60);
const DEFAULT_TEST_ROUTES: usize = 3;
const DEFAULT_MINIMUM_TEST_ROUTES: usize = 1;
+6
View File
@@ -2,6 +2,12 @@
## [Unreleased]
## [v1.0.11] (2023-02-28)
- NC - add the option to manually select and use a specific Service Provider ([#2953])
[#2953]: https://github.com/nymtech/nym/issues/2953
## [v1.1.10] (2023-02-21)
- NC - add logs window for troubleshooting ([#2951])
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@nym/nym-connect",
"version": "1.1.10",
"version": "1.1.11",
"main": "index.js",
"license": "MIT",
"scripts": {
@@ -113,4 +113,4 @@
"webpack-merge": "^5.8.0",
"yaml-loader": "^0.8.0"
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-connect"
version = "1.1.10"
version = "1.1.11"
description = "nym-connect"
authors = ["Nym Technologies SA"]
license = ""
@@ -138,6 +138,8 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str
register_gateway,
Some(chosen_gateway_id),
config.get_base(),
// TODO: another instance where this setting should probably get used
false,
)
.await?;
@@ -1,7 +1,7 @@
{
"package": {
"productName": "nym-connect",
"version": "1.1.10"
"version": "1.1.11"
},
"build": {
"distDir": "../dist",
@@ -6,12 +6,13 @@ import { CustomTitleBar } from './CustomTitleBar';
export const AppWindowFrame: FCWithChildren = ({ children }) => {
const location = useLocation();
const { userDefinedGateway, setUserDefinedGateway } = useClientContext();
const { userDefinedGateway, setUserDefinedGateway, userDefinedSPAddress, setUserDefinedSPAddress } =
useClientContext();
// defined functions to be used when moving away from pages
const onBack = () => {
switch (location.pathname) {
case '/menu/settings':
case '/menu/settings/gateway':
return () => {
// when the user moves away from the settings page and the gateway is not valid
// set isActive to false
@@ -19,6 +20,14 @@ export const AppWindowFrame: FCWithChildren = ({ children }) => {
setUserDefinedGateway((current) => ({ ...current, isActive: false }));
}
};
case '/menu/settings/service-provider':
return () => {
// when the user moves away from the settings page and the sp is not valid
// set isActive to false
if (!userDefinedSPAddress?.address) {
setUserDefinedSPAddress((current) => ({ ...current, isActive: false }));
}
};
default:
return undefined;
}
@@ -39,7 +39,8 @@ const ArrowBackIcon = ({ onBack }: { onBack?: () => void }) => {
return <CustomButton Icon={ArrowBack} onClick={handleBack} />;
};
const getTitleIcon = (path: string) => {
const getTitle = (path: string) => {
if (path.includes('settings')) return 'Settings';
if (path !== '/') {
const title = path.split('/').slice(-1);
return (
@@ -61,7 +62,7 @@ export const CustomTitleBar = ({ path = '/', onBack }: { path?: string; onBack?:
<Box data-tauri-drag-region style={customTitleBarStyles.titlebar}>
{/* set width to keep logo centered */}
<Box sx={{ width: '40px' }}>{path === '/' ? <MenuIcon /> : <ArrowBackIcon onBack={onBack} />}</Box>
{getTitleIcon(path)}
{getTitle(path)}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CustomButton Icon={Minimize} onClick={() => appWindow.minimize()} />
<CustomButton Icon={Close} onClick={() => appWindow.close()} />
+50 -35
View File
@@ -4,13 +4,14 @@ import { invoke } from '@tauri-apps/api';
import { Error } from 'src/types/error';
import { getVersion } from '@tauri-apps/api/app';
import { useEvents } from 'src/hooks/events';
import { UserDefinedGateway } from 'src/types/gateway';
import { forage } from '@tauri-apps/tauri-forage';
import { UserDefinedGateway, UserDefinedSPAddress } from 'src/types/service-provider';
import { getItemFromStorage, setItemInStorage } from 'src/utils';
import { ConnectionStatusKind, GatewayPerformance } from '../types';
import { ConnectionStatsItem } from '../components/ConnectionStats';
import { ServiceProvider } from '../types/directory';
const FORAGE_KEY = 'nym-connect-user-gateway';
const FORAGE_GATEWAY_KEY = 'nym-connect-user-gateway';
const FORAGE_SP_KEY = 'nym-connect-user-sp';
type ModeType = 'light' | 'dark';
@@ -25,16 +26,19 @@ export type TClientContext = {
selectedProvider?: ServiceProvider;
showInfoModal: boolean;
userDefinedGateway?: UserDefinedGateway;
userDefinedSPAddress: UserDefinedSPAddress;
serviceProviders?: ServiceProvider[];
setMode: (mode: ModeType) => void;
clearError: () => void;
setConnectionStatus: (connectionStatus: ConnectionStatusKind) => void;
setConnectionStats: (connectionStats: ConnectionStatsItem[] | undefined) => void;
setConnectedSince: (connectedSince: DateTime | undefined) => void;
setShowInfoModal: (show: boolean) => void;
setRandomSerivceProvider: () => void;
setSerivceProvider: () => void;
startConnecting: () => Promise<void>;
startDisconnecting: () => Promise<void>;
setUserDefinedGateway: React.Dispatch<React.SetStateAction<UserDefinedGateway>>;
setUserDefinedSPAddress: React.Dispatch<React.SetStateAction<UserDefinedSPAddress>>;
};
export const ClientContext = createContext({} as TClientContext);
@@ -50,43 +54,39 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
const [appVersion, setAppVersion] = useState<string>();
const [gatewayPerformance, setGatewayPerformance] = useState<GatewayPerformance>('Good');
const [showInfoModal, setShowInfoModal] = useState(false);
const [userDefinedGateway, setUserDefinedGateway] = useState<UserDefinedGateway>({ isActive: false, gateway: '' });
const [userDefinedGateway, setUserDefinedGateway] = useState<UserDefinedGateway>({
isActive: false,
gateway: undefined,
});
const [userDefinedSPAddress, setUserDefinedSPAddress] = useState<UserDefinedSPAddress>({
isActive: false,
address: undefined,
});
const getAppVersion = async () => {
const version = await getVersion();
return version;
};
const setUserGatewayInStorage = async (gateway: UserDefinedGateway) => {
try {
await forage.setItem({
key: FORAGE_KEY,
value: gateway,
} as any)();
} catch (e) {
console.warn(e);
}
return undefined;
};
useEffect(() => {
setItemInStorage({ key: FORAGE_GATEWAY_KEY, value: userDefinedGateway });
}, [userDefinedGateway]);
const getUserGatewayFromStorage = async (): Promise<UserDefinedGateway | undefined> => {
try {
const gatewayFromStorage = await forage.getItem({ key: FORAGE_KEY })();
return gatewayFromStorage;
} catch (e) {
console.warn(e);
}
return undefined;
};
useEffect(() => {
setItemInStorage({ key: FORAGE_SP_KEY, value: userDefinedSPAddress });
}, [userDefinedSPAddress]);
const initialiseApp = async () => {
const services = await invoke('get_services');
const AppVersion = await getAppVersion();
const storedUserDefinedGateway = await getUserGatewayFromStorage();
const storedUserDefinedGateway = await getItemFromStorage({ key: FORAGE_GATEWAY_KEY });
const storedUserDefinedSP = await getItemFromStorage({ key: FORAGE_SP_KEY });
setAppVersion(AppVersion);
setServiceProviders(services as ServiceProvider[]);
if (storedUserDefinedGateway) setUserDefinedGateway(storedUserDefinedGateway);
if (storedUserDefinedSP) setUserDefinedSPAddress(storedUserDefinedSP);
};
useEvents({
@@ -125,27 +125,37 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
}, []);
const shouldUseUserGateway = !!userDefinedGateway.gateway && userDefinedGateway.isActive;
const shouldUseUserSP = !!userDefinedSPAddress.address && userDefinedSPAddress.isActive;
const setServiceProvider = async (newServiceProvider: ServiceProvider) => {
await invoke('set_gateway', {
gateway: newServiceProvider.gateway,
gateway: shouldUseUserGateway ? userDefinedGateway.gateway : newServiceProvider.gateway,
});
await invoke('set_service_provider', {
serviceProvider: shouldUseUserSP ? userDefinedSPAddress.address : newServiceProvider.address,
});
await invoke('set_service_provider', { serviceProvider: newServiceProvider.address });
};
const getRandomSPFromList = (services: ServiceProvider[]) => {
const randomSelection = services[Math.floor(Math.random() * services.length)];
if (shouldUseUserGateway) return { ...randomSelection, gateway: userDefinedGateway.gateway } as ServiceProvider;
return randomSelection;
};
const setRandomSerivceProvider = async () => {
const buildServiceProvider = async (serviceProvider: ServiceProvider) => {
const sp = { ...serviceProvider };
if (shouldUseUserGateway) sp.gateway = userDefinedGateway.gateway as string;
if (shouldUseUserSP) sp.address = userDefinedSPAddress.address as string;
return sp;
};
const setSerivceProvider = async () => {
if (serviceProviders) {
const randomServiceProvider = getRandomSPFromList(serviceProviders);
await setServiceProvider(randomServiceProvider);
await setUserGatewayInStorage(userDefinedGateway);
setSelectedProvider(randomServiceProvider);
const withUserDefinitions = await buildServiceProvider(randomServiceProvider);
await setServiceProvider(withUserDefinitions);
setSelectedProvider(withUserDefinitions);
}
return undefined;
};
@@ -165,21 +175,25 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
showInfoModal,
setConnectionStats,
selectedProvider,
serviceProviders,
connectedSince,
setConnectedSince,
setRandomSerivceProvider,
setSerivceProvider,
startConnecting,
startDisconnecting,
gatewayPerformance,
setShowInfoModal,
userDefinedSPAddress,
userDefinedGateway,
setUserDefinedGateway,
setUserDefinedSPAddress,
}),
[
mode,
appVersion,
error,
showInfoModal,
serviceProviders,
connectedSince,
connectionStatus,
connectionStats,
@@ -187,6 +201,7 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
gatewayPerformance,
selectedProvider,
userDefinedGateway,
userDefinedSPAddress,
],
);
@@ -10,6 +10,7 @@ const mockValues: TClientContext = {
gatewayPerformance: 'Good',
showInfoModal: false,
userDefinedGateway: { isActive: false, gateway: '' },
userDefinedSPAddress: { isActive: false, address: '' },
setShowInfoModal: () => {},
setMode: () => {},
clearError: () => {},
@@ -18,8 +19,9 @@ const mockValues: TClientContext = {
setConnectionStatus: () => {},
startConnecting: async () => {},
startDisconnecting: async () => {},
setRandomSerivceProvider: () => {},
setSerivceProvider: () => {},
setUserDefinedGateway: () => {},
setUserDefinedSPAddress: () => {},
};
export const MockProvider: FCWithChildren<{
@@ -28,7 +28,7 @@ export const ConnectionPage = () => {
// eslint-disable-next-line default-case
switch (currentStatus) {
case 'disconnected':
await context.setRandomSerivceProvider();
await context.setSerivceProvider();
await context.startConnecting();
context.setConnectedSince(DateTime.now());
context.setShowInfoModal(true);
@@ -6,7 +6,7 @@ import { ConnectionStatusKind } from 'src/types';
import { AppVersion } from 'src/components/AppVersion';
export const GatewaySettings = () => {
const { userDefinedGateway, setUserDefinedGateway } = useClientContext();
const { userDefinedGateway, setUserDefinedGateway, connectionStatus } = useClientContext();
const [gatewayKey, setGatewayKey] = useState<string | undefined>(userDefinedGateway?.gateway);
const handleIsValidGatewayKey = (isValid: boolean) => {
@@ -23,8 +23,6 @@ export const GatewaySettings = () => {
setUserDefinedGateway((current) => ({ ...current, isActive: e.target.checked }));
};
const { connectionStatus } = useClientContext();
return (
<Box height="100%">
<Stack justifyContent="space-between" height="100%">
@@ -60,6 +58,7 @@ export const GatewaySettings = () => {
onValidate={handleIsValidGatewayKey}
sx={{ mt: 1 }}
disabled={connectionStatus === 'connected' || !userDefinedGateway?.isActive}
autoFocus
/>
)}
</FormControl>
@@ -0,0 +1,96 @@
import React, { ChangeEvent } from 'react';
import {
Autocomplete,
Box,
FormControl,
FormControlLabel,
FormHelperText,
Stack,
Switch,
TextField,
Typography,
} from '@mui/material';
import { AppVersion } from 'src/components/AppVersion';
import { ConnectionStatusKind } from 'src/types';
import { useClientContext } from 'src/context/main';
export const ServiceProviderSettings = () => {
const { connectionStatus, serviceProviders, userDefinedSPAddress, setUserDefinedSPAddress } = useClientContext();
const toggleOnOff = (e: ChangeEvent<HTMLInputElement>) => {
setUserDefinedSPAddress((current) => ({ ...current, isActive: e.target.checked }));
};
const handleSelectFromList = (value: string | null) => {
setUserDefinedSPAddress((current) => ({ ...current, address: value ?? undefined }));
};
const getSPDescription = (spAddress: string) => {
const match = serviceProviders?.find((sp) => sp.address === spAddress);
if (match) return match.description;
return 'The service provider specified is not in our known list.';
};
const validateInput = (value: string) => {
setUserDefinedSPAddress((current) => ({ ...current, address: !value.length ? undefined : value }));
};
return (
<Box height="100%">
<Stack justifyContent="space-between" height="100%">
<Box>
<Typography fontWeight="bold" variant="body2" mb={1}>
Select your Service Provider
</Typography>
<Typography color="grey.300" variant="body2" mb={2}>
Pick a service provider from the list or enter your own
</Typography>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={userDefinedSPAddress.isActive}
onChange={toggleOnOff}
disabled={connectionStatus === ConnectionStatusKind.connected}
size="small"
sx={{ ml: 1 }}
/>
}
label={userDefinedSPAddress.isActive ? 'On' : 'Off'}
/>
{connectionStatus === ConnectionStatusKind.connected && (
<FormHelperText sx={{ m: 0, my: 1 }}>This setting is disabled during an active connection</FormHelperText>
)}
{userDefinedSPAddress.isActive && serviceProviders && (
<Autocomplete
clearOnEscape
disabled={connectionStatus === 'connected'}
sx={{ mt: 1 }}
options={serviceProviders.map((sp) => sp.address)}
freeSolo
value={userDefinedSPAddress.address || ''}
onChange={(e, value) => handleSelectFromList(value)}
size="small"
renderInput={(params) => (
<TextField
autoFocus
{...params}
placeholder="Service provider"
onChange={(e) => validateInput(e.target.value)}
/>
)}
ListboxProps={{ style: { background: 'unset', fontSize: '14px' } }}
/>
)}
</FormControl>
{userDefinedSPAddress.address && userDefinedSPAddress.isActive && (
<Typography sx={{ mt: 1 }}>{getSPDescription(userDefinedSPAddress.address)}</Typography>
)}
</Box>
<AppVersion />
</Stack>
</Box>
);
};
@@ -4,7 +4,10 @@ import { Link, List, ListItem, ListItemButton, ListItemText, Stack } from '@mui/
import { AppVersion } from 'src/components/AppVersion';
import { toggleLogViewer } from 'src/utils';
const menuSchema = [{ title: 'Select your gateway', path: 'gateway' }];
const menuSchema = [
{ title: 'Select your gateway', path: 'gateway' },
{ title: 'Select a service provider', path: 'service-provider' },
];
export const SettingsMenu = () => (
<Stack justifyContent="space-between" height="100%">
+2
View File
@@ -6,6 +6,7 @@ import { CompatibleApps } from 'src/pages/menu/Apps';
import { HelpGuide } from 'src/pages/menu/Guide';
import { SettingsMenu } from 'src/pages/menu/settings';
import { GatewaySettings } from 'src/pages/menu/settings/GatewaySettings';
import { ServiceProviderSettings } from 'src/pages/menu/settings/ServiceProviderSettings';
export const AppRoutes = () => (
<Routes>
@@ -17,6 +18,7 @@ export const AppRoutes = () => (
<Route path="settings">
<Route index element={<SettingsMenu />} />
<Route path="gateway" element={<GatewaySettings />} />
<Route path="service-provider" element={<ServiceProviderSettings />} />
</Route>
</Route>
</Routes>
-4
View File
@@ -1,4 +0,0 @@
export interface UserDefinedGateway {
isActive: boolean;
gateway?: string;
}
@@ -0,0 +1,9 @@
export interface UserDefinedGateway {
isActive: boolean;
gateway?: string;
}
export interface UserDefinedSPAddress {
isActive: boolean;
address?: string;
}
+4
View File
@@ -0,0 +1,4 @@
export interface StorageKeyValue<T> {
key: string;
value: T;
}
+24
View File
@@ -1,6 +1,8 @@
import { useEffect, useRef } from 'react';
import { EventName, listen, UnlistenFn, EventCallback } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api';
import { forage } from '@tauri-apps/tauri-forage';
import { StorageKeyValue } from 'src/types/storage';
export const useTauriEvents = <T>(event: EventName, handler: EventCallback<T>) => {
const unlisten = useRef<UnlistenFn>();
@@ -22,3 +24,25 @@ export const useTauriEvents = <T>(event: EventName, handler: EventCallback<T>) =
export const toggleLogViewer = async () => {
await invoke('help_log_toggle_window');
};
export async function setItemInStorage<T>({ key, value }: StorageKeyValue<T>) {
try {
await forage.setItem({
key,
value,
} as any)();
} catch (e) {
console.warn(e);
}
return undefined;
}
export const getItemFromStorage = async ({ key }: Pick<StorageKeyValue<undefined>, 'key'>) => {
try {
const gatewayFromStorage = await forage.getItem({ key })();
return gatewayFromStorage;
} catch (e) {
console.warn(e);
}
return undefined;
};
@@ -118,6 +118,7 @@ pub async fn init_socks5_config(
&mut key_manager,
nym_api_endpoints,
Some(chosen_gateway_id),
false,
)
.await?;
+1 -1
View File
@@ -12,7 +12,7 @@ strum = { version = "0.23", features = ["derive"] }
ts-rs = "6.1.2"
cosmwasm-std = "1.0.0-beta8"
cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support" }
cosmrs = "0.8.0"
nym-config = { path = "../../common/config" }
nym-network-defaults = { path = "../../common/network-defaults" }
+1 -1
View File
@@ -52,7 +52,7 @@ base64 = "0.13"
zeroize = "1.4.3"
cosmwasm-std = "1.0.0"
cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support" }
cosmrs = "0.8.0"
validator-client = { path = "../../common/client-libs/validator-client", features = [
"nyxd-client",
+2
View File
@@ -259,6 +259,8 @@ where
&mut self.key_manager,
self.config.nym_api_endpoints.clone(),
user_chosen_gateway,
// TODO: this should probably be configurable with the config
false,
)
.await?;
@@ -3,7 +3,7 @@
[package]
name = "nym-network-requester"
version = "1.1.10"
version = "1.1.11"
authors.workspace = true
edition.workspace = true
rust-version = "1.65"
@@ -24,6 +24,11 @@ pub(crate) struct Init {
#[clap(long)]
gateway: Option<identity::PublicKey>,
/// Specifies whether the new gateway should be determined based by latency as opposed to being chosen
/// uniformly.
#[clap(long, conflicts_with = "gateway")]
latency_based_selection: bool,
/// Force register gateway. WARNING: this will overwrite any existing keys for the given id,
/// potentially causing loss of access.
#[clap(long)]
@@ -115,6 +120,7 @@ pub(crate) async fn execute(args: &Init) -> Result<(), NetworkRequesterError> {
register_gateway,
user_chosen_gateway_id,
config.get_base(),
args.latency_based_selection,
)
.await
.map_err(|source| {
@@ -1,6 +1,6 @@
[package]
name = "nym-network-statistics"
version = "1.1.10"
version = "1.1.11"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-cli"
version = "1.1.10"
version = "1.1.11"
authors.workspace = true
edition = "2021"
@@ -22,6 +22,7 @@ export const IdentityKeyFormField: FCWithChildren<{
size?: 'small' | 'medium';
sx?: SxProps;
disabled?: boolean;
autoFocus?: boolean;
}> = ({
required,
fullWidth,
@@ -37,6 +38,7 @@ export const IdentityKeyFormField: FCWithChildren<{
showTickOnValid = true,
size,
disabled,
autoFocus,
}) => {
const [value, setValue] = React.useState<string | undefined>(initialValue);
const [validationError, setValidationError] = React.useState<string | undefined>();
@@ -106,6 +108,7 @@ export const IdentityKeyFormField: FCWithChildren<{
InputLabelProps={{ shrink: true }}
size={size}
disabled={disabled}
autoFocus={autoFocus}
/>
);
};