Merge branch 'master' into develop

This commit is contained in:
farbanas
2023-03-07 15:22:49 +01:00
109 changed files with 1697 additions and 749 deletions
+14
View File
@@ -4,6 +4,20 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [v1.1.12] (2023-03-07)
- Fix generated docs for mixnet and vesting contract on docs.rs ([#3093])
- Introduce a way of injecting topology into the client ([#3044])
- Update mixnet TypeScript client methods #1 ([#2783])
- Update tooltips for routing and average score ([#3133])
- update selected service provider description style ([#3128])
[#3093]: https://github.com/nymtech/nym/issues/3093
[#3044]: https://github.com/nymtech/nym/issues/3044
[#2783]: https://github.com/nymtech/nym/issues/2783
[#3133]: https://github.com/nymtech/nym/pull/3133
[#3128]: https://github.com/nymtech/nym/pull/3128
## [v1.1.11] (2023-02-28)
- Fix empty dealer set loop ([#3105])
Generated
+20 -18
View File
@@ -654,7 +654,7 @@ dependencies = [
[[package]]
name = "client-core"
version = "1.1.11"
version = "1.1.12"
dependencies = [
"async-trait",
"dashmap 5.4.0",
@@ -1701,7 +1701,7 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "explorer-api"
version = "1.1.11"
version = "1.1.12"
dependencies = [
"chrono",
"clap 4.1.4",
@@ -3174,7 +3174,7 @@ dependencies = [
[[package]]
name = "nym-api"
version = "1.1.12"
version = "1.1.13"
dependencies = [
"anyhow",
"async-trait",
@@ -3254,7 +3254,7 @@ dependencies = [
[[package]]
name = "nym-bin-common"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"clap 4.1.4",
"clap_complete",
@@ -3283,7 +3283,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.1.11"
version = "1.1.12"
dependencies = [
"anyhow",
"base64 0.13.1",
@@ -3341,7 +3341,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.1.11"
version = "1.1.12"
dependencies = [
"clap 4.1.4",
"client-core",
@@ -3390,7 +3390,7 @@ dependencies = [
[[package]]
name = "nym-contracts-common"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"bs58",
"cosmwasm-std",
@@ -3402,7 +3402,7 @@ dependencies = [
[[package]]
name = "nym-crypto"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"aes 0.8.2",
"blake3",
@@ -3435,7 +3435,7 @@ dependencies = [
[[package]]
name = "nym-gateway"
version = "1.1.11"
version = "1.1.12"
dependencies = [
"anyhow",
"async-trait",
@@ -3493,7 +3493,7 @@ dependencies = [
[[package]]
name = "nym-mixnet-contract-common"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"bs58",
"cosmwasm-std",
@@ -3512,7 +3512,7 @@ dependencies = [
[[package]]
name = "nym-mixnode"
version = "1.1.12"
version = "1.1.13"
dependencies = [
"anyhow",
"atty",
@@ -3566,7 +3566,7 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "1.1.11"
version = "1.1.12"
dependencies = [
"async-trait",
"clap 4.1.4",
@@ -3604,7 +3604,7 @@ dependencies = [
[[package]]
name = "nym-network-statistics"
version = "1.1.11"
version = "1.1.12"
dependencies = [
"dirs",
"log",
@@ -3648,7 +3648,7 @@ dependencies = [
[[package]]
name = "nym-pemstore"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"pem",
]
@@ -3667,6 +3667,7 @@ dependencies = [
"nym-network-defaults",
"nym-sphinx",
"nym-task",
"nym-topology",
"pretty_env_logger",
"rand 0.7.3",
"tap",
@@ -3679,7 +3680,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.1.11"
version = "1.1.12"
dependencies = [
"clap 4.1.4",
"client-core",
@@ -3839,7 +3840,7 @@ dependencies = [
[[package]]
name = "nym-sphinx-types"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"sphinx-packet",
]
@@ -3874,6 +3875,7 @@ dependencies = [
name = "nym-topology"
version = "0.1.0"
dependencies = [
"async-trait",
"bs58",
"log",
"nym-bin-common",
@@ -3913,7 +3915,7 @@ dependencies = [
[[package]]
name = "nym-vesting-contract"
version = "1.1.4"
version = "1.2.0-pre.0"
dependencies = [
"cosmwasm-std",
"cw-storage-plus",
@@ -3930,7 +3932,7 @@ dependencies = [
[[package]]
name = "nym-vesting-contract-common"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"cosmwasm-std",
"nym-contracts-common",
+1 -1
View File
@@ -105,7 +105,7 @@ edition = "2021"
license = "Apache-2.0"
[workspace.dependencies]
async-trait = "0.1.63"
async-trait = "0.1.64"
cfg-if = "1.0.0"
dotenvy = "0.15.6"
lazy_static = "1.4.0"
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "client-core"
version = "1.1.11"
version = "1.1.12"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
rust-version = "1.66"
@@ -8,7 +8,7 @@ rust-version = "1.66"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = { version = "0.1.58" }
async-trait = { workspace = true }
dirs = "4.0"
dashmap = "5.4.0"
futures = "0.3"
@@ -15,6 +15,7 @@ use crate::client::replies::reply_controller::{ReplyControllerReceiver, ReplyCon
use crate::client::replies::reply_storage::{
CombinedReplyStorage, PersistentReplyStorage, ReplyStorageBackend, SentReplyKeys,
};
use crate::client::topology_control::nym_api_provider::NymApiTopologyProvider;
use crate::client::topology_control::{
TopologyAccessor, TopologyRefresher, TopologyRefresherConfig,
};
@@ -37,10 +38,12 @@ use nym_sphinx::addressing::nodes::NodeIdentity;
use nym_sphinx::receiver::ReconstructedMessage;
use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths};
use nym_task::{TaskClient, TaskManager};
use nym_topology::provider_trait::TopologyProvider;
use std::sync::Arc;
use std::time::Duration;
use tap::TapFallible;
use url::Url;
#[cfg(not(target_arch = "wasm32"))]
use validator_client::nyxd::CosmWasmClient;
@@ -90,6 +93,7 @@ impl ClientOutput {
pub struct ClientState {
pub shared_lane_queue_lengths: LaneQueueLengths,
pub reply_controller_sender: ReplyControllerSender,
pub topology_accessor: TopologyAccessor,
}
pub enum ClientInputStatus {
@@ -154,6 +158,7 @@ pub struct BaseClientBuilder<'a, B, C: Clone> {
nym_api_endpoints: Vec<Url>,
reply_storage_backend: B,
custom_topology_provider: Option<Box<dyn TopologyProvider>>,
bandwidth_controller: Option<BandwidthController<C>>,
key_manager: KeyManager,
}
@@ -177,6 +182,7 @@ where
bandwidth_controller,
reply_storage_backend,
key_manager,
custom_topology_provider: None,
}
}
@@ -195,11 +201,17 @@ where
disabled_credentials: credentials_toggle.is_disabled(),
nym_api_endpoints,
reply_storage_backend,
custom_topology_provider: None,
bandwidth_controller,
key_manager,
}
}
pub fn with_topology_provider(mut self, provider: Box<dyn TopologyProvider>) -> Self {
self.custom_topology_provider = Some(provider);
self
}
pub fn as_mix_recipient(&self) -> Recipient {
Recipient::new(
*self.key_manager.identity_keypair().public_key(),
@@ -341,25 +353,38 @@ where
Ok(gateway_client)
}
fn setup_topology_provider(
custom_provider: Option<Box<dyn TopologyProvider>>,
nym_api_urls: Vec<Url>,
) -> Box<dyn TopologyProvider> {
// if no custom provider was ... provided ..., create one using nym-api
custom_provider.unwrap_or_else(|| {
Box::new(NymApiTopologyProvider::new(
nym_api_urls,
env!("CARGO_PKG_VERSION").to_string(),
))
})
}
// future responsible for periodically polling directory server and updating
// the current global view of topology
async fn start_topology_refresher(
nym_api_urls: Vec<Url>,
topology_provider: Box<dyn TopologyProvider>,
refresh_rate: Duration,
topology_accessor: TopologyAccessor,
shutdown: TaskClient,
) -> Result<(), ClientCoreError> {
let topology_refresher_config = TopologyRefresherConfig::new(
nym_api_urls,
refresh_rate,
env!("CARGO_PKG_VERSION").to_string(),
let topology_refresher_config = TopologyRefresherConfig::new(refresh_rate);
let mut topology_refresher = TopologyRefresher::new(
topology_refresher_config,
topology_accessor,
topology_provider,
);
let mut topology_refresher =
TopologyRefresher::new(topology_refresher_config, topology_accessor);
// before returning, block entire runtime to refresh the current network view so that any
// components depending on topology would see a non-empty view
info!("Obtaining initial network topology");
topology_refresher.refresh().await;
topology_refresher.try_refresh().await;
if let Err(err) = topology_refresher.ensure_topology_is_routable().await {
log::error!(
@@ -468,8 +493,12 @@ where
)
.await?;
let topology_provider = Self::setup_topology_provider(
self.custom_topology_provider.take(),
self.nym_api_endpoints,
);
Self::start_topology_refresher(
self.nym_api_endpoints.clone(),
topology_provider,
self.debug_config.topology_refresh_rate,
shared_topology_accessor.clone(),
task_manager.subscribe(),
@@ -530,7 +559,7 @@ where
self.debug_config,
self.key_manager.ack_key(),
self_address,
shared_topology_accessor,
shared_topology_accessor.clone(),
sphinx_message_sender,
task_manager.subscribe(),
);
@@ -554,6 +583,7 @@ where
client_state: ClientState {
shared_lane_queue_lengths,
reply_controller_sender,
topology_accessor: shared_topology_accessor,
},
task_manager,
})
@@ -1,336 +0,0 @@
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::spawn_future;
use futures::StreamExt;
use log::*;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::params::DEFAULT_NUM_MIX_HOPS;
use nym_topology::{nym_topology_from_detailed, NymTopology, NymTopologyError};
use rand::seq::SliceRandom;
use rand::thread_rng;
use std::ops::Deref;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{RwLock, RwLockReadGuard};
use url::Url;
// I'm extremely curious why compiler NEVER complained about lack of Debug here before
#[derive(Debug)]
pub struct TopologyAccessorInner(Option<NymTopology>);
impl AsRef<Option<NymTopology>> for TopologyAccessorInner {
fn as_ref(&self) -> &Option<NymTopology> {
&self.0
}
}
impl TopologyAccessorInner {
fn new() -> Self {
TopologyAccessorInner(None)
}
fn update(&mut self, new: Option<NymTopology>) {
self.0 = new;
}
}
pub struct TopologyReadPermit<'a> {
permit: RwLockReadGuard<'a, TopologyAccessorInner>,
}
impl<'a> Deref for TopologyReadPermit<'a> {
type Target = TopologyAccessorInner;
fn deref(&self) -> &Self::Target {
&self.permit
}
}
impl<'a> TopologyReadPermit<'a> {
/// Using provided topology read permit, tries to get an immutable reference to the underlying
/// topology. For obvious reasons the lifetime of the topology reference is bound to the permit.
pub(super) fn try_get_valid_topology_ref(
&'a self,
ack_recipient: &Recipient,
packet_recipient: Option<&Recipient>,
) -> Result<&'a NymTopology, NymTopologyError> {
// 1. Have we managed to get anything from the refresher, i.e. have the nym-api queries gone through?
let topology = self
.permit
.as_ref()
.as_ref()
.ok_or(NymTopologyError::EmptyNetworkTopology)?;
// 2. does it have any mixnode at all?
// 3. does it have any gateways at all?
// 4. does it have a mixnode on each layer?
topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS)?;
// 5. does it contain OUR gateway (so that we could create an ack packet)?
if !topology.gateway_exists(ack_recipient.gateway()) {
return Err(NymTopologyError::NonExistentGatewayError {
identity_key: ack_recipient.gateway().to_base58_string(),
});
}
// 6. for our target recipient, does it contain THEIR gateway (so that we could create
if let Some(recipient) = packet_recipient {
if !topology.gateway_exists(recipient.gateway()) {
return Err(NymTopologyError::NonExistentGatewayError {
identity_key: recipient.gateway().to_base58_string(),
});
}
}
Ok(topology)
}
}
impl<'a> From<RwLockReadGuard<'a, TopologyAccessorInner>> for TopologyReadPermit<'a> {
fn from(read_permit: RwLockReadGuard<'a, TopologyAccessorInner>) -> Self {
TopologyReadPermit {
permit: read_permit,
}
}
}
#[derive(Clone, Debug)]
pub struct TopologyAccessor {
// `RwLock` *seems to* be the better approach for this as write access is only requested every
// few seconds, while reads are needed every single packet generated.
// However, proper benchmarks will be needed to determine if `RwLock` is indeed a better
// approach than a `Mutex`
inner: Arc<RwLock<TopologyAccessorInner>>,
}
impl TopologyAccessor {
pub fn new() -> Self {
TopologyAccessor {
inner: Arc::new(RwLock::new(TopologyAccessorInner::new())),
}
}
pub async fn get_read_permit(&self) -> TopologyReadPermit<'_> {
self.inner.read().await.into()
}
async fn update_global_topology(&self, new_topology: Option<NymTopology>) {
self.inner.write().await.update(new_topology);
}
// only used by the client at startup to get a slightly more reasonable error message
// (currently displays as unused because health checker is disabled due to required changes)
pub async fn ensure_is_routable(&self) -> Result<(), NymTopologyError> {
match &self.inner.read().await.0 {
None => Err(NymTopologyError::EmptyNetworkTopology),
Some(ref topology) => topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS),
}
}
}
impl Default for TopologyAccessor {
fn default() -> Self {
TopologyAccessor::new()
}
}
pub struct TopologyRefresherConfig {
nym_api_urls: Vec<Url>,
refresh_rate: Duration,
client_version: String,
}
impl TopologyRefresherConfig {
pub fn new(nym_api_urls: Vec<Url>, refresh_rate: Duration, client_version: String) -> Self {
TopologyRefresherConfig {
nym_api_urls,
refresh_rate,
client_version,
}
}
}
pub struct TopologyRefresher {
validator_client: validator_client::client::NymApiClient,
client_version: String,
nym_api_urls: Vec<Url>,
topology_accessor: TopologyAccessor,
refresh_rate: Duration,
currently_used_api: usize,
was_latest_valid: bool,
}
impl TopologyRefresher {
pub fn new(mut cfg: TopologyRefresherConfig, topology_accessor: TopologyAccessor) -> Self {
cfg.nym_api_urls.shuffle(&mut thread_rng());
TopologyRefresher {
validator_client: validator_client::client::NymApiClient::new(
cfg.nym_api_urls[0].clone(),
),
client_version: cfg.client_version,
nym_api_urls: cfg.nym_api_urls,
topology_accessor,
refresh_rate: cfg.refresh_rate,
currently_used_api: 0,
was_latest_valid: true,
}
}
fn use_next_nym_api(&mut self) {
if self.nym_api_urls.len() == 1 {
warn!("There's only a single nym API available - it won't be possible to use a different one");
return;
}
self.currently_used_api = (self.currently_used_api + 1) % self.nym_api_urls.len();
self.validator_client
.change_nym_api(self.nym_api_urls[self.currently_used_api].clone())
}
/// Verifies whether nodes a reasonably distributed among all mix layers.
///
/// In ideal world we would have 33% nodes on layer 1, 33% on layer 2 and 33% on layer 3.
/// However, this is a rather unrealistic expectation, instead we check whether there exists
/// a layer with more than 66% of nodes or with fewer than 15% and if so, we trigger a failure.
///
/// # Arguments
///
/// * `topology`: active topology constructed from validator api data
fn check_layer_distribution(&self, active_topology: &NymTopology) -> bool {
let mixes = active_topology.mixes();
let mixnodes_count = active_topology.num_mixnodes();
if active_topology.gateways().is_empty() {
return false;
}
// trivial check to see if have at least a single node on each layer (regardless of active set size)
if mixes.get(&1).is_none() || mixes.get(&2).is_none() || mixes.get(&3).is_none() {
return false;
}
let upper_bound = (mixnodes_count as f32 * 0.66) as usize;
let lower_bound = (mixnodes_count as f32 * 0.15) as usize;
let layer1 = mixes.get(&1).unwrap().len();
let layer2 = mixes.get(&2).unwrap().len();
let layer3 = mixes.get(&3).unwrap().len();
if layer1 < lower_bound || layer1 > upper_bound {
warn!(
"nodes: {}, layer1: {}, layer2: {}, layer3: {}",
mixnodes_count, layer1, layer2, layer3
);
return false;
}
if layer2 < lower_bound || layer2 > upper_bound {
warn!(
"nodes: {}, layer1: {}, layer2: {}, layer3: {}",
mixnodes_count, layer1, layer2, layer3
);
return false;
}
if layer3 < lower_bound || layer3 > upper_bound {
warn!(
"nodes: {}, layer1: {}, layer2: {}, layer3: {}",
mixnodes_count, layer1, layer2, layer3
);
return false;
}
true
}
async fn get_current_compatible_topology(&self) -> Option<NymTopology> {
// TODO: optimization for the future:
// only refresh mixnodes on timer and refresh gateways only when
// we have to send to a new, unknown, gateway
let mixnodes = match self.validator_client.get_cached_active_mixnodes().await {
Err(err) => {
error!("failed to get network mixnodes - {err}");
return None;
}
Ok(mixes) => mixes,
};
let gateways = match self.validator_client.get_cached_gateways().await {
Err(err) => {
error!("failed to get network gateways - {err}");
return None;
}
Ok(gateways) => gateways,
};
let topology = nym_topology_from_detailed(mixnodes, gateways)
.filter_system_version(&self.client_version);
if !self.check_layer_distribution(&topology) {
warn!("The current filtered active topology has extremely skewed layer distribution. It cannot be used.");
None
} else {
Some(topology)
}
}
pub async fn refresh(&mut self) {
trace!("Refreshing the topology");
let new_topology = self.get_current_compatible_topology().await;
if new_topology.is_none() {
self.use_next_nym_api();
}
if new_topology.is_none() && self.was_latest_valid {
// if we failed to grab this topology, but the one before it was alright, let's assume
// validator had a tiny hiccup and use the old data
warn!("we're going to keep on using the old topology for this iteration");
self.was_latest_valid = false;
return;
} else if new_topology.is_some() {
self.was_latest_valid = true;
}
self.topology_accessor
.update_global_topology(new_topology)
.await;
}
pub async fn ensure_topology_is_routable(&self) -> Result<(), NymTopologyError> {
self.topology_accessor.ensure_is_routable().await
}
pub fn start_with_shutdown(mut self, mut shutdown: nym_task::TaskClient) {
spawn_future(async move {
debug!("Started TopologyRefresher with graceful shutdown support");
#[cfg(not(target_arch = "wasm32"))]
let mut interval = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(
self.refresh_rate,
));
#[cfg(target_arch = "wasm32")]
let mut interval =
gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32);
while !shutdown.is_shutdown() {
tokio::select! {
_ = interval.next() => {
self.refresh().await;
},
_ = shutdown.recv() => {
log::trace!("TopologyRefresher: Received shutdown");
},
}
}
shutdown.recv_timeout().await;
log::debug!("TopologyRefresher: Exiting");
})
}
}
@@ -0,0 +1,154 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::params::DEFAULT_NUM_MIX_HOPS;
use nym_topology::{NymTopology, NymTopologyError};
use std::ops::Deref;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::sync::{Notify, RwLock, RwLockReadGuard};
#[derive(Debug)]
pub struct TopologyAccessorInner {
controlled_manually: AtomicBool,
released_manual_control: Notify,
// `RwLock` *seems to* be the better approach for this as write access is only requested every
// few seconds, while reads are needed every single packet generated.
// However, proper benchmarks will be needed to determine if `RwLock` is indeed a better
// approach than a `Mutex`
topology: RwLock<Option<NymTopology>>,
}
impl TopologyAccessorInner {
fn new() -> Self {
TopologyAccessorInner {
controlled_manually: AtomicBool::new(false),
released_manual_control: Notify::new(),
topology: RwLock::new(None),
}
}
async fn update(&self, new: Option<NymTopology>) {
*self.topology.write().await = new;
}
}
pub struct TopologyReadPermit<'a> {
permit: RwLockReadGuard<'a, Option<NymTopology>>,
}
impl<'a> Deref for TopologyReadPermit<'a> {
type Target = Option<NymTopology>;
fn deref(&self) -> &Self::Target {
&self.permit
}
}
impl<'a> TopologyReadPermit<'a> {
/// Using provided topology read permit, tries to get an immutable reference to the underlying
/// topology. For obvious reasons the lifetime of the topology reference is bound to the permit.
pub(crate) fn try_get_valid_topology_ref(
&'a self,
ack_recipient: &Recipient,
packet_recipient: Option<&Recipient>,
) -> Result<&'a NymTopology, NymTopologyError> {
// 1. Have we managed to get anything from the refresher, i.e. have the nym-api queries gone through?
let topology = self
.permit
.as_ref()
.ok_or(NymTopologyError::EmptyNetworkTopology)?;
// 2. does it have any mixnode at all?
// 3. does it have any gateways at all?
// 4. does it have a mixnode on each layer?
topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS)?;
// 5. does it contain OUR gateway (so that we could create an ack packet)?
if !topology.gateway_exists(ack_recipient.gateway()) {
return Err(NymTopologyError::NonExistentGatewayError {
identity_key: ack_recipient.gateway().to_base58_string(),
});
}
// 6. for our target recipient, does it contain THEIR gateway (so that we could create
if let Some(recipient) = packet_recipient {
if !topology.gateway_exists(recipient.gateway()) {
return Err(NymTopologyError::NonExistentGatewayError {
identity_key: recipient.gateway().to_base58_string(),
});
}
}
Ok(topology)
}
}
impl<'a> From<RwLockReadGuard<'a, Option<NymTopology>>> for TopologyReadPermit<'a> {
fn from(read_permit: RwLockReadGuard<'a, Option<NymTopology>>) -> Self {
TopologyReadPermit {
permit: read_permit,
}
}
}
#[derive(Clone, Debug)]
pub struct TopologyAccessor {
inner: Arc<TopologyAccessorInner>,
}
impl TopologyAccessor {
pub fn new() -> Self {
TopologyAccessor {
inner: Arc::new(TopologyAccessorInner::new()),
}
}
pub fn controlled_manually(&self) -> bool {
self.inner.controlled_manually.load(Ordering::SeqCst)
}
pub async fn get_read_permit(&self) -> TopologyReadPermit<'_> {
self.inner.topology.read().await.into()
}
pub(crate) async fn update_global_topology(&self, new_topology: Option<NymTopology>) {
self.inner.update(new_topology).await;
}
pub(crate) async fn wait_for_released_manual_control(&self) {
self.inner.released_manual_control.notified().await
}
pub async fn current_topology(&self) -> Option<NymTopology> {
self.inner.topology.read().await.clone()
}
pub async fn manually_change_topology(&self, new_topology: NymTopology) {
self.inner.controlled_manually.store(true, Ordering::SeqCst);
self.inner.update(Some(new_topology)).await;
}
pub fn release_manual_control(&self) {
self.inner
.controlled_manually
.store(false, Ordering::SeqCst);
self.inner.released_manual_control.notify_waiters();
}
// only used by the client at startup to get a slightly more reasonable error message
// (currently displays as unused because health checker is disabled due to required changes)
pub async fn ensure_is_routable(&self) -> Result<(), NymTopologyError> {
match self.inner.topology.read().await.deref() {
None => Err(NymTopologyError::EmptyNetworkTopology),
Some(ref topology) => topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS),
}
}
}
impl Default for TopologyAccessor {
fn default() -> Self {
TopologyAccessor::new()
}
}
@@ -0,0 +1,115 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::spawn_future;
pub(crate) use accessor::{TopologyAccessor, TopologyReadPermit};
use futures::StreamExt;
use log::*;
use nym_topology::provider_trait::TopologyProvider;
use nym_topology::NymTopologyError;
use std::time::Duration;
mod accessor;
pub(crate) mod nym_api_provider;
// TODO: move it to config later
const MAX_FAILURE_COUNT: usize = 10;
pub struct TopologyRefresherConfig {
refresh_rate: Duration,
}
impl TopologyRefresherConfig {
pub fn new(refresh_rate: Duration) -> Self {
TopologyRefresherConfig { refresh_rate }
}
}
pub struct TopologyRefresher {
topology_provider: Box<dyn TopologyProvider>,
topology_accessor: TopologyAccessor,
refresh_rate: Duration,
consecutive_failure_count: usize,
}
impl TopologyRefresher {
pub fn new(
cfg: TopologyRefresherConfig,
topology_accessor: TopologyAccessor,
topology_provider: Box<dyn TopologyProvider>,
) -> Self {
TopologyRefresher {
topology_provider,
topology_accessor,
refresh_rate: cfg.refresh_rate,
consecutive_failure_count: 0,
}
}
pub fn change_topology_provider(&mut self, provider: Box<dyn TopologyProvider>) {
self.topology_provider = provider;
}
pub async fn try_refresh(&mut self) {
trace!("Refreshing the topology");
if self.topology_accessor.controlled_manually() {
info!("topology is being controlled manually - we're going to wait until the control is released...");
self.topology_accessor
.wait_for_released_manual_control()
.await;
}
let new_topology = self.topology_provider.get_new_topology().await;
if new_topology.is_none() {
warn!("failed to obtain new network topology");
}
if new_topology.is_none() && self.consecutive_failure_count < MAX_FAILURE_COUNT {
// if we failed to grab this topology, but the one before it was alright, let's assume
// validator had a tiny hiccup and use the old data
warn!("we're going to keep on using the old topology for this iteration");
self.consecutive_failure_count += 1;
return;
} else if new_topology.is_some() {
self.consecutive_failure_count = 0;
}
self.topology_accessor
.update_global_topology(new_topology)
.await;
}
pub async fn ensure_topology_is_routable(&self) -> Result<(), NymTopologyError> {
self.topology_accessor.ensure_is_routable().await
}
pub fn start_with_shutdown(mut self, mut shutdown: nym_task::TaskClient) {
spawn_future(async move {
debug!("Started TopologyRefresher with graceful shutdown support");
#[cfg(not(target_arch = "wasm32"))]
let mut interval = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(
self.refresh_rate,
));
#[cfg(target_arch = "wasm32")]
let mut interval =
gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32);
while !shutdown.is_shutdown() {
tokio::select! {
_ = interval.next() => {
self.try_refresh().await;
},
_ = shutdown.recv() => {
log::trace!("TopologyRefresher: Received shutdown");
},
}
}
shutdown.recv_timeout().await;
log::debug!("TopologyRefresher: Exiting");
})
}
}
@@ -0,0 +1,106 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use async_trait::async_trait;
use log::{error, warn};
use nym_topology::provider_trait::TopologyProvider;
use nym_topology::{nym_topology_from_detailed, NymTopology, NymTopologyError};
use rand::prelude::SliceRandom;
use rand::thread_rng;
use url::Url;
pub(crate) struct NymApiTopologyProvider {
validator_client: validator_client::client::NymApiClient,
nym_api_urls: Vec<Url>,
client_version: String,
currently_used_api: usize,
}
impl NymApiTopologyProvider {
pub(crate) fn new(mut nym_api_urls: Vec<Url>, client_version: String) -> Self {
nym_api_urls.shuffle(&mut thread_rng());
NymApiTopologyProvider {
validator_client: validator_client::client::NymApiClient::new(nym_api_urls[0].clone()),
nym_api_urls,
client_version,
currently_used_api: 0,
}
}
fn use_next_nym_api(&mut self) {
if self.nym_api_urls.len() == 1 {
warn!("There's only a single nym API available - it won't be possible to use a different one");
return;
}
self.currently_used_api = (self.currently_used_api + 1) % self.nym_api_urls.len();
self.validator_client
.change_nym_api(self.nym_api_urls[self.currently_used_api].clone())
}
/// Verifies whether nodes a reasonably distributed among all mix layers.
///
/// In ideal world we would have 33% nodes on layer 1, 33% on layer 2 and 33% on layer 3.
/// However, this is a rather unrealistic expectation, instead we check whether there exists
/// a layer with more than 66% of nodes or with fewer than 15% and if so, we trigger a failure.
///
/// # Arguments
///
/// * `topology`: active topology constructed from validator api data
fn check_layer_distribution(
&self,
active_topology: &NymTopology,
) -> Result<(), NymTopologyError> {
let lower_threshold = 0.15;
let upper_threshold = 0.66;
active_topology.ensure_even_layer_distribution(lower_threshold, upper_threshold)
}
async fn get_current_compatible_topology(&mut self) -> Option<NymTopology> {
let mixnodes = match self.validator_client.get_cached_active_mixnodes().await {
Err(err) => {
error!("failed to get network mixnodes - {err}");
return None;
}
Ok(mixes) => mixes,
};
let gateways = match self.validator_client.get_cached_gateways().await {
Err(err) => {
error!("failed to get network gateways - {err}");
return None;
}
Ok(gateways) => gateways,
};
let topology = nym_topology_from_detailed(mixnodes, gateways)
.filter_system_version(&self.client_version);
if let Err(err) = self.check_layer_distribution(&topology) {
warn!("The current filtered active topology has extremely skewed layer distribution. It cannot be used: {err}");
self.use_next_nym_api();
None
} else {
Some(topology)
}
}
}
// hehe, wasm
#[cfg(not(target_arch = "wasm32"))]
#[async_trait]
impl TopologyProvider for NymApiTopologyProvider {
async fn get_new_topology(&mut self) -> Option<NymTopology> {
self.get_current_compatible_topology().await
}
}
#[cfg(target_arch = "wasm32")]
#[async_trait(?Send)]
impl TopologyProvider for NymApiTopologyProvider {
async fn get_new_topology(&mut self) -> Option<NymTopology> {
self.get_current_compatible_topology().await
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.11"
version = "1.1.12"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1
View File
@@ -101,6 +101,7 @@ impl SocketClient {
let ClientState {
shared_lane_queue_lengths,
reply_controller_sender,
..
} = client_state;
let websocket_handler = websocket::HandlerBuilder::new(
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.11"
version = "1.1.12"
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"
+1 -1
View File
@@ -123,7 +123,7 @@ impl NymClient {
let ClientState {
shared_lane_queue_lengths,
reply_controller_sender: _,
..
} = client_status;
let authenticator = Authenticator::new(auth_methods, allowed_users);
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-bin-common"
version = "0.1.0"
version = "0.2.0"
description = "Common code for nym binaries"
edition = { workspace = true }
authors = { workspace = true }
+1 -1
View File
@@ -14,7 +14,7 @@ log = { workspace = true }
thiserror = "1.0"
url = "2.2"
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
async-trait = { version = "0.1.51" }
async-trait = { workspace = true }
tokio = { version = "1.24.1", features = ["macros"] }
# internal
@@ -36,7 +36,7 @@ nym-execute = { path = "../../execute" }
# required for nyxd-client
# 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 }
async-trait = { workspace = true, optional = true }
bip39 = { version = "1", 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}
@@ -1,10 +1,13 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use serde::{Deserialize, Serialize};
use std::fmt;
use crate::nyxd::{Gas, GasPrice};
pub use cosmrs::Coin as CosmosCoin;
pub use cosmwasm_std::Coin as CosmWasmCoin;
use cosmwasm_std::{Fraction, Uint128};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::ops::Div;
#[derive(Serialize, Deserialize, Clone, Copy, Default, Debug, PartialEq, Eq)]
pub struct MismatchedDenoms;
@@ -19,6 +22,40 @@ pub struct Coin {
pub denom: String,
}
impl Div<GasPrice> for Coin {
type Output = Gas;
fn div(self, rhs: GasPrice) -> Self::Output {
&self / rhs
}
}
impl<'a> Div<GasPrice> for &'a Coin {
type Output = Gas;
fn div(self, rhs: GasPrice) -> Self::Output {
if self.denom != rhs.denom {
panic!(
"attempted to use two different denoms for gas calculation ({} and {})",
self.denom, rhs.denom
);
}
// tsk, tsk. somebody tried to divide by zero here!
let Some(gas_price_inv) = rhs.amount.inv() else {
panic!("attempted to divide by zero!")
};
let implicit_gas_limit = gas_price_inv * Uint128::new(self.amount);
if implicit_gas_limit.u128() >= u64::MAX as u128 {
u64::MAX
} else {
implicit_gas_limit.u128() as u64
}
.into()
}
}
impl Coin {
pub fn new<S: Into<String>>(amount: u128, denom: S) -> Self {
Coin {
@@ -128,3 +165,67 @@ impl CoinConverter for CosmWasmCoin {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn division_by_zero_gas_price() {
let gas_price: GasPrice = "0unym".parse().unwrap();
let amount = Coin::new(123, "unym");
let _res = amount / gas_price;
}
#[test]
#[should_panic]
fn division_by_gas_price_of_different_denom() {
let gas_price: GasPrice = "0.025unyx".parse().unwrap();
let amount = Coin::new(123, "unym");
let _res = amount / gas_price;
}
#[test]
fn gas_price_division() {
let amount = Coin::new(3938, "unym");
let gas_price = "0.025unym".parse().unwrap();
let res = amount / gas_price;
assert_eq!(157520, res.value());
let amount = Coin::new(1234567890, "unym");
let gas_price = "0.025unym".parse().unwrap();
let res = amount / gas_price;
assert_eq!(49382715600, res.value());
let amount = Coin::new(1, "unym");
let gas_price = "0.025unym".parse().unwrap();
let res = amount / gas_price;
assert_eq!(40, res.value());
let amount = Coin::new(150_000_000, "unym");
let gas_price = "0.001234unym".parse().unwrap();
let res = amount / gas_price;
assert_eq!(121555915721, res.value());
let amount = Coin::new(150_000_000, "unym");
let gas_price = "1unym".parse().unwrap();
let res = amount / gas_price;
assert_eq!(150_000_000, res.value());
let amount = Coin::new(150_000_000, "unym");
let gas_price = "1234.56unym".parse().unwrap();
let res = amount / gas_price;
assert_eq!(121500, res.value());
}
#[test]
fn gas_price_division_identity() {
let amount = Coin::new(1234567890, "unym");
let gas_price: GasPrice = "0.025unym".parse().unwrap();
let res1 = (&amount) / gas_price.clone();
let res2 = &gas_price * res1;
assert_eq!(amount, Coin::from(res2));
}
}
@@ -1,8 +1,8 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::nyxd::Coin;
use crate::nyxd::Gas;
use crate::nyxd::{Coin, GasPrice};
use cosmrs::{tx, AccountId};
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
@@ -64,6 +64,12 @@ impl Display for Fee {
}
impl Fee {
pub fn manual_with_gas_price(fee: Coin, gas_price: GasPrice) -> Self {
let gas_limit = &fee / gas_price;
Fee::Manual(tx::Fee::from_amount_and_gas(fee.into(), gas_limit))
}
pub fn new_payer_granter_auto(
gas_adjustment: Option<GasAdjustment>,
payer: Option<AccountId>,
@@ -1,6 +1,6 @@
[package]
name = "nym-contracts-common"
version = "0.1.0"
version = "0.2.0"
description = "Common library for Nym cosmwasm contracts"
edition = { workspace = true }
authors = { workspace = true }
@@ -1,6 +1,6 @@
[package]
name = "nym-mixnet-contract-common"
version = "0.1.0"
version = "0.2.0"
description = "Common library for the Nym mixnet contract"
rust-version = "1.62"
edition = { workspace = true }
@@ -15,7 +15,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_repr = "0.1"
schemars = "0.8"
thiserror = "1.0"
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.1.0" }
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.2.0" }
serde_json = "1.0.0"
humantime-serde = "1.1.1"
@@ -1,6 +1,6 @@
[package]
name = "nym-vesting-contract-common"
version = "0.1.0"
version = "0.2.0"
description = "Common library for the Nym vesting contract"
edition = { workspace = true }
authors = { workspace = true }
@@ -9,8 +9,8 @@ repository = { workspace = true }
[dependencies]
cosmwasm-std = "1.0.0"
mixnet-contract-common = { path = "../mixnet-contract", package = "nym-mixnet-contract-common", version = "0.1.0" }
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.1.0" }
mixnet-contract-common = { path = "../mixnet-contract", package = "nym-mixnet-contract-common", version = "0.2.0" }
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.2.0" }
serde = { version = "1.0", features = ["derive"] }
schemars = "0.8"
ts-rs = {version = "6.1.2", optional = true}
+1 -1
View File
@@ -6,7 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = { version = "0.1.51" }
async-trait = { workspace = true }
log = { workspace = true }
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]}
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-crypto"
version = "0.1.0"
version = "0.2.0"
description = "Crypto library for the nym mixnet"
edition = { workspace = true }
authors = { workspace = true }
@@ -26,8 +26,8 @@ subtle-encoding = { version = "0.5", features = ["bech32-preview"]}
thiserror = "1.0.37"
# internal
nym-sphinx-types = { path = "../nymsphinx/types", version = "0.1.0" }
nym-pemstore = { path = "../../common/pemstore", version = "0.1.0" }
nym-sphinx-types = { path = "../nymsphinx/types", version = "0.2.0" }
nym-pemstore = { path = "../../common/pemstore", version = "0.2.0" }
[dev-dependencies]
rand_chacha = "0.2"
@@ -3,6 +3,7 @@
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use std::fmt::{self, Display, Formatter};
use std::str::FromStr;
use thiserror::Error;
#[cfg(feature = "rand")]
@@ -131,6 +132,14 @@ impl PublicKey {
}
}
impl FromStr for PublicKey {
type Err = KeyRecoveryError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
PublicKey::from_base58_string(s)
}
}
#[cfg(feature = "serde")]
impl Serialize for PublicKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+1 -1
View File
@@ -6,6 +6,6 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = { version = "0.1.51" }
async-trait = { workspace = true }
thiserror = "1.0"
+1 -1
View File
@@ -23,7 +23,7 @@ nym-sphinx-types = { path = "types" }
# those dependencies are due to intriducing preparer and receiver. Perpaphs that indicates they should be moved
# to separate crate?
nym-crypto = { path = "../crypto", version = "0.1.0" }
nym-crypto = { path = "../crypto", version = "0.2.0" }
nym-topology = { path = "../topology" }
[dev-dependencies]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-sphinx-types"
version = "0.1.0"
version = "0.2.0"
description = "Re-export sphinx packet types"
edition = { workspace = true }
authors = { workspace = true }
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-pemstore"
description = "Store private-public keypairs in PEM format"
version = "0.1.0"
version = "0.2.0"
edition = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
+8
View File
@@ -0,0 +1,8 @@
# List of published common crates
nym-bin-common
nym-contracts-common
nym-crypto
nym-mixnet-contract-common
nym-pemstore
nym-sphinx-types
nym-vesting-contract-common
+1 -1
View File
@@ -9,7 +9,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = { version = "0.1.51" }
async-trait = { workspace = true }
log = { workspace = true }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
+1 -1
View File
@@ -5,7 +5,7 @@ use crate::error::StatsError;
use crate::StatsMessage;
pub const DEFAULT_STATISTICS_SERVICE_ADDRESS: &str = "127.0.0.1";
pub const DEFAULT_STATISTICS_SERVICE_PORT: u16 = 8090;
pub const DEFAULT_STATISTICS_SERVICE_PORT: u16 = 8091;
pub const STATISTICS_SERVICE_VERSION: &str = "/v1";
pub const STATISTICS_SERVICE_API_STATISTICS: &str = "statistic";
+5
View File
@@ -16,6 +16,7 @@ bs58 = "0.4"
log = { workspace = true }
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
thiserror = "1.0.37"
async-trait = { workspace = true, optional = true }
## internal
nym-crypto = { path = "../crypto" }
@@ -23,3 +24,7 @@ nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contra
nym-sphinx-addressing = { path = "../nymsphinx/addressing" }
nym-sphinx-types = { path = "../nymsphinx/types" }
nym-bin-common = { path = "../bin-common" }
[features]
default = ["provider-trait"]
provider-trait = ["async-trait"]
+54 -1
View File
@@ -20,6 +20,9 @@ pub mod filter;
pub mod gateway;
pub mod mix;
#[cfg(feature = "provider-trait")]
pub mod provider_trait;
#[derive(Debug, Clone, Error)]
pub enum NymTopologyError {
#[error("The provided network topology is empty - there are no mixnodes and no gateways on it - the network request(s) probably failed")]
@@ -39,6 +42,16 @@ pub enum NymTopologyError {
#[error("No mixnodes available on layer {layer}")]
EmptyMixLayer { layer: MixLayer },
#[error("Uneven layer distribution. Layer {layer} has {nodes} on it, while we expected a value between {lower_bound} and {upper_bound} as we have {total_nodes} nodes in total. Full breakdown: {layer_distribution:?}")]
UnevenLayerDistribution {
layer: MixLayer,
nodes: usize,
lower_bound: usize,
upper_bound: usize,
total_nodes: usize,
layer_distribution: Vec<(MixLayer, usize)>,
},
}
#[derive(Debug, Clone)]
@@ -97,7 +110,7 @@ impl NymTopology {
}
pub fn num_mixnodes(&self) -> usize {
self.mixes.values().flat_map(|m| m.iter()).count()
self.mixes.values().map(|m| m.len()).sum()
}
pub fn mixes_as_vec(&self) -> Vec<mix::Node> {
@@ -237,6 +250,46 @@ impl NymTopology {
Ok(())
}
pub fn ensure_even_layer_distribution(
&self,
lower_threshold: f32,
upper_threshold: f32,
) -> Result<(), NymTopologyError> {
let mixnodes_count = self.num_mixnodes();
let layers = self
.mixes
.iter()
.map(|(k, v)| (*k, v.len()))
.collect::<Vec<_>>();
if self.gateways.is_empty() {
return Err(NymTopologyError::NoGatewaysAvailable);
}
if layers.is_empty() {
return Err(NymTopologyError::NoMixnodesAvailable);
}
let upper_bound = (mixnodes_count as f32 * upper_threshold) as usize;
let lower_bound = (mixnodes_count as f32 * lower_threshold) as usize;
for (layer, nodes) in &layers {
if nodes < &lower_bound || nodes > &upper_bound {
return Err(NymTopologyError::UnevenLayerDistribution {
layer: *layer,
nodes: *nodes,
lower_bound,
upper_bound,
total_nodes: mixnodes_count,
layer_distribution: layers,
});
}
}
Ok(())
}
#[must_use]
pub fn filter_system_version(&self, expected_version: &str) -> Self {
self.filter_node_versions(expected_version)
+2 -1
View File
@@ -3,7 +3,8 @@
use crate::{filter, NetworkAddress};
use nym_crypto::asymmetric::{encryption, identity};
use nym_mixnet_contract_common::{Layer, MixId, MixNodeBond};
pub use nym_mixnet_contract_common::Layer;
use nym_mixnet_contract_common::{MixId, MixNodeBond};
use nym_sphinx_addressing::nodes::NymNodeRoutingAddress;
use nym_sphinx_types::Node as SphinxNode;
use std::convert::{TryFrom, TryInto};
+44
View File
@@ -0,0 +1,44 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::NymTopology;
pub use async_trait::async_trait;
// hehe, wasm
#[cfg(not(target_arch = "wasm32"))]
#[async_trait]
pub trait TopologyProvider: Send {
async fn get_new_topology(&mut self) -> Option<NymTopology>;
}
#[cfg(target_arch = "wasm32")]
#[async_trait(?Send)]
pub trait TopologyProvider {
async fn get_new_topology(&mut self) -> Option<NymTopology>;
}
pub struct HardcodedTopologyProvider {
topology: NymTopology,
}
impl HardcodedTopologyProvider {
pub fn new(topology: NymTopology) -> Self {
HardcodedTopologyProvider { topology }
}
}
#[cfg(not(target_arch = "wasm32"))]
#[async_trait]
impl TopologyProvider for HardcodedTopologyProvider {
async fn get_new_topology(&mut self) -> Option<NymTopology> {
Some(self.topology.clone())
}
}
#[cfg(target_arch = "wasm32")]
#[async_trait(?Send)]
impl TopologyProvider for HardcodedTopologyProvider {
async fn get_new_topology(&mut self) -> Option<NymTopology> {
Some(self.topology.clone())
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-mixnet-contract"
version = "1.1.2"
version = "1.2.0-pre.0"
description = "Nym mixnet contract"
edition = { workspace = true }
authors = { workspace = true }
@@ -22,8 +22,8 @@ name = "mixnet_contract"
crate-type = ["cdylib", "rlib"]
[dependencies]
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.1.0" }
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.1.0" }
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.2.0" }
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.2.0" }
#nym-config = { path = "../../common/config"}
cosmwasm-std = "1.0.0"
+6 -1
View File
@@ -4,5 +4,10 @@
use vergen::{vergen, Config};
fn main() {
vergen(Config::default()).expect("failed to extract build metadata")
let mut config = Config::default();
if std::env::var("DOCS_RS").is_ok() {
// If we don't have access to git information, such as in a docs.rs build, don't error
*config.git_mut().skip_if_error_mut() = true;
}
vergen(config).expect("failed to extract build metadata");
}
@@ -28,9 +28,13 @@ pub(crate) fn query_contract_version() -> ContractBuildInformation {
ContractBuildInformation {
build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(),
build_version: env!("VERGEN_BUILD_SEMVER").to_string(),
commit_sha: env!("VERGEN_GIT_SHA").to_string(),
commit_timestamp: env!("VERGEN_GIT_COMMIT_TIMESTAMP").to_string(),
commit_branch: env!("VERGEN_GIT_BRANCH").to_string(),
commit_sha: option_env!("VERGEN_GIT_SHA").unwrap_or("NONE").to_string(),
commit_timestamp: option_env!("VERGEN_GIT_COMMIT_TIMESTAMP")
.unwrap_or("NONE")
.to_string(),
commit_branch: option_env!("VERGEN_GIT_BRANCH")
.unwrap_or("NONE")
.to_string(),
rustc_version: env!("VERGEN_RUSTC_SEMVER").to_string(),
}
}
+5 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-vesting-contract"
version = "1.1.4"
version = "1.2.0-pre.0"
description = "Nym vesting contract"
edition = { workspace = true }
authors = { workspace = true }
@@ -20,9 +20,9 @@ name = "vesting_contract"
crate-type = ["cdylib", "rlib"]
[dependencies]
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.1.0" }
contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", package = "nym-contracts-common", version = "0.1.0" }
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.1.0" }
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.2.0" }
contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", package = "nym-contracts-common", version = "0.2.0" }
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.2.0" }
cosmwasm-std = { version = "1.0.0 "}
cw2 = { version = "0.13.4" }
@@ -43,4 +43,4 @@ serde_json = "1.0.66"
vergen = { version = "=7.4.3", default-features = false, features = ["build", "git", "rustc"] }
[profile.release]
overflow-checks = true
overflow-checks = true
+5
View File
@@ -4,5 +4,10 @@
use vergen::{vergen, Config};
fn main() {
let mut config = Config::default();
if std::env::var("DOCS_RS").is_ok() {
// If we don't have access to git information, such as in a docs.rs build, don't error
*config.git_mut().skip_if_error_mut() = true;
}
vergen(Config::default()).expect("failed to extract build metadata")
}
+7 -3
View File
@@ -820,9 +820,13 @@ pub fn get_contract_version() -> ContractBuildInformation {
ContractBuildInformation {
build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(),
build_version: env!("VERGEN_BUILD_SEMVER").to_string(),
commit_sha: env!("VERGEN_GIT_SHA").to_string(),
commit_timestamp: env!("VERGEN_GIT_COMMIT_TIMESTAMP").to_string(),
commit_branch: env!("VERGEN_GIT_BRANCH").to_string(),
commit_sha: option_env!("VERGEN_GIT_SHA").unwrap_or("NONE").to_string(),
commit_timestamp: option_env!("VERGEN_GIT_COMMIT_TIMESTAMP")
.unwrap_or("NONE")
.to_string(),
commit_branch: option_env!("VERGEN_GIT_BRANCH")
.unwrap_or("NONE")
.to_string(),
rustc_version: env!("VERGEN_RUSTC_SEMVER").to_string(),
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "explorer-api"
version = "1.1.11"
version = "1.1.12"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+2
View File
@@ -12,6 +12,7 @@ use crate::mix_node::http::mix_node_make_default_routes;
use crate::mix_nodes::http::mix_nodes_make_default_routes;
use crate::overview::http::overview_make_default_routes;
use crate::ping::http::ping_make_default_routes;
use crate::service_providers::http::service_providers_make_default_routes;
use crate::state::ExplorerApiStateContext;
use crate::validators::http::validators_make_default_routes;
@@ -56,6 +57,7 @@ fn configure_rocket(state: ExplorerApiStateContext) -> Rocket<Build> {
"/overview" => overview_make_default_routes(&openapi_settings),
"/ping" => ping_make_default_routes(&openapi_settings),
"/validators" => validators_make_default_routes(&openapi_settings),
"/service-providers" => service_providers_make_default_routes(&openapi_settings),
};
building_rocket
+1
View File
@@ -23,6 +23,7 @@ mod mix_node;
pub(crate) mod mix_nodes;
mod overview;
mod ping;
pub(crate) mod service_providers;
mod state;
mod tasks;
mod validators;
+2 -1
View File
@@ -11,7 +11,7 @@ use serde::Serialize;
use std::sync::Arc;
use std::time::SystemTime;
use tokio::sync::RwLock;
use validator_client::models::SelectionChance;
use validator_client::models::{NodePerformance, SelectionChance};
#[derive(Clone, Debug, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
@@ -36,6 +36,7 @@ pub(crate) struct PrettyDetailedMixNodeBond {
pub stake_saturation: f32,
pub uncapped_saturation: f32,
pub avg_uptime: u8,
pub node_performance: NodePerformance,
pub estimated_operator_apy: f64,
pub estimated_delegators_apy: f64,
pub operating_cost: Coin,
+1
View File
@@ -153,6 +153,7 @@ impl ThreadsafeMixNodesCache {
layer: node.mixnode_details.bond_information.layer,
mix_node: node.mixnode_details.bond_information.mix_node.clone(),
avg_uptime: node.performance.round_to_integer(),
node_performance: node.node_performance.clone(),
stake_saturation: best_effort_small_dec_to_f64(node.stake_saturation) as f32,
uncapped_saturation: best_effort_small_dec_to_f64(node.uncapped_stake_saturation)
as f32,
@@ -0,0 +1,26 @@
use crate::service_providers::models::DirectoryService;
use okapi::openapi3::OpenApi;
use reqwest::Error as ReqwestError;
use rocket::{serde::json::Json, Route};
use rocket_okapi::settings::OpenApiSettings;
static SERVICE_PROVIDER_WELLKNOWN_URL: &str =
"https://nymtech.net/.wellknown/connect/service-providers.json";
pub fn service_providers_make_default_routes(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![settings: get_service_providers]
}
pub async fn get_services() -> Result<Vec<DirectoryService>, ReqwestError> {
reqwest::get(SERVICE_PROVIDER_WELLKNOWN_URL)
.await?
.json::<Vec<DirectoryService>>()
.await
}
#[openapi(tag = "service_providers")]
#[get("/")]
pub(crate) async fn get_service_providers() -> Json<Vec<DirectoryService>> {
let result = get_services().await.unwrap();
Json(result)
}
@@ -0,0 +1,2 @@
pub(crate) mod http;
pub(crate) mod models;
@@ -0,0 +1,16 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct DirectoryServiceProvider {
pub id: String,
pub description: String,
pub address: String,
pub gateway: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct DirectoryService {
pub id: String,
pub description: String,
pub items: Vec<DirectoryServiceProvider>,
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nym/network-explorer",
"version": "1.0.6",
"version": "1.0.7",
"private": true,
"license": "Apache-2.0",
"dependencies": {
+2 -1
View File
@@ -14,7 +14,8 @@ export const VALIDATORS_API = `${VALIDATOR_BASE_URL}/validators`;
export const BLOCK_API = `${NYM_API_BASE_URL}/block`;
export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`;
export const UPTIME_STORY_API = `${NYM_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this.
export const UPTIME_STORY_API_GATEWAY = `${NYM_API_BASE_URL}/api/v1/status/gateway`; // add ID then '/history' or '/report' to this.
export const UPTIME_STORY_API_GATEWAY = `${NYM_API_BASE_URL}/api/v1/status/gateway`; // add ID then '/history' or '/report' to this
export const SERVICE_PROVIDERS = `${API_BASE_URL}/service-providers`;
// errors
export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us.";
+9
View File
@@ -9,6 +9,7 @@ import {
OVERVIEW_API,
UPTIME_STORY_API,
VALIDATORS_API,
SERVICE_PROVIDERS,
} from './constants';
import {
@@ -28,6 +29,7 @@ import {
ValidatorsResponse,
GatewayBondAnnotated,
GatewayBond,
DirectoryService,
} from '../typeDefs/explorer-api';
function getFromCache(key: string) {
@@ -62,6 +64,7 @@ export class Api {
if (cachedMixnodes) {
return cachedMixnodes;
}
const res = await fetch(MIXNODES_API);
const json = await res.json();
storeInCache('mixnodes', JSON.stringify(json));
@@ -147,4 +150,10 @@ export class Api {
static fetchUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
(await fetch(`${UPTIME_STORY_API}/${id}/history`)).json();
static fetchServiceProviders = async (): Promise<DirectoryService[]> => {
const res = await fetch(SERVICE_PROVIDERS);
const json = await res.json();
return json;
};
}
@@ -1,6 +1,5 @@
import * as React from 'react';
import { Box, Typography } from '@mui/material';
import { ExpandLess, ExpandMore } from '@mui/icons-material';
import { useTheme } from '@mui/material/styles';
import { Tooltip } from '@nymproject/react/tooltip/Tooltip';
@@ -8,14 +7,10 @@ export const CustomColumnHeading: FCWithChildren<{ headingTitle: string; tooltip
headingTitle,
tooltipInfo,
}) => {
const [filter, toggleFilter] = React.useState<boolean>(false);
const theme = useTheme();
const handleClick = () => {
toggleFilter(!filter);
};
return (
<Box alignItems="center" display="flex" onClick={handleClick}>
<Box alignItems="center" display="flex">
{tooltipInfo && (
<Tooltip
title={tooltipInfo}
@@ -27,19 +22,9 @@ export const CustomColumnHeading: FCWithChildren<{ headingTitle: string; tooltip
arrow
/>
)}
<Typography
sx={{
fontWeight: 600,
fontSize: 14,
padding: 0,
// border: '1px solid red',
// minWidth: 300,
}}
data-testid={headingTitle}
>
{headingTitle}&nbsp;
<Typography variant="body2" fontWeight={600} data-testid={headingTitle}>
{headingTitle}
</Typography>
{filter ? <ExpandMore /> : <ExpandLess />}
</Box>
);
};
+21 -7
View File
@@ -1,5 +1,15 @@
import * as React from 'react';
import { Link, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';
import {
Link,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TableCellProps,
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { Tooltip } from '@nymproject/react/tooltip/Tooltip';
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
@@ -8,13 +18,13 @@ import { cellStyles } from './Universal-DataGrid';
import { unymToNym } from '../utils/currency';
import { GatewayEnrichedRowType } from './Gateways';
import { MixnodeRowType } from './MixNodes';
import { StakeSaturationProgressBar } from './MixNodes/Economics/StakeSaturationProgressBar';
export type ColumnsType = {
field: string;
title: string;
headerAlign: string;
flex?: number;
width?: number;
headerAlign?: TableCellProps['align'];
width?: string | number;
tooltipInfo?: string;
};
@@ -50,6 +60,10 @@ function formatCellValues(val: string | number, field: string) {
);
}
if (field === 'stake_saturation') {
return <StakeSaturationProgressBar value={Number(val)} threshold={100} />;
}
return val;
}
@@ -61,11 +75,11 @@ export const DetailTable: FCWithChildren<{
const theme = useTheme();
return (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
<Table sx={{ minWidth: 1080 }} aria-label={tableName}>
<TableHead>
<TableRow>
{columnsData?.map(({ field, title, flex, tooltipInfo }) => (
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, flex }}>
{columnsData?.map(({ field, title, width, tooltipInfo }) => (
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, width }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{tooltipInfo && (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
@@ -4,57 +4,48 @@ export const EconomicsInfoColumns: ColumnsType[] = [
{
field: 'estimatedTotalReward',
title: 'Estimated Total Reward',
flex: 1,
headerAlign: 'left',
width: '15%',
tooltipInfo:
'Estimated node reward (total for the operator and delegators) in the current epoch. There are roughly 24 epochs in a day.',
},
{
field: 'estimatedOperatorReward',
title: 'Estimated Operator Reward',
flex: 1,
headerAlign: 'left',
width: '15%',
tooltipInfo:
"Estimated operator's reward (including PM and Operating Cost) in the current epoch. There are roughly 24 epochs in a day.",
},
{
field: 'selectionChance',
title: 'Active Set Probability',
flex: 1,
headerAlign: 'left',
width: '12.5%',
tooltipInfo:
'Probability of getting selected in the reward set (active and standby nodes) in the next epoch. The more your stake, the higher the chances to be selected.',
},
{
field: 'stakeSaturation',
title: 'Stake Saturation',
flex: 1,
headerAlign: 'left',
tooltipInfo:
'Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 730k NYM, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set.',
},
{
field: 'profitMargin',
title: 'Profit Margin',
flex: 1,
headerAlign: 'left',
width: '12.5%',
tooltipInfo:
'Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators.',
},
{
field: 'operatingCost',
title: 'Operating Cost',
flex: 1,
headerAlign: 'left',
width: '10%',
tooltipInfo:
'Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators.',
},
{
field: 'avgUptime',
title: 'Avg. Score',
width: '10%',
tooltipInfo: "Mixnode's average routing score in the last 24 hour",
},
{
field: 'nodePerformance',
title: 'Routing Score',
flex: 1,
headerAlign: 'left',
tooltipInfo:
'Nodes routing score is relative to that of the network. Each time a node is tested, the test packets have to go through the full path of the network (a gateway + 3 nodes). If a node in the path drop packets it will affect the score of other nodes in the test.',
"Mixnode's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test.",
},
];
@@ -9,14 +9,14 @@ const parseToNumber = (value: number | undefined | string) =>
export const EconomicsProgress: FCWithChildren<
LinearProgressProps & {
threshold?: number;
color: string;
}
> = ({ threshold, ...props }) => {
> = ({ threshold, color, ...props }) => {
const theme = useTheme();
const { value } = props;
const valueNumber: number = parseToNumber(value);
const thresholdNumber: number = parseToNumber(threshold);
const percentageColor = valueNumber > (threshold || 100) ? 'warning' : 'inherit';
const percentageToDisplay = Math.min(valueNumber, thresholdNumber);
return (
@@ -29,9 +29,9 @@ export const EconomicsProgress: FCWithChildren<
<LinearProgress
{...props}
variant="determinate"
color={percentageColor}
color={color}
value={percentageToDisplay}
sx={{ width: '100%', borderRadius: '5px', backgroundColor: theme.palette.common.white }}
sx={{ width: '100%', borderRadius: '5px' }}
/>
</Box>
);
@@ -14,9 +14,7 @@ const row: EconomicsInfoRowWithIndex = {
selectionChance: {
value: 'High',
},
avgUptime: {
value: '65 %',
},
estimatedOperatorReward: {
value: '80000.123456 NYM',
},
@@ -29,9 +27,11 @@ const row: EconomicsInfoRowWithIndex = {
operatingCost: {
value: '11121 NYM',
},
stakeSaturation: {
value: '80 %',
progressBarValue: 80,
avgUptime: {
value: '-',
},
nodePerformance: {
value: '-',
},
};
@@ -55,9 +55,7 @@ const emptyRow: EconomicsInfoRowWithIndex = {
value: '-',
progressBarValue: 0,
},
avgUptime: {
value: '-',
},
estimatedOperatorReward: {
value: '-',
},
@@ -70,9 +68,11 @@ const emptyRow: EconomicsInfoRowWithIndex = {
operatingCost: {
value: '-',
},
stakeSaturation: {
avgUptime: {
value: '-',
},
nodePerformance: {
value: '-',
progressBarValue: 0,
},
};
@@ -14,11 +14,15 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => {
currencyToString((economicDynamicsStats?.data?.estimated_total_node_reward || '').toString()) || '-';
const estimatedOperatorRewards =
currencyToString((economicDynamicsStats?.data?.estimated_operator_reward || '').toString()) || '-';
const stakeSaturation = economicDynamicsStats?.data?.uncapped_saturation || '-';
const profitMargin = mixNode?.data?.profit_margin_percent
? toPercentIntegerString(mixNode?.data?.profit_margin_percent)
: '-';
const avgUptime = economicDynamicsStats?.data?.current_interval_uptime;
const avgUptime = mixNode?.data?.node_performance
? toPercentIntegerString(mixNode?.data?.node_performance.last_24h)
: '-';
const nodePerformance = mixNode?.data?.node_performance
? toPercentIntegerString(mixNode?.data?.node_performance.most_recent)
: '-';
const opCost = mixNode?.data?.operating_cost;
@@ -33,10 +37,6 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => {
selectionChance: {
value: selectionChance(economicDynamicsStats),
},
stakeSaturation: {
progressBarValue: typeof stakeSaturation === 'number' ? stakeSaturation * 100 : 0,
value: typeof stakeSaturation === 'number' ? `${(stakeSaturation * 100).toFixed(2)} %` : '-',
},
profitMargin: {
value: profitMargin ? `${profitMargin} %` : '-',
},
@@ -46,5 +46,8 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => {
avgUptime: {
value: avgUptime ? `${avgUptime} %` : '-',
},
nodePerformance: {
value: nodePerformance,
},
};
};
@@ -0,0 +1,32 @@
import React from 'react';
import { Box, Typography } from '@mui/material';
import { useIsMobile } from '../../../hooks/useIsMobile';
import { EconomicsProgress } from './EconomicsProgress';
export const StakeSaturationProgressBar = ({ value, threshold }: { value: number; threshold: number }) => {
const isTablet = useIsMobile('lg');
const percentageColor = value > (threshold || 100) ? 'warning' : 'inherit';
const textColor = percentageColor === 'warning' ? 'warning.main' : 'nym.wallet.fee';
return (
<Box
sx={{ display: 'flex', alignItems: 'center', flexDirection: isTablet ? 'column' : 'row' }}
id="field"
color={percentageColor}
>
<Typography
sx={{
mr: isTablet ? 0 : 1,
mb: isTablet ? 1 : 0,
fontWeight: '600',
fontSize: '12px',
color: textColor,
}}
id="stake-saturation-progress-bar"
>
{value}%
</Typography>
<EconomicsProgress value={value} threshold={threshold} color={percentageColor} />
</Box>
);
};
@@ -1,70 +1,19 @@
import * as React from 'react';
import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from '@mui/material';
import { Box } from '@mui/system';
import { useTheme, Theme } from '@mui/material/styles';
import { useTheme } from '@mui/material/styles';
import { Tooltip } from '@nymproject/react/tooltip/Tooltip';
import { EconomicsRowsType, EconomicsInfoRowWithIndex } from './types';
import { EconomicsProgress } from './EconomicsProgress';
import { cellStyles } from '../../Universal-DataGrid';
import { UniversalTableProps } from '../../DetailTable';
import { useIsMobile } from '../../../hooks/useIsMobile';
import { textColour } from '../../../utils';
const threshold = 100;
const textColour = (value: EconomicsRowsType, field: string, theme: Theme) => {
const progressBarValue = value?.progressBarValue || 0;
const fieldValue = value.value;
if (progressBarValue > 100) {
return theme.palette.warning.main;
}
if (field === 'selectionChance') {
// TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow
switch (fieldValue) {
case 'High':
case 'VeryHigh':
return theme.palette.nym.networkExplorer.selectionChance.overModerate;
case 'Good':
case 'Moderate':
return theme.palette.nym.networkExplorer.selectionChance.moderate;
case 'Low':
case 'VeryLow':
return theme.palette.nym.networkExplorer.selectionChance.underModerate;
default:
return theme.palette.nym.wallet.fee;
}
}
return theme.palette.nym.wallet.fee;
};
const formatCellValues = (value: EconomicsRowsType, field: string) => {
const isTablet = useIsMobile('lg');
if (value.progressBarValue) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', flexDirection: isTablet ? 'column' : 'row' }} id="field">
<Typography
sx={{
mr: isTablet ? 0 : 1,
mb: isTablet ? 1 : 0,
fontWeight: '600',
fontSize: '12px',
}}
id={field}
>
{value.value}
</Typography>
<EconomicsProgress threshold={threshold} value={value.progressBarValue} />
</Box>
);
}
return (
<Box sx={{ display: 'flex', alignItems: 'center' }} id="field">
<Typography sx={{ mr: 1, fontWeight: '600', fontSize: '12px' }} id={field}>
{value.value}
</Typography>
</Box>
);
};
const formatCellValues = (value: EconomicsRowsType, field: string) => (
<Box sx={{ display: 'flex', alignItems: 'center' }} id="field">
<Typography sx={{ mr: 1, fontWeight: '600', fontSize: '12px' }} id={field}>
{value.value}
</Typography>
</Box>
);
export const DelegatorsInfoTable: FCWithChildren<UniversalTableProps<EconomicsInfoRowWithIndex>> = ({
tableName,
@@ -78,21 +27,19 @@ export const DelegatorsInfoTable: FCWithChildren<UniversalTableProps<EconomicsIn
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
<TableHead>
<TableRow>
{columnsData?.map(({ field, title, flex, tooltipInfo }) => (
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, flex }}>
{columnsData?.map(({ field, title, tooltipInfo, width }) => (
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, width }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{tooltipInfo && (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Tooltip
title={tooltipInfo}
id={field}
placement="top-start"
textColor={theme.palette.nym.networkExplorer.tooltip.color}
bgColor={theme.palette.nym.networkExplorer.tooltip.background}
maxWidth={230}
arrow
/>
</Box>
<Tooltip
title={tooltipInfo}
id={field}
placement="top-start"
textColor={theme.palette.nym.networkExplorer.tooltip.color}
bgColor={theme.palette.nym.networkExplorer.tooltip.background}
maxWidth={230}
arrow
/>
)}
{title}
</Box>
@@ -106,19 +53,11 @@ export const DelegatorsInfoTable: FCWithChildren<UniversalTableProps<EconomicsIn
{columnsData?.map((_, index: number) => {
const { field } = columnsData[index];
const value: EconomicsRowsType = (eachRow as any)[field];
console.log(value);
return (
<TableCell
key={_.title}
component="th"
scope="row"
variant="body"
sx={{
...cellStyles,
padding: 2,
width: 200,
fontSize: 12,
fontWeight: 600,
color: textColour(value, field, theme),
}}
data-testid={`${_.title.replace(/ /g, '-')}-value`}
@@ -3,14 +3,18 @@ export type EconomicsRowsType = {
value: string;
};
export interface EconomicsInfoRow {
estimatedTotalReward: EconomicsRowsType;
estimatedOperatorReward: EconomicsRowsType;
selectionChance: EconomicsRowsType;
stakeSaturation: EconomicsRowsType;
profitMargin: EconomicsRowsType;
avgUptime: EconomicsRowsType;
operatingCost: EconomicsRowsType;
}
type TEconomicsInfoProperties =
| 'estimatedTotalReward'
| 'estimatedOperatorReward'
| 'estimatedOperatorReward'
| 'selectionChance'
| 'profitMargin'
| 'avgUptime'
| 'nodePerformance'
| 'operatingCost';
export type EconomicsInfoRow = {
[k in TEconomicsInfoProperties]: EconomicsRowsType;
};
export type EconomicsInfoRowWithIndex = EconomicsInfoRow & { id: number };
@@ -1,5 +1,5 @@
/* eslint-disable camelcase */
import { MixNodeResponse, MixNodeResponseItem, MixnodeStatus } from '../../typeDefs/explorer-api';
import { MixNodeResponse, MixNodeResponseItem, MixnodeStatus, NodePerformance } from '../../typeDefs/explorer-api';
import { toPercentIntegerString } from '../../utils';
import { unymToNym } from '../../utils/currency';
@@ -17,8 +17,9 @@ export type MixnodeRowType = {
layer: string;
profit_percentage: string;
avg_uptime: string;
stake_saturation: number;
stake_saturation: React.ReactNode;
operating_cost: string;
node_performance: NodePerformance['most_recent'];
};
export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] {
@@ -46,8 +47,9 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem):
host: item?.mix_node?.host || '',
layer: item?.layer || '',
profit_percentage: `${profitPercentage}%`,
avg_uptime: `${item.avg_uptime}%` || '-',
stake_saturation: uncappedSaturation,
avg_uptime: `${toPercentIntegerString(item.node_performance.last_24h)}%`,
stake_saturation: uncappedSaturation.toFixed(2),
operating_cost: `${unymToNym(item.operating_cost?.amount, 6)} NYM`,
node_performance: `${toPercentIntegerString(item.node_performance.most_recent)}%`,
};
}
+39 -37
View File
@@ -1,5 +1,5 @@
import * as React from 'react';
import { Box, TextField, MenuItem } from '@mui/material';
import { Box, TextField, MenuItem, FormControl } from '@mui/material';
import Select, { SelectChangeEvent } from '@mui/material/Select';
import { Filters } from './Filters/Filters';
import { useIsMobile } from '../hooks/useIsMobile';
@@ -7,10 +7,10 @@ import { useIsMobile } from '../hooks/useIsMobile';
const fieldsHeight = '42.25px';
type TableToolBarProps = {
onChangeSearch: (arg: string) => void;
onChangeSearch?: (arg: string) => void;
onChangePageSize: (event: SelectChangeEvent<string>) => void;
pageSize: string;
searchTerm: string;
searchTerm?: string;
withFilters?: boolean;
childrenBefore?: React.ReactNode;
childrenAfter?: React.ReactNode;
@@ -39,41 +39,43 @@ export const TableToolbar: FCWithChildren<TableToolBarProps> = ({
<Box sx={{ display: 'flex', flexDirection: isMobile ? 'column-reverse' : 'row', alignItems: 'middle' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', height: fieldsHeight }}>
{childrenBefore}
<Select
value={pageSize}
onChange={onChangePageSize}
sx={{
width: isMobile ? '50%' : 200,
marginRight: isMobile ? 0 : 2,
}}
>
<MenuItem value={10} data-testid="ten">
10
</MenuItem>
<MenuItem value={30} data-testid="thirty">
30
</MenuItem>
<MenuItem value={50} data-testid="fifty">
50
</MenuItem>
<MenuItem value={100} data-testid="hundred">
100
</MenuItem>
</Select>
<FormControl size="small">
<Select
value={pageSize}
onChange={onChangePageSize}
sx={{
width: isMobile ? '100%' : 200,
marginRight: isMobile ? 0 : 2,
}}
>
<MenuItem value={10} data-testid="ten">
10
</MenuItem>
<MenuItem value={30} data-testid="thirty">
30
</MenuItem>
<MenuItem value={50} data-testid="fifty">
50
</MenuItem>
<MenuItem value={100} data-testid="hundred">
100
</MenuItem>
</Select>
</FormControl>
</Box>
<TextField
sx={{
width: isMobile ? '100%' : 200,
marginBottom: isMobile ? 2 : 0,
'& > :not(style)': {
height: fieldsHeight,
},
}}
value={searchTerm}
data-testid="search-box"
placeholder="search"
onChange={(event) => onChangeSearch(event.target.value)}
/>
{!!onChangeSearch && (
<TextField
sx={{
width: isMobile ? '100%' : 200,
marginBottom: isMobile ? 2 : 0,
}}
size="small"
value={searchTerm}
data-testid="search-box"
placeholder="search"
onChange={(event) => onChangeSearch(event.target.value)}
/>
)}
</Box>
<Box
sx={{
@@ -62,7 +62,6 @@ export const UniversalDataGrid: FCWithChildren<DataGridProps> = ({ rows, columns
}}
columns={columns}
pageSize={Number(pageSize)}
rowsPerPageOptions={[5]}
disableSelectionOnClick
autoHeight
hideFooter={!pagination}
+38 -2
View File
@@ -4,6 +4,7 @@ import {
ApiState,
BlockResponse,
CountryDataResponse,
DirectoryService,
GatewayResponse,
MixNodeResponse,
MixnodeStatus,
@@ -24,6 +25,7 @@ interface StateData {
mode: PaletteMode;
navState: NavOptionType[];
validators?: ApiState<ValidatorsResponse>;
serviceProviders?: ApiState<DirectoryService>;
}
interface StateApi {
@@ -63,6 +65,7 @@ export const MainContextProvider: FCWithChildren = ({ children }) => {
const [validators, setValidators] = React.useState<ApiState<ValidatorsResponse>>();
const [block, setBlock] = React.useState<ApiState<BlockResponse>>();
const [countryData, setCountryData] = React.useState<ApiState<CountryDataResponse>>();
const [serviceProviders, setServiceProviders] = React.useState<ApiState<DirectoryService>>();
const toggleMode = () => setMode((m) => (m !== 'light' ? 'light' : 'dark'));
@@ -156,6 +159,20 @@ export const MainContextProvider: FCWithChildren = ({ children }) => {
});
}
};
const fetchServiceProviders = async () => {
setServiceProviders({ data: undefined, isLoading: true });
try {
const [res] = await Api.fetchServiceProviders();
setServiceProviders({ data: res, isLoading: false });
} catch (error) {
setServiceProviders({
error: error instanceof Error ? error : new Error('Service provider api fail'),
isLoading: false,
});
}
};
const updateNavState = (id: number) => {
const updated = navState.map((option) => ({
...option,
@@ -165,7 +182,14 @@ export const MainContextProvider: FCWithChildren = ({ children }) => {
};
React.useEffect(() => {
Promise.all([fetchOverviewSummary(), fetchGateways(), fetchValidators(), fetchBlock(), fetchCountryData()]);
Promise.all([
fetchOverviewSummary(),
fetchGateways(),
fetchValidators(),
fetchBlock(),
fetchCountryData(),
fetchServiceProviders(),
]);
}, []);
const state = React.useMemo<State>(
@@ -183,8 +207,20 @@ export const MainContextProvider: FCWithChildren = ({ children }) => {
toggleMode,
updateNavState,
validators,
serviceProviders,
}),
[block, countryData, gateways, globalError, mixnodes, mode, navState, summaryOverview, validators],
[
block,
countryData,
gateways,
globalError,
mixnodes,
mode,
navState,
summaryOverview,
validators,
serviceProviders,
],
);
return <MainContext.Provider value={state}>{children}</MainContext.Provider>;
+5
View File
@@ -44,6 +44,11 @@ export const originalNavOptions: NavOptionType[] = [
url: `${BIG_DIPPER}/validators`,
title: 'Validators',
},
{
id: 6,
url: 'network-components/service-providers',
title: 'Service Providers',
},
],
},
{
+2 -8
View File
@@ -22,23 +22,20 @@ const columns: ColumnsType[] = [
{
field: 'bond',
title: 'Bond',
flex: 1,
headerAlign: 'left',
},
{
field: 'node_performance',
title: 'Routing Score',
flex: 1,
headerAlign: 'left',
tooltipInfo:
'Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test.',
"Gateway's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test",
},
{
field: 'avgUptime',
title: 'Avg. Score',
flex: 1,
headerAlign: 'left',
tooltipInfo: 'Is the average routing score in the last 24 hours',
tooltipInfo: "Gateway's average routing score in the last 24 hours",
},
{
field: 'host',
@@ -50,19 +47,16 @@ const columns: ColumnsType[] = [
field: 'location',
title: 'Location',
headerAlign: 'left',
flex: 1,
},
{
field: 'owner',
title: 'Owner',
headerAlign: 'left',
flex: 1,
},
{
field: 'version',
title: 'Version',
headerAlign: 'left',
flex: 1,
},
];
+62 -34
View File
@@ -1,10 +1,12 @@
import * as React from 'react';
import { Link as RRDLink } from 'react-router-dom';
import { Box, Card, Grid, Link as MuiLink } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
import { SelectChangeEvent } from '@mui/material/Select';
import { diff, rcompare } from 'semver';
import { diff, gte, rcompare } from 'semver';
import { Tooltip as InfoTooltip } from '@nymproject/react/tooltip/Tooltip';
import { useMainContext } from '../../context/main';
import { gatewayToGridRow } from '../../components/Gateways';
import { GatewayResponse } from '../../typeDefs/explorer-api';
@@ -25,6 +27,8 @@ export const PageGateways: FCWithChildren = () => {
const [searchTerm, setSearchTerm] = React.useState<string>('');
const [versionFilter, setVersionFilter] = React.useState<VersionSelectOptions>(VersionSelectOptions.latestVersion);
const theme = useTheme();
const handleSearch = (str: string) => {
setSearchTerm(str.toLowerCase());
};
@@ -89,6 +93,7 @@ export const PageGateways: FCWithChildren = () => {
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
headerClassName: 'MuiDataGrid-header-override',
width: 380,
disableColumnMenu: true,
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<>
@@ -107,28 +112,24 @@ export const PageGateways: FCWithChildren = () => {
</>
),
},
{
field: 'bond',
width: 150,
type: 'number',
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
headerClassName: 'MuiDataGrid-header-override',
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={{ ...cellStyles }}
component={RRDLink}
to={`/network-components/gateway/${params.row.identity_key}`}
data-testid="pledge-amount"
>
{unymToNym(params.value, 6)}
</MuiLink>
),
},
{
field: 'node_performance',
renderHeader: () => <CustomColumnHeading headingTitle="Routing Score" />,
width: 150,
renderHeader: () => (
<>
<InfoTooltip
id="gateways-list-routing-score"
title="Gateway's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test"
placement="top-start"
textColor={theme.palette.nym.networkExplorer.tooltip.color}
bgColor={theme.palette.nym.networkExplorer.tooltip.background}
maxWidth={230}
arrow
/>
<CustomColumnHeading headingTitle="Routing Score" />
</>
),
width: 175,
disableColumnMenu: true,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
@@ -143,26 +144,32 @@ export const PageGateways: FCWithChildren = () => {
),
},
{
field: 'host',
renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />,
width: 180,
field: 'version',
renderHeader: () => <CustomColumnHeading headingTitle="Version" />,
width: 150,
disableColumnMenu: true,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={{ ...cellStyles }}
component={RRDLink}
to={`/network-components/gateway/${params.row.identity_key}`}
data-testid="host"
href={`${NYM_BIG_DIPPER}/account/${params.value}`}
target="_blank"
data-testid="owner"
>
{params.value}
</MuiLink>
),
sortComparator: (a, b) => {
if (gte(a, b)) return 1;
return -1;
},
},
{
field: 'location',
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
width: 180,
disableColumnMenu: true,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
@@ -185,11 +192,30 @@ export const PageGateways: FCWithChildren = () => {
</Box>
),
},
{
field: 'host',
renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />,
width: 180,
disableColumnMenu: true,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={{ ...cellStyles }}
component={RRDLink}
to={`/network-components/gateway/${params.row.identity_key}`}
data-testid="host"
>
{params.value}
</MuiLink>
),
},
{
field: 'owner',
headerName: 'Owner',
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
width: 180,
disableColumnMenu: true,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
@@ -204,19 +230,21 @@ export const PageGateways: FCWithChildren = () => {
),
},
{
field: 'version',
renderHeader: () => <CustomColumnHeading headingTitle="Version" />,
field: 'bond',
width: 150,
headerAlign: 'left',
disableColumnMenu: true,
type: 'number',
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
headerClassName: 'MuiDataGrid-header-override',
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={{ ...cellStyles }}
href={`${NYM_BIG_DIPPER}/account/${params.value}`}
target="_blank"
data-testid="owner"
component={RRDLink}
to={`/network-components/gateway/${params.row.identity_key}`}
data-testid="pledge-amount"
>
{params.value}
{unymToNym(params.value, 6)}
</MuiLink>
),
},
+15 -21
View File
@@ -17,45 +17,45 @@ const columns: ColumnsType[] = [
{
field: 'owner',
title: 'Owner',
headerAlign: 'left',
width: 230,
width: '15%',
},
{
field: 'identity_key',
title: 'Identity Key',
headerAlign: 'left',
width: 230,
width: '15%',
},
{
field: 'bond',
title: 'Stake',
flex: 1,
headerAlign: 'left',
width: '12.5%',
},
{
field: 'stake_saturation',
title: 'Stake Saturation',
width: '12.5%',
tooltipInfo:
'Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 730k NYM, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set.',
},
{
field: 'self_percentage',
width: '10%',
title: 'Bond %',
headerAlign: 'left',
width: 99,
},
{
field: 'host',
width: '10%',
title: 'Host',
headerAlign: 'left',
flex: 1,
},
{
field: 'location',
title: 'Location',
headerAlign: 'left',
flex: 1,
},
{
field: 'layer',
title: 'Layer',
headerAlign: 'left',
flex: 1,
},
];
@@ -64,11 +64,10 @@ const columns: ColumnsType[] = [
*/
const PageMixnodeDetailWithState: FCWithChildren = () => {
const { mixNode, mixNodeRow, description, stats, status, uptimeStory, uniqDelegations } = useMixnodeContext();
console.log(mixNodeRow);
return (
<Box component="main">
<Title text="Mixnode Detail" />
<Grid container spacing={2} mt={1} mb={6}>
<Grid item xs={12}>
{mixNodeRow && description?.data && (
@@ -76,13 +75,11 @@ const PageMixnodeDetailWithState: FCWithChildren = () => {
)}
</Grid>
</Grid>
<Grid container>
<Grid item xs={12}>
<DetailTable columnsData={columns} tableName="Mixnode detail table" rows={mixNodeRow ? [mixNodeRow] : []} />
</Grid>
</Grid>
<Grid container spacing={2} mt={0}>
<Grid item xs={12}>
<DelegatorsInfoTable
@@ -92,7 +89,6 @@ const PageMixnodeDetailWithState: FCWithChildren = () => {
/>
</Grid>
</Grid>
<Grid container spacing={2} mt={0}>
<Grid item xs={12}>
<ContentCard title={`Stake Breakdown (${uniqDelegations?.data?.length} delegators)`}>
@@ -100,7 +96,6 @@ const PageMixnodeDetailWithState: FCWithChildren = () => {
</ContentCard>
</Grid>
</Grid>
<Grid container spacing={2} mt={0}>
<Grid item xs={12} md={4}>
<ContentCard title="Mixnode Stats">
@@ -144,7 +139,6 @@ const PageMixnodeDetailWithState: FCWithChildren = () => {
)}
</Grid>
</Grid>
<Grid container spacing={2} mt={0}>
<Grid item xs={12} md={4}>
{status && (
+14 -4
View File
@@ -85,6 +85,7 @@ export const PageMixnodes: FCWithChildren = () => {
{
field: 'mix_id',
headerName: 'Mix ID',
disableColumnMenu: true,
renderHeader: () => <CustomColumnHeading headingTitle="Mix ID" />,
headerClassName: 'MuiDataGrid-header-override',
width: 100,
@@ -103,6 +104,7 @@ export const PageMixnodes: FCWithChildren = () => {
{
field: 'identity_key',
headerName: 'Identity Key',
disableColumnMenu: true,
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
headerClassName: 'MuiDataGrid-header-override',
width: 170,
@@ -128,6 +130,7 @@ export const PageMixnodes: FCWithChildren = () => {
{
field: 'bond',
headerName: 'Stake',
disableColumnMenu: true,
renderHeader: () => <CustomColumnHeading headingTitle="Stake" />,
type: 'number',
headerClassName: 'MuiDataGrid-header-override',
@@ -146,6 +149,7 @@ export const PageMixnodes: FCWithChildren = () => {
{
field: 'stake_saturation',
headerName: 'Stake Saturation',
disableColumnMenu: true,
renderHeader: () => (
<CustomColumnHeading
headingTitle="Stake Saturation"
@@ -164,13 +168,14 @@ export const PageMixnodes: FCWithChildren = () => {
component={RRDLink}
to={`/network-components/mixnode/${params.row.mix_id}`}
>
{`${params.value.toFixed(2)} %`}
{`${params.value} %`}
</MuiLink>
),
},
{
field: 'pledge_amount',
headerName: 'Bond',
disableColumnMenu: true,
width: 175,
headerClassName: 'MuiDataGrid-header-override',
renderHeader: () => <CustomColumnHeading headingTitle="Bond" tooltipInfo="Node operator's share of stake." />,
@@ -189,6 +194,7 @@ export const PageMixnodes: FCWithChildren = () => {
{
field: 'profit_percentage',
headerName: 'Profit Margin',
disableColumnMenu: true,
renderHeader: () => (
<CustomColumnHeading
headingTitle="Profit Margin"
@@ -220,6 +226,7 @@ export const PageMixnodes: FCWithChildren = () => {
headerClassName: 'MuiDataGrid-header-override',
width: 170,
headerAlign: 'left',
disableColumnMenu: true,
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }}
@@ -231,12 +238,13 @@ export const PageMixnodes: FCWithChildren = () => {
),
},
{
field: 'avg_uptime',
field: 'node_performance',
headerName: 'Routing Score',
disableColumnMenu: true,
renderHeader: () => (
<CustomColumnHeading
headingTitle="Routing Score"
tooltipInfo="Nodes routing score is relative to that of the network. Each time a node is tested, the test packets have to go through the full path of the network (a gateway + 3 nodes). If a node in the path drop packets it will affect the score of other nodes in the test."
tooltipInfo="Mixnode's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test."
/>
),
headerClassName: 'MuiDataGrid-header-override',
@@ -255,6 +263,7 @@ export const PageMixnodes: FCWithChildren = () => {
{
field: 'owner',
headerName: 'Owner',
disableColumnMenu: true,
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
width: 120,
headerAlign: 'left',
@@ -274,6 +283,7 @@ export const PageMixnodes: FCWithChildren = () => {
field: 'location',
headerName: 'Location',
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
disableColumnMenu: true,
width: 120,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
@@ -303,6 +313,7 @@ export const PageMixnodes: FCWithChildren = () => {
field: 'host',
headerName: 'Host',
renderHeader: () => <CustomColumnHeading headingTitle="Host" />,
disableColumnMenu: true,
headerClassName: 'MuiDataGrid-header-override',
width: 130,
headerAlign: 'left',
@@ -321,7 +332,6 @@ export const PageMixnodes: FCWithChildren = () => {
const handlePageSize = (event: SelectChangeEvent<string>) => {
setPageSize(event.target.value);
};
return (
<>
<Title text="Mixnodes" />
+15 -4
View File
@@ -3,6 +3,7 @@ import { Box, Grid, Link, Typography } from '@mui/material';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import { useTheme } from '@mui/material/styles';
import { useNavigate } from 'react-router-dom';
import { PeopleAlt } from '@mui/icons-material';
import { WorldMap } from '../../components/WorldMap';
import { useMainContext } from '../../context/main';
import { formatNumber } from '../../utils';
@@ -18,7 +19,7 @@ import { Icons } from '../../components/Icons';
export const PageOverview: FCWithChildren = () => {
const theme = useTheme();
const navigate = useNavigate();
const { summaryOverview, gateways, validators, block, countryData } = useMainContext();
const { summaryOverview, gateways, validators, block, countryData, serviceProviders } = useMainContext();
return (
<Box component="main" sx={{ flexGrow: 1 }}>
<Grid>
@@ -61,7 +62,7 @@ export const PageOverview: FCWithChildren = () => {
</>
)}
{gateways && (
<Grid item xs={12} md={6}>
<Grid item xs={12} md={4}>
<StatsCard
onClick={() => navigate('/network-components/gateways')}
title="Gateways"
@@ -71,9 +72,19 @@ export const PageOverview: FCWithChildren = () => {
/>
</Grid>
)}
{serviceProviders && (
<Grid item xs={12} md={4}>
<StatsCard
onClick={() => navigate('/network-components/service-providers')}
title="Service providers"
icon={<PeopleAlt />}
count={serviceProviders.data?.items.length}
errorMsg={summaryOverview?.error}
/>
</Grid>
)}
{validators && (
<Grid item xs={12} md={6}>
<Grid item xs={12} md={4}>
<StatsCard
onClick={() => window.open(`${BIG_DIPPER}/validators`)}
title="Validators"
@@ -0,0 +1,73 @@
import React, { useEffect, useState } from 'react';
import { Card, FormControl, Grid, MenuItem, Select, SelectChangeEvent } from '@mui/material';
import { Api } from '../../api';
import { TableToolbar } from '../../components/TableToolbar';
import { Title } from '../../components/Title';
import { UniversalDataGrid } from '../../components/Universal-DataGrid';
import { DirectoryService } from '../../typeDefs/explorer-api';
const columns = [
{
headerName: 'Client ID',
field: 'address',
disableColumnMenu: true,
flex: 1,
},
];
const SupportedApps = () => {
const [selected, setSelected] = useState<string>('');
const handleChange = (e: SelectChangeEvent) => setSelected(e.target.value);
return (
<FormControl size="small">
<Select value={selected} onChange={handleChange} displayEmpty sx={{ mr: 2 }}>
<MenuItem value="">Supported Apps</MenuItem>
<MenuItem sx={{ opacity: 1 }}>Keybase</MenuItem>
<MenuItem>Telegram</MenuItem>
<MenuItem>Electrum</MenuItem>
</Select>
</FormControl>
);
};
export const ServiceProviders = () => {
const [serviceProviders, setServiceProviders] = useState<DirectoryService>();
const [pageSize, setPageSize] = React.useState('10');
const getServiceproviders = async () => {
const [data] = await Api.fetchServiceProviders();
setServiceProviders(data);
};
useEffect(() => {
getServiceproviders();
}, []);
const handleOnPageSizeChange = (event: SelectChangeEvent<string>) => {
setPageSize(event.target.value);
};
if (!serviceProviders) return null;
return (
<>
<Title text="Service Providers" />
<Grid container>
<Grid item xs={12}>
<Card
sx={{
padding: 2,
}}
>
<TableToolbar
onChangePageSize={handleOnPageSizeChange}
pageSize={pageSize}
childrenBefore={<SupportedApps />}
/>
<UniversalDataGrid pagination rows={serviceProviders.items} columns={columns} pageSize={pageSize} />
</Card>
</Grid>
</Grid>
</>
);
};
+2 -1
View File
@@ -5,6 +5,7 @@ import { PageGateways } from '../pages/Gateways';
import { PageGatewayDetail } from '../pages/GatewayDetail';
import { PageMixnodeDetail } from '../pages/MixnodeDetail';
import { PageMixnodes } from '../pages/Mixnodes';
import { ServiceProviders } from '../pages/ServiceProviders';
const ValidatorRoute: FCWithChildren = () => {
const navigate = useNavigate();
@@ -21,6 +22,6 @@ export const NetworkComponentsRoutes: FCWithChildren = () => (
<Route path="gateways" element={<PageGateways />} />
<Route path="gateway/:id" element={<PageGatewayDetail />} />
<Route path="validators" element={<ValidatorRoute />} />
<Route path="gateways/:id" element={<h1> Specific Gateways ID</h1>} />
<Route path="service-providers" element={<ServiceProviders />} />
</ReactRouterRoutes>
);
+8 -12
View File
@@ -4,31 +4,27 @@
the theme declaration in index.tsx or the style prop
in <DataGrid /> */
.MuiDataGrid-sortIcon, .MuiDataGrid-menuIcon, .MuiDataGrid-iconButtonContainer{
display: none !important;
}
.MuiDataGrid-columnSeparator {
visibility: hidden;
visibility: hidden;
}
/* TODO - this should be managed somehow in MUI DataGrid
but documentation doesnt offer a way to do it. Possibly only
included in Data Grid Pro package */
.MuiDataGrid-header-override {
height: 55px;
min-height: 55px;
height: 55px;
min-height: 55px;
}
/* Again, no offered way to add sx to this specific div to kill the padding
which puts it out of sync with other (sx styled) cells */
div div.MuiDataGrid-root .MuiDataGrid-columnHeaderTitleContainer {
padding-left: 0;
padding-left: 0;
}
@media screen and (max-width: 900px) {
.MuiDrawer-paperAnchorLeft {
min-width: 100vw;
margin-top: 58px;
}
.MuiDrawer-paperAnchorLeft {
min-width: 100vw;
margin-top: 58px;
}
}
+14
View File
@@ -84,6 +84,7 @@ export interface MixNodeResponseItem {
};
mix_node: MixNode;
avg_uptime: number;
node_performance: NodePerformance;
stake_saturation: number;
uncapped_saturation: number;
operating_cost: Amount;
@@ -236,3 +237,16 @@ export type MixNodeEconomicDynamicsStatsResponse = {
estimated_delegators_reward: number;
current_interval_uptime: number;
};
export type DirectoryServiceProvider = {
id: string;
description: string;
address: string;
gateway: string;
};
export type DirectoryService = {
id: string;
descrtiption: string;
items: DirectoryServiceProvider[];
};
+28
View File
@@ -1,7 +1,9 @@
/* eslint-disable camelcase */
import { MutableRefObject } from 'react';
import { Theme } from '@mui/material/styles';
import { registerLocale, getName } from 'i18n-iso-countries';
import { CountryData } from '../typeDefs/explorer-api';
import { EconomicsRowsType } from '../components/MixNodes/Economics/types';
registerLocale(require('i18n-iso-countries/langs/en.json'));
@@ -53,3 +55,29 @@ export const splice = (start: number, deleteCount: number, address?: string): st
* @returns A stringified integer
*/
export const toPercentIntegerString = (value: string) => Math.round(Number(value) * 100).toString();
export const textColour = (value: EconomicsRowsType, field: string, theme: Theme) => {
const progressBarValue = value?.progressBarValue || 0;
const fieldValue = value.value;
if (progressBarValue > 100) {
return theme.palette.warning.main;
}
if (field === 'selectionChance') {
// TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow
switch (fieldValue) {
case 'High':
case 'VeryHigh':
return theme.palette.nym.networkExplorer.selectionChance.overModerate;
case 'Good':
case 'Moderate':
return theme.palette.nym.networkExplorer.selectionChance.moderate;
case 'Low':
case 'VeryLow':
return theme.palette.nym.networkExplorer.selectionChance.underModerate;
default:
return theme.palette.nym.wallet.fee;
}
}
return theme.palette.nym.wallet.fee;
};
+2 -2
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-gateway"
version = "1.1.11"
version = "1.1.12"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
@@ -16,7 +16,7 @@ rust-version = "1.56"
[dependencies]
anyhow = "1.0.53"
async-trait = { version = "0.1.51" }
async-trait = { workspace = true }
bip39 = "1.0.1"
bs58 = "0.4.0"
clap = { version = "4.0", features = ["cargo", "derive"] }
+2 -2
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-mixnode"
version = "1.1.12"
version = "1.1.13"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
@@ -45,7 +45,7 @@ mixnet-client = { path="../common/client-libs/mixnet-client" }
mixnode-common = { path="../common/mixnode-common" }
nym-nonexhaustive-delayqueue = { path="../common/nonexhaustive-delayqueue" }
nym-sphinx = { path="../common/nymsphinx" }
nym-pemstore = { path = "../common/pemstore", version = "0.1.0" }
nym-pemstore = { path = "../common/pemstore", version = "0.2.0" }
nym-task = { path = "../common/task" }
nym-types = { path = "../common/types" }
nym-topology = { path="../common/topology" }
+2 -2
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-api"
version = "1.1.12"
version = "1.1.13"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
@@ -15,7 +15,7 @@ rust-version = "1.56"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = "0.1.52"
async-trait = { workspace = true }
bs58 = {version = "0.4.0" }
bip39 = "1"
cfg-if = "1.0"
+3 -10
View File
@@ -88,10 +88,7 @@ impl Monitor {
)
.await
{
error!(
"Failed to submit monitor run information to the database - {}",
err
);
error!("Failed to submit monitor run information to the database - {err}",);
// TODO: slightly more graceful shutdown here
process::exit(1);
@@ -124,12 +121,8 @@ impl Monitor {
for route in routes {
let mut packet_preparer = self.packet_preparer.clone();
let route = route.clone();
let route_test_packets = self.route_test_packets;
let gateway_packets = tokio::spawn(async move {
packet_preparer.prepare_test_route_viability_packets(&route, route_test_packets)
})
.await
.unwrap();
let gateway_packets = packet_preparer
.prepare_test_route_viability_packets(&route, self.route_test_packets);
packets.push(gateway_packets);
}
+6
View File
@@ -2,6 +2,12 @@
## [Unreleased]
## [v1.1.12] (2023-03-07)
- NymConnect - Update display for selected Service Provider ([#3116])
[#3116]: https://github.com/nymtech/nym/issues/3116
## [v1.1.11] (2023-02-28)
- NC - add the option to manually select and use a specific Service Provider ([#2953])
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nym/nym-connect",
"version": "1.1.11",
"version": "1.1.12",
"main": "index.js",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-connect"
version = "1.1.11"
version = "1.1.12"
description = "nym-connect"
authors = ["Nym Technologies SA"]
license = ""
@@ -1,7 +1,7 @@
{
"package": {
"productName": "nym-connect",
"version": "1.1.11"
"version": "1.1.12"
},
"build": {
"distDir": "../dist",
@@ -30,7 +30,7 @@ export const ServiceProviderSettings = () => {
if (match) return match.description;
return 'The service provider specified is not in our known list.';
return 'N/A';
};
const validateInput = (value: string) => {
@@ -86,7 +86,12 @@ export const ServiceProviderSettings = () => {
)}
</FormControl>
{userDefinedSPAddress.address && userDefinedSPAddress.isActive && (
<Typography sx={{ mt: 1 }}>{getSPDescription(userDefinedSPAddress.address)}</Typography>
<Box sx={{ mt: 2 }}>
<Typography variant="body2">Name of Service Provider</Typography>
<Typography variant="body2" sx={{ mt: 0.5 }} color="grey.400">
{getSPDescription(userDefinedSPAddress.address)}
</Typography>
</Box>
)}
</Box>
<AppVersion />
+1
View File
@@ -3782,6 +3782,7 @@ dependencies = [
name = "nym-topology"
version = "0.1.0"
dependencies = [
"async-trait",
"bs58",
"log",
"nym-bin-common",
+6
View File
@@ -2,6 +2,12 @@
## [Unreleased]
## [v1.1.11] (2023-03-07)
- Wallet: optional gas and memo fields ([#2222])
[#2222]: https://github.com/nymtech/nym/issues/2222
## [v1.1.10] (2023-02-20)
- Wallet - Fix send address bug ([#3030])
+7 -7
View File
@@ -2785,7 +2785,7 @@ dependencies = [
[[package]]
name = "nym-bin-common"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"clap",
"clap_complete",
@@ -2811,7 +2811,7 @@ dependencies = [
[[package]]
name = "nym-contracts-common"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"bs58",
"cosmwasm-std",
@@ -2830,7 +2830,7 @@ dependencies = [
[[package]]
name = "nym-mixnet-contract-common"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"bs58",
"cosmwasm-std",
@@ -2861,7 +2861,7 @@ dependencies = [
[[package]]
name = "nym-pemstore"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"pem",
]
@@ -2893,7 +2893,7 @@ dependencies = [
[[package]]
name = "nym-vesting-contract"
version = "1.1.4"
version = "1.2.0-pre.0"
dependencies = [
"cosmwasm-std",
"cw-storage-plus",
@@ -2910,7 +2910,7 @@ dependencies = [
[[package]]
name = "nym-vesting-contract-common"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"cosmwasm-std",
"nym-contracts-common",
@@ -2957,7 +2957,7 @@ dependencies = [
[[package]]
name = "nym_wallet"
version = "1.1.10"
version = "1.1.11"
dependencies = [
"aes-gcm",
"argon2 0.3.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nymproject/nym-wallet-app",
"version": "1.1.10",
"version": "1.1.11",
"main": "index.js",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym_wallet"
version = "1.1.10"
version = "1.1.11"
description = "Nym Native Wallet"
authors = ["Nym Technologies SA"]
license = ""
+1
View File
@@ -143,6 +143,7 @@ fn main() {
vesting::queries::vesting_start_time,
simulate::admin::simulate_update_contract_settings,
simulate::cosmos::simulate_send,
simulate::cosmos::get_custom_fees,
simulate::mixnet::simulate_bond_gateway,
simulate::mixnet::simulate_unbond_gateway,
simulate::mixnet::simulate_bond_mixnode,
@@ -33,3 +33,13 @@ pub async fn simulate_send(
let result = client.nyxd.simulate(vec![msg]).await?;
guard.create_detailed_fee(result)
}
#[tauri::command]
pub async fn get_custom_fees(
fees_amount: DecCoin,
state: tauri::State<'_, WalletState>,
) -> Result<FeeDetails, BackendError> {
let guard = state.read().await;
let fee = guard.attempt_convert_to_fixed_fee(fees_amount.clone())?;
Ok(FeeDetails::new(Some(fees_amount), fee))
}
+11
View File
@@ -88,6 +88,17 @@ pub(crate) struct WalletAccountIds {
}
impl WalletStateInner {
pub fn attempt_convert_to_fixed_fee(&self, coin: DecCoin) -> Result<Fee, BackendError> {
// first we have to convert the coin to its base denomination
let base_coin = self.attempt_convert_to_base_coin(coin)?;
// then we get the gas price for the current network
let current_client = self.current_client()?;
let gas_price = current_client.nyxd.gas_price();
Ok(Fee::manual_with_gas_price(base_coin, gas_price.clone()))
}
// note that `Coin` is ALWAYS the base coin
pub fn attempt_convert_to_base_coin(&self, coin: DecCoin) -> Result<Coin, BackendError> {
let registered_coins = self
+1 -1
View File
@@ -1,7 +1,7 @@
{
"package": {
"productName": "nym-wallet",
"version": "1.1.10"
"version": "1.1.11"
},
"build": {
"distDir": "../dist",
@@ -1,5 +1,5 @@
import React from 'react';
import { Box, Stack, Typography, TypographyProps } from '@mui/material';
import { Box, Stack, SxProps, Typography, TypographyProps } from '@mui/material';
import { ModalDivider } from './ModalDivider';
export const ModalListItem: FCWithChildren<{
@@ -9,14 +9,15 @@ export const ModalListItem: FCWithChildren<{
fontWeight?: TypographyProps['fontWeight'];
light?: boolean;
value?: React.ReactNode;
}> = ({ label, value, hidden, fontWeight, divider }) => (
sxValue?: SxProps;
}> = ({ label, value, hidden, fontWeight, divider, sxValue }) => (
<Box sx={{ display: hidden ? 'none' : 'block' }}>
<Stack direction="row" justifyContent="space-between">
<Typography fontSize="smaller" fontWeight={fontWeight} sx={{ color: 'text.primary', fontSize: 14 }}>
{label}
</Typography>
{value && (
<Typography fontSize="smaller" fontWeight={fontWeight} sx={{ color: 'text.primary', fontSize: 14 }}>
<Typography fontSize="smaller" fontWeight={fontWeight} sx={{ color: 'text.primary', fontSize: 14, ...sxValue }}>
{value}
</Typography>
)}
@@ -35,6 +35,9 @@ export const SendInput = () => {
onClose={() => {}}
onAddressChange={() => {}}
onAmountChange={() => {}}
onUserFeesChange={() => {}}
onMemoChange={() => {}}
setShowMore={() => {}}
{...storybookStylesModal(theme)}
/>
);
@@ -16,6 +16,7 @@ export const SendDetailsModal = ({
onSend,
sx,
backdropProps,
memo,
}: {
fromAddress?: string;
toAddress: string;
@@ -27,6 +28,7 @@ export const SendDetailsModal = ({
onSend: (data: { val: DecCoin; to: string }) => void;
sx?: SxProps;
backdropProps?: object;
memo?: string;
}) => (
<SimpleModal
header="Send details"
@@ -43,6 +45,20 @@ export const SendDetailsModal = ({
<ModalListItem label="To" value={toAddress} divider />
<ModalListItem label="Amount" value={`${amount?.amount} ${denom.toUpperCase()}`} divider />
<ModalFee fee={fee} divider isLoading={false} />
{memo && (
<ModalListItem
label="Memo"
value={memo}
sxValue={{
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
overflowWrap: 'anywhere',
maxWidth: '300px',
}}
divider
/>
)}
<ModalTotalAmount fee={fee} amount={amount?.amount} divider isLoading={false} />
</Stack>
</SimpleModal>
@@ -1,11 +1,15 @@
import React, { useEffect, useState } from 'react';
import { Stack, TextField, Typography, SxProps } from '@mui/material';
import { Stack, TextField, Typography, SxProps, FormControlLabel, Checkbox } from '@mui/material';
import Big from 'big.js';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { CurrencyDenom, DecCoin } from '@nymproject/types';
import { CurrencyDenom, DecCoin, isValidRawCoin } from '@nymproject/types';
import { validateAmount } from 'src/utils';
import { SimpleModal } from '../Modals/SimpleModal';
import { ModalListItem } from '../Modals/ModalListItem';
const maxUserFees = '10.0';
const minUserFees = '0.000001'; // aka 1 unym
export const SendInputModal = ({
fromAddress,
toAddress,
@@ -19,6 +23,12 @@ export const SendInputModal = ({
onAddressChange,
sx,
backdropProps,
userFees,
memo,
onUserFeesChange,
onMemoChange,
showMore,
setShowMore,
}: {
fromAddress?: string;
toAddress: string;
@@ -26,24 +36,61 @@ export const SendInputModal = ({
balance?: string;
denom?: CurrencyDenom;
error?: string;
showMore?: boolean;
setShowMore: (show: boolean) => void;
onNext: () => void;
onClose: () => void;
onAmountChange: (value: DecCoin) => void;
onAddressChange: (value: string) => void;
sx?: SxProps;
backdropProps?: object;
userFees?: DecCoin;
memo?: string;
onUserFeesChange: (value: DecCoin) => void;
onMemoChange: (value: string) => void;
}) => {
const [isValid, setIsValid] = useState(false);
const [memoIsValid, setMemoIsValid] = useState(true);
const [feeAmountIsValid, setFeeAmountIsValid] = useState(true);
const validate = async (value: DecCoin) => {
const isValidAmount = await validateAmount(value.amount, '0');
setIsValid(isValidAmount);
};
const validateUserFees = (fees: DecCoin) => {
if (!isValidRawCoin(fees.amount) || !Number(fees.amount)) {
setFeeAmountIsValid(false);
return;
}
const f = Big(fees.amount);
if (f.gt(maxUserFees) || f.lt(minUserFees)) {
setFeeAmountIsValid(false);
return;
}
setFeeAmountIsValid(true);
};
useEffect(() => {
if (amount) validate(amount);
}, []);
useEffect(() => {
if (memo && !/^(\w|\s)+$/.test(memo)) {
setMemoIsValid(false);
return;
}
setMemoIsValid(true);
}, [memo]);
useEffect(() => {
if (userFees) {
validateUserFees(userFees);
} else {
setFeeAmountIsValid(true);
}
}, [userFees]);
return (
<SimpleModal
header="Send"
@@ -51,7 +98,7 @@ export const SendInputModal = ({
onClose={onClose}
okLabel="Next"
onOk={async () => onNext()}
okDisabled={!isValid}
okDisabled={!isValid || !memoIsValid || !feeAmountIsValid}
sx={sx}
backdropProps={backdropProps}
>
@@ -84,6 +131,35 @@ export const SendInputModal = ({
Est. fee for this transaction will be show on the next page
</Typography>
</Stack>
<FormControlLabel
control={<Checkbox onChange={() => setShowMore(!showMore)} checked={showMore} />}
label="More options"
sx={{ mt: 2 }}
/>
{showMore && (
<Stack direction="column" gap={3} mt={2} mb={3}>
<CurrencyFormField
label="Fees"
onChanged={(v) => onUserFeesChange(v)}
initialValue={userFees?.amount}
fullWidth
/>
<TextField
name="memo"
label="Memo"
onChange={(e) => onMemoChange(e.target.value)}
value={memo}
error={!memoIsValid}
helperText={
!memoIsValid
? ' The text is invalid, only alphanumeric characters and white spaces are allowed'
: undefined
}
InputLabelProps={{ shrink: true }}
fullWidth
/>
</Stack>
)}
</SimpleModal>
);
};
+41 -5
View File
@@ -1,4 +1,4 @@
import React, { useContext, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { DecCoin } from '@nymproject/types';
import { AppContext, urls } from 'src/context';
import { useGetFee } from 'src/hooks/useGetFee';
@@ -18,11 +18,28 @@ export const SendModal = ({ onClose, hasStorybookStyles }: { onClose: () => void
const [modal, setModal] = useState<'send' | 'send details'>('send');
const [error, setError] = useState<string>();
const [sendError, setSendError] = useState(false);
const [gasError, setGasError] = useState<string>();
const [isLoading, setIsLoading] = useState(false);
const [userFees, setUserFees] = useState<DecCoin>();
const [memo, setMemo] = useState<string>();
const [txDetails, setTxDetails] = useState<TTransactionDetails>();
const [showMoreOptions, setShowMoreOptions] = useState(false);
const { clientDetails, userBalance, network } = useContext(AppContext);
const { fee, getFee, feeError } = useGetFee();
const { fee, getFee, feeError, setFeeManually } = useGetFee();
useEffect(() => {
if (userFees?.amount.length === 0) {
setUserFees(undefined);
}
}, [userFees]);
useEffect(() => {
if (!showMoreOptions) {
setUserFees(undefined);
setMemo(undefined);
}
}, [showMoreOptions]);
// removes any zero-width spaces and trailing white space
const sanitizeAddress = (address: string) => address.replace(/[\u200B-\u200D\uFEFF]/g, '').trim();
@@ -32,7 +49,11 @@ export const SendModal = ({ onClose, hasStorybookStyles }: { onClose: () => void
setIsLoading(true);
setError(undefined);
try {
await getFee(simulateSend, { address: toAddress, amount });
if (userFees) {
await setFeeManually(userFees);
} else {
await getFee(simulateSend, { address: toAddress, amount });
}
setModal('send details');
} catch (e) {
setError(e as string);
@@ -48,14 +69,18 @@ export const SendModal = ({ onClose, hasStorybookStyles }: { onClose: () => void
setIsLoading(true);
setError(undefined);
try {
const txResponse = await send({ amount: val, address: to, memo: '', fee: fee?.fee });
const txResponse = await send({ amount: val, address: to, memo: memo || '', fee: fee?.fee });
setTxDetails({
amount: `${amount?.amount} ${clientDetails?.display_mix_denom.toUpperCase()}`,
txUrl: `${urls(network).blockExplorer}/transaction/${txResponse.tx_hash}`,
});
} catch (e) {
Console.error(e as string);
setSendError(true);
if (/Raw log: out of gas/.test(e as string)) {
setGasError('Out of gas, please increase the amount of fees');
} else {
setSendError(true);
}
} finally {
setIsLoading(false);
}
@@ -65,6 +90,10 @@ export const SendModal = ({ onClose, hasStorybookStyles }: { onClose: () => void
if (sendError) return <SendErrorModal onClose={onClose} error={feeError} />;
if (gasError) {
return <SendErrorModal onClose={onClose} error={gasError} />;
}
if (txDetails) return <SendSuccessModal txDetails={txDetails} onClose={onClose} />;
if (modal === 'send details')
@@ -78,6 +107,7 @@ export const SendModal = ({ onClose, hasStorybookStyles }: { onClose: () => void
onPrev={() => setModal('send')}
onSend={handleSend}
denom={clientDetails?.display_mix_denom || 'nym'}
memo={memo}
{...hasStorybookStyles}
/>
);
@@ -92,8 +122,14 @@ export const SendModal = ({ onClose, hasStorybookStyles }: { onClose: () => void
onNext={handleOnNext}
error={error}
denom={clientDetails?.display_mix_denom}
userFees={userFees}
memo={memo}
showMore={showMoreOptions}
onAmountChange={(value) => setAmount(value)}
onAddressChange={(value) => setToAddress(sanitizeAddress(value))}
onUserFeesChange={(value) => setUserFees(value)}
onMemoChange={(value) => setMemo(value)}
setShowMore={setShowMoreOptions}
{...hasStorybookStyles}
/>
);
+15 -1
View File
@@ -1,6 +1,7 @@
import { FeeDetails } from '@nymproject/types';
import { DecCoin, FeeDetails } from '@nymproject/types';
import { useState } from 'react';
import { Console } from 'src/utils/console';
import { getCustomFees } from '../requests';
export function useGetFee() {
const [fee, setFee] = useState<FeeDetails>();
@@ -19,6 +20,18 @@ export function useGetFee() {
setIsFeeLoading(false);
}
async function setFeeManually(amount: DecCoin) {
try {
setIsFeeLoading(true);
const fees = await getCustomFees({ feesAmount: amount });
setFee(fees);
} catch (e) {
Console.error(e);
setFeeError(e as string);
}
setIsFeeLoading(false);
}
const resetFeeState = () => {
setFee(undefined);
setIsFeeLoading(false);
@@ -30,6 +43,7 @@ export function useGetFee() {
isFeeLoading,
feeError,
getFee,
setFeeManually,
resetFeeState,
};
}

Some files were not shown because too many files have changed in this diff Show More