diff --git a/CHANGELOG.md b/CHANGELOG.md index ae9f5f1e29..7db3d58a71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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]) diff --git a/Cargo.lock b/Cargo.lock index 4f8d33aacf..4f0fab00f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/clients/client-core/Cargo.toml b/clients/client-core/Cargo.toml index df20c45d98..4334cc66e4 100644 --- a/clients/client-core/Cargo.toml +++ b/clients/client-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "client-core" -version = "1.1.10" +version = "1.1.11" authors = ["Dave Hrycyszyn "] 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" diff --git a/clients/client-core/src/client/base_client/mod.rs b/clients/client-core/src/client/base_client/mod.rs index f517698bf2..64a292d2b8 100644 --- a/clients/client-core/src/client/base_client/mod.rs +++ b/clients/client-core/src/client/base_client/mod.rs @@ -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) diff --git a/clients/client-core/src/config/mod.rs b/clients/client-core/src/config/mod.rs index a48b2b59b9..5d9a243bbb 100644 --- a/clients/client-core/src/config/mod.rs +++ b/clients/client-core/src/config/mod.rs @@ -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 Client { } 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) } } diff --git a/clients/client-core/src/error.rs b/clients/client-core/src/error.rs index 0436187ed8..4c9e9e5b34 100644 --- a/clients/client-core/src/error.rs +++ b/clients/client-core/src/error.rs @@ -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, diff --git a/clients/client-core/src/init/helpers.rs b/clients/client-core/src/init/helpers.rs index e7d03d585a..b3f5e1c46d 100644 --- a/clients/client-core/src/init/helpers.rs +++ b/clients/client-core/src/init/helpers.rs @@ -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, - chosen_gateway_id: Option, -) -> Result { - let nym_api = validator_servers - .choose(&mut thread_rng()) +#[cfg(not(target_arch = "wasm32"))] +type WsConn = WebSocketStream>; + +#[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( + rng: &mut R, + nym_apis: Vec, +) -> Result, 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::>(); + // 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 { + 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 { + JSWebsocket::new(endpoint).map_err(|_| ClientCoreError::GatewayJsConnectionFailure) +} + +async fn measure_latency(gateway: gateway::Node) -> Result { + 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( + rng: &mut R, + gateways: Vec, +) -> Result { + 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( + rng: &mut R, + gateways: Vec, +) -> Result { + gateways + .choose(rng) + .ok_or(ClientCoreError::NoGatewaysOnNetwork) + .cloned() +} + +pub(super) async fn query_gateway_details( + validator_servers: Vec, + chosen_gateway_id: Option, + by_latency: bool, +) -> Result { + 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) } } diff --git a/clients/client-core/src/init/mod.rs b/clients/client-core/src/init/mod.rs index f99fa318c2..6e1700d7ae 100644 --- a/clients/client-core/src/init/mod.rs +++ b/clients/client-core/src/init/mod.rs @@ -77,9 +77,11 @@ pub async fn register_with_gateway( key_manager: &mut KeyManager, nym_api_endpoints: Vec, chosen_gateway_id: Option, + by_latency: bool, ) -> Result { // 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( register_gateway: bool, user_chosen_gateway_id: Option, config: &Config, + by_latency: bool, ) -> Result 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 diff --git a/clients/credential/src/main.rs b/clients/credential/src/main.rs index 50d256bf59..90e7a27f32 100644 --- a/clients/credential/src/main.rs +++ b/clients/credential/src/main.rs @@ -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 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)?; diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index 8c98ab9487..ffebf29114 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-client" -version = "1.1.10" +version = "1.1.11" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" diff --git a/clients/native/src/commands/init.rs b/clients/native/src/commands/init.rs index 756adfa493..c41c4796a0 100644 --- a/clients/native/src/commands/init.rs +++ b/clients/native/src/commands/init.rs @@ -25,6 +25,11 @@ pub(crate) struct Init { #[clap(long)] gateway: Option, + /// 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}"))?; diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index 12f84f0d06..c1724aa8bb 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-socks5-client" -version = "1.1.10" +version = "1.1.11" authors = ["Dave Hrycyszyn "] description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address" edition = "2021" diff --git a/clients/socks5/src/commands/init.rs b/clients/socks5/src/commands/init.rs index 2c6ae23730..c921834ec3 100644 --- a/clients/socks5/src/commands/init.rs +++ b/clients/socks5/src/commands/init.rs @@ -37,6 +37,11 @@ pub(crate) struct Init { #[clap(long)] gateway: Option, + /// 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}"))?; diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index d75542e83f..68e3358576 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -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}; diff --git a/common/config/src/lib.rs b/common/config/src/lib.rs index 746e1c6443..4d38e55467 100644 --- a/common/config/src/lib.rs +++ b/common/config/src/lib.rs @@ -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; diff --git a/contracts/coconut-dkg/src/dealers/transactions.rs b/contracts/coconut-dkg/src/dealers/transactions.rs index 0c2d0b4241..9a4197dd8d 100644 --- a/contracts/coconut-dkg/src/dealers/transactions.rs +++ b/contracts/coconut-dkg/src/dealers/transactions.rs @@ -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( diff --git a/contracts/coconut-dkg/src/dealings/transactions.rs b/contracts/coconut-dkg/src/dealings/transactions.rs index 3f6a797775..b9220cff9e 100644 --- a/contracts/coconut-dkg/src/dealings/transactions.rs +++ b/contracts/coconut-dkg/src/dealings/transactions.rs @@ -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) diff --git a/contracts/coconut-dkg/src/epoch_state/transactions.rs b/contracts/coconut-dkg/src/epoch_state/transactions.rs index 414ba2344c..3a812f6437 100644 --- a/contracts/coconut-dkg/src/epoch_state/transactions.rs +++ b/contracts/coconut-dkg/src/epoch_state/transactions.rs @@ -89,23 +89,29 @@ pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result, _>>()?; - // 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!( diff --git a/contracts/coconut-dkg/src/support/tests/helpers.rs b/contracts/coconut-dkg/src/support/tests/helpers.rs index 2b8dd56115..b806dd4989 100644 --- a/contracts/coconut-dkg/src/support/tests/helpers.rs +++ b/contracts/coconut-dkg/src/support/tests/helpers.rs @@ -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> = 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 } => { diff --git a/contracts/coconut-dkg/src/verification_key_shares/transactions.rs b/contracts/coconut-dkg/src/verification_key_shares/transactions.rs index f7b2e80844..2d6002a003 100644 --- a/contracts/coconut-dkg/src/verification_key_shares/transactions.rs +++ b/contracts/coconut-dkg/src/verification_key_shares/transactions.rs @@ -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 diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index dd1b18a807..0a84299d5a 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -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 diff --git a/explorer/package.json b/explorer/package.json index 56901cdec1..5e5f347052 100644 --- a/explorer/package.json +++ b/explorer/package.json @@ -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" ] } -} +} \ No newline at end of file diff --git a/explorer/src/api/index.ts b/explorer/src/api/index.ts index 49dcb04e85..894dd3e179 100644 --- a/explorer/src/api/index.ts +++ b/explorer/src/api/index.ts @@ -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 => { 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, })); }; diff --git a/explorer/src/components/Gateways.ts b/explorer/src/components/Gateways.ts index 6d8ff8ad6a..b78b1320da 100644 --- a/explorer/src/components/Gateways.ts +++ b/explorer/src/components/Gateways.ts @@ -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), }; } diff --git a/explorer/src/components/Universal-DataGrid.tsx b/explorer/src/components/Universal-DataGrid.tsx index 9dd1718112..928eb62e94 100644 --- a/explorer/src/components/Universal-DataGrid.tsx +++ b/explorer/src/components/Universal-DataGrid.tsx @@ -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)} /> ); }; diff --git a/explorer/src/pages/GatewayDetail/index.tsx b/explorer/src/pages/GatewayDetail/index.tsx index 14aac28589..8995b1733f 100644 --- a/explorer/src/pages/GatewayDetail/index.tsx +++ b/explorer/src/pages/GatewayDetail/index.tsx @@ -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(); + const [selectedGateway, setSelectedGateway] = React.useState(); 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]); diff --git a/explorer/src/pages/Gateways/index.tsx b/explorer/src/pages/Gateways/index.tsx index c2a0ba1405..98e14be808 100644 --- a/explorer/src/pages/Gateways/index.tsx +++ b/explorer/src/pages/Gateways/index.tsx @@ -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: () => , headerClassName: 'MuiDataGrid-header-override', width: 380, @@ -119,7 +118,7 @@ export const PageGateways: FCWithChildren = () => { {unymToNym(params.value, 6)} @@ -127,8 +126,7 @@ export const PageGateways: FCWithChildren = () => { ), }, { - field: 'performance', - headerName: 'Routing Score', + field: 'node_performance', renderHeader: () => , width: 150, headerAlign: 'left', @@ -137,7 +135,7 @@ export const PageGateways: FCWithChildren = () => { {`${params.value}%`} @@ -154,7 +152,7 @@ export const PageGateways: FCWithChildren = () => { {params.value} @@ -168,9 +166,9 @@ export const PageGateways: FCWithChildren = () => { headerAlign: 'left', headerClassName: 'MuiDataGrid-header-override', renderCell: (params: GridRenderCellParams) => ( - + ), }, { @@ -207,7 +205,6 @@ export const PageGateways: FCWithChildren = () => { }, { field: 'version', - headerName: 'Version', renderHeader: () => , width: 150, headerAlign: 'left', diff --git a/explorer/src/typeDefs/explorer-api.ts b/explorer/src/typeDefs/explorer-api.ts index 48cf2091d9..e430427aff 100644 --- a/explorer/src/typeDefs/explorer-api.ts +++ b/explorer/src/typeDefs/explorer-api.ts @@ -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[]; diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 6e3bb78e08..b3d975adbc 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-gateway" -version = "1.1.10" +version = "1.1.11" authors = [ "Dave Hrycyszyn ", "Jędrzej Stuczyński ", diff --git a/mixnode/Cargo.toml b/mixnode/Cargo.toml index cbe1bd77e0..408bda5fdf 100644 --- a/mixnode/Cargo.toml +++ b/mixnode/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-mixnode" -version = "1.1.11" +version = "1.1.12" authors = [ "Dave Hrycyszyn ", "Jędrzej Stuczyński ", diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index bae10eece0..5378923d39 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-api" -version = "1.1.11" +version = "1.1.12" authors = [ "Dave Hrycyszyn ", "Jędrzej Stuczyński ", diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 0954d4b9d6..daebc0b0ac 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -1,7 +1,7 @@ // Copyright 2022 - Nym Technologies SA // 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, @@ -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)] diff --git a/nym-api/src/node_status_api/cache/node_sets.rs b/nym-api/src/node_status_api/cache/node_sets.rs index 11e622052e..2d856bf9e7 100644 --- a/nym-api/src/node_status_api/cache/node_sets.rs +++ b/nym-api/src/node_status_api/cache/node_sets.rs @@ -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 diff --git a/nym-api/src/node_status_api/helpers.rs b/nym-api/src/node_status_api/helpers.rs index 2c6a03b678..d131c6088e 100644 --- a/nym-api/src/node_status_api/helpers.rs +++ b/nym-api/src/node_status_api/helpers.rs @@ -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 { + 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 { + 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 { + 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 { + 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, + identity: &str, + since: Option, +) -> Result { + 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 { - 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 { - 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 { - 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 { + 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, )) } diff --git a/nym-api/src/node_status_api/mod.rs b/nym-api/src/node_status_api/mod.rs index c8b3718fa6..1b05827e97 100644 --- a/nym-api/src/node_status_api/mod.rs +++ b/nym-api/src/node_status_api/mod.rs @@ -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, diff --git a/nym-api/src/node_status_api/models.rs b/nym-api/src/node_status_api/models.rs index c65397a1f2..b1cb8cd79b 100644 --- a/nym-api/src/node_status_api/models.rs +++ b/nym-api/src/node_status_api/models.rs @@ -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 for MixnodeStatusReportResponse { } } +impl From 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 for GatewayStatusReportResponse { } } +impl From 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, diff --git a/nym-api/src/node_status_api/routes.rs b/nym-api/src/node_status_api/routes.rs index a5bd3b7b6e..9d1f9d7dc0 100644 --- a/nym-api/src/node_status_api/routes.rs +++ b/nym-api/src/node_status_api/routes.rs @@ -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//report")] pub(crate) async fn gateway_report( - storage: &State, + cache: &State, identity: &str, ) -> Result, 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, identity: &str, ) -> Result, 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, identity: &str, since: Option, -) -> Json { - let count = storage - .get_core_gateway_status_count(identity, since) - .await - .unwrap_or_default(); - - Json(GatewayCoreStatusResponse { - identity: identity.to_string(), - count, - }) +) -> Result, ErrorResponse> { + Ok(Json( + _gateway_core_status_count(storage, identity, since).await?, + )) } #[openapi(tag = "status")] #[get("/mixnode//report")] pub(crate) async fn mixnode_report( - storage: &State, + cache: &State, mix_id: MixId, ) -> Result, 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//avg_uptime")] pub(crate) async fn get_mixnode_avg_uptime( - cache: &State, - storage: &State, + cache: &State, mix_id: MixId, ) -> Result, 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//avg_uptime")] +pub(crate) async fn get_gateway_avg_uptime( + cache: &State, + identity: &str, +) -> Result, ErrorResponse> { + Ok(Json(_get_gateway_avg_uptime(cache, identity).await?)) } #[openapi(tag = "status")] diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index 25d949cd11..e9d8f02914 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -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; diff --git a/nym-connect/desktop/CHANGELOG.md b/nym-connect/desktop/CHANGELOG.md index 52edcc5f66..9e6f08fe2d 100644 --- a/nym-connect/desktop/CHANGELOG.md +++ b/nym-connect/desktop/CHANGELOG.md @@ -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]) diff --git a/nym-connect/desktop/package.json b/nym-connect/desktop/package.json index 87e2640c71..f1e5bef205 100644 --- a/nym-connect/desktop/package.json +++ b/nym-connect/desktop/package.json @@ -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" } -} +} \ No newline at end of file diff --git a/nym-connect/desktop/src-tauri/Cargo.toml b/nym-connect/desktop/src-tauri/Cargo.toml index 92efb8b1dc..9fe106ecdd 100644 --- a/nym-connect/desktop/src-tauri/Cargo.toml +++ b/nym-connect/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-connect" -version = "1.1.10" +version = "1.1.11" description = "nym-connect" authors = ["Nym Technologies SA"] license = "" diff --git a/nym-connect/desktop/src-tauri/src/config/mod.rs b/nym-connect/desktop/src-tauri/src/config/mod.rs index 43691fb49c..e1d3379f6b 100644 --- a/nym-connect/desktop/src-tauri/src/config/mod.rs +++ b/nym-connect/desktop/src-tauri/src/config/mod.rs @@ -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?; diff --git a/nym-connect/desktop/src-tauri/tauri.conf.json b/nym-connect/desktop/src-tauri/tauri.conf.json index 915e0421fc..8a52a95e2d 100644 --- a/nym-connect/desktop/src-tauri/tauri.conf.json +++ b/nym-connect/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "package": { "productName": "nym-connect", - "version": "1.1.10" + "version": "1.1.11" }, "build": { "distDir": "../dist", diff --git a/nym-connect/desktop/src/components/AppWindowFrame.tsx b/nym-connect/desktop/src/components/AppWindowFrame.tsx index 929b0f269f..90e01fffa7 100644 --- a/nym-connect/desktop/src/components/AppWindowFrame.tsx +++ b/nym-connect/desktop/src/components/AppWindowFrame.tsx @@ -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; } diff --git a/nym-connect/desktop/src/components/CustomTitleBar.tsx b/nym-connect/desktop/src/components/CustomTitleBar.tsx index 88235e6074..5baa05c5b0 100644 --- a/nym-connect/desktop/src/components/CustomTitleBar.tsx +++ b/nym-connect/desktop/src/components/CustomTitleBar.tsx @@ -39,7 +39,8 @@ const ArrowBackIcon = ({ onBack }: { onBack?: () => void }) => { return ; }; -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?: {/* set width to keep logo centered */} {path === '/' ? : } - {getTitleIcon(path)} + {getTitle(path)} appWindow.minimize()} /> appWindow.close()} /> diff --git a/nym-connect/desktop/src/context/main.tsx b/nym-connect/desktop/src/context/main.tsx index a933a8c36f..b999b957a2 100644 --- a/nym-connect/desktop/src/context/main.tsx +++ b/nym-connect/desktop/src/context/main.tsx @@ -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; startDisconnecting: () => Promise; setUserDefinedGateway: React.Dispatch>; + setUserDefinedSPAddress: React.Dispatch>; }; export const ClientContext = createContext({} as TClientContext); @@ -50,43 +54,39 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => { const [appVersion, setAppVersion] = useState(); const [gatewayPerformance, setGatewayPerformance] = useState('Good'); const [showInfoModal, setShowInfoModal] = useState(false); - const [userDefinedGateway, setUserDefinedGateway] = useState({ isActive: false, gateway: '' }); + const [userDefinedGateway, setUserDefinedGateway] = useState({ + isActive: false, + gateway: undefined, + }); + const [userDefinedSPAddress, setUserDefinedSPAddress] = useState({ + 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 => { - 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, ], ); diff --git a/nym-connect/desktop/src/context/mocks/main.tsx b/nym-connect/desktop/src/context/mocks/main.tsx index 6d0f7d9a82..78028b03f8 100644 --- a/nym-connect/desktop/src/context/mocks/main.tsx +++ b/nym-connect/desktop/src/context/mocks/main.tsx @@ -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<{ diff --git a/nym-connect/desktop/src/pages/connection/index.tsx b/nym-connect/desktop/src/pages/connection/index.tsx index 028ddcf21d..2ba081331f 100644 --- a/nym-connect/desktop/src/pages/connection/index.tsx +++ b/nym-connect/desktop/src/pages/connection/index.tsx @@ -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); diff --git a/nym-connect/desktop/src/pages/menu/settings/GatewaySettings.tsx b/nym-connect/desktop/src/pages/menu/settings/GatewaySettings.tsx index c273a8a007..80efa7988a 100644 --- a/nym-connect/desktop/src/pages/menu/settings/GatewaySettings.tsx +++ b/nym-connect/desktop/src/pages/menu/settings/GatewaySettings.tsx @@ -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(userDefinedGateway?.gateway); const handleIsValidGatewayKey = (isValid: boolean) => { @@ -23,8 +23,6 @@ export const GatewaySettings = () => { setUserDefinedGateway((current) => ({ ...current, isActive: e.target.checked })); }; - const { connectionStatus } = useClientContext(); - return ( @@ -60,6 +58,7 @@ export const GatewaySettings = () => { onValidate={handleIsValidGatewayKey} sx={{ mt: 1 }} disabled={connectionStatus === 'connected' || !userDefinedGateway?.isActive} + autoFocus /> )} diff --git a/nym-connect/desktop/src/pages/menu/settings/ServiceProviderSettings.tsx b/nym-connect/desktop/src/pages/menu/settings/ServiceProviderSettings.tsx new file mode 100644 index 0000000000..8f18caaacd --- /dev/null +++ b/nym-connect/desktop/src/pages/menu/settings/ServiceProviderSettings.tsx @@ -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) => { + 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 ( + + + + + Select your Service Provider + + + Pick a service provider from the list or enter your own + + + + } + label={userDefinedSPAddress.isActive ? 'On' : 'Off'} + /> + {connectionStatus === ConnectionStatusKind.connected && ( + This setting is disabled during an active connection + )} + {userDefinedSPAddress.isActive && serviceProviders && ( + sp.address)} + freeSolo + value={userDefinedSPAddress.address || ''} + onChange={(e, value) => handleSelectFromList(value)} + size="small" + renderInput={(params) => ( + validateInput(e.target.value)} + /> + )} + ListboxProps={{ style: { background: 'unset', fontSize: '14px' } }} + /> + )} + + {userDefinedSPAddress.address && userDefinedSPAddress.isActive && ( + {getSPDescription(userDefinedSPAddress.address)} + )} + + + + + ); +}; diff --git a/nym-connect/desktop/src/pages/menu/settings/index.tsx b/nym-connect/desktop/src/pages/menu/settings/index.tsx index edd1050272..bc58b69f1b 100644 --- a/nym-connect/desktop/src/pages/menu/settings/index.tsx +++ b/nym-connect/desktop/src/pages/menu/settings/index.tsx @@ -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 = () => ( diff --git a/nym-connect/desktop/src/routes/index.tsx b/nym-connect/desktop/src/routes/index.tsx index aaf09fac16..dd3eec678e 100644 --- a/nym-connect/desktop/src/routes/index.tsx +++ b/nym-connect/desktop/src/routes/index.tsx @@ -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 = () => ( @@ -17,6 +18,7 @@ export const AppRoutes = () => ( } /> } /> + } /> diff --git a/nym-connect/desktop/src/types/gateway.ts b/nym-connect/desktop/src/types/gateway.ts deleted file mode 100644 index 735a3cdb03..0000000000 --- a/nym-connect/desktop/src/types/gateway.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface UserDefinedGateway { - isActive: boolean; - gateway?: string; -} diff --git a/nym-connect/desktop/src/types/service-provider.ts b/nym-connect/desktop/src/types/service-provider.ts new file mode 100644 index 0000000000..ed370e2755 --- /dev/null +++ b/nym-connect/desktop/src/types/service-provider.ts @@ -0,0 +1,9 @@ +export interface UserDefinedGateway { + isActive: boolean; + gateway?: string; +} + +export interface UserDefinedSPAddress { + isActive: boolean; + address?: string; +} diff --git a/nym-connect/desktop/src/types/storage.ts b/nym-connect/desktop/src/types/storage.ts new file mode 100644 index 0000000000..ca792b65d9 --- /dev/null +++ b/nym-connect/desktop/src/types/storage.ts @@ -0,0 +1,4 @@ +export interface StorageKeyValue { + key: string; + value: T; +} diff --git a/nym-connect/desktop/src/utils/index.ts b/nym-connect/desktop/src/utils/index.ts index fefef166d7..04b415c026 100644 --- a/nym-connect/desktop/src/utils/index.ts +++ b/nym-connect/desktop/src/utils/index.ts @@ -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 = (event: EventName, handler: EventCallback) => { const unlisten = useRef(); @@ -22,3 +24,25 @@ export const useTauriEvents = (event: EventName, handler: EventCallback) = export const toggleLogViewer = async () => { await invoke('help_log_toggle_window'); }; + +export async function setItemInStorage({ key, value }: StorageKeyValue) { + try { + await forage.setItem({ + key, + value, + } as any)(); + } catch (e) { + console.warn(e); + } + return undefined; +} + +export const getItemFromStorage = async ({ key }: Pick, 'key'>) => { + try { + const gatewayFromStorage = await forage.getItem({ key })(); + return gatewayFromStorage; + } catch (e) { + console.warn(e); + } + return undefined; +}; diff --git a/nym-connect/mobile/src-tauri/src/config/mod.rs b/nym-connect/mobile/src-tauri/src/config/mod.rs index c6c649ab63..7d1c3f0a99 100644 --- a/nym-connect/mobile/src-tauri/src/config/mod.rs +++ b/nym-connect/mobile/src-tauri/src/config/mod.rs @@ -118,6 +118,7 @@ pub async fn init_socks5_config( &mut key_manager, nym_api_endpoints, Some(chosen_gateway_id), + false, ) .await?; diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index d8b1e06b65..36b51a0428 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -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?; diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index 3fc72dca27..615d17b9c4 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -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" diff --git a/service-providers/network-requester/src/cli/init.rs b/service-providers/network-requester/src/cli/init.rs index 0c9907db3a..0c8057b7cd 100644 --- a/service-providers/network-requester/src/cli/init.rs +++ b/service-providers/network-requester/src/cli/init.rs @@ -24,6 +24,11 @@ pub(crate) struct Init { #[clap(long)] gateway: Option, + /// 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| { diff --git a/service-providers/network-statistics/Cargo.toml b/service-providers/network-statistics/Cargo.toml index 6a150d97f6..60bdaff297 100644 --- a/service-providers/network-statistics/Cargo.toml +++ b/service-providers/network-statistics/Cargo.toml @@ -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 diff --git a/tools/nym-cli/Cargo.toml b/tools/nym-cli/Cargo.toml index 936767c456..ef9e2d1575 100644 --- a/tools/nym-cli/Cargo.toml +++ b/tools/nym-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-cli" -version = "1.1.10" +version = "1.1.11" authors.workspace = true edition = "2021" diff --git a/ts-packages/react-components/src/components/mixnodes/IdentityKeyFormField.tsx b/ts-packages/react-components/src/components/mixnodes/IdentityKeyFormField.tsx index 4417bbffe5..692e666fe7 100644 --- a/ts-packages/react-components/src/components/mixnodes/IdentityKeyFormField.tsx +++ b/ts-packages/react-components/src/components/mixnodes/IdentityKeyFormField.tsx @@ -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(initialValue); const [validationError, setValidationError] = React.useState(); @@ -106,6 +108,7 @@ export const IdentityKeyFormField: FCWithChildren<{ InputLabelProps={{ shrink: true }} size={size} disabled={disabled} + autoFocus={autoFocus} /> ); };