Merge branch 'master' into develop
This commit is contained in:
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -101,6 +101,7 @@ impl SocketClient {
|
||||
let ClientState {
|
||||
shared_lane_queue_lengths,
|
||||
reply_controller_sender,
|
||||
..
|
||||
} = client_state;
|
||||
|
||||
let websocket_handler = websocket::HandlerBuilder::new(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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 }
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"]}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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,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,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 }
|
||||
|
||||
@@ -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
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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"]
|
||||
@@ -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)
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@nym/network-explorer",
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.7",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -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,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}
|
||||
<Typography variant="body2" fontWeight={600} data-testid={headingTitle}>
|
||||
{headingTitle}
|
||||
</Typography>
|
||||
{filter ? <ExpandMore /> : <ExpandLess />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
'Node’s 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 };
|
||||
|
||||
+6
-4
@@ -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)}%`,
|
||||
};
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -44,6 +44,11 @@ export const originalNavOptions: NavOptionType[] = [
|
||||
url: `${BIG_DIPPER}/validators`,
|
||||
title: 'Validators',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
url: 'network-components/service-providers',
|
||||
title: 'Service Providers',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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="Node’s 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" />
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@nym/nym-connect",
|
||||
"version": "1.1.11",
|
||||
"version": "1.1.12",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -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 />
|
||||
|
||||
Generated
+1
@@ -3782,6 +3782,7 @@ dependencies = [
|
||||
name = "nym-topology"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bs58",
|
||||
"log",
|
||||
"nym-bin-common",
|
||||
|
||||
@@ -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])
|
||||
|
||||
Generated
+7
-7
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@nymproject/nym-wallet-app",
|
||||
"version": "1.1.10",
|
||||
"version": "1.1.11",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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,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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user