Compare commits

...

16 Commits

Author SHA1 Message Date
Bogdan-Ștefan Neacşu b656003306 Expect that previously regitrated clients don't have v6 addr 2024-12-17 16:59:01 +02:00
benedettadavico a491e6a71a update changelog for crunch 2024-12-11 10:28:47 +01:00
Jędrzej Stuczyński 96f99bb9e4 bugfix: added explicit openapi servers to account for route prefixes (#5237) 2024-12-10 10:37:04 +00:00
benedettadavico 10933ff8f1 update node version to 1.2.0 again 2024-12-09 16:58:55 +01:00
Jędrzej Stuczyński 5454b36022 Further config score adjustments (#5225)
* wip

* changed minor/patch weights and introduced full release chain history for more accurate calculations

* clippy

* updated contract schema

* added nym-api endpoint for current rewarded set nodes

* added nym-api endpoint for internal config score data

* guard mixnet contract against decreasing semver

* fixed config score calculation if there are skipped versions
2024-12-09 14:33:34 +00:00
Jędrzej Stuczyński 675e5a0305 removed semver filtering (#5224) 2024-12-06 17:21:21 +00:00
Tommy Verrall 210cc5286e Update Cargo.toml
amend version back to 13
2024-12-06 17:29:08 +01:00
benedettadavico d07e293cb5 amend nym-node version 2024-12-06 11:34:21 +01:00
benedettadavico 4b055a9bf0 bumping nym-node version 2024-12-05 18:13:13 +01:00
Jędrzej Stuczyński 80d1a24164 dont consider legacy nodes for rewarded set selection (#5215)
* dont consider legacy nodes for rewarded set selection

* removed dead imports
2024-12-05 16:50:34 +00:00
Jędrzej Stuczyński b481da9c55 nym-api NMv1 adjustments (#5209)
* ignore legacy nodes for test route selection and bias selection with existing score

* feature: dont keep persistent GatewayClient inside NMv1 (#5211)

* removed overly complex logic for requesting mutex permits for packet processing

* dont keep persistent gateway connections. instead make them on demand

* clippy
2024-12-05 16:18:14 +00:00
Bogdan-Ștefan Neacşu 585d752c83 Extend raw ws fd for gateway client (#5218) (#5220) 2024-12-05 17:43:43 +02:00
Jon Häggblad 6eddc913f4 Derive serialize for UserAgent (#5210) (#5217) 2024-12-05 11:34:44 +01:00
Mark Sinclair 15c3012199 explorer-api: add nym node endpoints + UI to show nym-nodes and account balances (#5183)
* explorer-api: add nym node endpoints + UI to show nym-nodes and account balances

* explorer-api: add endpoints to get operator rewards
explorer-ui: show delegations on nym-nodes, show operator rewards, bug fixes

* explorer-ui: change summary screen to only show nym-node stats

* explorer-api: add unstable routes to get legacy mixnodes and gateways from the contract instead of the Nym API
explorer-ui: adapt front-end to show less information in legacy nodes with plain bond types

* explorer-ui: fix up source of legacy mixnode data

* explorer-ui: add more account page null and undefined checks

* explorer-ui: filter out null gateway versions

* explorer-ui: sanitise gateway versions

* explorer-ui: add more guards on the balance parts to check that greater than 0

* explorer-api: make /tmp/unstable/gateways endpoint compatible with the current Harbour Master API

* explorer-ui: fix typo

* cargo fmt

* Add node-id, total stake and links to nodes list

---------

Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
2024-12-05 08:17:30 +00:00
Jędrzej Stuczyński 78bf413e6a introduce UNSTABLE endpoints for returning network monitor run details (#5214) 2024-12-04 16:49:26 +00:00
Jędrzej Stuczyński 29ea4623c8 adjusted config score penalty calculation (#5206)
* adjusted config score penalty calculation

* updated contract schema
2024-12-03 11:24:46 +00:00
150 changed files with 5138 additions and 2262 deletions
+142
View File
@@ -4,6 +4,148 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2024.14-crunch] (2024-12-11)
- Merge/release/2024.14-crunch ([#5242])
- bugfix: added explicit openapi servers to account for route prefixes ([#5237])
- Further config score adjustments ([#5225])
- feature: remve any filtering on node semver ([#5224])
- Backport #5218 ([#5220])
- Derive serialize for UserAgent (#5210) ([#5217])
- dont consider legacy nodes for rewarded set selection ([#5215])
- introduce UNSTABLE endpoints for returning network monitor run details ([#5214])
- Nmv2 add debug config ([#5212])
- nym-api NMv1 adjustments ([#5209])
- adjusted config score penalty calculation ([#5206])
- Fix backwards compat mac generation ([#5202])
- merge crunch into develop ([#5199])
- Update Security disclosure email, public key and policy ([#5195])
- Guard storage access with cache ([#5193])
- chore: apply 1.84 linter suggestions ([#5192])
- improvement: make internal gateway clients use the same topology cache ([#5191])
- Bugfix/credential proxy sequencing ([#5187])
- Add monitor_run and testing_route indexes ([#5182])
- Add indexes to monitor run and testing route ([#5181])
- bugfix: fixed nym-node config migrations (again) ([#5179])
- bugfix: use default value for verloc config when deserialising missing values ([#5177])
- Remove peers with no allowed ip from storage ([#5175])
- Move two minor jobs to free tier github hosted runners ([#5169])
- Add support for DELETE to nym-http-api-client ([#5166])
- Fix env var name ([#5165])
- Add strum::EnumIter for TicketType ([#5164])
- Add export_to_env to NymNetworkDetails ([#5162])
- bugfix: correctly expose ecash-related data on nym-api ([#5155])
- fix: validator-rewarder GH job ([#5151])
- build(deps): bump cross-spawn from 7.0.3 to 7.0.6 in /testnet-faucet ([#5150])
- build(deps): bump mikefarah/yq from 4.44.3 to 4.44.5 ([#5149])
- start session collection for exit gateways ([#5148])
- add version to clientStatsReport ([#5147])
- update serde_json_path due to compilation issue ([#5144])
- chore: remove standalone legacy mixnode/gateway binaries ([#5135])
- [Product Data] Set up country reporting from vpn-client ([#5134])
- removed ci-nym-api-tests.yml which was running outdated (and broken) tests ([#5133])
- CI: reduce jobs running on cluster ([#5132])
- [DOCS/operators]: Release changes v2024.13-magura & Tokenomics pages v1.0 ([#5128])
- NS Agent auth with NS API ([#5127])
- [Product Data] Config deserialization bug fix ([#5126])
- bugfix: don't send empty BankMsg in ecash contract ([#5121])
- [Product data] Data consumption with ecash ticket ([#5120])
- feat: add GH workflow for nym-validator-rewarder ([#5119])
- feat: add Dockerfile and add env vars for clap arguments ([#5118])
- feature: config score ([#5117])
- [Product Data] Add stats reporting configuration in client config ([#5115])
- Correct IPv6 address generation ([#5113])
- feature: rewarding for ticketbook issuance ([#5112])
- Add granular log on nym-node ([#5111])
- Send mixnet packet stats using task client ([#5109])
- Expose time range ([#5108])
- [Product Data] Client-side stats collection ([#5107])
- chore: ecash contract migration to remove unused 'redemption_gateway_share' ([#5104])
- [Product Data] Better unique user count on gateways ([#5084])
- feat: add nym node GH workflow ([#5080])
- IPv6 support for wireguard ([#5059])
- Node Status API ([#5050])
- Authenticator CLI client mode ([#5044])
- Integrate nym-credential-proxy into workspace ([#5027])
- [Product Data] Introduce data persistence on gateways ([#5022])
- Bump the patch-updates group across 1 directory with 10 updates ([#5011])
- build(deps): bump once_cell from 1.19.0 to 1.20.2 ([#4952])
- Create TaskStatusEvent trait instead of piggybacking on Error ([#4919])
- build(deps): bump lazy_static from 1.4.0 to 1.5.0 ([#4913])
- Sync code with .env in build.rs ([#4876])
- build(deps): bump axios from 1.6.0 to 1.7.5 in /nym-api/tests ([#4790])
- Bump elliptic from 6.5.4 to 6.5.7 in /testnet-faucet ([#4768])
[#5242]: https://github.com/nymtech/nym/pull/5242
[#5237]: https://github.com/nymtech/nym/pull/5237
[#5225]: https://github.com/nymtech/nym/pull/5225
[#5224]: https://github.com/nymtech/nym/pull/5224
[#5220]: https://github.com/nymtech/nym/pull/5220
[#5217]: https://github.com/nymtech/nym/pull/5217
[#5215]: https://github.com/nymtech/nym/pull/5215
[#5214]: https://github.com/nymtech/nym/pull/5214
[#5212]: https://github.com/nymtech/nym/pull/5212
[#5209]: https://github.com/nymtech/nym/pull/5209
[#5206]: https://github.com/nymtech/nym/pull/5206
[#5202]: https://github.com/nymtech/nym/pull/5202
[#5199]: https://github.com/nymtech/nym/pull/5199
[#5195]: https://github.com/nymtech/nym/pull/5195
[#5193]: https://github.com/nymtech/nym/pull/5193
[#5192]: https://github.com/nymtech/nym/pull/5192
[#5191]: https://github.com/nymtech/nym/pull/5191
[#5187]: https://github.com/nymtech/nym/pull/5187
[#5182]: https://github.com/nymtech/nym/pull/5182
[#5181]: https://github.com/nymtech/nym/pull/5181
[#5179]: https://github.com/nymtech/nym/pull/5179
[#5177]: https://github.com/nymtech/nym/pull/5177
[#5175]: https://github.com/nymtech/nym/pull/5175
[#5169]: https://github.com/nymtech/nym/pull/5169
[#5166]: https://github.com/nymtech/nym/pull/5166
[#5165]: https://github.com/nymtech/nym/pull/5165
[#5164]: https://github.com/nymtech/nym/pull/5164
[#5162]: https://github.com/nymtech/nym/pull/5162
[#5155]: https://github.com/nymtech/nym/pull/5155
[#5151]: https://github.com/nymtech/nym/pull/5151
[#5150]: https://github.com/nymtech/nym/pull/5150
[#5149]: https://github.com/nymtech/nym/pull/5149
[#5148]: https://github.com/nymtech/nym/pull/5148
[#5147]: https://github.com/nymtech/nym/pull/5147
[#5144]: https://github.com/nymtech/nym/pull/5144
[#5135]: https://github.com/nymtech/nym/pull/5135
[#5134]: https://github.com/nymtech/nym/pull/5134
[#5133]: https://github.com/nymtech/nym/pull/5133
[#5132]: https://github.com/nymtech/nym/pull/5132
[#5128]: https://github.com/nymtech/nym/pull/5128
[#5127]: https://github.com/nymtech/nym/pull/5127
[#5126]: https://github.com/nymtech/nym/pull/5126
[#5121]: https://github.com/nymtech/nym/pull/5121
[#5120]: https://github.com/nymtech/nym/pull/5120
[#5119]: https://github.com/nymtech/nym/pull/5119
[#5118]: https://github.com/nymtech/nym/pull/5118
[#5117]: https://github.com/nymtech/nym/pull/5117
[#5115]: https://github.com/nymtech/nym/pull/5115
[#5113]: https://github.com/nymtech/nym/pull/5113
[#5112]: https://github.com/nymtech/nym/pull/5112
[#5111]: https://github.com/nymtech/nym/pull/5111
[#5109]: https://github.com/nymtech/nym/pull/5109
[#5108]: https://github.com/nymtech/nym/pull/5108
[#5107]: https://github.com/nymtech/nym/pull/5107
[#5104]: https://github.com/nymtech/nym/pull/5104
[#5084]: https://github.com/nymtech/nym/pull/5084
[#5080]: https://github.com/nymtech/nym/pull/5080
[#5059]: https://github.com/nymtech/nym/pull/5059
[#5050]: https://github.com/nymtech/nym/pull/5050
[#5044]: https://github.com/nymtech/nym/pull/5044
[#5027]: https://github.com/nymtech/nym/pull/5027
[#5022]: https://github.com/nymtech/nym/pull/5022
[#5011]: https://github.com/nymtech/nym/pull/5011
[#4952]: https://github.com/nymtech/nym/pull/4952
[#4919]: https://github.com/nymtech/nym/pull/4919
[#4913]: https://github.com/nymtech/nym/pull/4913
[#4876]: https://github.com/nymtech/nym/pull/4876
[#4790]: https://github.com/nymtech/nym/pull/4790
[#4768]: https://github.com/nymtech/nym/pull/4768
## [2024.13-magura-drift] (2024-11-29)
- Optimised syncing bandwidth information to storage
Generated
+1 -2
View File
@@ -4674,7 +4674,6 @@ dependencies = [
"opentelemetry-jaeger",
"pretty_env_logger",
"schemars",
"semver 1.0.23",
"serde",
"serde_json",
"tracing-opentelemetry",
@@ -5954,7 +5953,7 @@ dependencies = [
[[package]]
name = "nym-node"
version = "1.1.12"
version = "1.2.0"
dependencies = [
"anyhow",
"bip39",
+1 -28
View File
@@ -3,13 +3,10 @@
use crate::commands::try_load_current_config;
use crate::{
client::{config::Config, SocketClient},
client::SocketClient,
commands::{override_config, OverrideConfig},
error::ClientError,
};
use clap::Args;
use log::*;
use nym_bin_common::version_checker::is_minor_version_compatible;
use nym_client_core::cli_helpers::client_run::CommonClientRunArgs;
use std::error::Error;
use std::net::IpAddr;
@@ -48,36 +45,12 @@ impl From<Run> for OverrideConfig {
}
}
// this only checks compatibility between config the binary. It does not take into consideration
// network version. It might do so in the future.
fn version_check(cfg: &Config) -> bool {
let binary_version = env!("CARGO_PKG_VERSION");
let config_version = &cfg.base.client.version;
if binary_version == config_version {
true
} else {
warn!("The native-client binary has different version than what is specified in config file! {} and {}", binary_version, config_version);
if is_minor_version_compatible(binary_version, config_version) {
info!("but they are still semver compatible. However, consider running the `upgrade` command");
true
} else {
error!("and they are semver incompatible! - please run the `upgrade` command before attempting `run` again");
false
}
}
}
pub(crate) async fn execute(args: Run) -> Result<(), Box<dyn Error + Send + Sync>> {
eprintln!("Starting client {}...", args.common_args.id);
let mut config = try_load_current_config(&args.common_args.id).await?;
config = override_config(config, OverrideConfig::from(args.clone()));
if !version_check(&config) {
error!("failed the local version check");
return Err(Box::new(ClientError::FailedLocalVersionCheck));
}
SocketClient::new(config, args.common_args.custom_mixnet)
.run_socket_forever()
.await
-3
View File
@@ -17,9 +17,6 @@ pub enum ClientError {
#[error("Failed to validate the loaded config")]
ConfigValidationFailure,
#[error("Failed local version check, client and config mismatch")]
FailedLocalVersionCheck,
#[error("Attempted to start the client in invalid socket mode")]
InvalidSocketMode,
+1 -33
View File
@@ -2,14 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
use crate::commands::try_load_current_config;
use crate::config::Config;
use crate::{
commands::{override_config, OverrideConfig},
error::Socks5ClientError,
};
use crate::commands::{override_config, OverrideConfig};
use clap::Args;
use log::*;
use nym_bin_common::version_checker::is_minor_version_compatible;
use nym_client_core::cli_helpers::client_run::CommonClientRunArgs;
use nym_client_core::client::base_client::storage::OnDiskPersistent;
use nym_client_core::client::topology_control::geo_aware_provider::CountryGroup;
@@ -82,38 +76,12 @@ fn validate_country_group(s: &str) -> Result<CountryGroup, String> {
}
}
// this only checks compatibility between config the binary. It does not take into consideration
// network version. It might do so in the future.
fn version_check(cfg: &Config) -> bool {
let binary_version = env!("CARGO_PKG_VERSION");
let config_version = &cfg.core.base.client.version;
if binary_version == config_version {
true
} else {
warn!(
"The socks5-client binary has different version than what is specified in config file! {binary_version} and {config_version}",
);
if is_minor_version_compatible(binary_version, config_version) {
info!("but they are still semver compatible. However, consider running the `upgrade` command");
true
} else {
error!("and they are semver incompatible! - please run the `upgrade` command before attempting `run` again");
false
}
}
}
pub(crate) async fn execute(args: Run) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
eprintln!("Starting client {}...", args.common_args.id);
let mut config = try_load_current_config(&args.common_args.id).await?;
config = override_config(config, OverrideConfig::from(args.clone()));
if !version_check(&config) {
error!("failed the local version check");
return Err(Box::new(Socks5ClientError::FailedLocalVersionCheck));
}
let storage =
OnDiskPersistent::from_paths(config.storage_paths.common_paths, &config.core.base.debug)
.await?;
-3
View File
@@ -14,9 +14,6 @@ pub enum Socks5ClientError {
#[error("Failed to validate the loaded config")]
ConfigValidationFailure,
#[error("Failed local version check, client and config mismatch")]
FailedLocalVersionCheck,
#[error("Fail to bind address")]
FailToBindAddress,
+1 -2
View File
@@ -15,7 +15,6 @@ const-str = { workspace = true }
log = { workspace = true }
pretty_env_logger = { workspace = true }
schemars = { workspace = true, features = ["preserve_order"], optional = true }
semver.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, optional = true }
@@ -44,5 +43,5 @@ tracing = [
"tracing-opentelemetry",
"opentelemetry",
]
clap = [ "dep:clap", "dep:clap_complete", "dep:clap_complete_fig" ]
clap = ["dep:clap", "dep:clap_complete", "dep:clap_complete_fig"]
models = []
-1
View File
@@ -3,7 +3,6 @@
pub mod build_information;
pub mod logging;
pub mod version_checker;
#[cfg(feature = "clap")]
pub mod completions;
@@ -1,78 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub use semver::Version;
/// Checks if the version is minor version compatible.
///
/// Checks whether given `version` is compatible with a given semantic version requirement `req`
/// according to major-minor semver rules. The semantic version requirement can be passed as a full,
/// concrete version number, because that's what we'll have in our Cargo.toml files (e.g. 0.3.2).
/// The patch number in the requirement gets dropped and replaced with a wildcard (0.3.*) as all
/// minor versions should be compatible with each other.
pub fn is_minor_version_compatible(version: &str, req: &str) -> bool {
let expected_version = match Version::parse(version) {
Ok(v) => v,
Err(_) => return false,
};
let req_version = match Version::parse(req) {
Ok(v) => v,
Err(_) => return false,
};
expected_version.major == req_version.major && expected_version.minor == req_version.minor
}
pub fn parse_version(raw_version: &str) -> Result<Version, semver::Error> {
Version::parse(raw_version)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_0_3_0_is_compatible_with_requirement_0_3_x() {
assert!(is_minor_version_compatible("0.3.0", "0.3.2"));
}
#[test]
fn version_0_3_1_is_compatible_with_minimum_requirement_0_3_x() {
assert!(is_minor_version_compatible("0.3.1", "0.3.2"));
}
#[test]
fn version_0_3_2_is_compatible_with_minimum_requirement_0_3_x() {
assert!(is_minor_version_compatible("0.3.2", "0.3.0"));
}
#[test]
fn version_0_2_0_is_not_compatible_with_requirement_0_3_x() {
assert!(!is_minor_version_compatible("0.2.0", "0.3.2"));
}
#[test]
fn version_0_4_0_is_not_compatible_with_requirement_0_3_x() {
assert!(!is_minor_version_compatible("0.4.0", "0.3.2"));
}
#[test]
fn version_1_3_2_is_not_compatible_with_requirement_0_3_x() {
assert!(!is_minor_version_compatible("1.3.2", "0.3.2"));
}
#[test]
fn version_0_4_0_rc_1_is_compatible_with_version_0_4_0_rc_1() {
assert!(is_minor_version_compatible("0.4.0-rc.1", "0.4.0-rc.1"));
}
#[test]
fn returns_false_on_foo_version() {
assert!(!is_minor_version_compatible("foo", "0.3.2"));
}
#[test]
fn returns_false_on_bar_version() {
assert!(!is_minor_version_compatible("0.3.2", "bar"));
}
}
@@ -514,15 +514,10 @@ where
min_gateway_performance: config_topology.minimum_gateway_performance,
},
nym_api_urls,
env!("CARGO_PKG_VERSION").to_string(),
user_agent,
)),
config::TopologyStructure::GeoAware(group_by) => {
Box::new(GeoAwareTopologyProvider::new(
nym_api_urls,
env!("CARGO_PKG_VERSION").to_string(),
group_by,
))
Box::new(GeoAwareTopologyProvider::new(nym_api_urls, group_by))
}
})
}
@@ -85,15 +85,10 @@ fn check_layer_integrity(topology: NymTopology) -> Result<(), ()> {
pub struct GeoAwareTopologyProvider {
validator_client: nym_validator_client::client::NymApiClient,
filter_on: GroupBy,
client_version: String,
}
impl GeoAwareTopologyProvider {
pub fn new(
mut nym_api_urls: Vec<Url>,
client_version: String,
filter_on: GroupBy,
) -> GeoAwareTopologyProvider {
pub fn new(mut nym_api_urls: Vec<Url>, filter_on: GroupBy) -> GeoAwareTopologyProvider {
log::info!(
"Creating geo-aware topology provider with filter on {}",
filter_on
@@ -105,14 +100,13 @@ impl GeoAwareTopologyProvider {
nym_api_urls[0].clone(),
),
filter_on,
client_version,
}
}
async fn get_topology(&self) -> Option<NymTopology> {
let mixnodes = match self
.validator_client
.get_all_basic_active_mixing_assigned_nodes(Some(self.client_version.clone()))
.get_all_basic_active_mixing_assigned_nodes()
.await
{
Err(err) => {
@@ -124,7 +118,7 @@ impl GeoAwareTopologyProvider {
let gateways = match self
.validator_client
.get_all_basic_entry_assigned_nodes(Some(self.client_version.clone()))
.get_all_basic_entry_assigned_nodes()
.await
{
Err(err) => {
@@ -35,18 +35,11 @@ pub struct NymApiTopologyProvider {
validator_client: nym_validator_client::client::NymApiClient,
nym_api_urls: Vec<Url>,
client_version: String,
currently_used_api: usize,
}
impl NymApiTopologyProvider {
pub fn new(
config: Config,
mut nym_api_urls: Vec<Url>,
client_version: String,
user_agent: Option<UserAgent>,
) -> Self {
pub fn new(config: Config, mut nym_api_urls: Vec<Url>, user_agent: Option<UserAgent>) -> Self {
nym_api_urls.shuffle(&mut thread_rng());
let validator_client = if let Some(user_agent) = user_agent {
@@ -62,7 +55,6 @@ impl NymApiTopologyProvider {
config,
validator_client,
nym_api_urls,
client_version,
currently_used_api: 0,
}
}
@@ -99,7 +91,7 @@ impl NymApiTopologyProvider {
async fn get_current_compatible_topology(&mut self) -> Option<NymTopology> {
let mixnodes = match self
.validator_client
.get_all_basic_active_mixing_assigned_nodes(Some(self.client_version.clone()))
.get_all_basic_active_mixing_assigned_nodes()
.await
{
Err(err) => {
@@ -111,7 +103,7 @@ impl NymApiTopologyProvider {
let gateways = match self
.validator_client
.get_all_basic_entry_assigned_nodes(Some(self.client_version.clone()))
.get_all_basic_entry_assigned_nodes()
.await
{
Err(err) => {
+2 -4
View File
@@ -94,7 +94,7 @@ pub async fn current_gateways<R: Rng>(
log::debug!("Fetching list of gateways from: {nym_api}");
let gateways = client.get_all_basic_entry_assigned_nodes(None).await?;
let gateways = client.get_all_basic_entry_assigned_nodes().await?;
log::debug!("Found {} gateways", gateways.len());
log::trace!("Gateways: {:#?}", gateways);
@@ -121,9 +121,7 @@ pub async fn current_mixnodes<R: Rng>(
log::trace!("Fetching list of mixnodes from: {nym_api}");
let mixnodes = client
.get_all_basic_active_mixing_assigned_nodes(None)
.await?;
let mixnodes = client.get_all_basic_active_mixing_assigned_nodes().await?;
let valid_mixnodes = mixnodes
.iter()
.filter_map(|mixnode| mixnode.try_into().ok())
@@ -87,8 +87,10 @@ impl ClientBandwidth {
if remaining < 0 {
tracing::warn!("OUT OF BANDWIDTH. remaining: {remaining_bi2}");
} else {
} else if remaining < 1_000_000 {
tracing::info!("remaining bandwidth: {remaining_bi2}");
} else {
tracing::debug!("remaining bandwidth: {remaining_bi2}");
}
self.inner
@@ -139,6 +139,10 @@ impl<C, St> GatewayClient<C, St> {
self.gateway_identity
}
pub fn shared_key(&self) -> Option<Arc<SharedGatewayKey>> {
self.shared_key.clone()
}
pub fn ws_fd(&self) -> Option<RawFd> {
match &self.connection {
SocketState::Available(conn) => ws_fd(conn.as_ref()),
@@ -408,7 +412,7 @@ impl<C, St> GatewayClient<C, St> {
}
Some(_) => {
info!("the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!");
debug!("the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!");
Ok(())
}
}
@@ -992,24 +996,6 @@ impl<C, St> GatewayClient<C, St> {
}
Ok(())
}
#[deprecated(note = "this method does not deal with upgraded keys for legacy clients")]
pub async fn authenticate_and_start(
&mut self,
) -> Result<AuthenticationResponse, GatewayClientError>
where
C: DkgQueryClient + Send + Sync,
St: CredentialStorage,
<St as CredentialStorage>::StorageError: Send + Sync + 'static,
{
let shared_key = self.perform_initial_authentication().await?;
self.claim_initial_bandwidth().await?;
// this call is NON-blocking
self.start_listening_for_mixnet_messages()?;
Ok(shared_key)
}
}
// type alias for an ease of use
@@ -46,7 +46,8 @@ pub(crate) fn ws_fd(_conn: &WsConn) -> Option<RawFd> {
#[cfg(unix)]
match _conn.get_ref() {
MaybeTlsStream::Plain(stream) => Some(stream.as_raw_fd()),
&_ => None,
MaybeTlsStream::Rustls(tls_stream) => Some(tls_stream.as_raw_fd()),
_ => None,
}
#[cfg(not(unix))]
None
@@ -110,6 +111,11 @@ impl PartiallyDelegatedRouter {
}
};
if self.stream_return.is_canceled() {
// nothing to do, receiver has been dropped
return;
}
let return_res = match ret {
Err(err) => self.stream_return.send(Err(err)),
Ok(_) => {
@@ -19,8 +19,8 @@ use nym_api_requests::ecash::{
PartialExpirationDateSignatureResponse, VerificationKeyResponse,
};
use nym_api_requests::models::{
ApiHealthResponse, GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse,
NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse,
ApiHealthResponse, GatewayBondAnnotated, GatewayCoreStatusResponse, MixnodeCoreStatusResponse,
MixnodeStatusResponse, NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse,
};
use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated};
use nym_api_requests::nym_nodes::SkimmedNode;
@@ -257,6 +257,13 @@ impl<C, S> Client<C, S> {
Ok(self.nym_api.get_gateways().await?)
}
#[deprecated]
pub async fn get_cached_gateways_detailed_unfiltered(
&self,
) -> Result<Vec<GatewayBondAnnotated>, ValidatorClientError> {
Ok(self.nym_api.get_gateways_detailed_unfiltered().await?)
}
// TODO: combine with NymApiClient...
pub async fn get_all_cached_described_nodes(
&self,
@@ -351,34 +358,19 @@ impl NymApiClient {
}
#[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes instead")]
pub async fn get_basic_mixnodes(
&self,
semver_compatibility: Option<String>,
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
Ok(self
.nym_api
.get_basic_mixnodes(semver_compatibility)
.await?
.nodes)
pub async fn get_basic_mixnodes(&self) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
Ok(self.nym_api.get_basic_mixnodes().await?.nodes)
}
#[deprecated(note = "use get_all_basic_entry_assigned_nodes instead")]
pub async fn get_basic_gateways(
&self,
semver_compatibility: Option<String>,
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
Ok(self
.nym_api
.get_basic_gateways(semver_compatibility)
.await?
.nodes)
pub async fn get_basic_gateways(&self) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
Ok(self.nym_api.get_basic_gateways().await?.nodes)
}
/// retrieve basic information for nodes are capable of operating as an entry gateway
/// this includes legacy gateways and nym-nodes
pub async fn get_all_basic_entry_assigned_nodes(
&self,
semver_compatibility: Option<String>,
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
@@ -387,12 +379,7 @@ impl NymApiClient {
loop {
let mut res = self
.nym_api
.get_basic_entry_assigned_nodes(
semver_compatibility.clone(),
false,
Some(page),
None,
)
.get_basic_entry_assigned_nodes(false, Some(page), None)
.await?;
nodes.append(&mut res.nodes.data);
@@ -410,7 +397,6 @@ impl NymApiClient {
/// this includes legacy mixnodes and nym-nodes
pub async fn get_all_basic_active_mixing_assigned_nodes(
&self,
semver_compatibility: Option<String>,
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
@@ -419,12 +405,7 @@ impl NymApiClient {
loop {
let mut res = self
.nym_api
.get_basic_active_mixing_assigned_nodes(
semver_compatibility.clone(),
false,
Some(page),
None,
)
.get_basic_active_mixing_assigned_nodes(false, Some(page), None)
.await?;
nodes.append(&mut res.nodes.data);
@@ -442,7 +423,6 @@ impl NymApiClient {
/// this includes legacy mixnodes and nym-nodes
pub async fn get_all_basic_mixing_capable_nodes(
&self,
semver_compatibility: Option<String>,
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
@@ -451,12 +431,7 @@ impl NymApiClient {
loop {
let mut res = self
.nym_api
.get_basic_mixing_capable_nodes(
semver_compatibility.clone(),
false,
Some(page),
None,
)
.get_basic_mixing_capable_nodes(false, Some(page), None)
.await?;
nodes.append(&mut res.nodes.data);
@@ -471,10 +446,7 @@ impl NymApiClient {
}
/// retrieve basic information for all bonded nodes on the network
pub async fn get_all_basic_nodes(
&self,
semver_compatibility: Option<String>,
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
pub async fn get_all_basic_nodes(&self) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut nodes = Vec::new();
@@ -482,7 +454,7 @@ impl NymApiClient {
loop {
let mut res = self
.nym_api
.get_basic_nodes(semver_compatibility.clone(), false, Some(page), None)
.get_basic_nodes(false, Some(page), None)
.await?;
nodes.append(&mut res.nodes.data);
@@ -102,6 +102,23 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_gateways_detailed_unfiltered(
&self,
) -> Result<Vec<GatewayBondAnnotated>, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::STATUS,
routes::GATEWAYS,
routes::DETAILED_UNFILTERED,
],
NO_PARAMS,
)
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_mixnodes_detailed_unfiltered(
@@ -188,16 +205,7 @@ pub trait NymApiClientExt: ApiClient {
#[deprecated]
#[tracing::instrument(level = "debug", skip_all)]
async fn get_basic_mixnodes(
&self,
semver_compatibility: Option<String>,
) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
let params = if let Some(semver_compatibility) = &semver_compatibility {
vec![("semver_compatibility", semver_compatibility.as_str())]
} else {
vec![]
};
async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
@@ -206,23 +214,14 @@ pub trait NymApiClientExt: ApiClient {
"mixnodes",
"skimmed",
],
&params,
NO_PARAMS,
)
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_basic_gateways(
&self,
semver_compatibility: Option<String>,
) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
let params = if let Some(semver_compatibility) = &semver_compatibility {
vec![("semver_compatibility", semver_compatibility.as_str())]
} else {
vec![]
};
async fn get_basic_gateways(&self) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
@@ -231,7 +230,7 @@ pub trait NymApiClientExt: ApiClient {
"gateways",
"skimmed",
],
&params,
NO_PARAMS,
)
.await
}
@@ -241,17 +240,12 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_basic_entry_assigned_nodes(
&self,
semver_compatibility: Option<String>,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if let Some(arg) = &semver_compatibility {
params.push(("semver_compatibility", arg.clone()))
}
if no_legacy {
params.push(("no_legacy", "true".to_string()))
}
@@ -283,17 +277,12 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_basic_active_mixing_assigned_nodes(
&self,
semver_compatibility: Option<String>,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if let Some(arg) = &semver_compatibility {
params.push(("semver_compatibility", arg.clone()))
}
if no_legacy {
params.push(("no_legacy", "true".to_string()))
}
@@ -325,17 +314,12 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_basic_mixing_capable_nodes(
&self,
semver_compatibility: Option<String>,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if let Some(arg) = &semver_compatibility {
params.push(("semver_compatibility", arg.clone()))
}
if no_legacy {
params.push(("no_legacy", "true".to_string()))
}
@@ -365,17 +349,12 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_basic_nodes(
&self,
semver_compatibility: Option<String>,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if let Some(arg) = &semver_compatibility {
params.push(("semver_compatibility", arg.clone()))
}
if no_legacy {
params.push(("no_legacy", "true".to_string()))
}
@@ -26,10 +26,11 @@ use nym_mixnet_contract_common::{
reward_params::{Performance, RewardingParams},
rewarding::{EstimatedCurrentEpochRewardResponse, PendingRewardResponse},
ContractBuildInformation, ContractState, ContractStateParams, CurrentIntervalResponse,
Delegation, EpochEventId, EpochStatus, GatewayBond, GatewayBondResponse,
GatewayOwnershipResponse, IdentityKey, IdentityKeyRef, IntervalEventId, MixNodeBond,
MixNodeDetails, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse,
NodeId, NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails,
CurrentNymNodeVersionResponse, Delegation, EpochEventId, EpochStatus, GatewayBond,
GatewayBondResponse, GatewayOwnershipResponse, HistoricalNymNodeVersionEntry, IdentityKey,
IdentityKeyRef, IntervalEventId, MixNodeBond, MixNodeDetails, MixOwnershipResponse,
MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeId,
NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails, NymNodeVersionHistoryResponse,
PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedGatewayResponse,
PagedMixnodeBondsResponse, PagedNodeDelegationsResponse, PendingEpochEvent,
PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEvent,
@@ -71,6 +72,22 @@ pub trait MixnetQueryClient {
.await
}
async fn get_nym_node_version_history_paged(
&self,
start_after: Option<u32>,
limit: Option<u32>,
) -> Result<NymNodeVersionHistoryResponse, NyxdError> {
self.query_mixnet_contract(MixnetQueryMsg::GetNymNodeVersionHistory { limit, start_after })
.await
}
async fn get_current_nym_node_version(
&self,
) -> Result<CurrentNymNodeVersionResponse, NyxdError> {
self.query_mixnet_contract(MixnetQueryMsg::GetCurrentNymNodeVersion {})
.await
}
async fn get_mixnet_contract_state(&self) -> Result<ContractState, NyxdError> {
self.query_mixnet_contract(MixnetQueryMsg::GetState {})
.await
@@ -638,6 +655,12 @@ pub trait PagedMixnetQueryClient: MixnetQueryClient {
) -> Result<Vec<PendingIntervalEvent>, NyxdError> {
collect_paged!(self, get_pending_interval_events_paged, events)
}
async fn get_full_nym_node_version_history(
&self,
) -> Result<Vec<HistoricalNymNodeVersionEntry>, NyxdError> {
collect_paged!(self, get_nym_node_version_history_paged, history)
}
}
#[async_trait]
@@ -724,6 +747,7 @@ where
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
use nym_mixnet_contract_common::QueryMsg;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
@@ -924,6 +948,10 @@ mod tests {
MixnetQueryMsg::GetRewardedSetMetadata {} => {
client.get_rewarded_set_metadata().ignore()
}
QueryMsg::GetCurrentNymNodeVersion {} => client.get_current_nym_node_version().ignore(),
QueryMsg::GetNymNodeVersionHistory { limit, start_after } => client
.get_nym_node_version_history_paged(start_after, limit)
.ignore(),
}
}
}
@@ -0,0 +1,673 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Decimal;
use std::cmp::Ordering;
use std::ops::{Add, Sub};
#[cw_serde]
pub struct HistoricalNymNodeVersion {
/// Version of the nym node that is going to be used for determining the version score of a node.
/// note: value stored here is pre-validated `semver::Version`
pub semver: String,
/// Block height of when this version has been added to the contract
pub introduced_at_height: u64,
/// The absolute version difference as compared against the first version introduced into the contract.
pub difference_since_genesis: TotalVersionDifference,
}
impl HistoricalNymNodeVersion {
pub fn genesis(semver: String, height: u64) -> HistoricalNymNodeVersion {
HistoricalNymNodeVersion {
semver,
introduced_at_height: height,
difference_since_genesis: Default::default(),
}
}
// SAFETY: the value stored in the contract is always valid
// if you manually construct that struct with invalid value, it's on you.
#[allow(clippy::unwrap_used)]
pub fn semver_unchecked(&self) -> semver::Version {
self.semver.parse().unwrap()
}
/// Return [`TotalVersionDifference`] for a new release version that is going to be pushed right after this one
/// this function cannot be called against 2 arbitrary versions
#[inline]
pub fn cumulative_difference_since_genesis(
&self,
new_version: &semver::Version,
) -> TotalVersionDifference {
let self_semver = self.semver_unchecked();
let mut new_absolute = self.difference_since_genesis;
if new_version.major > self_semver.major {
new_absolute.major += (new_version.major - self_semver.major) as u32
} else if new_version.minor > self_semver.minor {
new_absolute.minor += (new_version.minor - self_semver.minor) as u32
} else if new_version.patch > self_semver.patch {
new_absolute.patch += (new_version.patch - self_semver.patch) as u32
} else if new_version.pre != self_semver.pre {
new_absolute.prerelease += 1
}
new_absolute
}
pub fn relative_difference(&self, other: &Self) -> TotalVersionDifference {
if self.difference_since_genesis > other.difference_since_genesis {
self.difference_since_genesis - other.difference_since_genesis
} else {
other.difference_since_genesis - self.difference_since_genesis
}
}
pub fn difference_against_legacy(
&self,
legacy_version: &semver::Version,
) -> TotalVersionDifference {
let current = self.semver_unchecked();
let major_diff = (current.major as i64 - legacy_version.major as i64).unsigned_abs() as u32;
let minor_diff = (current.minor as i64 - legacy_version.minor as i64).unsigned_abs() as u32;
let patch_diff = (current.patch as i64 - legacy_version.patch as i64).unsigned_abs() as u32;
let prerelease_diff = if current.pre == legacy_version.pre {
0
} else {
1
};
let mut diff = TotalVersionDifference::default();
// if there's a major increase, ignore minor and patch and treat it as 0
if major_diff != 0 {
diff.major += major_diff;
return diff;
}
// if there's a minor increase, ignore patch and treat is as 0
if minor_diff != 0 {
diff.minor += minor_diff;
return diff;
}
diff.patch = patch_diff;
diff.prerelease = prerelease_diff;
diff
}
}
#[cw_serde]
#[derive(Default, Copy, PartialOrd, Ord, Eq)]
pub struct TotalVersionDifference {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub prerelease: u32,
}
impl Add for TotalVersionDifference {
type Output = TotalVersionDifference;
fn add(self, rhs: TotalVersionDifference) -> Self::Output {
TotalVersionDifference {
major: self.major.add(rhs.major),
minor: self.minor.add(rhs.minor),
patch: self.patch.add(rhs.patch),
prerelease: self.prerelease.add(rhs.prerelease),
}
}
}
impl Sub for TotalVersionDifference {
type Output = TotalVersionDifference;
fn sub(self, rhs: TotalVersionDifference) -> Self::Output {
TotalVersionDifference {
major: self.major.saturating_sub(rhs.major),
minor: self.minor.saturating_sub(rhs.minor),
patch: self.patch.saturating_sub(rhs.patch),
prerelease: self.prerelease.saturating_sub(rhs.prerelease),
}
}
}
#[cw_serde]
pub struct HistoricalNymNodeVersionEntry {
/// The unique, ordered, id of this particular entry
pub id: u32,
/// Data associated with this particular version
pub version_information: HistoricalNymNodeVersion,
}
impl From<(u32, HistoricalNymNodeVersion)> for HistoricalNymNodeVersionEntry {
fn from((id, version_information): (u32, HistoricalNymNodeVersion)) -> Self {
HistoricalNymNodeVersionEntry {
id,
version_information,
}
}
}
impl PartialOrd for HistoricalNymNodeVersionEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
// we only care about id for the purposes of ordering as they should have unique data
self.id.partial_cmp(&other.id)
}
}
#[cw_serde]
pub struct NymNodeVersionHistoryResponse {
pub history: Vec<HistoricalNymNodeVersionEntry>,
/// Field indicating paging information for the following queries if the caller wishes to get further entries.
pub start_next_after: Option<u32>,
}
#[cw_serde]
pub struct CurrentNymNodeVersionResponse {
pub version: Option<HistoricalNymNodeVersionEntry>,
}
#[cw_serde]
pub struct ConfigScoreParams {
/// Defines weights for calculating numbers of versions behind the current release.
pub version_weights: OutdatedVersionWeights,
/// Defines the parameters of the formula for calculating the version score
pub version_score_formula_params: VersionScoreFormulaParams,
}
/// Defines weights for calculating numbers of versions behind the current release.
#[cw_serde]
#[derive(Copy)]
pub struct OutdatedVersionWeights {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub prerelease: u32,
}
fn is_one_semver_difference(this: &semver::Version, other: &semver::Version) -> bool {
let major_diff = (this.major as i64 - other.major as i64).unsigned_abs() as u32;
let minor_diff = (this.minor as i64 - other.minor as i64).unsigned_abs() as u32;
let patch_diff = (this.patch as i64 - other.patch as i64).unsigned_abs() as u32;
let prerelease_diff = if this.pre == other.pre { 0 } else { 1 };
if major_diff == 1 {
return true;
}
if major_diff == 0 && minor_diff == 1 {
return true;
}
if major_diff == 0 && minor_diff == 0 && patch_diff == 1 {
return true;
}
prerelease_diff == 1
}
impl OutdatedVersionWeights {
pub fn difference_to_versions_behind_factor(&self, diff: TotalVersionDifference) -> u32 {
diff.major * self.major
+ diff.minor * self.minor
+ diff.patch * self.patch
+ diff.prerelease * self.prerelease
}
// INVARIANT: release chain is sorted
// do NOT call this method directly from inside the contract. it's too inefficient
// it relies on some external caching.
pub fn versions_behind_factor(
&self,
node_version: &semver::Version,
release_chain: &[HistoricalNymNodeVersionEntry],
) -> u32 {
let Some(latest) = release_chain.last() else {
return 0;
};
let latest_semver = latest.version_information.semver_unchecked();
// if you're more recent than the latest, you get the benefit of the doubt, the release might have not yet been commited to the chain
// but only if you're only a single semver ahead, otherwise you get penalty equivalent of being major version behind for cheating
if node_version > &latest_semver {
return if is_one_semver_difference(node_version, &latest_semver) {
0
} else {
self.major
};
}
// find your position in the release chain, if we fail, we assume that the node comes from before the changes were introduced
// in which case we simply calculate the absolute difference between the genesis entry and add up the total difference
let version_diff = match release_chain
.iter()
.rfind(|h| &h.version_information.semver_unchecked() <= node_version)
{
Some(h) => {
// first chain entry that is smaller (or equal) to the provided node version
// now, calculate the difference to the genesis version and ultimately against the current head
let diff_since_genesis = if h.version_information.semver == node_version.to_string()
{
h.version_information.difference_since_genesis
} else {
h.version_information
.cumulative_difference_since_genesis(node_version)
};
latest.version_information.difference_since_genesis - diff_since_genesis
}
None => {
// SAFETY: since we managed to get 'last' entry, it means the release chain is not empty,
// so we must be able to obtain the first entry
#[allow(clippy::unwrap_used)]
let genesis = release_chain.first().unwrap();
let difference_from_genesis = genesis
.version_information
.difference_against_legacy(node_version);
difference_from_genesis + latest.version_information.difference_since_genesis
}
};
self.difference_to_versions_behind_factor(version_diff)
}
}
impl Default for OutdatedVersionWeights {
fn default() -> Self {
OutdatedVersionWeights {
major: 100,
minor: 10,
patch: 1,
prerelease: 1,
}
}
}
/// Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling)
/// define the relevant parameters
#[cw_serde]
#[derive(Copy)]
pub struct VersionScoreFormulaParams {
pub penalty: Decimal,
pub penalty_scaling: Decimal,
}
impl Default for VersionScoreFormulaParams {
fn default() -> Self {
#[allow(clippy::unwrap_used)]
VersionScoreFormulaParams {
penalty: "0.995".parse().unwrap(),
penalty_scaling: "1.65".parse().unwrap(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ops::Deref;
// simple wrapper for tests
struct ReleaseChain {
inner: Vec<HistoricalNymNodeVersionEntry>,
}
impl Deref for ReleaseChain {
type Target = [HistoricalNymNodeVersionEntry];
fn deref(&self) -> &Self::Target {
self.inner.deref()
}
}
impl ReleaseChain {
fn new(initial: &str) -> Self {
ReleaseChain {
inner: vec![HistoricalNymNodeVersionEntry {
id: 0,
version_information: HistoricalNymNodeVersion {
semver: initial.to_string(),
introduced_at_height: 123,
difference_since_genesis: TotalVersionDifference::default(),
},
}],
}
}
fn with_release(mut self, raw: &str) -> Self {
self.push_new(raw);
self
}
fn push_new(&mut self, raw: &str) {
let latest = self.inner.last().unwrap();
let new_version: semver::Version = raw.parse().unwrap();
let new_absolute = latest
.version_information
.cumulative_difference_since_genesis(&new_version);
self.inner.push(HistoricalNymNodeVersionEntry {
id: latest.id + 1,
version_information: HistoricalNymNodeVersion {
semver: new_version.to_string(),
introduced_at_height: latest.version_information.introduced_at_height + 1,
difference_since_genesis: new_absolute,
},
})
}
}
#[test]
fn versions_behind_factor() {
// helper to compact the parsing
fn s(raw: &str) -> semver::Version {
raw.parse().unwrap()
}
let weights = OutdatedVersionWeights::default();
// no releases:
let res = weights.versions_behind_factor(&s("1.1.13"), &[]);
assert_eq!(0, res);
// ###############################
// single released version (1.1.13)
// ###############################
let mut release_chain = ReleaseChain::new("1.1.13");
// "legacy" versions
let res = weights.versions_behind_factor(&s("1.0.12"), &release_chain);
assert_eq!(10, res);
let res = weights.versions_behind_factor(&s("1.0.4"), &release_chain);
assert_eq!(10, res);
let res = weights.versions_behind_factor(&s("1.0.1"), &release_chain);
assert_eq!(10, res);
let res = weights.versions_behind_factor(&s("0.1.12"), &release_chain);
assert_eq!(100, res);
let res = weights.versions_behind_factor(&s("1.1.12"), &release_chain);
assert_eq!(1, res);
let res = weights.versions_behind_factor(&s("1.1.11"), &release_chain);
assert_eq!(2, res);
let res = weights.versions_behind_factor(&s("1.1.9"), &release_chain);
assert_eq!(4, res);
// current version
let res = weights.versions_behind_factor(&s("1.1.13"), &release_chain);
assert_eq!(0, res);
// "ahead" versions
let res = weights.versions_behind_factor(&s("1.1.14"), &release_chain);
assert_eq!(0, res);
let res = weights.versions_behind_factor(&s("1.2.0"), &release_chain);
assert_eq!(0, res);
let res = weights.versions_behind_factor(&s("2.0.0"), &release_chain);
assert_eq!(0, res);
// cheating ahead:
let res = weights.versions_behind_factor(&s("1.1.15"), &release_chain);
assert_eq!(100, res);
let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain);
assert_eq!(100, res);
let res = weights.versions_behind_factor(&s("3.0.0"), &release_chain);
assert_eq!(100, res);
// ###############################
// small patch release chain (1.1.13 => 1.1.14 => 1.1.15 => 1.1.16)
// ###############################
release_chain.push_new("1.1.14");
release_chain.push_new("1.1.15");
release_chain.push_new("1.1.16");
// "legacy" versions
let res = weights.versions_behind_factor(&s("1.0.12"), &release_chain);
assert_eq!(13, res);
let res = weights.versions_behind_factor(&s("1.0.4"), &release_chain);
assert_eq!(13, res);
let res = weights.versions_behind_factor(&s("1.0.1"), &release_chain);
assert_eq!(13, res);
let res = weights.versions_behind_factor(&s("0.1.12"), &release_chain);
assert_eq!(103, res);
let res = weights.versions_behind_factor(&s("1.1.12"), &release_chain);
assert_eq!(4, res);
let res = weights.versions_behind_factor(&s("1.1.11"), &release_chain);
assert_eq!(5, res);
let res = weights.versions_behind_factor(&s("1.1.9"), &release_chain);
assert_eq!(7, res);
// current version
let res = weights.versions_behind_factor(&s("1.1.16"), &release_chain);
assert_eq!(0, res);
// present in the chain
let res = weights.versions_behind_factor(&s("1.1.15"), &release_chain);
assert_eq!(1, res);
let res = weights.versions_behind_factor(&s("1.1.14"), &release_chain);
assert_eq!(2, res);
let res = weights.versions_behind_factor(&s("1.1.13"), &release_chain);
assert_eq!(3, res);
// "ahead" versions
let res = weights.versions_behind_factor(&s("1.1.17"), &release_chain);
assert_eq!(0, res);
let res = weights.versions_behind_factor(&s("1.2.0"), &release_chain);
assert_eq!(0, res);
let res = weights.versions_behind_factor(&s("2.0.0"), &release_chain);
assert_eq!(0, res);
// cheating ahead:
let res = weights.versions_behind_factor(&s("1.1.18"), &release_chain);
assert_eq!(100, res);
let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain);
assert_eq!(100, res);
let res = weights.versions_behind_factor(&s("3.0.0"), &release_chain);
assert_eq!(100, res);
// ###############################
// small minor release chain (1.2.0 => 1.3.0 => 1.4.0)
// ###############################
let release_chain = ReleaseChain::new("1.2.0")
.with_release("1.3.0")
.with_release("1.4.0");
// "legacy" versions
let res = weights.versions_behind_factor(&s("1.0.12"), &release_chain);
assert_eq!(40, res);
let res = weights.versions_behind_factor(&s("1.0.4"), &release_chain);
assert_eq!(40, res);
let res = weights.versions_behind_factor(&s("1.0.1"), &release_chain);
assert_eq!(40, res);
let res = weights.versions_behind_factor(&s("0.1.12"), &release_chain);
assert_eq!(120, res);
let res = weights.versions_behind_factor(&s("1.1.12"), &release_chain);
assert_eq!(30, res);
let res = weights.versions_behind_factor(&s("1.1.11"), &release_chain);
assert_eq!(30, res);
let res = weights.versions_behind_factor(&s("1.1.9"), &release_chain);
assert_eq!(30, res);
// current version
let res = weights.versions_behind_factor(&s("1.4.0"), &release_chain);
assert_eq!(0, res);
// present in the chain
let res = weights.versions_behind_factor(&s("1.2.0"), &release_chain);
assert_eq!(20, res);
let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain);
assert_eq!(10, res);
// weird in between
let res = weights.versions_behind_factor(&s("1.2.1"), &release_chain);
assert_eq!(20, res);
let res = weights.versions_behind_factor(&s("1.3.3"), &release_chain);
assert_eq!(10, res);
// "ahead" versions
let res = weights.versions_behind_factor(&s("1.4.1"), &release_chain);
assert_eq!(0, res);
let res = weights.versions_behind_factor(&s("1.5.0"), &release_chain);
assert_eq!(0, res);
let res = weights.versions_behind_factor(&s("2.0.0"), &release_chain);
assert_eq!(0, res);
// cheating ahead:
let res = weights.versions_behind_factor(&s("1.4.2"), &release_chain);
assert_eq!(100, res);
let res = weights.versions_behind_factor(&s("1.6.0"), &release_chain);
assert_eq!(100, res);
let res = weights.versions_behind_factor(&s("3.0.0"), &release_chain);
assert_eq!(100, res);
// ###############################
// mixed release chain (1.1.13 => 1.2.0 => 1.2.1 => 1.3.0 => 1.3.1 => 1.3.2 => 1.4.0)
// ###############################
let release_chain = ReleaseChain::new("1.1.13")
.with_release("1.2.0")
.with_release("1.2.1")
.with_release("1.3.0")
.with_release("1.3.1-importantpre")
.with_release("1.3.1")
.with_release("1.3.2")
.with_release("1.4.0");
// "legacy" versions
let res = weights.versions_behind_factor(&s("1.0.12"), &release_chain);
assert_eq!(44, res);
let res = weights.versions_behind_factor(&s("1.0.4"), &release_chain);
assert_eq!(44, res);
let res = weights.versions_behind_factor(&s("1.0.1"), &release_chain);
assert_eq!(44, res);
let res = weights.versions_behind_factor(&s("0.1.12"), &release_chain);
assert_eq!(134, res);
let res = weights.versions_behind_factor(&s("1.1.12"), &release_chain);
assert_eq!(35, res);
let res = weights.versions_behind_factor(&s("1.1.11"), &release_chain);
assert_eq!(36, res);
let res = weights.versions_behind_factor(&s("1.1.9"), &release_chain);
assert_eq!(38, res);
// current version
let res = weights.versions_behind_factor(&s("1.4.0"), &release_chain);
assert_eq!(0, res);
// present in the chain
let res = weights.versions_behind_factor(&s("1.1.13"), &release_chain);
assert_eq!(34, res);
let res = weights.versions_behind_factor(&s("1.2.0"), &release_chain);
assert_eq!(24, res);
let res = weights.versions_behind_factor(&s("1.2.1"), &release_chain);
assert_eq!(23, res);
let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain);
assert_eq!(13, res);
let res = weights.versions_behind_factor(&s("1.3.1-importantpre"), &release_chain);
assert_eq!(12, res);
let res = weights.versions_behind_factor(&s("1.3.1"), &release_chain);
assert_eq!(11, res);
let res = weights.versions_behind_factor(&s("1.3.2"), &release_chain);
assert_eq!(10, res);
// weird in between
let res = weights.versions_behind_factor(&s("1.2.3"), &release_chain);
assert_eq!(21, res);
let res = weights.versions_behind_factor(&s("1.3.69"), &release_chain);
assert_eq!(10, res);
// "ahead" versions
let res = weights.versions_behind_factor(&s("1.4.1"), &release_chain);
assert_eq!(0, res);
let res = weights.versions_behind_factor(&s("1.5.0"), &release_chain);
assert_eq!(0, res);
let res = weights.versions_behind_factor(&s("2.0.0"), &release_chain);
assert_eq!(0, res);
// cheating ahead:
let res = weights.versions_behind_factor(&s("1.4.2"), &release_chain);
assert_eq!(100, res);
let res = weights.versions_behind_factor(&s("1.6.0"), &release_chain);
assert_eq!(100, res);
let res = weights.versions_behind_factor(&s("3.0.0"), &release_chain);
assert_eq!(100, res);
// ###############################
// skipped patch chain (1.1.13 => 1.2.0 => 1.2.1 => 1.2.4 => [1.3.0])
// ###############################
let mut release_chain = ReleaseChain::new("1.1.13")
.with_release("1.2.0")
.with_release("1.2.1")
.with_release("1.2.4");
// current
let res = weights.versions_behind_factor(&s("1.2.4"), &release_chain);
assert_eq!(0, res);
// on 'skipped' version
let res = weights.versions_behind_factor(&s("1.2.2"), &release_chain);
assert_eq!(2, res);
// on version before the skip
let res = weights.versions_behind_factor(&s("1.2.1"), &release_chain);
assert_eq!(3, res);
release_chain.push_new("1.3.0");
// current
let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain);
assert_eq!(0, res);
// on 'skipped' version
let res = weights.versions_behind_factor(&s("1.2.2"), &release_chain);
assert_eq!(12, res);
// on version before the skip
let res = weights.versions_behind_factor(&s("1.2.1"), &release_chain);
assert_eq!(13, res);
// ###############################
// skipped minor chain (1.1.13 => 1.2.0 => 1.2.1 => 1.4.0 => [1.5.0])
// ###############################
let mut release_chain = ReleaseChain::new("1.1.13")
.with_release("1.2.0")
.with_release("1.2.1")
.with_release("1.4.0");
// current
let res = weights.versions_behind_factor(&s("1.4.0"), &release_chain);
assert_eq!(0, res);
// on 'skipped' version
let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain);
assert_eq!(10, res);
// on version before the skip
let res = weights.versions_behind_factor(&s("1.2.1"), &release_chain);
assert_eq!(20, res);
let res = weights.versions_behind_factor(&s("1.2.0"), &release_chain);
assert_eq!(21, res);
release_chain.push_new("1.5.0");
// current
let res = weights.versions_behind_factor(&s("1.5.0"), &release_chain);
assert_eq!(0, res);
let res = weights.versions_behind_factor(&s("1.4.0"), &release_chain);
assert_eq!(10, res);
// on 'skipped' version
let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain);
assert_eq!(20, res);
// on version before the skip
let res = weights.versions_behind_factor(&s("1.2.1"), &release_chain);
assert_eq!(30, res);
let res = weights.versions_behind_factor(&s("1.2.0"), &release_chain);
assert_eq!(31, res);
}
}
@@ -275,6 +275,9 @@ pub enum MixnetContractError {
#[error("the provided nym-node version is not a valid semver. got: {provided}")]
InvalidNymNodeSemver { provided: String },
#[error("the provided nym-node version is not greater than the current one. got: {provided}. current: {current}")]
NonIncreasingSemver { provided: String, current: String },
}
impl MixnetContractError {
@@ -141,6 +141,7 @@ pub const NEW_INTERVAL_OPERATING_COST_RANGE_KEY: &str = "new_interval_operating_
pub const NEW_VERSION_WEIGHTS_RANGE_KEY: &str = "new_version_weights_range";
pub const NEW_VERSION_SCORE_FORMULA_PARAMS_KEY: &str = "new_version_score_formula_params";
pub const NYM_NODE_CURRENT_SEMVER_KEY: &str = "new_current_semver";
pub const NYM_NODE_CURRENT_SEMVER_ID_KEY: &str = "new_current_semver_id";
pub const OLD_REWARDING_VALIDATOR_ADDRESS_KEY: &str = "old_rewarding_validator_address";
pub const NEW_REWARDING_VALIDATOR_ADDRESS_KEY: &str = "new_rewarding_validator_address";
@@ -481,12 +482,6 @@ pub fn new_settings_update_event(update: &ContractStateParamsUpdate) -> Event {
// check for config score params updates
if let Some(config_score_update) = &update.config_score_params {
if let Some(current_nym_node_semver) = &config_score_update.current_nym_node_semver {
event.attributes.push(attr(
NYM_NODE_CURRENT_SEMVER_KEY,
current_nym_node_semver.to_string(),
))
}
if let Some(version_weights) = &config_score_update.version_weights {
event.attributes.push(attr(
NEW_VERSION_WEIGHTS_RANGE_KEY,
@@ -506,9 +501,10 @@ pub fn new_settings_update_event(update: &ContractStateParamsUpdate) -> Event {
event
}
pub fn new_update_nym_node_semver_event(new_version: &str) -> Event {
pub fn new_update_nym_node_semver_event(new_version: &str, new_id: u32) -> Event {
Event::new(MixnetEventType::NymNodeSemverUpdate)
.add_attribute(NYM_NODE_CURRENT_SEMVER_KEY, new_version)
.add_attribute(NYM_NODE_CURRENT_SEMVER_ID_KEY, new_id.to_string())
}
pub fn new_not_found_node_operator_rewarding_event(interval: Interval, node_id: NodeId) -> Event {
@@ -5,6 +5,7 @@
#![warn(clippy::unwrap_used)]
#![warn(clippy::todo)]
mod config_score;
pub mod constants;
pub mod delegation;
pub mod error;
@@ -21,6 +22,7 @@ pub mod rewarding;
pub mod signing_types;
pub mod types;
pub use config_score::*;
pub use constants::*;
pub use contracts_common::types::*;
pub use cosmwasm_std::{Addr, Coin, Decimal, Fraction};
@@ -25,6 +25,7 @@ use std::time::Duration;
#[cfg(feature = "schema")]
use crate::{
config_score::{CurrentNymNodeVersionResponse, NymNodeVersionHistoryResponse},
delegation::{
NodeDelegationResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse,
PagedNodeDelegationsResponse,
@@ -423,6 +424,20 @@ pub enum QueryMsg {
#[cfg_attr(feature = "schema", returns(ContractState))]
GetState {},
/// Get the current expected version of a Nym Node.
#[cfg_attr(feature = "schema", returns(CurrentNymNodeVersionResponse))]
GetCurrentNymNodeVersion {},
/// Get the version history of Nym Node.
#[cfg_attr(feature = "schema", returns(NymNodeVersionHistoryResponse))]
GetNymNodeVersionHistory {
/// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.
limit: Option<u32>,
/// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.
start_after: Option<u32>,
},
/// Gets the current parameters used for reward calculation.
#[cfg_attr(feature = "schema", returns(RewardingParams))]
GetRewardingParams {},
@@ -1,11 +1,12 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::config_score::{ConfigScoreParams, OutdatedVersionWeights, VersionScoreFormulaParams};
use crate::nym_node::Role;
use contracts_common::Percent;
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Coin;
use cosmwasm_std::{Addr, Uint128};
use cosmwasm_std::{Coin, Decimal};
use std::fmt::{Display, Formatter};
// type aliases for better reasoning about available data
@@ -221,96 +222,14 @@ impl OperatorsParamsUpdate {
}
}
#[cw_serde]
pub struct ConfigScoreParams {
/// Current version of the nym node that is going to be used for determining the version score of a node.
/// note: value stored here is pre-validated `semver::Version`
pub current_nym_node_semver: String,
/// Defines weights for calculating numbers of versions behind the current release.
pub version_weights: OutdatedVersionWeights,
/// Defines the parameters of the formula for calculating the version score
pub version_score_formula_params: VersionScoreFormulaParams,
}
impl ConfigScoreParams {
// SAFETY: the value stored in the contract is always valid
#[allow(clippy::unwrap_used)]
pub fn unchecked_nym_node_version(&self) -> semver::Version {
self.current_nym_node_semver.parse().unwrap()
}
pub fn versions_behind(&self, node_semver: &semver::Version) -> u32 {
let expected = self.unchecked_nym_node_version();
let major_diff = (node_semver.major as i64 - expected.major as i64).unsigned_abs() as u32;
let minor_diff = (node_semver.minor as i64 - expected.minor as i64).unsigned_abs() as u32;
let patch_diff = (node_semver.patch as i64 - expected.patch as i64).unsigned_abs() as u32;
let prerelease_diff = if node_semver.pre == expected.pre {
0
} else {
1
};
major_diff * self.version_weights.major
+ minor_diff * self.version_weights.minor
+ patch_diff * self.version_weights.patch
+ prerelease_diff * self.version_weights.prerelease
}
}
/// Defines weights for calculating numbers of versions behind the current release.
#[cw_serde]
#[derive(Copy)]
pub struct OutdatedVersionWeights {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub prerelease: u32,
}
impl Default for OutdatedVersionWeights {
fn default() -> Self {
OutdatedVersionWeights {
major: 100,
minor: 10,
patch: 1,
prerelease: 1,
}
}
}
/// Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling)
/// define the relevant parameters
#[cw_serde]
#[derive(Copy)]
pub struct VersionScoreFormulaParams {
pub penalty: Decimal,
pub penalty_scaling: Decimal,
}
impl Default for VersionScoreFormulaParams {
fn default() -> Self {
#[allow(clippy::unwrap_used)]
VersionScoreFormulaParams {
penalty: "0.8".parse().unwrap(),
penalty_scaling: "2.0".parse().unwrap(),
}
}
}
#[cw_serde]
pub struct ConfigScoreParamsUpdate {
pub current_nym_node_semver: Option<String>,
pub version_weights: Option<OutdatedVersionWeights>,
pub version_score_formula_params: Option<VersionScoreFormulaParams>,
}
impl ConfigScoreParamsUpdate {
pub fn contains_updates(&self) -> bool {
self.current_nym_node_semver.is_some()
|| self.version_weights.is_some()
|| self.version_score_formula_params.is_some()
self.version_weights.is_some() || self.version_score_formula_params.is_some()
}
}
@@ -6,6 +6,7 @@ use ed25519_dalek::{Signer, SigningKey};
pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH};
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use std::fmt::{self, Debug, Display, Formatter};
use std::hash::{Hash, Hasher};
use std::str::FromStr;
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop};
@@ -122,6 +123,14 @@ impl PemStorableKeyPair for KeyPair {
#[derive(Copy, Clone, Eq, PartialEq)]
pub struct PublicKey(ed25519_dalek::VerifyingKey);
impl Hash for PublicKey {
fn hash<H: Hasher>(&self, state: &mut H) {
// each public key has unique bytes representation which can be used
// for the hash implementation
self.to_bytes().hash(state)
}
}
impl Display for PublicKey {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(&self.to_base58_string(), f)
+2 -1
View File
@@ -5,8 +5,9 @@ use std::fmt;
use http::HeaderValue;
use nym_bin_common::build_information::{BinaryBuildInformation, BinaryBuildInformationOwned};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct UserAgent {
pub application: String,
pub version: String,
-13
View File
@@ -6,7 +6,6 @@ use crate::verloc::sender::{PacketSender, TestedNode};
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use log::*;
use nym_bin_common::version_checker::{self, parse_version};
use nym_crypto::asymmetric::identity;
use nym_network_defaults::mainnet::NYM_API;
use nym_node_http_api::state::metrics::{SharedVerlocStats, VerlocNodeResult};
@@ -30,9 +29,6 @@ pub(crate) mod measurement;
pub(crate) mod packet;
pub(crate) mod sender;
// TODO: MUST BE UPDATED BEFORE ACTUAL RELEASE!!
pub const MINIMUM_NODE_VERSION: &str = "0.10.1";
// by default all of those are overwritten by config data from mixnodes directly
const DEFAULT_VERLOC_PORT: u16 = 1790;
const DEFAULT_PACKETS_PER_NODE: usize = 100;
@@ -45,9 +41,6 @@ const DEFAULT_RETRY_TIMEOUT: Duration = Duration::from_secs(60 * 30);
#[derive(Clone, Debug)]
pub struct Config {
/// Minimum semver version of a node (gateway or mixnode) that is capable of replying to echo packets.
minimum_compatible_node_version: version_checker::Version,
/// Socket address of this node on which it will be listening for the measurement packets.
listening_address: SocketAddr,
@@ -91,11 +84,6 @@ impl ConfigBuilder {
Self::default()
}
pub fn minimum_compatible_node_version(mut self, version: version_checker::Version) -> Self {
self.0.minimum_compatible_node_version = version;
self
}
pub fn listening_address(mut self, listening_address: SocketAddr) -> Self {
self.0.listening_address = listening_address;
self
@@ -154,7 +142,6 @@ impl ConfigBuilder {
impl Default for ConfigBuilder {
fn default() -> Self {
ConfigBuilder(Config {
minimum_compatible_node_version: parse_version(MINIMUM_NODE_VERSION).unwrap(),
listening_address: format!("[::]:{DEFAULT_VERLOC_PORT}").parse().unwrap(),
packets_per_node: DEFAULT_PACKETS_PER_NODE,
packet_timeout: DEFAULT_PACKET_TIMEOUT,
-55
View File
@@ -1,55 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_bin_common::version_checker;
use std::collections::{BTreeMap, HashMap};
use std::hash::Hash;
pub trait Versioned: Clone {
fn version(&self) -> String;
}
pub trait VersionFilterable<T> {
#[must_use]
fn filter_by_version(&self, expected_version: &str) -> Self;
}
impl<T> VersionFilterable<T> for Vec<T>
where
T: Versioned,
{
fn filter_by_version(&self, expected_version: &str) -> Self {
self.iter()
.filter(|node| {
version_checker::is_minor_version_compatible(&node.version(), expected_version)
})
.cloned()
.collect()
}
}
impl<T, K, V> VersionFilterable<T> for HashMap<K, V>
where
K: Eq + Hash + Clone,
V: VersionFilterable<T>,
T: Versioned,
{
fn filter_by_version(&self, expected_version: &str) -> Self {
self.iter()
.map(|(k, v)| (k.clone(), v.filter_by_version(expected_version)))
.collect()
}
}
impl<T, K, V> VersionFilterable<T> for BTreeMap<K, V>
where
K: Eq + Ord + Clone,
V: VersionFilterable<T>,
T: Versioned,
{
fn filter_by_version(&self, expected_version: &str) -> Self {
self.iter()
.map(|(k, v)| (k.clone(), v.filter_by_version(expected_version)))
.collect()
}
}
+1 -8
View File
@@ -1,7 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{filter, NetworkAddress, NodeVersion};
use crate::{NetworkAddress, NodeVersion};
use nym_api_requests::nym_nodes::SkimmedNode;
use nym_crypto::asymmetric::{encryption, identity};
use nym_mixnet_contract_common::NodeId;
@@ -126,13 +126,6 @@ impl fmt::Display for LegacyNode {
}
}
impl filter::Versioned for LegacyNode {
fn version(&self) -> String {
// TODO: return semver instead
self.version.to_string()
}
}
impl<'a> From<&'a LegacyNode> for SphinxNode {
fn from(node: &'a LegacyNode) -> Self {
let node_address_bytes = NymNodeRoutingAddress::from(node.mix_host)
-15
View File
@@ -4,7 +4,6 @@
#![allow(unknown_lints)]
// clippy::to_string_trait_impl is not on stable as of 1.77
use crate::filter::VersionFilterable;
pub use error::NymTopologyError;
use log::{debug, info, warn};
use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode};
@@ -25,7 +24,6 @@ use std::str::FromStr;
use ::serde::{Deserialize, Deserializer, Serialize, Serializer};
pub mod error;
pub mod filter;
pub mod gateway;
pub mod mix;
pub mod random_route_provider;
@@ -465,19 +463,6 @@ impl NymTopology {
Ok(())
}
#[must_use]
pub fn filter_system_version(&self, expected_version: &str) -> Self {
self.filter_node_versions(expected_version)
}
#[must_use]
pub fn filter_node_versions(&self, expected_mix_version: &str) -> Self {
NymTopology {
mixes: self.mixes.filter_by_version(expected_mix_version),
gateways: self.gateways.clone(),
}
}
}
#[cfg(feature = "serializable")]
+1 -8
View File
@@ -1,7 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{filter, NetworkAddress, NodeVersion};
use crate::{NetworkAddress, NodeVersion};
use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode};
use nym_crypto::asymmetric::{encryption, identity};
pub use nym_mixnet_contract_common::LegacyMixLayer;
@@ -89,13 +89,6 @@ impl LegacyNode {
}
}
impl filter::Versioned for LegacyNode {
fn version(&self) -> String {
// TODO: return semver instead
self.version.to_string()
}
}
impl<'a> From<&'a LegacyNode> for SphinxNode {
fn from(node: &'a LegacyNode) -> Self {
let node_address_bytes = NymNodeRoutingAddress::from(node.mix_host)
+2 -2
View File
@@ -68,9 +68,9 @@ pub async fn current_network_topology_async(
let api_client = NymApiClient::new(url);
let mixnodes = api_client
.get_all_basic_active_mixing_assigned_nodes(None)
.get_all_basic_active_mixing_assigned_nodes()
.await?;
let gateways = api_client.get_all_basic_entry_assigned_nodes(None).await?;
let gateways = api_client.get_all_basic_entry_assigned_nodes().await?;
Ok(NymTopology::from_basic(&mixnodes, &gateways).into())
}
+275 -25
View File
@@ -60,8 +60,8 @@
},
"version_score_params": {
"default": {
"penalty": "0.8",
"penalty_scaling": "2"
"penalty": "0.995",
"penalty_scaling": "1.65"
},
"allOf": [
{
@@ -261,7 +261,7 @@
"type": "string"
},
"VersionScoreFormulaParams": {
"description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters",
"description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters",
"type": "object",
"required": [
"penalty",
@@ -1308,12 +1308,6 @@
"ConfigScoreParamsUpdate": {
"type": "object",
"properties": {
"current_nym_node_semver": {
"type": [
"string",
"null"
]
},
"version_score_formula_params": {
"anyOf": [
{
@@ -1938,7 +1932,7 @@
"type": "string"
},
"VersionScoreFormulaParams": {
"description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters",
"description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters",
"type": "object",
"required": [
"penalty",
@@ -2043,6 +2037,54 @@
},
"additionalProperties": false
},
{
"description": "Get the current expected version of a Nym Node.",
"type": "object",
"required": [
"get_current_nym_node_version"
],
"properties": {
"get_current_nym_node_version": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Get the version history of Nym Node.",
"type": "object",
"required": [
"get_nym_node_version_history"
],
"properties": {
"get_nym_node_version_history": {
"type": "object",
"properties": {
"limit": {
"description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Gets the current parameters used for reward calculation.",
"type": "object",
@@ -3443,8 +3485,8 @@
},
"version_score_params": {
"default": {
"penalty": "0.8",
"penalty_scaling": "2"
"penalty": "0.995",
"penalty_scaling": "1.65"
},
"allOf": [
{
@@ -3512,7 +3554,7 @@
"additionalProperties": false
},
"VersionScoreFormulaParams": {
"description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters",
"description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters",
"type": "object",
"required": [
"penalty",
@@ -4180,6 +4222,111 @@
}
}
},
"get_current_nym_node_version": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "CurrentNymNodeVersionResponse",
"type": "object",
"properties": {
"version": {
"anyOf": [
{
"$ref": "#/definitions/HistoricalNymNodeVersionEntry"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false,
"definitions": {
"HistoricalNymNodeVersion": {
"type": "object",
"required": [
"difference_since_genesis",
"introduced_at_height",
"semver"
],
"properties": {
"difference_since_genesis": {
"description": "The absolute version difference as compared against the first version introduced into the contract.",
"allOf": [
{
"$ref": "#/definitions/TotalVersionDifference"
}
]
},
"introduced_at_height": {
"description": "Block height of when this version has been added to the contract",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"semver": {
"description": "Version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`",
"type": "string"
}
},
"additionalProperties": false
},
"HistoricalNymNodeVersionEntry": {
"type": "object",
"required": [
"id",
"version_information"
],
"properties": {
"id": {
"description": "The unique, ordered, id of this particular entry",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"version_information": {
"description": "Data associated with this particular version",
"allOf": [
{
"$ref": "#/definitions/HistoricalNymNodeVersion"
}
]
}
},
"additionalProperties": false
},
"TotalVersionDifference": {
"type": "object",
"required": [
"major",
"minor",
"patch",
"prerelease"
],
"properties": {
"major": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"minor": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"patch": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"prerelease": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
}
},
"get_delegation_details": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "NodeDelegationResponse",
@@ -7119,6 +7266,119 @@
}
}
},
"get_nym_node_version_history": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "NymNodeVersionHistoryResponse",
"type": "object",
"required": [
"history"
],
"properties": {
"history": {
"type": "array",
"items": {
"$ref": "#/definitions/HistoricalNymNodeVersionEntry"
}
},
"start_next_after": {
"description": "Field indicating paging information for the following queries if the caller wishes to get further entries.",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false,
"definitions": {
"HistoricalNymNodeVersion": {
"type": "object",
"required": [
"difference_since_genesis",
"introduced_at_height",
"semver"
],
"properties": {
"difference_since_genesis": {
"description": "The absolute version difference as compared against the first version introduced into the contract.",
"allOf": [
{
"$ref": "#/definitions/TotalVersionDifference"
}
]
},
"introduced_at_height": {
"description": "Block height of when this version has been added to the contract",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"semver": {
"description": "Version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`",
"type": "string"
}
},
"additionalProperties": false
},
"HistoricalNymNodeVersionEntry": {
"type": "object",
"required": [
"id",
"version_information"
],
"properties": {
"id": {
"description": "The unique, ordered, id of this particular entry",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"version_information": {
"description": "Data associated with this particular version",
"allOf": [
{
"$ref": "#/definitions/HistoricalNymNodeVersion"
}
]
}
},
"additionalProperties": false
},
"TotalVersionDifference": {
"type": "object",
"required": [
"major",
"minor",
"patch",
"prerelease"
],
"properties": {
"major": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"minor": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"patch": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"prerelease": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
}
},
"get_nym_nodes_detailed_paged": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PagedNymNodeDetailsResponse",
@@ -10554,15 +10814,10 @@
"ConfigScoreParams": {
"type": "object",
"required": [
"current_nym_node_semver",
"version_score_formula_params",
"version_weights"
],
"properties": {
"current_nym_node_semver": {
"description": "Current version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`",
"type": "string"
},
"version_score_formula_params": {
"description": "Defines the parameters of the formula for calculating the version score",
"allOf": [
@@ -10752,7 +11007,7 @@
"type": "string"
},
"VersionScoreFormulaParams": {
"description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters",
"description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters",
"type": "object",
"required": [
"penalty",
@@ -10826,15 +11081,10 @@
"ConfigScoreParams": {
"type": "object",
"required": [
"current_nym_node_semver",
"version_score_formula_params",
"version_weights"
],
"properties": {
"current_nym_node_semver": {
"description": "Current version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`",
"type": "string"
},
"version_score_formula_params": {
"description": "Defines the parameters of the formula for calculating the version score",
"allOf": [
@@ -10988,7 +11238,7 @@
"type": "string"
},
"VersionScoreFormulaParams": {
"description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters",
"description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters",
"type": "object",
"required": [
"penalty",
+1 -7
View File
@@ -1027,12 +1027,6 @@
"ConfigScoreParamsUpdate": {
"type": "object",
"properties": {
"current_nym_node_semver": {
"type": [
"string",
"null"
]
},
"version_score_formula_params": {
"anyOf": [
{
@@ -1657,7 +1651,7 @@
"type": "string"
},
"VersionScoreFormulaParams": {
"description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters",
"description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters",
"type": "object",
"required": [
"penalty",
+3 -3
View File
@@ -56,8 +56,8 @@
},
"version_score_params": {
"default": {
"penalty": "0.8",
"penalty_scaling": "2"
"penalty": "0.995",
"penalty_scaling": "1.65"
},
"allOf": [
{
@@ -257,7 +257,7 @@
"type": "string"
},
"VersionScoreFormulaParams": {
"description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters",
"description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters",
"type": "object",
"required": [
"penalty",
+3 -3
View File
@@ -17,8 +17,8 @@
},
"version_score_params": {
"default": {
"penalty": "0.8",
"penalty_scaling": "2"
"penalty": "0.995",
"penalty_scaling": "1.65"
},
"allOf": [
{
@@ -86,7 +86,7 @@
"additionalProperties": false
},
"VersionScoreFormulaParams": {
"description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters",
"description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters",
"type": "object",
"required": [
"penalty",
+48
View File
@@ -85,6 +85,54 @@
},
"additionalProperties": false
},
{
"description": "Get the current expected version of a Nym Node.",
"type": "object",
"required": [
"get_current_nym_node_version"
],
"properties": {
"get_current_nym_node_version": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Get the version history of Nym Node.",
"type": "object",
"required": [
"get_nym_node_version_history"
],
"properties": {
"get_nym_node_version_history": {
"type": "object",
"properties": {
"limit": {
"description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Gets the current parameters used for reward calculation.",
"type": "object",
@@ -0,0 +1,105 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "CurrentNymNodeVersionResponse",
"type": "object",
"properties": {
"version": {
"anyOf": [
{
"$ref": "#/definitions/HistoricalNymNodeVersionEntry"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false,
"definitions": {
"HistoricalNymNodeVersion": {
"type": "object",
"required": [
"difference_since_genesis",
"introduced_at_height",
"semver"
],
"properties": {
"difference_since_genesis": {
"description": "The absolute version difference as compared against the first version introduced into the contract.",
"allOf": [
{
"$ref": "#/definitions/TotalVersionDifference"
}
]
},
"introduced_at_height": {
"description": "Block height of when this version has been added to the contract",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"semver": {
"description": "Version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`",
"type": "string"
}
},
"additionalProperties": false
},
"HistoricalNymNodeVersionEntry": {
"type": "object",
"required": [
"id",
"version_information"
],
"properties": {
"id": {
"description": "The unique, ordered, id of this particular entry",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"version_information": {
"description": "Data associated with this particular version",
"allOf": [
{
"$ref": "#/definitions/HistoricalNymNodeVersion"
}
]
}
},
"additionalProperties": false
},
"TotalVersionDifference": {
"type": "object",
"required": [
"major",
"minor",
"patch",
"prerelease"
],
"properties": {
"major": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"minor": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"patch": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"prerelease": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
}
}
@@ -0,0 +1,113 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "NymNodeVersionHistoryResponse",
"type": "object",
"required": [
"history"
],
"properties": {
"history": {
"type": "array",
"items": {
"$ref": "#/definitions/HistoricalNymNodeVersionEntry"
}
},
"start_next_after": {
"description": "Field indicating paging information for the following queries if the caller wishes to get further entries.",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false,
"definitions": {
"HistoricalNymNodeVersion": {
"type": "object",
"required": [
"difference_since_genesis",
"introduced_at_height",
"semver"
],
"properties": {
"difference_since_genesis": {
"description": "The absolute version difference as compared against the first version introduced into the contract.",
"allOf": [
{
"$ref": "#/definitions/TotalVersionDifference"
}
]
},
"introduced_at_height": {
"description": "Block height of when this version has been added to the contract",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"semver": {
"description": "Version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`",
"type": "string"
}
},
"additionalProperties": false
},
"HistoricalNymNodeVersionEntry": {
"type": "object",
"required": [
"id",
"version_information"
],
"properties": {
"id": {
"description": "The unique, ordered, id of this particular entry",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"version_information": {
"description": "Data associated with this particular version",
"allOf": [
{
"$ref": "#/definitions/HistoricalNymNodeVersion"
}
]
}
},
"additionalProperties": false
},
"TotalVersionDifference": {
"type": "object",
"required": [
"major",
"minor",
"patch",
"prerelease"
],
"properties": {
"major": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"minor": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"patch": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"prerelease": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
}
}
@@ -76,15 +76,10 @@
"ConfigScoreParams": {
"type": "object",
"required": [
"current_nym_node_semver",
"version_score_formula_params",
"version_weights"
],
"properties": {
"current_nym_node_semver": {
"description": "Current version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`",
"type": "string"
},
"version_score_formula_params": {
"description": "Defines the parameters of the formula for calculating the version score",
"allOf": [
@@ -274,7 +269,7 @@
"type": "string"
},
"VersionScoreFormulaParams": {
"description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters",
"description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters",
"type": "object",
"required": [
"penalty",
@@ -54,15 +54,10 @@
"ConfigScoreParams": {
"type": "object",
"required": [
"current_nym_node_semver",
"version_score_formula_params",
"version_weights"
],
"properties": {
"current_nym_node_semver": {
"description": "Current version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`",
"type": "string"
},
"version_score_formula_params": {
"description": "Defines the parameters of the formula for calculating the version score",
"allOf": [
@@ -216,7 +211,7 @@
"type": "string"
},
"VersionScoreFormulaParams": {
"description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters",
"description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters",
"type": "object",
"required": [
"penalty",
+3
View File
@@ -71,6 +71,9 @@ pub const LAST_INTERVAL_EVENT_ID_KEY: &str = "lie";
pub const ADMIN_STORAGE_KEY: &str = "admin";
pub const CONTRACT_STATE_KEY: &str = "state";
pub const VERSION_HISTORY_ID_COUNTER_KEY: &str = "vhid";
pub const VERSION_HISTORY_NAMESPACE: &str = "vh";
pub const NYMNODE_ROLES_ASSIGNMENT_NAMESPACE: &str = "roles";
pub const NYMNODE_REWARDED_SET_METADATA_NAMESPACE: &str = "roles_metadata";
pub const NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY: &str = "active_roles";
+20 -5
View File
@@ -48,7 +48,6 @@ fn default_initial_state(
},
config_score_params: ConfigScoreParams {
current_nym_node_semver: msg.current_nym_node_version.clone(),
version_weights: msg.version_score_weights,
version_score_formula_params: msg.version_score_params,
},
@@ -101,7 +100,13 @@ pub fn instantiate(
starting_interval,
rewarding_validator_address,
)?;
mixnet_params_storage::initialise_storage(deps.branch(), state, info.sender)?;
mixnet_params_storage::initialise_storage(
deps.branch(),
&env,
state,
info.sender,
msg.current_nym_node_version,
)?;
RewardingStorage::new().initialise(deps.storage, reward_params)?;
nymnodes_storage::initialise_storage(deps.storage)?;
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
@@ -138,6 +143,7 @@ pub fn execute(
ExecuteMsg::UpdateCurrentNymNodeSemver { current_version } => {
crate::mixnet_contract_settings::transactions::try_update_current_nym_node_semver(
deps,
env,
info,
current_version,
)
@@ -334,6 +340,16 @@ pub fn query(
QueryMsg::GetState {} => {
to_binary(&crate::mixnet_contract_settings::queries::query_contract_state(deps)?)
}
QueryMsg::GetCurrentNymNodeVersion {} => to_binary(
&crate::mixnet_contract_settings::queries::query_current_nym_node_version(deps)?,
),
QueryMsg::GetNymNodeVersionHistory { limit, start_after } => to_binary(
&crate::mixnet_contract_settings::queries::query_nym_node_version_history_paged(
deps,
start_after,
limit,
)?,
),
QueryMsg::Admin {} => to_binary(&crate::mixnet_contract_settings::queries::query_admin(
deps,
)?),
@@ -587,7 +603,7 @@ pub fn query(
#[entry_point]
pub fn migrate(
mut deps: DepsMut<'_>,
_env: Env,
env: Env,
msg: MigrateMsg,
) -> Result<Response, MixnetContractError> {
set_build_information!(deps.storage)?;
@@ -596,7 +612,7 @@ pub fn migrate(
let skip_state_updates = msg.unsafe_skip_state_updates.unwrap_or(false);
if !skip_state_updates {
crate::queued_migrations::add_config_score_params(deps.branch(), &msg)?;
crate::queued_migrations::add_config_score_params(deps.branch(), env, &msg)?;
}
// due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address
@@ -693,7 +709,6 @@ mod tests {
},
},
config_score_params: ConfigScoreParams {
current_nym_node_semver: "1.1.10".to_string(),
version_weights: Default::default(),
version_score_formula_params: Default::default(),
},
@@ -3,9 +3,14 @@
use super::storage;
use crate::mixnet_contract_settings::storage::ADMIN;
use cosmwasm_std::{Deps, StdResult};
use cosmwasm_std::{Deps, Order, StdResult};
use cw_controllers::AdminResponse;
use mixnet_contract_common::{ContractBuildInformation, ContractState, ContractStateParams};
use cw_storage_plus::Bound;
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::{
ContractBuildInformation, ContractState, ContractStateParams, CurrentNymNodeVersionResponse,
HistoricalNymNodeVersionEntry, NymNodeVersionHistoryResponse,
};
use nym_contracts_common::get_build_information;
pub(crate) fn query_admin(deps: Deps<'_>) -> StdResult<AdminResponse> {
@@ -32,6 +37,36 @@ pub(crate) fn query_contract_version() -> ContractBuildInformation {
get_build_information!()
}
pub(crate) fn query_nym_node_version_history_paged(
deps: Deps<'_>,
start_after: Option<u32>,
limit: Option<u32>,
) -> StdResult<NymNodeVersionHistoryResponse> {
let limit = limit.unwrap_or(100).min(200) as usize;
let start = start_after.map(Bound::exclusive);
let history = storage::NymNodeVersionHistory::new()
.version_history
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|r| r.map(Into::<HistoricalNymNodeVersionEntry>::into))
.collect::<StdResult<Vec<_>>>()?;
let start_next_after = history.last().map(|entry| entry.id);
Ok(NymNodeVersionHistoryResponse {
history,
start_next_after,
})
}
pub(crate) fn query_current_nym_node_version(
deps: Deps<'_>,
) -> Result<CurrentNymNodeVersionResponse, MixnetContractError> {
let version = storage::NymNodeVersionHistory::new().current_version(deps.storage)?;
Ok(CurrentNymNodeVersionResponse { version })
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
@@ -59,7 +94,6 @@ pub(crate) mod tests {
interval_operating_cost: Default::default(),
},
config_score_params: ConfigScoreParams {
current_nym_node_semver: "1.1.10".to_string(),
version_weights: Default::default(),
version_score_formula_params: Default::default(),
},
@@ -1,19 +1,108 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::constants::{ADMIN_STORAGE_KEY, CONTRACT_STATE_KEY};
use cosmwasm_std::{Addr, DepsMut, Storage};
use cosmwasm_std::{Coin, StdResult};
use crate::constants::{
ADMIN_STORAGE_KEY, CONTRACT_STATE_KEY, VERSION_HISTORY_ID_COUNTER_KEY,
VERSION_HISTORY_NAMESPACE,
};
use cosmwasm_std::Coin;
use cosmwasm_std::{Addr, DepsMut, Env, Storage};
use cw_controllers::Admin;
use cw_storage_plus::Item;
use cw_storage_plus::{Item, Map};
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::{
ContractState, ContractStateParams, OperatingCostRange, ProfitMarginRange,
ContractState, ContractStateParams, HistoricalNymNodeVersion, HistoricalNymNodeVersionEntry,
OperatingCostRange, ProfitMarginRange,
};
use std::str::FromStr;
pub(crate) const CONTRACT_STATE: Item<'_, ContractState> = Item::new(CONTRACT_STATE_KEY);
pub(crate) const ADMIN: Admin = Admin::new(ADMIN_STORAGE_KEY);
pub(crate) struct NymNodeVersionHistory<'a> {
pub(crate) id_counter: Item<'a, u32>,
pub(crate) version_history: Map<'a, u32, HistoricalNymNodeVersion>,
}
impl NymNodeVersionHistory<'_> {
#[allow(clippy::new_without_default)]
pub const fn new() -> Self {
Self {
id_counter: Item::new(VERSION_HISTORY_ID_COUNTER_KEY),
version_history: Map::new(VERSION_HISTORY_NAMESPACE),
}
}
fn next_id(&self, storage: &mut dyn Storage) -> Result<u32, MixnetContractError> {
let next = self.id_counter.may_load(storage)?.unwrap_or_default();
self.id_counter.save(storage, &next)?;
Ok(next)
}
pub fn current_version(
&self,
storage: &dyn Storage,
) -> Result<Option<HistoricalNymNodeVersionEntry>, MixnetContractError> {
let Some(current_id) = self.id_counter.may_load(storage)? else {
return Ok(None);
};
let version_information = self.version_history.load(storage, current_id)?;
Ok(Some(HistoricalNymNodeVersionEntry {
id: current_id,
version_information,
}))
}
pub fn insert_new(
&self,
storage: &mut dyn Storage,
entry: HistoricalNymNodeVersion,
) -> Result<u32, MixnetContractError> {
let next_id = self.next_id(storage)?;
self.version_history.save(storage, next_id, &entry)?;
Ok(next_id)
}
pub fn try_insert_new(
&self,
storage: &mut dyn Storage,
env: &Env,
raw_semver: &str,
) -> Result<u32, MixnetContractError> {
let Ok(new_semver) = semver::Version::from_str(raw_semver) else {
return Err(MixnetContractError::InvalidNymNodeSemver {
provided: raw_semver.to_string(),
});
};
let Some(current) = self.current_version(storage)? else {
// treat this as genesis
let genesis =
HistoricalNymNodeVersion::genesis(raw_semver.to_string(), env.block.height);
return self.insert_new(storage, genesis);
};
let current_semver = current.version_information.semver_unchecked();
if new_semver <= current_semver {
// make sure the new semver is strictly more recent than the current head
return Err(MixnetContractError::NonIncreasingSemver {
provided: raw_semver.to_string(),
current: current.version_information.semver,
});
}
let diff = current
.version_information
.cumulative_difference_since_genesis(&new_semver);
let entry = HistoricalNymNodeVersion {
semver: raw_semver.to_string(),
introduced_at_height: env.block.height,
difference_since_genesis: diff,
};
self.insert_new(storage, entry)
}
}
pub fn rewarding_validator_address(storage: &dyn Storage) -> Result<Addr, MixnetContractError> {
Ok(CONTRACT_STATE
.load(storage)
@@ -71,9 +160,13 @@ pub(crate) fn state_params(
pub(crate) fn initialise_storage(
deps: DepsMut<'_>,
env: &Env,
initial_state: ContractState,
initial_admin: Addr,
) -> StdResult<()> {
initial_nymnode_version: String,
) -> Result<(), MixnetContractError> {
CONTRACT_STATE.save(deps.storage, &initial_state)?;
ADMIN.set(deps, Some(initial_admin))
NymNodeVersionHistory::new().try_insert_new(deps.storage, env, &initial_nymnode_version)?;
ADMIN.set(deps, Some(initial_admin))?;
Ok(())
}
@@ -3,16 +3,15 @@
use super::storage;
use crate::mixnet_contract_settings::storage::ADMIN;
use cosmwasm_std::MessageInfo;
use cosmwasm_std::Response;
use cosmwasm_std::{DepsMut, StdResult};
use cosmwasm_std::{Env, MessageInfo};
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::events::{
new_rewarding_validator_address_update_event, new_settings_update_event,
new_update_nym_node_semver_event,
};
use mixnet_contract_common::ContractStateParamsUpdate;
use std::str::FromStr;
pub fn try_update_contract_admin(
mut deps: DepsMut<'_>,
@@ -92,15 +91,6 @@ pub(crate) fn try_update_contract_settings(
// check for config score params updates
if let Some(config_score_update) = update.config_score_params {
// if semver is to be updated - validate the provided value
if let Some(current_nym_node_semver) = config_score_update.current_nym_node_semver {
if semver::Version::from_str(&current_nym_node_semver).is_err() {
return Err(MixnetContractError::InvalidNymNodeSemver {
provided: current_nym_node_semver,
});
}
state.params.config_score_params.current_nym_node_semver = current_nym_node_semver
}
if let Some(version_weights) = config_score_update.version_weights {
state.params.config_score_params.version_weights = version_weights
}
@@ -119,23 +109,19 @@ pub(crate) fn try_update_contract_settings(
pub(crate) fn try_update_current_nym_node_semver(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
current_version: String,
) -> Result<Response, MixnetContractError> {
let mut state = storage::CONTRACT_STATE.load(deps.storage)?;
ADMIN.assert_admin(deps.as_ref(), &info.sender)?;
let response = Response::new().add_event(new_update_nym_node_semver_event(&current_version));
let new_id = storage::NymNodeVersionHistory::new().try_insert_new(
deps.storage,
&env,
&current_version,
)?;
if semver::Version::from_str(&current_version).is_err() {
return Err(MixnetContractError::InvalidNymNodeSemver {
provided: current_version,
});
}
state.params.config_score_params.current_nym_node_semver = current_version;
storage::CONTRACT_STATE.save(deps.storage, &state)?;
Ok(response)
Ok(Response::new().add_event(new_update_nym_node_semver_event(&current_version, new_id)))
}
#[cfg(test)]
+11 -2
View File
@@ -4,7 +4,8 @@
mod config_score_params {
use crate::constants::CONTRACT_STATE_KEY;
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use cosmwasm_std::{Addr, Coin, DepsMut};
use crate::mixnet_contract_settings::storage::NymNodeVersionHistory;
use cosmwasm_std::{Addr, Coin, DepsMut, Env};
use cw_storage_plus::Item;
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::{
@@ -16,6 +17,7 @@ mod config_score_params {
pub(crate) fn add_config_score_params(
deps: DepsMut<'_>,
env: Env,
msg: &MigrateMsg,
) -> Result<(), MixnetContractError> {
if semver::Version::from_str(&msg.current_nym_node_semver).is_err() {
@@ -62,7 +64,6 @@ mod config_score_params {
interval_operating_cost: old_state.params.interval_operating_cost,
},
config_score_params: ConfigScoreParams {
current_nym_node_semver: msg.current_nym_node_semver.to_string(),
version_weights: msg.version_score_weights,
version_score_formula_params: msg.version_score_params,
},
@@ -70,6 +71,14 @@ mod config_score_params {
};
mixnet_params_storage::CONTRACT_STATE.save(deps.storage, &new_state)?;
// initialise the version chain
NymNodeVersionHistory::new().try_insert_new(
deps.storage,
&env,
&msg.current_nym_node_semver,
)?;
Ok(())
}
}
+33 -2
View File
@@ -1,6 +1,8 @@
use nym_api_requests::models::NodePerformance;
use nym_api_requests::models::{DescribedNodeType, NodePerformance, NymNodeData};
use nym_contracts_common::Percent;
use nym_mixnet_contract_common::{Addr, Coin, Gateway, LegacyMixLayer, MixNode, NodeId};
use nym_mixnet_contract_common::{
Addr, Coin, Delegation, Gateway, LegacyMixLayer, MixNode, NodeId, NodeRewarding, NymNodeBond,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -52,3 +54,32 @@ pub struct PrettyDetailedGatewayBond {
pub proxy: Option<Addr>,
pub location: Option<Location>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct NymNodeWithDescriptionAndLocation {
pub node_id: NodeId,
pub contract_node_type: Option<DescribedNodeType>,
pub description: Option<NymNodeData>,
pub bond_information: NymNodeBond,
pub rewarding_details: NodeRewarding,
pub location: Option<Location>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct NymNodeWithDescriptionAndLocationAndDelegations {
pub node_id: NodeId,
pub contract_node_type: Option<DescribedNodeType>,
pub description: Option<NymNodeData>,
pub bond_information: NymNodeBond,
pub rewarding_details: NodeRewarding,
pub location: Option<Location>,
pub delegations: Option<Vec<Delegation>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct NymVestingAccount {
pub locked: Coin,
pub vested: Coin,
pub vesting: Coin,
pub spendable: Coin,
}
@@ -38,8 +38,14 @@ impl CountryStatisticsDistributionTask {
/// Retrieves the current list of mixnodes from the validators and calculates how many nodes are in each country
async fn calculate_nodes_per_country(&mut self) {
let cache = self.state.inner.mixnodes.get_locations().await;
let nym_nodes = self
.state
.inner
.nymnodes
.get_bonded_nymnodes_locations()
.await;
let three_letter_iso_country_codes: Vec<String> = cache
let mut three_letter_iso_country_codes: Vec<String> = cache
.values()
.flat_map(|i| {
i.location
@@ -48,6 +54,10 @@ impl CountryStatisticsDistributionTask {
})
.collect();
for node in nym_nodes {
three_letter_iso_country_codes.push(node.three_letter_iso_country_code);
}
let mut distribution = CountryNodesDistribution::new();
info!("Calculating country distribution from located mixnodes...");
+50 -6
View File
@@ -2,8 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
use crate::{cache::Cache, location::LocationCacheItem};
use nym_contracts_common::IdentityKey;
use nym_explorer_api_requests::{Location, PrettyDetailedGatewayBond};
use nym_mixnet_contract_common::{GatewayBond, IdentityKey};
use nym_mixnet_contract_common::GatewayBond;
use nym_validator_client::models::GatewayBondAnnotated;
use serde::Serialize;
use std::{sync::Arc, time::SystemTime};
use tokio::sync::RwLock;
@@ -11,7 +13,7 @@ use tokio::sync::RwLock;
use super::location::GatewayLocationCache;
pub(crate) struct GatewayCache {
pub(crate) gateways: Cache<IdentityKey, GatewayBond>,
pub(crate) gateways: Cache<IdentityKey, GatewayBondAnnotated>,
}
#[derive(Clone, Debug, Serialize, JsonSchema)]
@@ -22,6 +24,7 @@ pub(crate) struct GatewaySummary {
#[derive(Clone)]
pub(crate) struct ThreadsafeGatewayCache {
gateways: Arc<RwLock<GatewayCache>>,
legacy_gateway_bonds: Arc<RwLock<GatewayCache>>,
locations: Arc<RwLock<GatewayLocationCache>>,
}
@@ -31,6 +34,9 @@ impl ThreadsafeGatewayCache {
gateways: Arc::new(RwLock::new(GatewayCache {
gateways: Cache::new(),
})),
legacy_gateway_bonds: Arc::new(RwLock::new(GatewayCache {
gateways: Cache::new(),
})),
locations: Arc::new(RwLock::new(GatewayLocationCache::new())),
}
}
@@ -51,7 +57,14 @@ impl ThreadsafeGatewayCache {
}
pub(crate) async fn get_gateways(&self) -> Vec<GatewayBond> {
self.gateways.read().await.gateways.get_all()
self.gateways
.read()
.await
.gateways
.get_all()
.iter()
.map(|g| g.gateway_bond.bond.clone())
.collect()
}
pub(crate) async fn get_detailed_gateways(&self) -> Vec<PrettyDetailedGatewayBond> {
@@ -64,7 +77,22 @@ impl ThreadsafeGatewayCache {
.iter()
.map(|bond| {
let location = location_guard.get(bond.identity());
self.create_detailed_gateway(bond.to_owned(), location)
self.create_detailed_gateway(bond.gateway_bond.bond.to_owned(), location)
})
.collect()
}
pub(crate) async fn get_legacy_detailed_gateways(&self) -> Vec<PrettyDetailedGatewayBond> {
let legacy_gateways = self.legacy_gateway_bonds.read().await;
let location_guard = self.locations.read().await;
legacy_gateways
.gateways
.get_all()
.iter()
.map(|bond| {
let location = location_guard.get(bond.identity());
self.create_detailed_gateway(bond.gateway_bond.bond.to_owned(), location)
})
.collect()
}
@@ -80,6 +108,9 @@ impl ThreadsafeGatewayCache {
gateways: Arc::new(RwLock::new(GatewayCache {
gateways: Cache::new(),
})),
legacy_gateway_bonds: Arc::new(RwLock::new(GatewayCache {
gateways: Cache::new(),
})),
locations: Arc::new(RwLock::new(locations)),
}
}
@@ -106,13 +137,26 @@ impl ThreadsafeGatewayCache {
.insert(identy_key, LocationCacheItem::new_from_location(location));
}
pub(crate) async fn update_cache(&self, gateways: Vec<GatewayBond>) {
pub(crate) async fn update_cache(
&self,
gateways: Vec<GatewayBondAnnotated>,
legacy_gateway_bonds: Vec<GatewayBond>,
) {
let mut guard = self.gateways.write().await;
let mut guard_legacy_gateways = self.legacy_gateway_bonds.write().await;
for gateway in gateways {
guard
.gateways
.set(gateway.gateway.identity_key.clone(), gateway)
.set(gateway.gateway_bond.gateway.identity_key.clone(), gateway)
}
for legacy_gateway in legacy_gateway_bonds {
if let Some(g) = guard.gateways.get(&legacy_gateway.gateway.identity_key) {
guard_legacy_gateways
.gateways
.set(legacy_gateway.gateway.identity_key, g.clone());
}
}
}
}
+2 -2
View File
@@ -10,11 +10,11 @@ use crate::gateways::http::gateways_make_default_routes;
use crate::http::swagger::get_docs;
use crate::mix_node::http::mix_node_make_default_routes;
use crate::mix_nodes::http::mix_nodes_make_default_routes;
use crate::nym_nodes::http::unstable_temp_nymnodes_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::unstable::http::unstable_temp_make_default_routes;
use crate::validators::http::validators_make_default_routes;
mod swagger;
@@ -59,7 +59,7 @@ fn configure_rocket(state: ExplorerApiStateContext) -> Rocket<Build> {
"/ping" => ping_make_default_routes(&openapi_settings),
"/validators" => validators_make_default_routes(&openapi_settings),
"/service-providers" => service_providers_make_default_routes(&openapi_settings),
"/tmp/unstable" => unstable_temp_nymnodes_make_default_routes(&openapi_settings),
"/tmp/unstable" => unstable_temp_make_default_routes(&openapi_settings),
};
building_rocket
+1 -1
View File
@@ -22,12 +22,12 @@ mod http;
mod location;
mod mix_node;
pub(crate) mod mix_nodes;
mod nym_nodes;
mod overview;
mod ping;
pub(crate) mod service_providers;
mod state;
mod tasks;
mod unstable;
mod validators;
const COUNTRY_DATA_REFRESH_INTERVAL: u64 = 60 * 15; // every 15 minutes
+1 -1
View File
@@ -44,7 +44,7 @@ async fn get_mix_node_stats(host: &str, port: u16) -> Result<NodeStats, ReqwestE
.await
}
#[openapi(tag = "mix_nodes")]
#[openapi(tag = "mix_node")]
#[get("/<mix_id>")]
pub(crate) async fn get_by_id(
mix_id: NodeId,
+32 -1
View File
@@ -7,7 +7,7 @@ use crate::location::LocationCacheItem;
use crate::mix_nodes::CACHE_ENTRY_TTL;
use nym_explorer_api_requests::{Location, MixnodeStatus, PrettyDetailedMixNodeBond};
use nym_mixnet_contract_common::rewarding::helpers::truncate_reward;
use nym_mixnet_contract_common::NodeId;
use nym_mixnet_contract_common::{MixNodeBond, NodeId};
use nym_validator_client::models::MixNodeBondAnnotated;
use serde::Serialize;
use std::collections::{HashMap, HashSet};
@@ -80,6 +80,7 @@ impl MixNodesResult {
#[derive(Clone)]
pub(crate) struct ThreadsafeMixNodesCache {
mixnodes: Arc<RwLock<MixNodesResult>>,
legacy_mixnode_bonds: Arc<RwLock<MixNodesResult>>,
locations: Arc<RwLock<MixnodeLocationCache>>,
}
@@ -87,6 +88,7 @@ impl ThreadsafeMixNodesCache {
pub(crate) fn new() -> Self {
ThreadsafeMixNodesCache {
mixnodes: Arc::new(RwLock::new(MixNodesResult::new())),
legacy_mixnode_bonds: Arc::new(RwLock::new(MixNodesResult::new())),
locations: Arc::new(RwLock::new(MixnodeLocationCache::new())),
}
}
@@ -94,6 +96,7 @@ impl ThreadsafeMixNodesCache {
pub(crate) fn new_with_location_cache(locations: MixnodeLocationCache) -> Self {
ThreadsafeMixNodesCache {
mixnodes: Arc::new(RwLock::new(MixNodesResult::new())),
legacy_mixnode_bonds: Arc::new(RwLock::new(MixNodesResult::new())),
locations: Arc::new(RwLock::new(locations)),
}
}
@@ -188,13 +191,35 @@ impl ThreadsafeMixNodesCache {
.collect()
}
pub(crate) async fn get_legacy_detailed_mixnodes(&self) -> Vec<PrettyDetailedMixNodeBond> {
let legacy_mixnodes = self.legacy_mixnode_bonds.read().await;
let location_guard = self.locations.read().await;
legacy_mixnodes
.all_mixnodes
.values()
.map(|bond| {
let location = location_guard.get(&bond.mix_id());
self.create_detailed_mixnode(bond.mix_id(), &legacy_mixnodes, location, bond)
})
.collect()
}
pub(crate) async fn update_cache(
&self,
all_bonds: Vec<MixNodeBondAnnotated>,
rewarded_nodes: HashSet<NodeId>,
active_nodes: HashSet<NodeId>,
legacy_mixnode_bonds: Vec<MixNodeBond>,
) {
let mut guard = self.mixnodes.write().await;
let mut guard_legacy_mixnodes = self.legacy_mixnode_bonds.write().await;
let legacy_mixnode_bond_ids: Vec<&NodeId> = legacy_mixnode_bonds
.iter()
.map(|bond| &bond.mix_id)
.collect();
guard.all_mixnodes = all_bonds
.into_iter()
.map(|bond| (bond.mix_id(), bond))
@@ -202,5 +227,11 @@ impl ThreadsafeMixNodesCache {
guard.rewarded_mixnodes = rewarded_nodes;
guard.active_mixnodes = active_nodes;
guard.valid_until = SystemTime::now() + CACHE_ENTRY_TTL;
guard_legacy_mixnodes.all_mixnodes = guard
.all_mixnodes
.clone()
.into_iter()
.filter(|(node_id, _bond)| legacy_mixnode_bond_ids.iter().any(|i| **i == *node_id))
.collect();
}
}
-26
View File
@@ -1,26 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::state::ExplorerApiStateContext;
use nym_explorer_api_requests::PrettyDetailedGatewayBond;
use okapi::openapi3::OpenApi;
use rocket::serde::json::Json;
use rocket::{Route, State};
use rocket_okapi::settings::OpenApiSettings;
pub fn unstable_temp_nymnodes_make_default_routes(
settings: &OpenApiSettings,
) -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![settings: all_gateways]
}
#[openapi(tag = "UNSTABLE")]
#[get("/gateways")]
pub(crate) async fn all_gateways(
state: &State<ExplorerApiStateContext>,
) -> Json<Vec<PrettyDetailedGatewayBond>> {
let mut gateways = state.inner.gateways.get_detailed_gateways().await;
gateways.append(&mut state.inner.nymnodes.pretty_gateways().await);
Json(gateways)
}
+34 -1
View File
@@ -1,3 +1,4 @@
use nym_validator_client::models::NymNodeData;
use rocket::serde::json::Json;
use rocket::{Route, State};
use rocket_okapi::okapi::openapi3::OpenApi;
@@ -5,19 +6,51 @@ use rocket_okapi::openapi_get_routes_spec;
use rocket_okapi::settings::OpenApiSettings;
use crate::mix_nodes::http::get_mixnode_summary;
use crate::overview::models::OverviewSummary;
use crate::overview::models::{NymNodeSummary, OverviewSummary, RoleSummary};
use crate::state::ExplorerApiStateContext;
pub fn overview_make_default_routes(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![settings: summary]
}
fn get_nym_nodes_by_role(nodes: &Vec<NymNodeData>) -> RoleSummary {
let mut summary = RoleSummary::default();
for node in nodes {
if node.declared_role.entry {
summary.entry += 1;
}
if node.declared_role.exit_ipr {
summary.exit_ipr += 1;
}
if node.declared_role.exit_nr {
summary.exit_nr += 1;
}
if node.declared_role.mixnode {
summary.mixnode += 1;
}
}
summary
}
#[openapi(tag = "overview")]
#[get("/summary")]
pub(crate) async fn summary(state: &State<ExplorerApiStateContext>) -> Json<OverviewSummary> {
let nym_nodes = state
.inner
.nymnodes
.get_bonded_nymnodes_descriptions()
.await;
let roles = get_nym_nodes_by_role(&nym_nodes);
Json(OverviewSummary {
mixnodes: get_mixnode_summary(state).await,
validators: state.inner.validators.get_validator_summary().await,
gateways: state.inner.gateways.get_gateway_summary().await,
nymnodes: NymNodeSummary {
count: nym_nodes.len(),
roles,
},
})
}
+15
View File
@@ -7,9 +7,24 @@ use crate::gateways::models::GatewaySummary;
use crate::mix_nodes::models::MixNodeSummary;
use crate::validators::models::ValidatorSummary;
#[derive(Clone, Debug, Serialize, JsonSchema, Default)]
pub(crate) struct RoleSummary {
pub mixnode: usize,
pub entry: usize,
pub exit_nr: usize,
pub exit_ipr: usize,
}
#[derive(Clone, Debug, Serialize, JsonSchema, Default)]
pub(crate) struct NymNodeSummary {
pub count: usize,
pub roles: RoleSummary,
}
#[derive(Clone, Debug, Serialize, JsonSchema)]
pub(crate) struct OverviewSummary {
pub mixnodes: MixNodeSummary,
pub gateways: GatewaySummary,
pub validators: ValidatorSummary,
pub nymnodes: NymNodeSummary,
}
+153 -3
View File
@@ -3,12 +3,18 @@ use std::path::Path;
use chrono::{DateTime, Utc};
use log::info;
use nym_mixnet_contract_common::NodeId;
use nym_explorer_api_requests::NymVestingAccount;
use nym_mixnet_contract_common::{Addr, Delegation, NodeId, PendingRewardResponse};
use serde::{Deserialize, Serialize};
use crate::client::ThreadsafeValidatorClient;
use crate::geo_ip::location::ThreadsafeGeoIp;
use nym_mixnet_contract_common::Coin as CosmWasmCoin;
use nym_validator_client::models::MixNodeBondAnnotated;
use nym_validator_client::nyxd::contract_traits::{
MixnetQueryClient, PagedMixnetQueryClient, VestingQueryClient,
};
use nym_validator_client::nyxd::{AccountId, Coin, CosmWasmClient};
use crate::country_statistics::country_nodes_distribution::{
CountryNodesDistribution, ThreadsafeCountryNodesDistribution,
@@ -18,9 +24,9 @@ use crate::gateways::models::ThreadsafeGatewayCache;
use crate::mix_node::models::ThreadsafeMixNodeCache;
use crate::mix_nodes::location::MixnodeLocationCache;
use crate::mix_nodes::models::ThreadsafeMixNodesCache;
use crate::nym_nodes::location::NymNodeLocationCache;
use crate::nym_nodes::models::ThreadSafeNymNodesCache;
use crate::ping::models::ThreadsafePingCache;
use crate::unstable::location::NymNodeLocationCache;
use crate::unstable::models::ThreadSafeNymNodesCache;
use crate::validators::models::ThreadsafeValidatorCache;
// TODO: change to an environment variable with a default value
@@ -45,6 +51,150 @@ impl ExplorerApiState {
pub(crate) async fn get_mix_node(&self, mix_id: NodeId) -> Option<MixNodeBondAnnotated> {
self.mixnodes.get_mixnode(mix_id).await
}
pub(crate) async fn get_delegations_by_node(
&self,
node_id: NodeId,
) -> Result<Vec<Delegation>, rocket::response::status::NotFound<String>> {
match self
.validator_client
.0
.nyxd
.get_all_single_mixnode_delegations(node_id)
.await
{
Ok(res) => Ok(res),
Err(e) => Err(rocket::response::status::NotFound(format!("{}", e))),
}
}
pub(crate) async fn get_balance(
&self,
addr: &AccountId,
) -> Result<Vec<Coin>, rocket::response::status::NotFound<String>> {
match self.validator_client.0.nyxd.get_all_balances(addr).await {
Ok(res) => Ok(res),
Err(e) => Err(rocket::response::status::NotFound(format!("{}", e))),
}
}
pub(crate) async fn get_vesting_balance(
&self,
addr: &AccountId,
) -> Result<Option<NymVestingAccount>, rocket::response::status::NotFound<String>> {
match nym_validator_client::nyxd::contract_traits::VestingQueryClient::get_account(
&self.validator_client.0.nyxd,
addr.as_ref(),
)
.await
{
// 1. is there a vesting account?
Ok(_res) => {
// 2. there is vesting account, get all the coins
let mut locked = CosmWasmCoin::default();
let mut vested = CosmWasmCoin::default();
let mut vesting = CosmWasmCoin::default();
let mut spendable = CosmWasmCoin::default();
// 3. try to get each coin type
if let Ok(coin) = self
.validator_client
.0
.nyxd
.locked_coins(addr.as_ref(), None)
.await
{
locked = coin.into();
}
if let Ok(coin) = self
.validator_client
.0
.nyxd
.vested_coins(addr.as_ref(), None)
.await
{
vested = coin.into();
}
if let Ok(coin) = self
.validator_client
.0
.nyxd
.vesting_coins(addr.as_ref(), None)
.await
{
vesting = coin.into();
}
if let Ok(coin) = self
.validator_client
.0
.nyxd
.spendable_coins(addr.as_ref(), None)
.await
{
spendable = coin.into();
}
// 4.combine into a response
Ok(Some(NymVestingAccount {
locked,
vested,
vesting,
spendable,
}))
}
Err(e) => Err(rocket::response::status::NotFound(format!("{}", e))),
}
}
pub(crate) async fn get_delegations(
&self,
addr: &AccountId,
) -> Result<Vec<Delegation>, rocket::response::status::NotFound<String>> {
match self
.validator_client
.0
.nyxd
.get_all_delegator_delegations(addr)
.await
{
Ok(res) => Ok(res),
Err(e) => Err(rocket::response::status::NotFound(format!("{}", e))),
}
}
pub(crate) async fn get_delegation_rewards(
&self,
addr: &AccountId,
node_id: &NodeId,
proxy: &Option<Addr>,
) -> Result<PendingRewardResponse, rocket::response::status::NotFound<String>> {
match self
.validator_client
.0
.nyxd
.get_pending_delegator_reward(addr, *node_id, proxy.clone().map(|d| d.to_string()))
.await
{
Ok(res) => Ok(res),
Err(e) => Err(rocket::response::status::NotFound(format!("{}", e))),
}
}
pub(crate) async fn get_operator_rewards(
&self,
addr: &AccountId,
) -> Result<PendingRewardResponse, rocket::response::status::NotFound<String>> {
match self
.validator_client
.0
.nyxd
.get_pending_operator_reward(addr)
.await
{
Ok(res) => Ok(res),
Err(e) => Err(rocket::response::status::NotFound(format!("{}", e))),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
+46 -6
View File
@@ -3,9 +3,12 @@
use crate::mix_nodes::CACHE_REFRESH_RATE;
use crate::state::ExplorerApiStateContext;
use nym_mixnet_contract_common::{GatewayBond, NymNodeDetails};
use nym_mixnet_contract_common::{GatewayBond, MixNodeBond, NymNodeDetails};
use nym_task::TaskClient;
use nym_validator_client::models::{MixNodeBondAnnotated, NymNodeDescription};
use nym_validator_client::models::{
GatewayBondAnnotated, MixNodeBondAnnotated, NymNodeDescription,
};
use nym_validator_client::nyxd::contract_traits::PagedMixnetQueryClient;
use nym_validator_client::nyxd::error::NyxdError;
use nym_validator_client::nyxd::{Paging, TendermintRpcClient, ValidatorResponse};
use nym_validator_client::{QueryHttpRpcValidatorClient, ValidatorClientError};
@@ -71,13 +74,15 @@ impl ExplorerApiTasks {
.await
}
async fn retrieve_all_gateways(&self) -> Result<Vec<GatewayBond>, ValidatorClientError> {
async fn retrieve_all_gateways(
&self,
) -> Result<Vec<GatewayBondAnnotated>, ValidatorClientError> {
info!("About to retrieve all gateways...");
self.state
.inner
.validator_client
.0
.get_cached_gateways()
.get_cached_gateways_detailed_unfiltered()
.await
}
@@ -115,7 +120,30 @@ impl ExplorerApiTasks {
.await
}
async fn retrieve_legacy_gateway_bonds(&self) -> Vec<GatewayBond> {
self.state
.inner
.validator_client
.0
.nyxd
.get_all_gateways()
.await
.unwrap_or(vec![])
}
async fn retrieve_legacy_mixnode_bonds(&self) -> Vec<MixNodeBond> {
self.state
.inner
.validator_client
.0
.nyxd
.get_all_mixnode_bonds()
.await
.unwrap_or(vec![])
}
async fn update_mixnode_cache(&self) {
let legacy_mixnode_bonds = self.retrieve_legacy_mixnode_bonds().await;
let all_bonds = self.retrieve_all_mixnodes().await;
let rewarded_nodes = self
.retrieve_rewarded_mixnodes()
@@ -132,7 +160,12 @@ impl ExplorerApiTasks {
self.state
.inner
.mixnodes
.update_cache(all_bonds, rewarded_nodes, active_nodes)
.update_cache(
all_bonds,
rewarded_nodes,
active_nodes,
legacy_mixnode_bonds,
)
.await;
}
@@ -146,8 +179,15 @@ impl ExplorerApiTasks {
}
async fn update_gateways_cache(&self) {
let legacy_gateway_bonds = self.retrieve_legacy_gateway_bonds().await;
match self.retrieve_all_gateways().await {
Ok(response) => self.state.inner.gateways.update_cache(response).await,
Ok(response) => {
self.state
.inner
.gateways
.update_cache(response, legacy_gateway_bonds)
.await
}
Err(err) => {
error!("Failed to get gateways: {err}")
}
+258
View File
@@ -0,0 +1,258 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::state::ExplorerApiStateContext;
use nym_explorer_api_requests::{
NymNodeWithDescriptionAndLocation, NymNodeWithDescriptionAndLocationAndDelegations,
NymVestingAccount, PrettyDetailedGatewayBond, PrettyDetailedMixNodeBond,
};
use nym_mixnet_contract_common::{Addr, Coin, NodeId};
use nym_validator_client::nyxd::AccountId;
use okapi::openapi3::OpenApi;
use rocket::response::status::NotFound;
use rocket::serde::json::Json;
use rocket::serde::{Deserialize, Serialize};
use rocket::{Route, State};
use rocket_okapi::settings::OpenApiSettings;
use std::collections::HashMap;
use std::str::FromStr;
pub fn unstable_temp_make_default_routes(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![settings: all_gateways, all_gateway_bonds, all_mixnode_bonds, all_nym_nodes, get_nym_node_by_id, get_account_by_addr]
}
#[openapi(tag = "UNSTABLE")]
#[get("/gateways")]
pub(crate) async fn all_gateways(
state: &State<ExplorerApiStateContext>,
) -> Json<Vec<PrettyDetailedGatewayBond>> {
let mut gateways = state.inner.gateways.get_legacy_detailed_gateways().await;
let mut nym_node_gateways: Vec<PrettyDetailedGatewayBond> = state
.inner
.nymnodes
.pretty_gateways()
.await
.clone()
.into_iter()
.filter(|g| {
!gateways
.iter()
.any(|g2| g.gateway.identity_key == g2.gateway.identity_key)
})
.collect();
gateways.append(&mut nym_node_gateways);
Json(gateways)
}
#[openapi(tag = "UNSTABLE")]
#[get("/legacy-gateway-bonds")]
pub(crate) async fn all_gateway_bonds(
state: &State<ExplorerApiStateContext>,
) -> Json<Vec<PrettyDetailedGatewayBond>> {
Json(state.inner.gateways.get_legacy_detailed_gateways().await)
}
#[openapi(tag = "UNSTABLE")]
#[get("/legacy-mixnode-bonds")]
pub(crate) async fn all_mixnode_bonds(
state: &State<ExplorerApiStateContext>,
) -> Json<Vec<PrettyDetailedMixNodeBond>> {
Json(state.inner.mixnodes.get_legacy_detailed_mixnodes().await)
}
#[openapi(tag = "UNSTABLE")]
#[get("/nym-nodes")]
pub(crate) async fn all_nym_nodes(
state: &State<ExplorerApiStateContext>,
) -> Json<Vec<NymNodeWithDescriptionAndLocation>> {
let nodes = state
.inner
.nymnodes
.get_bonded_nymnodes_with_description_and_location()
.await;
Json(nodes.values().cloned().collect())
}
#[openapi(tag = "UNSTABLE")]
#[get("/nym-nodes/<node_id>")]
pub(crate) async fn get_nym_node_by_id(
node_id: NodeId,
state: &State<ExplorerApiStateContext>,
) -> Json<Option<NymNodeWithDescriptionAndLocationAndDelegations>> {
let nodes = state
.inner
.nymnodes
.get_bonded_nymnodes_with_description_and_location()
.await;
Json(match nodes.get(&node_id).cloned() {
None => None,
Some(node) => {
let delegations = state.inner.get_delegations_by_node(node_id).await.ok();
Some(NymNodeWithDescriptionAndLocationAndDelegations {
node_id: node.node_id,
contract_node_type: node.contract_node_type,
description: node.description,
bond_information: node.bond_information,
rewarding_details: node.rewarding_details,
location: node.location,
delegations,
})
}
})
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct NyxAccountDelegationDetails {
pub node_id: NodeId,
pub delegated: Coin,
pub height: u64,
pub proxy: Option<Addr>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct NyxAccountDelegationRewardDetails {
pub node_id: NodeId,
pub rewards: Coin,
pub amount_staked: Coin,
pub node_still_fully_bonded: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct NyxAccountDetails {
pub address: String,
pub balances: Vec<Coin>,
pub total_value: Coin,
pub delegations: Vec<NyxAccountDelegationDetails>,
pub accumulated_rewards: Vec<NyxAccountDelegationRewardDetails>,
pub total_delegations: Coin,
pub claimable_rewards: Coin,
pub vesting_account: Option<NymVestingAccount>,
pub operator_rewards: Option<Coin>,
}
#[openapi(tag = "UNSTABLE")]
#[get("/account/<addr>")]
pub(crate) async fn get_account_by_addr(
addr: String,
state: &State<ExplorerApiStateContext>,
) -> Result<Json<NyxAccountDetails>, NotFound<String>> {
match AccountId::from_str(&addr) {
Ok(address) => {
let mut total_value = 0u128;
// 1. get balances of chain tokens
let balances: Vec<Coin> = state
.inner
.get_balance(&address)
.await?
.into_iter()
.map(|c| {
if c.denom == "unym" {
total_value += c.amount;
}
c.into()
})
.collect();
// 2. get list of delegations (history)
let delegations: Vec<NyxAccountDelegationDetails> = state
.inner
.get_delegations(&address)
.await?
.into_iter()
.map(|d| NyxAccountDelegationDetails {
delegated: d.amount,
height: d.height,
node_id: d.node_id,
proxy: d.proxy,
})
.collect();
// 3. get the current reward for each active delegation
let mut rewards_map: HashMap<&NodeId, NyxAccountDelegationRewardDetails> =
HashMap::new();
for d in &delegations {
if rewards_map.contains_key(&d.node_id) {
continue;
}
if let Ok(r) = state
.inner
.get_delegation_rewards(&address, &d.node_id, &d.proxy)
.await
{
if let Some(rewards) = r.amount_earned {
rewards_map.insert(
&d.node_id,
NyxAccountDelegationRewardDetails {
node_id: d.node_id,
rewards,
amount_staked: r.amount_staked.unwrap_or_default(),
node_still_fully_bonded: r.node_still_fully_bonded,
},
);
}
}
}
// 4. make the map of rewards into a vec and sum the rewards and delegations
let accumulated_rewards: Vec<NyxAccountDelegationRewardDetails> =
rewards_map.values().cloned().collect();
let mut claimable_rewards = 0u128;
let mut total_delegations = 0u128;
for r in &accumulated_rewards {
claimable_rewards += r.rewards.amount.u128();
total_delegations += r.amount_staked.amount.u128();
total_value += r.rewards.amount.u128();
total_value += r.amount_staked.amount.u128();
}
// 5. get vesting account details (if present)
let vesting_account = state
.inner
.get_vesting_balance(&address)
.await
.unwrap_or_default();
if let Some(vesting_account) = vesting_account.clone() {
total_value += vesting_account.locked.amount.u128();
total_value += vesting_account.spendable.amount.u128();
}
// 6. get operator rewards
let operator_rewards: Option<Coin> = if let Ok(operator_rewards_res) =
state.inner.get_operator_rewards(&address).await
{
if let Some(operator_reward_amount) = &operator_rewards_res.amount_earned {
total_value += operator_reward_amount.amount.u128();
}
operator_rewards_res.amount_earned
} else {
None
};
// 7. convert totals
let claimable_rewards = Coin::new(claimable_rewards, "unym");
let total_delegations = Coin::new(total_delegations, "unym");
let total_value = Coin::new(total_value, "unym");
Ok(Json(NyxAccountDetails {
address: address.to_string(),
balances,
delegations,
accumulated_rewards,
claimable_rewards,
total_delegations,
total_value,
vesting_account,
operator_rewards,
}))
}
Err(_e) => Err(NotFound("Account not found".to_string())),
}
}
@@ -2,11 +2,13 @@
// SPDX-License-Identifier: Apache-2.0
use crate::location::{LocationCache, LocationCacheItem};
use crate::nym_nodes::location::NymNodeLocationCache;
use crate::nym_nodes::CACHE_ENTRY_TTL;
use nym_explorer_api_requests::{Location, PrettyDetailedGatewayBond};
use crate::unstable::location::NymNodeLocationCache;
use crate::unstable::CACHE_ENTRY_TTL;
use nym_explorer_api_requests::{
Location, NymNodeWithDescriptionAndLocation, PrettyDetailedGatewayBond,
};
use nym_mixnet_contract_common::{Gateway, NodeId, NymNodeDetails};
use nym_validator_client::models::NymNodeDescription;
use nym_validator_client::models::{NymNodeData, NymNodeDescription};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
@@ -70,6 +72,54 @@ impl ThreadSafeNymNodesCache {
RwLockReadGuard::map(guard, |n| &n.bonded_nym_nodes)
}
pub(crate) async fn get_bonded_nymnodes_descriptions(&self) -> Vec<NymNodeData> {
let guard = self.nymnodes.read().await;
guard
.described_nodes
.values()
.map(|i| i.description.clone())
.collect()
}
pub(crate) async fn get_bonded_nymnodes_locations(&self) -> Vec<Location> {
let guard_locations = self.locations.read().await;
let mut locations: Vec<Location> = vec![];
for location in guard_locations.values() {
if let Some(l) = &location.location {
locations.push(l.clone());
}
}
locations
}
pub(crate) async fn get_bonded_nymnodes_with_description_and_location(
&self,
) -> HashMap<NodeId, NymNodeWithDescriptionAndLocation> {
let guard_nodes = self.nymnodes.read().await;
let guard_locations = self.locations.read().await;
let mut map: HashMap<NodeId, NymNodeWithDescriptionAndLocation> = HashMap::new();
for (node_id, node) in guard_nodes.bonded_nym_nodes.clone() {
let description = guard_nodes.described_nodes.get(&node_id);
let location = guard_locations.get(&node_id);
map.insert(
node_id,
NymNodeWithDescriptionAndLocation {
node_id,
description: description.map(|d| d.description.clone()),
location: location.and_then(|l| l.location.clone()),
contract_node_type: description.map(|d| d.contract_node_type),
bond_information: node.bond_information,
rewarding_details: node.rewarding_details,
},
);
}
map
}
pub(crate) async fn get_locations(&self) -> NymNodeLocationCache {
self.locations.read().await.clone()
}
+407
View File
@@ -0,0 +1,407 @@
'use client'
import * as React from 'react'
import {Alert, AlertTitle, Box, Button, Chip, CircularProgress, Grid, Tooltip, Typography} from '@mui/material'
import { useParams } from 'next/navigation'
import { useMainContext } from '@/app/context/main'
import { Title } from '@/app/components/Title'
import { MaterialReactTable, MRT_ColumnDef, useMaterialReactTable } from "material-react-table";
import { useMemo } from "react";
import { humanReadableCurrencyToString } from "@/app/utils/currency";
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
import { PieChart } from '@mui/x-charts/PieChart';
import { useTheme } from "@mui/material/styles";
import { useIsMobile } from "@/app/hooks";
import { StyledLink } from "@/app/components";
const AccumulatedRewards = ({account}: { account?: any}) => {
const columns = useMemo<
MRT_ColumnDef<any>[]
>(() => {
return [
{
id: 'accumulated-rewards-data',
header: 'Accumulated Rewards Data',
columns: [
{
id: 'node_id',
accessorKey: 'node_id',
header: 'Node ID',
size: 150,
Cell: ({ row }) => (<StyledLink
color="text.primary"
to={`/network-components/nodes/${row.original.node_id}`}>
{row.original.node_id}
</StyledLink>),
},
{
id: 'node_still_fully_bonded',
accessorKey: 'node_still_fully_bonded',
header: 'Node still bonded?',
width: 150,
Cell: ({ row }) => (
<>{row.original.node_still_fully_bonded ? <CheckCircleOutlineIcon/> :
<Typography fontSize="inherit" alignItems="center" display="flex" sx={{ color: theme => theme.palette.warning.main }}>
<WarningAmberIcon sx={{ mr: 1 }}/>
Unbonded
</Typography>}</>
)
},
{
id: 'amount_staked',
accessorKey: 'amount_staked',
header: 'Amount',
width: 150,
Cell: ({ row }) => (
<>{humanReadableCurrencyToString(row.original.amount_staked)}</>
)
},
{
id: 'rewards',
accessorKey: 'rewards',
header: 'Rewards',
width: 150,
Cell: ({ row }) => (
<Typography fontSize="inherit" color="success.main">{humanReadableCurrencyToString(row.original.rewards)}</Typography>
)
},
],
},
]
}, [])
const table = useMaterialReactTable({
columns,
data: account?.accumulated_rewards || [],
enableFullScreenToggle: false,
})
return (<MaterialReactTable table={table} />);
}
const DelegationHistory = ({account}: { account?: any}) => {
const columns = useMemo<
MRT_ColumnDef<any>[]
>(() => {
return [
{
id: 'delegation-history-data',
header: 'Delegation History',
columns: [
{
id: 'node_id',
accessorKey: 'node_id',
header: 'Node ID',
size: 150,
},
{
id: 'delegated',
accessorKey: 'delegated',
header: 'Amount',
width: 150,
Cell: ({ row }) => (
<>{humanReadableCurrencyToString(row.original.delegated)}</>
)
},
{
id: 'height',
accessorKey: 'height',
header: 'Delegated at height',
width: 150,
Cell: ({ row }) => (
<>{row.original.height}</>
)
},
],
},
]
}, [])
const table = useMaterialReactTable({
columns,
data: account?.delegations || [],
enableFullScreenToggle: false,
})
return (<MaterialReactTable table={table} />);
}
/**
* Shows account details
*/
const PageAccountWithState = ({ account }: {
account?: any;
}) => {
const theme = useTheme();
const isMobile = useIsMobile();
const pieChartData = React.useMemo(() => {
if(!account) {
return [];
}
const parts = [];
const nymBalance = Number.parseFloat(account.balances.find((b: any) => b.denom === "unym")?.amount || "0") / 1e6;
if(nymBalance > 0) {
parts.push({label: "Spendable", value: nymBalance, color: theme.palette.primary.main});
}
if(account.vesting_account) {
if (`${account.vesting_account.locked?.amount}` !== "0") {
const value = Number.parseFloat(account.vesting_account.locked.amount) / 1e6;
if(value > 0) {
parts.push({
label: "Vesting locked",
value,
color: 'red'
});
}
}
if (`${account.vesting_account.spendable?.amount}` !== "0") {
const value = Number.parseFloat(account.vesting_account.spendable.amount) / 1e6;
if(value > 0) {
parts.push({
label: "Vesting spendable",
value,
color: theme.palette.primary.light
});
}
}
}
if (account.claimable_rewards &&`${account.claimable_rewards.amount}` !== "0") {
const value = Number.parseFloat(account.claimable_rewards.amount) / 1e6;
if(value > 0) {
parts.push({
label: "Claimable delegation rewards",
value,
color: theme.palette.success.light
});
}
}
if (account.operator_rewards && `${account.operator_rewards.amount}` !== "0") {
const value = Number.parseFloat(account.operator_rewards.amount) / 1e6;
if(value > 0) {
parts.push({
label: "Claimable operator rewards",
value,
color: theme.palette.success.dark
});
}
}
if (account.total_delegations && `${account.total_delegations.amount}` !== "0") {
const value = Number.parseFloat(account.total_delegations.amount) / 1e6;
if(value > 0) {
parts.push({
label: "Total delegations",
value,
color: '#888'
});
}
}
return parts;
}, [account]);
return (
<Box component="main">
<Box overflow="scroll">
<Title text={`Account ${account.address}`} />
</Box>
<Box mt={4} sx={{ maxWidth: "600px" }}>
<PieChart
series={[
{
data: pieChartData,
innerRadius: 40,
outerRadius: 80,
cy: isMobile ? 200 : undefined,
},
]}
height={300}
slotProps={isMobile ? {
legend: { position: { vertical: "top", horizontal: "right" } }
} : undefined}
/>
</Box>
<Box mt={4}>
<TableContainer component={Paper} sx={{ maxWidth: "400px" }}>
<Table>
<TableBody>
<TableRow sx={{ color: theme => theme.palette.primary.main }}>
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
<strong>Spendable Balance</strong>
</TableCell>
<TableCell align="right" sx={{ color: "inherit" }}>
{account.balances.map((b: any) => (<strong key={`balance-${b.denom}`}>{humanReadableCurrencyToString(b)}<br/></strong>))}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
Total delegations
</TableCell>
<TableCell align="right">
{humanReadableCurrencyToString(account.total_delegations)}
</TableCell>
</TableRow>
{account.claimable_rewards && <TableRow sx={{ color: theme => theme.palette.success.light }}>
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
Claimable delegation rewards
</TableCell>
<TableCell align="right" sx={{ color: "inherit" }}>
{humanReadableCurrencyToString(account.claimable_rewards)}
</TableCell>
</TableRow>}
{account.operator_rewards && `${account.operator_rewards.amount}` !== "0" && <TableRow sx={{ color: theme => theme.palette.success.light }}>
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
Claimable operator rewards
</TableCell>
<TableCell align="right" sx={{ color: "inherit" }}>
{humanReadableCurrencyToString(account.operator_rewards)}
</TableCell>
</TableRow>}
{account.vesting_account && (
<>
<TableRow>
<TableCell component="th" scope="row" colSpan={2}>
Vesting account
</TableCell>
</TableRow>
{`${account.vesting_account.locked.amount}` !== "0" &&
<TableRow>
<TableCell component="th" scope="row" sx={{ pl: 4 }}>
Locked
</TableCell>
<TableCell align="right" sx={{ color: "inherit" }}>
{humanReadableCurrencyToString(account.vesting_account.locked)}
</TableCell>
</TableRow>
}
{`${account.vesting_account.vested.amount}` !== "0" &&
<TableRow>
<TableCell component="th" scope="row" sx={{ pl: 4 }}>
Vested
</TableCell>
<TableCell align="right" sx={{ color: "inherit" }}>
{humanReadableCurrencyToString(account.vesting_account.vested)}
</TableCell>
</TableRow>
}
{`${account.vesting_account.vesting.amount}` !== "0" &&
<TableRow>
<TableCell component="th" scope="row" sx={{ pl: 4 }}>
Vesting
</TableCell>
<TableCell align="right" sx={{ color: "inherit" }}>
{humanReadableCurrencyToString(account.vesting_account.vesting)}
</TableCell>
</TableRow>
}
{`${account.vesting_account.spendable.amount}` !== "0" &&
<TableRow>
<TableCell component="th" scope="row" sx={{ pl: 4 }}>
Spendable
</TableCell>
<TableCell align="right" sx={{ color: "inherit" }}>
{humanReadableCurrencyToString(account.vesting_account.spendable)}
</TableCell>
</TableRow>
}
</>
)}
<TableRow>
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
<h3>Total value</h3>
</TableCell>
<TableCell align="right" sx={{ color: "inherit" }}>
<h3>{humanReadableCurrencyToString(account.total_value)}</h3>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Box>
<Box mt={4}>
<AccumulatedRewards account={account}/>
</Box>
<Box mt={4}>
<DelegationHistory account={account}/>
</Box>
</Box>
)
}
/**
* Guard component to handle loading and not found states
*/
const PageAccountDetailGuard = ({ account } : { account: string }) => {
const [accountDetails, setAccountDetails] = React.useState<any>();
const [isLoading, setLoading] = React.useState<boolean>(true);
const [error, setError] = React.useState<string>();
const { fetchAccountById } = useMainContext()
const { id } = useParams()
React.useEffect(() => {
setLoading(true);
(async () => {
if(typeof(id) === "string") {
try {
const res = await fetchAccountById(account);
setAccountDetails(res);
} catch(e: any) {
setError(e.message);
}
finally {
setLoading(false);
}
}
})();
}, [id])
if (isLoading) {
return <CircularProgress />
}
// loaded, but not found
if (error) {
return (
<Alert severity="warning">
<AlertTitle>Account not found</AlertTitle>
Sorry, we could not find the account <code>{id || ''}</code>
</Alert>
)
}
return <PageAccountWithState account={accountDetails} />
}
/**
* Wrapper component that adds the account details based on the `id` in the address URL
*/
const PageAccountDetail = () => {
const { id } = useParams()
if (!id || typeof id !== 'string') {
return (
<Alert severity="error">Oh no! Could not find that account</Alert>
)
}
return (
<PageAccountDetailGuard account={id} />
)
}
export default PageAccountDetail
+9 -4
View File
@@ -5,28 +5,33 @@ export const NYM_API_BASE_URL = process.env.NEXT_PUBLIC_NYM_API_URL || 'https://
export const NYX_RPC_BASE_URL = process.env.NEXT_PUBLIC_NYX_RPC_BASE_URL || 'https://rpc.nymtech.net';
export const VALIDATOR_BASE_URL = process.env.NEXT_PUBLIC_VALIDATOR_URL || 'https://rpc.nymtech.net';
export const BIG_DIPPER = process.env.NEXT_PUBLIC_BIG_DIPPER_URL || 'https://nym.explorers.guru';
export const BLOCK_EXPLORER_BASE_URL = process.env.NEXT_PUBLIC_BIG_DIPPER_URL || 'https://nym.explorers.guru';
// specific API routes
export const OVERVIEW_API = `${API_BASE_URL}/overview`;
export const MIXNODE_PING = `${API_BASE_URL}/ping`;
export const MIXNODES_API = `${API_BASE_URL}/mix-nodes`;
export const MIXNODE_API = `${API_BASE_URL}/mix-node`;
export const GATEWAYS_EXPLORER_API = `${API_BASE_URL}/gateways`;
export const GATEWAYS_API = `${NYM_API_BASE_URL}/api/v1/status/gateways/detailed`;
export const VALIDATORS_API = `${NYX_RPC_BASE_URL}/validators`;
export const BLOCK_API = `${NYX_RPC_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 SERVICE_PROVIDERS = `${API_BASE_URL}/service-providers`;
export const TEMP_UNSTABLE_NYM_NODES = `${API_BASE_URL}/tmp/unstable/nym-nodes`;
export const TEMP_UNSTABLE_ACCOUNT = `${API_BASE_URL}/tmp/unstable/account`;
export const NYM_API_NODE_UPTIME = `${NYM_API_BASE_URL}/api/v1/nym-nodes/uptime-history`;
export const NYM_API_NODE_PERFORMANCE = `${NYM_API_BASE_URL}/api/v1/nym-nodes/performance-history`;
export const LEGACY_MIXNODES_API = `${API_BASE_URL}/tmp/unstable/legacy-mixnode-bonds`;
export const LEGACY_GATEWAYS_API = `${API_BASE_URL}/tmp/unstable/legacy-gateway-bonds`;
// errors
export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us.";
export const NYM_WEBSITE = 'https://nymtech.net';
export const NYM_BIG_DIPPER = 'https://mixnet.explorers.guru';
export const EXPLORER_FOR_ACCOUNTS = ''; // set to empty to use this Nym Explorer and NOT an external one
export const NYM_MIXNET_CONTRACT =
process.env.NYM_MIXNET_CONTRACT || 'n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr';
+59 -15
View File
@@ -3,7 +3,6 @@ import {
API_BASE_URL,
BLOCK_API,
COUNTRY_DATA_API,
GATEWAYS_API,
UPTIME_STORY_API_GATEWAY,
MIXNODE_API,
MIXNODE_PING,
@@ -12,7 +11,11 @@ import {
UPTIME_STORY_API,
VALIDATORS_API,
SERVICE_PROVIDERS,
GATEWAYS_EXPLORER_API,
TEMP_UNSTABLE_NYM_NODES,
NYM_API_NODE_UPTIME,
NYM_API_NODE_PERFORMANCE,
TEMP_UNSTABLE_ACCOUNT,
LEGACY_MIXNODES_API, LEGACY_GATEWAYS_API,
} from './constants';
import {
@@ -59,7 +62,14 @@ export class Api {
return cache;
}
const res = await fetch(`${OVERVIEW_API}/summary`);
const json = await res.json();
const json: SummaryOverviewResponse = await res.json();
if (json.nymnodes?.roles) {
json.mixnodes.count += json.nymnodes.roles.mixnode;
json.gateways.count += json.nymnodes.roles.entry;
json.gateways.count += Math.max(json.nymnodes.roles.exit_ipr, json.nymnodes.roles.exit_nr);
}
storeInCache('overview-summary', JSON.stringify(json));
return json;
};
@@ -70,7 +80,7 @@ export class Api {
return cachedMixnodes;
}
const res = await fetch(MIXNODES_API);
const res = await fetch(LEGACY_MIXNODES_API);
const json = await res.json();
storeInCache('mixnodes', JSON.stringify(json));
return json;
@@ -98,17 +108,21 @@ export class Api {
return response.json();
};
static fetchGateways = async (): Promise<GatewayBond[]> => {
const res = await fetch(GATEWAYS_API);
const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json();
const res2 = await fetch(GATEWAYS_EXPLORER_API);
const locatedGateways: LocatedGateway[] = await res2.json();
const locatedGatewaysByOwner = keyBy(locatedGateways, 'owner');
return gatewaysAnnotated.map(({ gateway_bond, node_performance }) => ({
...gateway_bond,
node_performance,
location: locatedGatewaysByOwner[gateway_bond.owner]?.location,
}));
static fetchGateways = async (): Promise<LocatedGateway[]> => {
// const res = await fetch(GATEWAYS_API);
// const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json();
// const res2 = await fetch(GATEWAYS_EXPLORER_API);
// const locatedGateways: LocatedGateway[] = await res2.json();
// const locatedGatewaysByOwner = keyBy(locatedGateways, 'owner');
// return gatewaysAnnotated.map(({ gateway_bond, node_performance }) => ({
// ...gateway_bond,
// node_performance,
// location: locatedGatewaysByOwner[gateway_bond.owner]?.location,
// }));
const res = await fetch(LEGACY_GATEWAYS_API);
const locatedGateways: LocatedGateway[] = await res.json();
return locatedGateways;
};
static fetchGatewayUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
@@ -165,6 +179,36 @@ export class Api {
const json = await res.json();
return json;
};
static fetchNodes = async () => {
const res = await fetch(TEMP_UNSTABLE_NYM_NODES);
const json = await res.json();
return json;
}
static fetchNodeById = async (id: number) => {
const res = await fetch(`${TEMP_UNSTABLE_NYM_NODES}/${id}`);
const json = await res.json();
return json;
}
static fetchNymNodeUptimeHistoryById = async (id: number | string) => {
const res = await fetch(`${NYM_API_NODE_UPTIME}/${id}`)
const json = await res.json();
return json;
}
static fetchNymNodePerformanceById = async (id: number | string) => {
const res = await fetch(`${NYM_API_NODE_PERFORMANCE}/${id}`)
const json = await res.json();
return json;
}
static fetchAccountById = async (id: string) => {
const res = await fetch(`${TEMP_UNSTABLE_ACCOUNT}/${id}`);
const json = await res.json();
return json;
}
}
export const getEnvironment = (): Environment => {
@@ -18,6 +18,7 @@ import { unymToNym } from '@/app/utils/currency'
import { GatewayEnrichedRowType } from './Gateways/Gateways'
import { MixnodeRowType } from './MixNodes'
import { StakeSaturationProgressBar } from './MixNodes/Economics/StakeSaturationProgressBar'
import {EXPLORER_FOR_ACCOUNTS} from "@/app/api/constants";
export type ColumnsType = {
field: string
@@ -57,7 +58,7 @@ function formatCellValues(val: string | number, field: string) {
underline="none"
color="inherit"
target="_blank"
href={`https://mixnet.explorers.guru/account/${val}`}
href={`${EXPLORER_FOR_ACCOUNTS}/account/${val}`}
>
{val}
</Link>
@@ -74,7 +75,7 @@ function formatCellValues(val: string | number, field: string) {
export const DetailTable: FCWithChildren<{
tableName: string
columnsData: ColumnsType[]
rows: MixnodeRowType[] | GatewayEnrichedRowType[]
rows: MixnodeRowType[] | GatewayEnrichedRowType[] | any[]
}> = ({ tableName, columnsData, rows }: UniversalTableProps) => {
const theme = useTheme()
return (
@@ -1,4 +1,4 @@
import { GatewayResponse, GatewayBond, GatewayReportResponse } from '@/app/typeDefs/explorer-api';
import {GatewayResponse, GatewayBond, GatewayReportResponse, LocatedGateway} from '@/app/typeDefs/explorer-api';
import { toPercentInteger } from '@/app/utils';
export type GatewayRowType = {
@@ -9,7 +9,7 @@ export type GatewayRowType = {
host: string;
location: string;
version: string;
node_performance: number;
// node_performance: number;
};
export type GatewayEnrichedRowType = GatewayRowType & {
@@ -30,11 +30,11 @@ export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowTy
bond: gw.pledge_amount.amount || 0,
host: gw.gateway.host || '',
version: gw.gateway.version || '',
node_performance: toPercentInteger(gw.node_performance.last_24h),
// node_performance: toPercentInteger(gw.node_performance.last_24h),
}));
}
export function gatewayEnrichedToGridRow(gateway: GatewayBond, report: GatewayReportResponse): GatewayEnrichedRowType {
export function gatewayEnrichedToGridRow(gateway: LocatedGateway, report: GatewayReportResponse): GatewayEnrichedRowType {
return {
id: gateway.owner,
owner: gateway.owner,
@@ -47,6 +47,6 @@ export function gatewayEnrichedToGridRow(gateway: GatewayBond, report: GatewayRe
mixPort: gateway.gateway.mix_port || 0,
routingScore: `${report.most_recent}%`,
avgUptime: `${report.last_day || report.last_hour}%`,
node_performance: toPercentInteger(gateway.node_performance.most_recent),
// node_performance: toPercentInteger(gateway.node_performance.most_recent),
};
}
@@ -24,6 +24,7 @@ import { DarkLightSwitchDesktop } from '@/app/components/Switch'
import { Footer } from '@/app/components/Footer'
import { ConnectKeplrWallet } from '@/app/components/Wallet/ConnectKeplrWallet'
import { usePathname, useRouter } from 'next/navigation'
import {SearchToolbar} from "@/app/components/Nav/Search";
const drawerWidth = 255
const bannerHeight = 80
@@ -292,6 +293,9 @@ export const Nav: FCWithChildren = ({ children }) => {
display: 'flex',
}}
>
<Box>
<SearchToolbar/>
</Box>
<Box
sx={{
display: 'flex',
@@ -22,6 +22,7 @@ import { ExpandableButton } from './DesktopNav'
import { ConnectKeplrWallet } from '../Wallet/ConnectKeplrWallet'
import { NetworkTitle } from '../NetworkTitle'
import { originalNavOptions } from '@/app/context/nav'
import {SearchToolbar} from "@/app/components/Nav/Search";
export const MobileNav: FCWithChildren = ({ children }) => {
const theme = useTheme()
@@ -70,7 +71,15 @@ export const MobileNav: FCWithChildren = ({ children }) => {
</IconButton>
{!isSmallMobile && <NetworkTitle />}
</Box>
<ConnectKeplrWallet />
<Box sx={{
alignItems: 'center',
display: 'flex',
}}>
<Box mr={0.5}>
<SearchToolbar/>
</Box>
<ConnectKeplrWallet />
</Box>
</Toolbar>
</AppBar>
<Drawer
@@ -0,0 +1,76 @@
import React from "react";
import { styled, alpha } from '@mui/material/styles';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import InputBase from '@mui/material/InputBase';
import MenuIcon from '@mui/icons-material/Menu';
import SearchIcon from '@mui/icons-material/Search';
import {useRouter} from "next/navigation";
const Search = styled('div')(({ theme }) => ({
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: alpha(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: alpha(theme.palette.common.white, 0.25),
},
marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing(1),
width: 'auto',
},
}));
const SearchIconWrapper = styled('div')(({ theme }) => ({
padding: theme.spacing(0, 2),
height: '100%',
position: 'absolute',
pointerEvents: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}));
const StyledInputBase = styled(InputBase)(({ theme }) => ({
color: 'inherit',
width: '100%',
'& .MuiInputBase-input': {
padding: theme.spacing(1, 1, 1, 0),
// vertical padding + font size from searchIcon
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
[theme.breakpoints.up('sm')]: {
width: '30ch',
},
},
}));
export const SearchToolbar = () => {
const [search, setSearch] = React.useState<string>();
const router = useRouter();
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
e.preventDefault();
if(search?.trim().length) {
router.push(`/account/${search.trim()}`);
}
}
return (
<Search>
<SearchIconWrapper>
<SearchIcon />
</SearchIconWrapper>
<form onSubmit={handleSubmit}>
<StyledInputBase
placeholder="Search for account id…"
inputProps={{ 'aria-label': 'search' }}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setSearch(event.target.value);
}}
/>
</form>
</Search>
);
}
@@ -4,7 +4,7 @@ import Link from 'next/link'
type StyledLinkProps = {
to: string
children: string
children: React.ReactNode
target?: React.HTMLAttributeAnchorTarget
dataTestId?: string
color?: string
+48 -7
View File
@@ -26,6 +26,7 @@ interface StateData {
gateways?: ApiState<GatewayResponse>
globalError?: string | undefined
mixnodes?: ApiState<MixNodeResponse>
nodes?: ApiState<any>
mode: PaletteMode
validators?: ApiState<ValidatorsResponse>
environment?: Environment
@@ -37,6 +38,9 @@ interface StateApi {
status?: MixnodeStatus
) => Promise<MixNodeResponse | undefined>
filterMixnodes: (filters: any, status: any) => void
fetchNodes: () => Promise<any>
fetchNodeById: (id: number) => Promise<any>
fetchAccountById: (accountAddr: string) => Promise<any>
toggleMode: () => void
}
@@ -47,6 +51,9 @@ export const MainContext = React.createContext<State>({
toggleMode: () => undefined,
filterMixnodes: () => null,
fetchMixnodes: () => Promise.resolve(undefined),
fetchNodes: async () => undefined,
fetchNodeById: async () => undefined,
fetchAccountById: async () => undefined,
})
export const useMainContext = (): React.ContextType<typeof MainContext> =>
@@ -65,6 +72,7 @@ export const MainContextProvider: FCWithChildren = ({ children }) => {
// various APIs for Overview page
const [summaryOverview, setSummaryOverview] =
React.useState<ApiState<SummaryOverviewResponse>>()
const [nodes, setNodes] = React.useState<ApiState<any>>()
const [mixnodes, setMixnodes] = React.useState<ApiState<MixNodeResponse>>()
const [gateways, setGateways] = React.useState<ApiState<GatewayResponse>>()
const [validators, setValidators] =
@@ -92,13 +100,11 @@ export const MainContextProvider: FCWithChildren = ({ children }) => {
}
}
const fetchMixnodes = async (status?: MixnodeStatus) => {
const fetchMixnodes = async () => {
let data
setMixnodes((d) => ({ ...d, isLoading: true }))
try {
data = status
? await Api.fetchMixnodesActiveSetByStatus(status)
: await Api.fetchMixnodes()
data = await Api.fetchMixnodes()
setMixnodes({ data, isLoading: false })
} catch (error) {
setMixnodes({
@@ -114,9 +120,7 @@ export const MainContextProvider: FCWithChildren = ({ children }) => {
status?: MixnodeStatus
) => {
setMixnodes((d) => ({ ...d, isLoading: true }))
const mxns = status
? await Api.fetchMixnodesActiveSetByStatus(status)
: await Api.fetchMixnodes()
const mxns = await Api.fetchMixnodes()
const filtered = mxns?.filter(
(m) =>
@@ -205,6 +209,38 @@ export const MainContextProvider: FCWithChildren = ({ children }) => {
}
}
const fetchNodes = async () => {
setNodes({ data: undefined, isLoading: true })
try {
const res = await Api.fetchNodes();
res.forEach((node: any) => node.total_stake =
Math.round(Number.parseFloat(node.rewarding_details?.operator || "0")
+ Number.parseFloat(node.rewarding_details?.delegates || "0"))
);
setNodes({
data: res.sort((a: any, b: any) => b.total_stake - a.total_stake),
isLoading: false,
})
} catch (error) {
setNodes({
error:
error instanceof Error
? error
: new Error('Service provider api fail'),
isLoading: false,
})
} };
const fetchNodeById = async (id: number) => {
const res = await Api.fetchNodeById(id);
return res;
};
const fetchAccountById = async (id: string) => {
const res = await Api.fetchAccountById(id);
return res;
};
React.useEffect(() => {
if (environment === 'mainnet') {
fetchServiceProviders()
@@ -231,12 +267,16 @@ export const MainContextProvider: FCWithChildren = ({ children }) => {
globalError,
mixnodes,
mode,
nodes,
summaryOverview,
validators,
serviceProviders,
toggleMode,
fetchMixnodes,
filterMixnodes,
fetchNodes,
fetchNodeById,
fetchAccountById,
}),
[
environment,
@@ -246,6 +286,7 @@ export const MainContextProvider: FCWithChildren = ({ children }) => {
globalError,
mixnodes,
mode,
nodes,
summaryOverview,
validators,
serviceProviders,
+8 -8
View File
@@ -2,7 +2,7 @@
import * as React from 'react'
import { DelegateIcon } from '@/app/icons/DelevateSVG'
import { BIG_DIPPER } from '@/app/api/constants'
import { BLOCK_EXPLORER_BASE_URL } from '@/app/api/constants'
import { OverviewSVG } from '@/app/icons/OverviewSVG'
import { NodemapSVG } from '@/app/icons/NodemapSVG'
import { NetworkComponentsSVG } from '@/app/icons/NetworksSVG'
@@ -27,23 +27,23 @@ export const originalNavOptions: NavOptionType[] = [
title: 'Network Components',
Icon: <NetworkComponentsSVG />,
nested: [
{
url: '/network-components/nodes',
title: 'Nodes',
},
{
url: '/network-components/mixnodes',
title: 'Mixnodes',
title: 'Mixnodes (legacy)',
},
{
url: '/network-components/gateways',
title: 'Gateways',
title: 'Gateways (legacy)',
},
{
url: `${BIG_DIPPER}/validators`,
url: `${BLOCK_EXPLORER_BASE_URL}/validators`,
title: 'Validators',
isExternal: true,
},
{
url: '/network-components/service-providers',
title: 'Service Providers',
},
],
},
{
+77
View File
@@ -0,0 +1,77 @@
'use client'
import * as React from 'react'
import {
ApiState,
NymNodeReportResponse,
UptimeStoryResponse,
} from '@/app/typeDefs/explorer-api'
import { Api } from '@/app/api'
import { useApiState } from './hooks'
/**
* This context provides the state for a single gateway by identity key.
*/
interface NymNodeState {
uptimeReport?: ApiState<NymNodeReportResponse>
uptimeHistory?: ApiState<UptimeStoryResponse>
}
export const NymNodeContext = React.createContext<NymNodeState>({})
export const useNymNodeContext = (): React.ContextType<typeof NymNodeContext> =>
React.useContext<NymNodeState>(NymNodeContext)
/**
* Provides a state context for a gateway by identity
* @param gatewayIdentityKey The identity key of the gateway
*/
export const NymNodeContextProvider = ({
nymNodeId,
children,
}: {
nymNodeId: string
children: JSX.Element
}) => {
const [uptimeReport, fetchUptimeReportById, clearUptimeReportById] =
useApiState<any>(
nymNodeId,
Api.fetchNymNodePerformanceById,
'Failed to fetch gateway uptime report by id'
)
const [uptimeHistory, fetchUptimeHistory, clearUptimeHistory] =
useApiState<UptimeStoryResponse>(
nymNodeId,
async (arg) => {
const res = await Api.fetchNymNodeUptimeHistoryById(arg);
const uptimeHistory: UptimeStoryResponse = {
history: res.history.data,
identity: '',
owner: '',
}
return uptimeHistory;
},
'Failed to fetch gateway uptime history'
)
React.useEffect(() => {
// when the identity key changes, remove all previous data
clearUptimeReportById()
clearUptimeHistory()
Promise.all([fetchUptimeReportById(), fetchUptimeHistory()])
}, [nymNodeId])
const state = React.useMemo<NymNodeState>(
() => ({
uptimeReport,
uptimeHistory,
}),
[uptimeReport, uptimeHistory]
)
return (
<NymNodeContext.Provider value={state}>{children}</NymNodeContext.Provider>
)
}
@@ -3,7 +3,7 @@
import * as React from 'react'
import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material'
import { useParams } from 'next/navigation'
import { GatewayBond } from '@/app/typeDefs/explorer-api'
import {GatewayBond, LocatedGateway} from '@/app/typeDefs/explorer-api'
import { ColumnsType, DetailTable } from '@/app/components/DetailTable'
import {
gatewayEnrichedToGridRow,
@@ -32,13 +32,6 @@ const columns: ColumnsType[] = [
title: 'Bond',
headerAlign: 'left',
},
{
field: 'node_performance',
title: 'Routing Score',
headerAlign: 'left',
tooltipInfo:
"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',
@@ -74,7 +67,7 @@ const columns: ColumnsType[] = [
const PageGatewayDetailsWithState = ({
selectedGateway,
}: {
selectedGateway: GatewayBond | undefined
selectedGateway: LocatedGateway | undefined
}) => {
const [enrichGateway, setEnrichGateway] =
React.useState<GatewayEnrichedRowType>()
@@ -97,7 +90,13 @@ const PageGatewayDetailsWithState = ({
return (
<Box component="main">
<Title text="Gateway Detail" />
<Title text="Legacy Gateway Detail" />
<Alert variant="filled" severity="warning" sx={{ my : 2, pt: 2 }}>
<AlertTitle>
Please update to the latest <code>nym-node</code> binary and migrate your bond and delegations from the wallet
</AlertTitle>
</Alert>
<Grid container>
<Grid item xs={12}>
@@ -146,7 +145,7 @@ const PageGatewayDetailsWithState = ({
* Guard component to handle loadingW and not found states
*/
const PageGatewayDetailGuard = () => {
const [selectedGateway, setSelectedGateway] = React.useState<GatewayBond>()
const [selectedGateway, setSelectedGateway] = React.useState<LocatedGateway>()
const { gateways } = useMainContext()
const { id } = useParams()
@@ -18,7 +18,7 @@ import { CustomColumnHeading } from '@/app/components/CustomColumnHeading'
import { Title } from '@/app/components/Title'
import { unymToNym } from '@/app/utils/currency'
import { Tooltip } from '@/app/components/Tooltip'
import { NYM_BIG_DIPPER } from '@/app/api/constants'
import { EXPLORER_FOR_ACCOUNTS } from '@/app/api/constants'
import { splice } from '@/app/utils'
import {
VersionDisplaySelector,
@@ -29,6 +29,23 @@ import {
GatewayRowType,
gatewayToGridRow,
} from '@/app/components/Gateways/Gateways'
import {LocatedGateway} from "@/app/typeDefs/explorer-api";
const gatewaySanitize = (g?: LocatedGateway): boolean => {
if(!g) {
return false;
}
if(!g.gateway.version || !g.gateway.version.trim().length) {
return false;
}
if(g.gateway.version === "null") {
return false;
}
return true;
}
const PageGateways = () => {
const { gateways } = useMainContext()
@@ -39,7 +56,7 @@ const PageGateways = () => {
const highestVersion = React.useMemo(() => {
if (gateways?.data) {
const versions = gateways.data.reduce(
const versions = gateways.data.filter(gatewaySanitize).reduce(
(a: string[], b) => [...a, b.gateway.version],
[]
)
@@ -51,7 +68,7 @@ const PageGateways = () => {
}, [gateways])
const filterByLatestVersions = React.useMemo(() => {
const filtered = gateways?.data?.filter((gw) => {
const filtered = gateways?.data?.filter(gatewaySanitize).filter((gw) => {
const versionDiff = diff(highestVersion, gw.gateway.version)
return versionDiff === 'patch' || versionDiff === null
})
@@ -60,7 +77,7 @@ const PageGateways = () => {
}, [gateways])
const filterByOlderVersions = React.useMemo(() => {
const filtered = gateways?.data?.filter((gw) => {
const filtered = gateways?.data?.filter(gatewaySanitize).filter((gw) => {
const versionDiff = diff(highestVersion, gw.gateway.version)
return versionDiff === 'major' || versionDiff === 'minor'
})
@@ -89,7 +106,7 @@ const PageGateways = () => {
return [
{
id: 'gateway-data',
header: 'Gatewsay Data',
header: 'Gateways Data',
columns: [
{
id: 'identity_key',
@@ -116,41 +133,6 @@ const PageGateways = () => {
)
},
},
{
id: 'node_performance',
header: 'Node Performance',
accessorKey: 'node_performance',
size: 200,
Header: () => {
return (
<Box display="flex">
<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" />
</Box>
)
},
Cell: ({ row }) => {
return (
<StyledLink
to={`/network-components/gateways/${row.original.identity_key}`}
data-testid="node-performance"
color="text.primary"
>
{`${row.original.node_performance}%`}
</StyledLink>
)
},
},
{
id: 'version',
header: 'Version',
@@ -222,7 +204,7 @@ const PageGateways = () => {
Cell: ({ row }) => {
return (
<StyledLink
to={`${NYM_BIG_DIPPER}/account/${row.original.owner}`}
to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.owner}`}
target="_blank"
data-testid="owner"
color="text.primary"
@@ -237,137 +219,6 @@ const PageGateways = () => {
]
}, [])
const _columns: GridColDef[] = [
{
field: 'node_performance',
align: 'center',
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: 120,
disableColumnMenu: true,
headerAlign: 'center',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<StyledLink
to={`/network-components/gateways/${params.row.identity_key}`}
data-testid="pledge-amount"
>
{`${params.value}%`}
</StyledLink>
),
},
{
field: 'version',
align: 'center',
renderHeader: () => <CustomColumnHeading headingTitle="Version" />,
width: 150,
disableColumnMenu: true,
headerAlign: 'center',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<StyledLink
to={`/network-components/gateways/${params.row.identity_key}`}
data-testid="version"
>
{params.value}
</StyledLink>
),
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) => (
<Box
sx={{ justifyContent: 'flex-start', cursor: 'pointer' }}
data-testid="location-button"
>
<Tooltip text={params.value} id="gateway-location-text">
<Box
sx={{
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{params.value}
</Box>
</Tooltip>
</Box>
),
},
{
field: 'host',
renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />,
width: 180,
disableColumnMenu: true,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<StyledLink
to={`/network-components/gateways/${params.row.identity_key}`}
data-testid="host"
>
{params.value}
</StyledLink>
),
},
{
field: 'owner',
headerName: 'Owner',
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
width: 180,
disableColumnMenu: true,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<StyledLink
to={`${NYM_BIG_DIPPER}/account/${params.value}`}
target="_blank"
data-testid="owner"
>
{splice(7, 29, params.value)}
</StyledLink>
),
},
{
field: 'bond',
width: 150,
disableColumnMenu: true,
type: 'number',
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
headerClassName: 'MuiDataGrid-header-override',
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<StyledLink
to={`/network-components/gateways/${params.row.identity_key}`}
data-testid="pledge-amount"
>
{`${unymToNym(params.value, 6)}`}
</StyledLink>
),
},
]
const table = useMaterialReactTable({
columns,
data,
@@ -376,7 +227,7 @@ const PageGateways = () => {
return (
<>
<Box mb={2}>
<Title text="Gateways" />
<Title text="Legacy Gateways" />
</Box>
<Grid container>
<Grid item xs={12}>
@@ -94,7 +94,12 @@ const PageMixnodeDetailWithState = () => {
const isMobile = useIsMobile()
return (
<Box component="main">
<Title text="Mixnode Detail" />
<Title text="Legacy Mixnode Detail" />
<Alert variant="filled" severity="warning" sx={{ my : 2, pt: 2 }}>
<AlertTitle>
Please update to the latest <code>nym-node</code> binary and migrate your bond and delegations from the wallet
</AlertTitle>
</Alert>
<Grid container spacing={2} mt={1} mb={6}>
<Grid item xs={12}>
{mixNodeRow && description?.data && (
@@ -29,7 +29,7 @@ import { useMainContext } from '@/app/context/main'
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'
import { splice } from '@/app/utils'
import { currencyToString } from '@/app/utils/currency'
import { NYM_BIG_DIPPER } from '@/app/api/constants'
import { EXPLORER_FOR_ACCOUNTS } from '@/app/api/constants'
import {
MixnodeStatusWithAll,
toMixnodeStatus,
@@ -229,24 +229,6 @@ export default function MixnodesPage() {
>{`${row.original.operating_cost} NYM`}</StyledLink>
),
},
{
id: 'node_performance',
accessorKey: 'node_performance',
size: 200,
header: 'Routing Score',
Header: () => (
<CustomColumnHeading
headingTitle="Routing Score"
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."
/>
),
Cell: ({ row }) => (
<StyledLink
to={`/network-components/mixnodes/${row.original.mix_id}`}
color={useGetMixNodeStatusColor(row.original.status)}
>{`${row.original.node_performance}%`}</StyledLink>
),
},
{
id: 'owner',
accessorKey: 'owner',
@@ -255,7 +237,7 @@ export default function MixnodesPage() {
Header: () => <CustomColumnHeading headingTitle="Owner" />,
Cell: ({ row }) => (
<StyledLink
to={`${NYM_BIG_DIPPER}/account/${row.original.owner}`}
to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.owner}`}
color={useGetMixNodeStatusColor(row.original.status)}
target="_blank"
data-testid="big-dipper-link"
@@ -326,7 +308,7 @@ export default function MixnodesPage() {
return (
<DelegationsProvider>
<Box mb={2}>
<Title text="Mixnodes" />
<Title text="Legacy Mixnodes" />
</Box>
<Grid container>
<Grid item xs={12}>
@@ -0,0 +1,11 @@
import React from "react";
import { Chip } from "@mui/material";
export const DeclaredRole = ({ declared_role }: { declared_role?: any }) => (
<>
{declared_role?.mixnode && <Chip size="small" label="Mixnode" sx={{ mr: 0.5 }} color="info" />}
{declared_role?.entry && <Chip size="small" label="Entry" sx={{ mr: 0.5 }} color="success" />}
{declared_role?.exit_nr && <Chip size="small" label="Exit NR" sx={{ mr: 0.5 }} color="warning" />}
{declared_role?.exit_ipr && <Chip size="small" label="Exit IPR" sx={{ mr: 0.5 }} color="warning" />}
</>
)
@@ -0,0 +1,93 @@
import React, {useMemo} from "react";
import {MaterialReactTable, MRT_ColumnDef, useMaterialReactTable} from "material-react-table";
import StyledLink from "../../../components/StyledLink";
import {EXPLORER_FOR_ACCOUNTS} from "@/app/api/constants";
import {splice} from "@/app/utils";
import {humanReadableCurrencyToString} from "@/app/utils/currency";
import {Typography} from "@mui/material";
import {useTheme} from "@mui/material/styles";
import WarningIcon from '@mui/icons-material/Warning';
import { Tooltip } from '@/app/components/Tooltip'
export const NodeDelegationsTable = ({ node }: { node: any}) => {
const columns = useMemo<MRT_ColumnDef<any>[]>(() => {
return [
{
id: 'nym-node-delegation-data',
header: 'Nym Node Delegations',
columns: [
{
id: 'owner',
header: 'Delegator',
accessorKey: 'owner',
size: 150,
Cell: ({ row }) => {
return (
<StyledLink
to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.owner || "-"}`}
target="_blank"
data-testid="bond_information.node.owner"
color="text.primary"
>
{splice(7, 29, row.original.owner)}
</StyledLink>
)
},
},
{
id: 'amount',
header: 'Amount',
accessorKey: 'amount',
size: 150,
Cell: ({ row }) => (
<>{humanReadableCurrencyToString(row.original.amount)}</>
)
},
{
id: 'height',
header: 'Delegated at height',
accessorKey: 'height',
size: 150,
},
{
id: 'proxy',
header: 'From vesting account?',
accessorKey: 'proxy',
size: 250,
Cell: ({ row }) => {
if(row.original.proxy?.length) {
return (
<VestingDelegationWarning>Please re-delegate from your main account</VestingDelegationWarning>
)
}
}
},
]
}
];
}, []);
const table = useMaterialReactTable({
columns,
data: node ? node.delegations : [],
});
return (
<MaterialReactTable table={table} />
);
}
export const VestingDelegationWarning = ({children, plural}: { plural?: boolean, children: React.ReactNode}) => {
const theme = useTheme();
return (
<Tooltip
text={`${plural ? 'These delegations have' : 'This delegation has'} been made with a vesting account. All tokens are liquid, if you are the delegator, please move the tokens into your main account and make the delegation from there.`}
id="delegations"
>
<Typography fontSize="inherit" color={theme.palette.warning.main} display="flex" alignItems="center">
<WarningIcon sx={{ mr: 0.5 }}/>
{children}
</Typography>
</Tooltip>
);
}
@@ -0,0 +1,278 @@
'use client'
import * as React from 'react'
import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material'
import { useParams } from 'next/navigation'
import { ColumnsType, DetailTable } from '@/app/components/DetailTable'
import { ComponentError } from '@/app/components/ComponentError'
import { ContentCard } from '@/app/components/ContentCard'
import { UptimeChart } from '@/app/components/UptimeChart'
import {
NymNodeContextProvider,
useNymNodeContext,
} from '@/app/context/node'
import { useMainContext } from '@/app/context/main'
import { Title } from '@/app/components/Title'
import Paper from "@mui/material/Paper";
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableRow from '@mui/material/TableRow';
import {humanReadableCurrencyToString} from "@/app/utils/currency";
import {DeclaredRole} from "@/app/network-components/nodes/DeclaredRole";
import {NodeDelegationsTable, VestingDelegationWarning} from "@/app/network-components/nodes/[id]/NodeDelegationsTable";
const columns: ColumnsType[] = [
{
field: 'identity_key',
title: 'Identity Key',
headerAlign: 'left',
width: 230,
},
{
field: 'bond',
title: 'Bond',
headerAlign: 'left',
},
{
field: 'host',
title: 'IP',
headerAlign: 'left',
width: 99,
},
{
field: 'location',
title: 'Location',
headerAlign: 'left',
},
{
field: 'owner',
title: 'Owner',
headerAlign: 'left',
},
{
field: 'version',
title: 'Version',
headerAlign: 'left',
},
]
interface NodeEnrichedRowType {
node_id: number;
identity_key: string;
bond: string;
host: string;
location: string;
owner: string;
version: string;
}
function nodeEnrichedToGridRow(node: any): NodeEnrichedRowType {
return {
node_id: node.node_id,
owner: node.bond_information?.owner || '',
identity_key: node.bond_information?.node?.identity_key || '',
location: node.location?.country_name || '',
bond: node.bond_information?.original_pledge.amount || 0, // TODO: format
host: node.bond_information?.node?.host || '',
version: node.description?.build_information?.build_version || '',
};
}
/**
* Shows nym node details
*/
const PageNymNodeDetailsWithState = ({
selectedNymNode,
}: {
selectedNymNode?: any
}) => {
const { uptimeHistory } = useNymNodeContext()
const enrichedData = React.useMemo(() => selectedNymNode ? [nodeEnrichedToGridRow(selectedNymNode)] : [], []);
const hasVestingContractDelegations = React.useMemo(() => selectedNymNode?.delegations?.filter((d: any) => d.proxy)?.length, [selectedNymNode]);
return (
<Box component="main">
<Title text="Nym Node Detail" />
<Grid container mt={4}>
<Grid item xs={12}>
<DetailTable
columnsData={columns}
tableName="Node detail table"
rows={enrichedData}
/>
</Grid>
</Grid>
<Grid container mt={2} spacing={2}>
{selectedNymNode.rewarding_details &&
<Grid item xs={12} md={4}>
<TableContainer component={Paper}>
<Table>
<TableBody>
<TableRow>
<TableCell colSpan={2}>
Delegations and Rewards
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
<strong>Operator</strong>
</TableCell>
<TableCell align="right">
{humanReadableCurrencyToString({ amount : selectedNymNode.rewarding_details.operator.split('.')[0], denom: "unym" })}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
<strong>
{hasVestingContractDelegations ?
<VestingDelegationWarning plural={true}>
Delegates&nbsp;({selectedNymNode.rewarding_details.unique_delegations} delegates)
</VestingDelegationWarning> :
<>Delegates&nbsp;({selectedNymNode.rewarding_details.unique_delegations} delegates)</>
}
</strong>
</TableCell>
<TableCell align="right">
{humanReadableCurrencyToString({ amount : selectedNymNode.rewarding_details.delegates.split('.')[0], denom: "unym" })}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
<strong>Profit margin</strong>
</TableCell>
<TableCell align="right">
{selectedNymNode.rewarding_details.cost_params.profit_margin_percent * 100}%
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
<strong>Operator costs</strong>
</TableCell>
<TableCell align="right">
{humanReadableCurrencyToString(selectedNymNode.rewarding_details.cost_params.interval_operating_cost)}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Grid>}
{selectedNymNode.description?.declared_role && <Grid item xs={12} md={4}>
<TableContainer component={Paper}>
<Table>
<TableBody>
<TableRow>
<TableCell colSpan={2}>
Node roles
</TableCell>
</TableRow>
<TableRow>
<TableCell>
Self declared roles
</TableCell>
<TableCell>
<DeclaredRole declared_role={selectedNymNode.description?.declared_role}/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Grid>}
</Grid>
<Grid container spacing={2} mt={2}>
<Grid item xs={12} md={8}>
{uptimeHistory && (
<ContentCard title="Routing Score">
{uptimeHistory.error && (
<ComponentError text="There was a problem retrieving routing score." />
)}
<UptimeChart
loading={uptimeHistory.isLoading}
xLabel="Date"
yLabel="Daily average"
uptimeStory={uptimeHistory}
/>
</ContentCard>
)}
</Grid>
</Grid>
<Box mt={2}>
<NodeDelegationsTable node={selectedNymNode}/>
</Box>
</Box>
)
}
/**
* Guard component to handle loading and not found states
*/
const PageNymNodeDetailGuard = () => {
const [selectedNode, setSelectedNode] = React.useState<any>()
const [isLoading, setLoading] = React.useState<boolean>(true);
const [error, setError] = React.useState<string>();
const { fetchNodeById } = useMainContext()
const { id } = useParams()
React.useEffect(() => {
setSelectedNode(undefined);
setLoading(true);
(async () => {
if(typeof(id) === "string") {
try {
const res = await fetchNodeById(Number.parseInt(id));
setSelectedNode(res);
} catch(e: any) {
setError(e.message);
}
finally {
setLoading(false);
}
}
})();
}, [id])
if (isLoading) {
return <CircularProgress />
}
// loaded, but not found
if (error) {
return (
<Alert severity="warning">
<AlertTitle>Nym node not found</AlertTitle>
Sorry, we could not find a node with id <code>{id || ''}</code>
</Alert>
)
}
return <PageNymNodeDetailsWithState selectedNymNode={selectedNode} />
}
/**
* Wrapper component that adds the node content based on the `id` in the address URL
*/
const PageNymNodeDetail = () => {
const { id } = useParams()
if (!id || typeof id !== 'string') {
return (
<Alert severity="error">Oh no! Could not find that node</Alert>
)
}
return (
<NymNodeContextProvider nymNodeId={id}>
<PageNymNodeDetailGuard />
</NymNodeContextProvider>
)
}
export default PageNymNodeDetail
@@ -0,0 +1,254 @@
'use client'
import React, { useMemo } from 'react'
import { Box, Card, Grid, Stack, Chip } from '@mui/material'
import { useTheme } from '@mui/material/styles'
import {
MRT_ColumnDef,
MaterialReactTable,
useMaterialReactTable,
} from 'material-react-table'
import { diff, gte, rcompare } from 'semver'
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'
import { Tooltip as InfoTooltip } from '@nymproject/react/tooltip/Tooltip'
import { useMainContext } from '@/app/context/main'
import { CustomColumnHeading } from '@/app/components/CustomColumnHeading'
import { Title } from '@/app/components/Title'
import { humanReadableCurrencyToString } from '@/app/utils/currency'
import { Tooltip } from '@/app/components/Tooltip'
import { EXPLORER_FOR_ACCOUNTS } from '@/app/api/constants'
import { splice } from '@/app/utils'
import StyledLink from '@/app/components/StyledLink'
import {DeclaredRole} from "@/app/network-components/nodes/DeclaredRole";
function getFlagEmoji(countryCode: string) {
const codePoints = countryCode
.toUpperCase()
.split('')
.map(char => 127397 + char.charCodeAt(0));
return String.fromCodePoint(...codePoints);
}
const PageNodes = () => {
const [isLoading, setLoading] = React.useState(true);
const { nodes, fetchNodes } = useMainContext()
React.useEffect(() => {
(async () => {
try {
await fetchNodes();
} finally {
setLoading(false);
}
})();
}, []);
const columns = useMemo<MRT_ColumnDef<any>[]>(() => {
return [
{
id: 'nym-node-data',
header: 'Nym Node Data',
columns: [
{
id: 'node_id',
header: 'Node Id',
accessorKey: 'node_id',
size: 75,
},
{
id: 'identity_key',
header: 'Identity Key',
accessorKey: 'identity_key',
size: 250,
Cell: ({ row }) => {
return (
<Stack direction="row" alignItems="center" gap={1}>
<CopyToClipboard
sx={{ mr: 0.5, color: 'grey.400' }}
smallIcons
value={row.original.bond_information.node.identity_key}
tooltip={`Copy identity key ${row.original.bond_information.node.identity_key} to clipboard`}
/>
<StyledLink
to={`/network-components/nodes/${row.original.node_id}`}
dataTestId="identity-link"
color="text.primary"
>
{splice(7, 29, row.original.bond_information.node.identity_key)}
</StyledLink>
</Stack>
)
},
},
{
id: 'version',
header: 'Version',
accessorKey: 'description.build_information.build_version',
size: 75,
Cell: ({ row }) => {
return (
<StyledLink
to={`/network-components/nodes/${row.original.node_id}`}
data-testid="version"
color="text.primary"
>
{row.original.description?.build_information?.build_version || "-"}
</StyledLink>
)
},
},
{
id: 'contract_node_type',
header: 'Kind',
accessorKey: 'contract_node_type',
size: 150,
Cell: ({ row }) => {
return (
<StyledLink
to={`/network-components/nodes/${row.original.node_id}`}
data-testid="contract_node_type"
color="text.primary"
>
<code>{row.original.contract_node_type || "-"}</code>
</StyledLink>
)
},
},
{
id: 'declared_role',
header: 'Declare Role',
accessorKey: 'description.declared_role',
size: 250,
Cell: ({ row }) => {
return (
<Box
sx={{ justifyContent: 'flex-start', cursor: 'pointer' }}
data-testid="declared_role-button"
>
<DeclaredRole declared_role={row.original.description?.declared_role}/>
</Box>
)
},
},
{
id: 'total_stake',
header: 'Total Stake',
accessorKey: 'description.total_stake',
size: 250,
Cell: ({ row }) => {
return (
<Box
sx={{ justifyContent: 'flex-start', cursor: 'pointer' }}
data-testid="total_stake-button"
>
{humanReadableCurrencyToString({ amount: row.original.total_stake || 0, denom: "unym" })}
</Box>
)
},
},
{
id: 'location',
header: 'Location',
accessorKey: 'location.country_name',
size: 75,
Cell: ({ row }) => {
return (
<Box
sx={{ justifyContent: 'flex-start', cursor: 'pointer' }}
data-testid="location-button"
>
<Tooltip
text={row.original.location?.country_name || "-"}
id="nym-node-location-text"
>
<Box
sx={{
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{row.original.location?.country_name ? <>{getFlagEmoji(row.original.location.two_letter_iso_country_code.toUpperCase())}&nbsp;{row.original.location.two_letter_iso_country_code}</> : <>-</> }
</Box>
</Tooltip>
</Box>
)
},
},
{
id: 'host',
header: 'IP',
accessorKey: 'bond_information.node.host',
size: 150,
Cell: ({ row }) => {
return (
<StyledLink
to={`/network-components/nodes/${row.original.node_id}`}
data-testid="host"
color="text.primary"
>
{row.original.bond_information?.node?.host || "-"}
</StyledLink>
)
},
},
{
id: 'owner',
header: 'Owner',
accessorKey: 'bond_information.owner',
size: 150,
Cell: ({ row }) => {
return (
<StyledLink
to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.bond_information?.owner || "-"}`}
target="_blank"
data-testid="bond_information.node.owner"
color="text.primary"
>
{splice(7, 29, row.original.bond_information?.owner)}
</StyledLink>
)
},
},
],
},
]
}, [])
const table = useMaterialReactTable({
columns,
data: nodes?.data || [],
state: {
isLoading,
showLoadingOverlay: isLoading,
},
initialState: {
isLoading: true,
showLoadingOverlay: true,
}
})
return (
<>
<Box mb={2}>
<Title text="Nym Nodes" />
</Box>
<Grid container>
<Grid item xs={12}>
<Card
sx={{
padding: 2,
height: '100%',
}}
>
<MaterialReactTable table={table} />
</Card>
</Grid>
</Grid>
</>
)
}
export default PageNodes
@@ -1,140 +0,0 @@
'use client'
import React, { useMemo } from 'react'
import {
Box,
Button,
Card,
FormControl,
Grid,
ListItem,
Menu,
Typography,
} from '@mui/material'
import { TableToolbar } from '@/app/components/TableToolbar'
import { Title } from '@/app/components/Title'
import { useMainContext } from '@/app/context/main'
import { CustomColumnHeading } from '@/app/components/CustomColumnHeading'
import {
MRT_ColumnDef,
MaterialReactTable,
useMaterialReactTable,
} from 'material-react-table'
import { DirectoryServiceProvider } from '@/app/typeDefs/explorer-api'
const SupportedApps = () => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null)
const open = Boolean(anchorEl)
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget)
}
const handleClose = () => {
setAnchorEl(null)
}
const anchorRef = React.useRef<HTMLButtonElement>(null)
return (
<FormControl size="small">
<Button
ref={anchorRef}
onClick={handleClick}
size="large"
variant="outlined"
color="inherit"
sx={{ mr: 2, textTransform: 'capitalize' }}
>
Supported Apps
</Button>
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
<ListItem>Keybase</ListItem>
<ListItem>Telegram</ListItem>
<ListItem>Electrum</ListItem>
<ListItem>Blockstream Green</ListItem>
</Menu>
</FormControl>
)
}
const ServiceProviders = () => {
const { serviceProviders } = useMainContext()
const columns = useMemo<MRT_ColumnDef<DirectoryServiceProvider>[]>(() => {
return [
{
id: 'service-providers-data',
header: 'Service Providers Data',
columns: [
{
id: 'address',
accessorKey: 'address',
header: 'Client ID',
size: 450,
},
{
id: 'service_type-type',
accessorKey: 'service_type',
header: 'Type',
size: 100,
},
{
id: 'routing_score-score',
accessorKey: 'routing_score',
header: 'Routing score',
Header() {
return (
<CustomColumnHeading
headingTitle="Routing score"
tooltipInfo="Routing score is only displayed for the service providers that had a successful ping within the last two hours"
/>
)
},
Cell({ row }) {
return row.original.routing_score || '-'
},
},
],
},
]
}, [])
const table = useMaterialReactTable({
columns,
data: serviceProviders?.data || [],
layoutMode: 'grid',
state: {
isLoading: serviceProviders?.isLoading,
},
initialState: {
sorting: [
{
id: 'routing_score',
desc: true,
},
],
},
})
return (
<>
<Box mb={2}>
<Title text="Service Providers" />
</Box>
<Grid container>
<Grid item xs={12}>
<Card
sx={{
padding: 2,
}}
>
<>
<TableToolbar childrenBefore={<SupportedApps />} />
<MaterialReactTable table={table} />
</>
</Card>
</Grid>
</Grid>
</>
)
}
export default ServiceProviders
+35 -47
View File
@@ -13,7 +13,7 @@ import { GatewaysSVG } from '@/app/icons/GatewaysSVG'
import { ValidatorsSVG } from '@/app/icons/ValidatorsSVG'
import { ContentCard } from '@/app/components/ContentCard'
import { WorldMap } from '@/app/components/WorldMap'
import { BIG_DIPPER } from '@/app/api/constants'
import { BLOCK_EXPLORER_BASE_URL } from '@/app/api/constants'
import { formatNumber } from '@/app/utils'
import { useMainContext } from './context/main'
import { useRouter } from 'next/navigation'
@@ -42,71 +42,59 @@ const PageOverview = () => {
<>
<Grid item xs={12} md={4}>
<StatsCard
onClick={() => router.push('/network-components/mixnodes')}
title="Mixnodes"
onClick={() => router.push('/network-components/nodes')}
title="Nodes"
icon={<MixnodesSVG />}
count={summaryOverview.data?.mixnodes.count || ''}
errorMsg={summaryOverview?.error}
/>
</Grid>
<Grid item xs={12} md={4}>
<StatsCard
onClick={() =>
router.push('/network-components/mixnodes?status=active')
}
title="Active nodes"
icon={<Icons.Mixnodes.Status.Active />}
color={
theme.palette.nym.networkExplorer.mixnodes.status.active
}
count={summaryOverview.data?.mixnodes.activeset.active}
errorMsg={summaryOverview?.error}
/>
</Grid>
<Grid item xs={12} md={4}>
<StatsCard
onClick={() =>
router.push('/network-components/mixnodes?status=standby')
}
title="Standby nodes"
color={
theme.palette.nym.networkExplorer.mixnodes.status.standby
}
icon={<Icons.Mixnodes.Status.Standby />}
count={summaryOverview.data?.mixnodes.activeset.standby}
count={summaryOverview.data?.nymnodes?.count || ''}
errorMsg={summaryOverview?.error}
/>
</Grid>
</>
)}
{gateways && (
{summaryOverview && (
<Grid item xs={12} md={4}>
<StatsCard
onClick={() => router.push('/network-components/gateways')}
title="Gateways"
count={gateways?.data?.length || ''}
errorMsg={gateways?.error}
onClick={() => router.push('/network-components/nodes')}
title="Mixnodes"
count={summaryOverview.data?.nymnodes?.roles?.mixnode || ''}
icon={<GatewaysSVG />}
/>
</Grid>
)}
{serviceProviders && (
{summaryOverview && (
<Grid item xs={12} md={4}>
<StatsCard
onClick={() =>
router.push('/network-components/service-providers')
}
title="Service providers"
icon={<PeopleAlt />}
count={serviceProviders.data?.length}
errorMsg={summaryOverview?.error}
onClick={() => router.push('/network-components/nodes')}
title="Entry Gateways"
count={summaryOverview.data?.nymnodes?.roles?.entry || ''}
icon={<GatewaysSVG />}
/>
</Grid>
)}
{summaryOverview && (
<Grid item xs={12} md={4}>
<StatsCard
onClick={() => router.push('/network-components/nodes')}
title="Exit Gateways"
count={summaryOverview.data?.nymnodes?.roles?.exit_ipr || ''}
icon={<GatewaysSVG />}
/>
</Grid>
)}
{summaryOverview && (
<Grid item xs={12} md={4}>
<StatsCard
onClick={() => router.push('/network-components/nodes')}
title="SOCKS5 Network Requesters"
count={summaryOverview.data?.nymnodes?.roles?.exit_nr || ''}
icon={<GatewaysSVG />}
/>
</Grid>
)}
{validators && (
<Grid item xs={12} md={4}>
<StatsCard
onClick={() => window.open(`${BIG_DIPPER}/validators`)}
onClick={() => window.open(`${BLOCK_EXPLORER_BASE_URL}/validators`)}
title="Validators"
count={validators?.data?.count || ''}
errorMsg={validators?.error}
@@ -117,7 +105,7 @@ const PageOverview = () => {
{block?.data && (
<Grid item xs={12}>
<Link
href={`${BIG_DIPPER}/blocks`}
href={`${BLOCK_EXPLORER_BASE_URL}/blocks`}
target="_blank"
rel="noreferrer"
underline="none"
+18 -1
View File
@@ -20,6 +20,15 @@ export interface SummaryOverviewResponse {
validators: {
count: number;
};
nymnodes: {
count: number;
roles: {
mixnode: number;
entry: number;
exit_nr: number;
exit_ipr: number;
};
};
}
export interface MixNode {
@@ -156,7 +165,15 @@ export interface LocatedGateway {
location?: Location;
}
export type GatewayResponse = GatewayBond[];
export type GatewayResponse = LocatedGateway[];
export interface NymNodeReportResponse {
identity: string;
owner: string;
most_recent: number;
last_hour: number;
last_day: number;
}
export interface GatewayReportResponse {
identity: string;
+10
View File
@@ -31,6 +31,16 @@ export const currencyToString = ({ amount, dp, denom = DENOM }: { amount: string
return `${toDisplay(printableAmount, dp)} ${printableDenom}`;
};
function addThousandsSeparator(value: string) {
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
}
export const humanReadableCurrencyToString = ({ amount, denom }: { amount: string, denom: string }) => {
const str = currencyToString({ amount, denom, dp: 2 });
const parts = str.split('.');
return [addThousandsSeparator(parts[0]), parts[1]].join('.');
}
export const stakingCurrencyToString = (amount: string, denom: string = DENOM_STAKING) =>
printableCoin({
amount,
+2 -1
View File
@@ -17,7 +17,8 @@
"react-error-boundary": "^4.0.13",
"material-react-table": "^2.12.1",
"@mui/x-date-pickers": "7.1.1",
"@mui/x-data-grid": "7.1.1"
"@mui/x-data-grid": "7.1.1",
"@mui/x-charts": "^7.22.3"
},
"devDependencies": {
"@types/node": "^20",
-1
View File
@@ -31,7 +31,6 @@ impl GatewayTopologyProvider {
min_gateway_performance: 0,
},
nym_api_url,
env!("CARGO_PKG_VERSION").to_string(),
Some(user_agent),
),
cache_ttl,
+1 -1
View File
@@ -540,7 +540,7 @@ impl<St> Gateway<St> {
// as opposed to the validator API
for api_url in self.config.get_nym_api_endpoints() {
let client = nym_validator_client::NymApiClient::new(api_url.clone());
match client.get_all_basic_nodes(None).await {
match client.get_all_basic_nodes().await {
Ok(nodes) => {
return nodes.iter().any(|node| {
&node.ed25519_identity_pubkey == self.identity_keypair.public_key()
+1 -1
View File
@@ -160,7 +160,7 @@ impl MixNode {
// as opposed to the validator API
for api_url in self.config.get_nym_api_endpoints() {
let client = nym_validator_client::NymApiClient::new(api_url.clone());
match client.get_all_basic_nodes(None).await {
match client.get_all_basic_nodes().await {
Ok(nodes) => {
return nodes.iter().any(|node| {
&node.ed25519_identity_pubkey == self.identity_keypair.public_key()
@@ -0,0 +1,23 @@
/*
* Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: GPL-3.0-only
*/
CREATE TABLE monitor_run_report
(
monitor_run_id INTEGER PRIMARY KEY REFERENCES monitor_run (id),
network_reliability FLOAT NOT NULL,
packets_sent INTEGER NOT NULL,
packets_received INTEGER NOT NULL
);
CREATE TABLE monitor_run_score
(
-- mixnode or gateway
typ TEXT NOT NULL,
monitor_run_id INTEGER NOT NULL REFERENCES monitor_run_report (monitor_run_id),
rounded_score INTEGER NOT NULL,
nodes_count INTEGER NOT NULL
);
CREATE INDEX monitor_run_score_id ON monitor_run_score (monitor_run_id);
+145
View File
@@ -32,6 +32,7 @@ use schemars::gen::SchemaGenerator;
use schemars::schema::{InstanceType, Schema, SchemaObject};
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::BTreeMap;
use std::fmt::{Debug, Display, Formatter};
use std::net::IpAddr;
use std::ops::{Deref, DerefMut};
@@ -1255,6 +1256,18 @@ pub struct PartialTestResult {
pub type MixnodeTestResultResponse = PaginatedResponse<PartialTestResult>;
pub type GatewayTestResultResponse = PaginatedResponse<PartialTestResult>;
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NetworkMonitorRunDetailsResponse {
pub monitor_run_id: i64,
pub network_reliability: f64,
pub total_sent: usize,
pub total_received: usize,
// integer score to number of nodes with that score
pub mixnode_results: BTreeMap<u8, usize>,
pub gateway_results: BTreeMap<u8, usize>,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NoiseDetails {
#[schemars(with = "String")]
@@ -1327,6 +1340,138 @@ impl NodeRefreshBody {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct RewardedSetResponse {
pub entry_gateways: Vec<NodeId>,
pub exit_gateways: Vec<NodeId>,
pub layer1: Vec<NodeId>,
pub layer2: Vec<NodeId>,
pub layer3: Vec<NodeId>,
pub standby: Vec<NodeId>,
}
pub use config_score::*;
pub mod config_score {
use nym_contracts_common::NaiveFloat;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use utoipa::ToSchema;
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct ConfigScoreDataResponse {
pub parameters: ConfigScoreParams,
pub version_history: Vec<HistoricalNymNodeVersionEntry>,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)]
pub struct HistoricalNymNodeVersionEntry {
/// The unique, ordered, id of this particular entry
pub id: u32,
/// Data associated with this particular version
pub version_information: HistoricalNymNodeVersion,
}
impl PartialOrd for HistoricalNymNodeVersionEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
// we only care about id for the purposes of ordering as they should have unique data
self.id.partial_cmp(&other.id)
}
}
impl From<nym_mixnet_contract_common::HistoricalNymNodeVersionEntry>
for HistoricalNymNodeVersionEntry
{
fn from(value: nym_mixnet_contract_common::HistoricalNymNodeVersionEntry) -> Self {
HistoricalNymNodeVersionEntry {
id: value.id,
version_information: value.version_information.into(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)]
pub struct HistoricalNymNodeVersion {
/// Version of the nym node that is going to be used for determining the version score of a node.
/// note: value stored here is pre-validated `semver::Version`
pub semver: String,
/// Block height of when this version has been added to the contract
pub introduced_at_height: u64,
// for now ignore that field. it will give nothing useful to the users
// pub difference_since_genesis: TotalVersionDifference,
}
impl From<nym_mixnet_contract_common::HistoricalNymNodeVersion> for HistoricalNymNodeVersion {
fn from(value: nym_mixnet_contract_common::HistoricalNymNodeVersion) -> Self {
HistoricalNymNodeVersion {
semver: value.semver,
introduced_at_height: value.introduced_at_height,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct ConfigScoreParams {
/// Defines weights for calculating numbers of versions behind the current release.
pub version_weights: OutdatedVersionWeights,
/// Defines the parameters of the formula for calculating the version score
pub version_score_formula_params: VersionScoreFormulaParams,
}
/// Defines weights for calculating numbers of versions behind the current release.
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct OutdatedVersionWeights {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub prerelease: u32,
}
/// Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling)
/// define the relevant parameters
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct VersionScoreFormulaParams {
pub penalty: f64,
pub penalty_scaling: f64,
}
impl From<nym_mixnet_contract_common::ConfigScoreParams> for ConfigScoreParams {
fn from(value: nym_mixnet_contract_common::ConfigScoreParams) -> Self {
ConfigScoreParams {
version_weights: value.version_weights.into(),
version_score_formula_params: value.version_score_formula_params.into(),
}
}
}
impl From<nym_mixnet_contract_common::OutdatedVersionWeights> for OutdatedVersionWeights {
fn from(value: nym_mixnet_contract_common::OutdatedVersionWeights) -> Self {
OutdatedVersionWeights {
major: value.major,
minor: value.minor,
patch: value.patch,
prerelease: value.prerelease,
}
}
}
impl From<nym_mixnet_contract_common::VersionScoreFormulaParams> for VersionScoreFormulaParams {
fn from(value: nym_mixnet_contract_common::VersionScoreFormulaParams) -> Self {
VersionScoreFormulaParams {
penalty: value.penalty.naive_to_f64(),
penalty_scaling: value.penalty_scaling.naive_to_f64(),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
+2 -7
View File
@@ -120,14 +120,9 @@ impl EpochAdvancer {
let epoch_end = interval.current_epoch_end();
let legacy_mixnodes = self.nym_contract_cache.legacy_mixnodes_filtered().await;
let legacy_gateways = self.nym_contract_cache.legacy_gateways_filtered().await;
// TODO: for the purposes of rewarding, this might have to grab some pre-filtered nodes instead,
// such as ones that use up to date version or have correct 'peanut' score
let nym_nodes = self.nym_contract_cache.nym_nodes().await;
if legacy_mixnodes.is_empty() && legacy_gateways.is_empty() && nym_nodes.is_empty() {
if nym_nodes.is_empty() {
// that's a bit weird, but ok
warn!("there don't seem to be any nodes on the network!")
}
@@ -160,7 +155,7 @@ impl EpochAdvancer {
// note: those operations don't really have to be atomic, so it's fine to send them
// as separate transactions
self.reconcile_epoch_events().await?;
self.update_rewarded_set_and_advance_epoch(&legacy_mixnodes, &legacy_gateways, &nym_nodes)
self.update_rewarded_set_and_advance_epoch(&nym_nodes)
.await?;
info!("Purging old node statuses from the storage...");
@@ -5,8 +5,6 @@ use crate::epoch_operations::error::RewardingError;
use crate::epoch_operations::helpers::stake_to_f64;
use crate::EpochAdvancer;
use cosmwasm_std::Decimal;
use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer};
use nym_mixnet_contract_common::helpers::IntoBaseDecimal;
use nym_mixnet_contract_common::reward_params::{Performance, RewardedSetParams};
use nym_mixnet_contract_common::{EpochState, NodeId, NymNodeDetails, RewardedSet};
use rand::prelude::SliceRandom;
@@ -204,8 +202,6 @@ impl EpochAdvancer {
async fn attach_performance_to_eligible_nodes(
&self,
legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer],
legacy_gateways: &[LegacyGatewayBondWithId],
nym_nodes: &[NymNodeDetails],
) -> Vec<NodeWithStakeAndPerformance> {
let mut with_performance = Vec::new();
@@ -218,62 +214,6 @@ impl EpochAdvancer {
return Vec::new();
};
for mix in legacy_mixnodes {
let node_id = mix.mix_id();
let total_stake = mix.total_stake();
let Some(annotation) = status_cache.get(&node_id) else {
debug!("couldn't find annotation for legacy mixnode {node_id}");
continue;
};
if mix.bond_information.proxy.is_some() {
debug!("legacy mixnode {node_id} is using vested tokens");
continue;
}
let performance = annotation.detailed_performance.to_rewarding_performance();
debug!(
"legacy mixnode {}: stake: {total_stake}, performance: {performance}",
mix.mix_id()
);
with_performance.push(NodeWithStakeAndPerformance {
node_id: mix.mix_id(),
available_roles: vec![AvailableRole::Mix],
total_stake,
performance,
})
}
for gateway in legacy_gateways {
let node_id = gateway.node_id;
let total_stake = gateway
.bond
.pledge_amount
.amount
.into_base_decimal()
.unwrap_or_default();
let Some(annotation) = status_cache.get(&node_id) else {
debug!("couldn't find annotation for legacy gateway {node_id}");
continue;
};
let performance = annotation.detailed_performance.to_rewarding_performance();
debug!(
"legacy gateway {}: stake: {total_stake}, performance: {performance}",
gateway.node_id
);
with_performance.push(NodeWithStakeAndPerformance {
node_id: gateway.node_id,
available_roles: vec![AvailableRole::EntryGateway],
total_stake,
performance,
})
}
for nym_node in nym_nodes {
let node_id = nym_node.node_id();
let total_stake = nym_node.total_stake();
@@ -283,7 +223,7 @@ impl EpochAdvancer {
};
let Some(annotation) = status_cache.get(&node_id) else {
debug!("couldn't find annotation for nym-node gateway {node_id}");
debug!("couldn't find annotation for nym-node {node_id}");
continue;
};
@@ -319,8 +259,6 @@ impl EpochAdvancer {
pub(super) async fn update_rewarded_set_and_advance_epoch(
&self,
legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer],
legacy_gateways: &[LegacyGatewayBondWithId],
nym_nodes: &[NymNodeDetails],
) -> Result<(), RewardingError> {
let epoch_status = self.nyxd_client.get_current_epoch_status().await?;
@@ -333,13 +271,8 @@ impl EpochAdvancer {
}
info!("attempting to assign the rewarded set for the upcoming epoch...");
let nodes_with_performance = self
.attach_performance_to_eligible_nodes(
legacy_mixnodes,
legacy_gateways,
nym_nodes,
)
.await;
let nodes_with_performance =
self.attach_performance_to_eligible_nodes(nym_nodes).await;
if let Err(err) = self
._update_rewarded_set_and_advance_epoch(nodes_with_performance)
+9 -11
View File
@@ -2,9 +2,8 @@
// SPDX-License-Identifier: GPL-3.0-only
use futures::Stream;
use nym_crypto::asymmetric::identity;
use nym_crypto::asymmetric::{ed25519, identity};
use nym_gateway_client::{AcknowledgementReceiver, MixnetMessageReceiver};
use nym_mixnet_contract_common::IdentityKey;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio_stream::StreamMap;
@@ -15,8 +14,8 @@ pub(crate) enum GatewayMessages {
}
pub(crate) struct GatewaysReader {
ack_map: StreamMap<IdentityKey, AcknowledgementReceiver>,
stream_map: StreamMap<IdentityKey, MixnetMessageReceiver>,
ack_map: StreamMap<ed25519::PublicKey, AcknowledgementReceiver>,
stream_map: StreamMap<ed25519::PublicKey, MixnetMessageReceiver>,
}
impl GatewaysReader {
@@ -33,19 +32,18 @@ impl GatewaysReader {
message_receiver: MixnetMessageReceiver,
ack_receiver: AcknowledgementReceiver,
) {
let channel_id = id.to_string();
self.stream_map.insert(channel_id.clone(), message_receiver);
self.ack_map.insert(channel_id, ack_receiver);
self.stream_map.insert(id, message_receiver);
self.ack_map.insert(id, ack_receiver);
}
pub fn remove_receivers(&mut self, id: &str) {
self.stream_map.remove(id);
self.ack_map.remove(id);
pub fn remove_receivers(&mut self, id: ed25519::PublicKey) {
self.stream_map.remove(&id);
self.ack_map.remove(&id);
}
}
impl Stream for GatewaysReader {
type Item = (IdentityKey, GatewayMessages);
type Item = (ed25519::PublicKey, GatewayMessages);
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match Pin::new(&mut self.ack_map).poll_next(cx) {
+32 -23
View File
@@ -12,10 +12,12 @@ use crate::network_monitor::monitor::sender::PacketSender;
use crate::network_monitor::monitor::summary_producer::SummaryProducer;
use crate::network_monitor::monitor::Monitor;
use crate::node_describe_cache::DescribedNodes;
use crate::node_status_api::NodeStatusCache;
use crate::nym_contract_cache::cache::NymContractCache;
use crate::storage::NymApiStorage;
use crate::support::caching::cache::SharedCache;
use crate::support::{config, nyxd};
use crate::support::config::Config;
use crate::support::nyxd;
use futures::channel::mpsc;
use nym_bandwidth_controller::BandwidthController;
use nym_credential_storage::persistent_storage::PersistentStorage;
@@ -35,9 +37,10 @@ pub(crate) mod test_route;
pub(crate) const ROUTE_TESTING_TEST_NONCE: u64 = 0;
pub(crate) fn setup<'a>(
config: &'a config::NetworkMonitor,
config: &'a Config,
nym_contract_cache: &NymContractCache,
described_cache: SharedCache<DescribedNodes>,
node_status_cache: NodeStatusCache,
storage: &NymApiStorage,
nyxd_client: nyxd::Client,
) -> NetworkMonitorBuilder<'a> {
@@ -47,24 +50,27 @@ pub(crate) fn setup<'a>(
storage.to_owned(),
nym_contract_cache.clone(),
described_cache,
node_status_cache,
)
}
pub(crate) struct NetworkMonitorBuilder<'a> {
config: &'a config::NetworkMonitor,
config: &'a Config,
nyxd_client: nyxd::Client,
node_status_storage: NymApiStorage,
contract_cache: NymContractCache,
described_cache: SharedCache<DescribedNodes>,
node_status_cache: NodeStatusCache,
}
impl<'a> NetworkMonitorBuilder<'a> {
pub(crate) fn new(
config: &'a config::NetworkMonitor,
config: &'a Config,
nyxd_client: nyxd::Client,
node_status_storage: NymApiStorage,
contract_cache: NymContractCache,
described_cache: SharedCache<DescribedNodes>,
node_status_cache: NodeStatusCache,
) -> Self {
NetworkMonitorBuilder {
config,
@@ -72,10 +78,11 @@ impl<'a> NetworkMonitorBuilder<'a> {
node_status_storage,
contract_cache,
described_cache,
node_status_cache,
}
}
pub(crate) async fn build<R: MessageReceiver + Send + 'static>(
pub(crate) async fn build<R: MessageReceiver + Send + Sync + 'static>(
self,
) -> NetworkMonitorRunnables<R> {
// TODO: those keys change constant throughout the whole execution of the monitor.
@@ -94,7 +101,8 @@ impl<'a> NetworkMonitorBuilder<'a> {
let packet_preparer = new_packet_preparer(
self.contract_cache,
self.described_cache,
self.config.debug.per_node_test_packets,
self.node_status_cache,
self.config.network_monitor.debug.per_node_test_packets,
Arc::clone(&ack_key),
*identity_keypair.public_key(),
*encryption_keypair.public_key(),
@@ -103,7 +111,11 @@ impl<'a> NetworkMonitorBuilder<'a> {
let bandwidth_controller = {
BandwidthController::new(
nym_credential_storage::initialise_persistent_storage(
&self.config.storage_paths.credentials_database_path,
&self
.config
.network_monitor
.storage_paths
.credentials_database_path,
)
.await,
self.nyxd_client.clone(),
@@ -114,9 +126,7 @@ impl<'a> NetworkMonitorBuilder<'a> {
self.config,
gateway_status_update_sender,
Arc::clone(&identity_keypair),
self.config.debug.gateway_sending_rate,
bandwidth_controller,
self.config.debug.disabled_credentials_mode,
);
let received_processor = new_received_processor(
@@ -124,14 +134,15 @@ impl<'a> NetworkMonitorBuilder<'a> {
Arc::clone(&encryption_keypair),
ack_key,
);
let summary_producer = new_summary_producer(self.config.debug.per_node_test_packets);
let summary_producer =
new_summary_producer(self.config.network_monitor.debug.per_node_test_packets);
let packet_receiver = new_packet_receiver(
gateway_status_update_receiver,
received_processor_sender_channel,
);
let monitor = Monitor::new(
self.config,
&self.config.network_monitor,
packet_preparer,
packet_sender,
received_processor,
@@ -147,12 +158,12 @@ impl<'a> NetworkMonitorBuilder<'a> {
}
}
pub(crate) struct NetworkMonitorRunnables<R: MessageReceiver + Send + 'static> {
pub(crate) struct NetworkMonitorRunnables<R: MessageReceiver + Send + Sync + 'static> {
monitor: Monitor<R>,
packet_receiver: PacketReceiver,
}
impl<R: MessageReceiver + Send + 'static> NetworkMonitorRunnables<R> {
impl<R: MessageReceiver + Send + Sync + 'static> NetworkMonitorRunnables<R> {
// TODO: note, that is not exactly doing what we want, because when
// `ReceivedProcessor` is constructed, it already spawns a future
// this needs to be refactored!
@@ -169,6 +180,7 @@ impl<R: MessageReceiver + Send + 'static> NetworkMonitorRunnables<R> {
fn new_packet_preparer(
contract_cache: NymContractCache,
described_cache: SharedCache<DescribedNodes>,
node_status_cache: NodeStatusCache,
per_node_test_packets: usize,
ack_key: Arc<AckKey>,
self_public_identity: identity::PublicKey,
@@ -177,6 +189,7 @@ fn new_packet_preparer(
PacketPreparer::new(
contract_cache,
described_cache,
node_status_cache,
per_node_test_packets,
ack_key,
self_public_identity,
@@ -185,22 +198,16 @@ fn new_packet_preparer(
}
fn new_packet_sender(
config: &config::NetworkMonitor,
config: &Config,
gateways_status_updater: GatewayClientUpdateSender,
local_identity: Arc<identity::KeyPair>,
max_sending_rate: usize,
bandwidth_controller: BandwidthController<nyxd::Client, PersistentStorage>,
disabled_credentials_mode: bool,
) -> PacketSender {
PacketSender::new(
config,
gateways_status_updater,
local_identity,
config.debug.gateway_response_timeout,
config.debug.gateway_connection_timeout,
config.debug.max_concurrent_gateway_clients,
max_sending_rate,
bandwidth_controller,
disabled_credentials_mode,
)
}
@@ -227,10 +234,11 @@ fn new_packet_receiver(
// TODO: 1) does it still have to have separate builder or could we get rid of it now?
// TODO: 2) how do we make it non-async as other 'start' methods?
pub(crate) async fn start<R: MessageReceiver + Send + 'static>(
config: &config::NetworkMonitor,
pub(crate) async fn start<R: MessageReceiver + Send + Sync + 'static>(
config: &Config,
nym_contract_cache: &NymContractCache,
described_cache: SharedCache<DescribedNodes>,
node_status_cache: NodeStatusCache,
storage: &NymApiStorage,
nyxd_client: nyxd::Client,
shutdown: &TaskManager,
@@ -239,6 +247,7 @@ pub(crate) async fn start<R: MessageReceiver + Send + 'static>(
config,
nym_contract_cache,
described_cache,
node_status_cache,
storage,
nyxd_client,
);
@@ -0,0 +1,54 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::network_monitor::monitor::receiver::{GatewayClientUpdate, GatewayClientUpdateSender};
use crate::support::nyxd;
use nym_credential_storage::persistent_storage::PersistentStorage;
use nym_gateway_client::GatewayClient;
use std::ops::{Deref, DerefMut};
use tracing::warn;
pub(crate) struct GatewayClientHandle {
client: GatewayClient<nyxd::Client, PersistentStorage>,
gateways_status_updater: GatewayClientUpdateSender,
}
impl GatewayClientHandle {
pub(crate) fn new(
client: GatewayClient<nyxd::Client, PersistentStorage>,
gateways_status_updater: GatewayClientUpdateSender,
) -> Self {
GatewayClientHandle {
client,
gateways_status_updater,
}
}
}
impl Drop for GatewayClientHandle {
fn drop(&mut self) {
if self
.gateways_status_updater
.unbounded_send(GatewayClientUpdate::Disconnect(
self.client.gateway_identity(),
))
.is_err()
{
warn!("fail to cleanly shutdown gateway connection")
}
}
}
impl Deref for GatewayClientHandle {
type Target = GatewayClient<nyxd::Client, PersistentStorage>;
fn deref(&self) -> &Self::Target {
&self.client
}
}
impl DerefMut for GatewayClientHandle {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.client
}
}
@@ -1,102 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::support::nyxd;
use nym_credential_storage::persistent_storage::PersistentStorage;
use nym_crypto::asymmetric::identity::PUBLIC_KEY_LENGTH;
use nym_gateway_client::GatewayClient;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{Mutex, MutexGuard, TryLockError};
pub(crate) struct GatewayClientHandle(Arc<GatewayClientHandleInner>);
struct GatewayClientHandleInner {
client: Mutex<Option<GatewayClient<nyxd::Client, PersistentStorage>>>,
raw_identity: [u8; PUBLIC_KEY_LENGTH],
}
pub(crate) struct UnlockedGatewayClientHandle<'a>(
MutexGuard<'a, Option<GatewayClient<nyxd::Client, PersistentStorage>>>,
);
impl GatewayClientHandle {
pub(crate) fn new(gateway_client: GatewayClient<nyxd::Client, PersistentStorage>) -> Self {
GatewayClientHandle(Arc::new(GatewayClientHandleInner {
raw_identity: gateway_client.gateway_identity().to_bytes(),
client: Mutex::new(Some(gateway_client)),
}))
}
pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
// this could have also been achieved with a normal #[derive(Clone)] but I prefer to be explicit about it,
// because clippy would suggest some potentially confusing 'simplifications' regarding clone
pub(crate) fn clone_data_pointer(&self) -> Self {
GatewayClientHandle(Arc::clone(&self.0))
}
pub(crate) fn raw_identity(&self) -> [u8; PUBLIC_KEY_LENGTH] {
self.0.raw_identity
}
pub(crate) async fn is_invalid(&self) -> bool {
self.0.client.lock().await.is_none()
}
pub(crate) async fn lock_client(&self) -> UnlockedGatewayClientHandle<'_> {
UnlockedGatewayClientHandle(self.0.client.lock().await)
}
pub(crate) fn lock_client_unchecked(&self) -> UnlockedGatewayClientHandle<'_> {
UnlockedGatewayClientHandle(self.0.client.try_lock().unwrap())
}
pub(crate) fn try_lock_client(&self) -> Result<UnlockedGatewayClientHandle<'_>, TryLockError> {
self.0.client.try_lock().map(UnlockedGatewayClientHandle)
}
}
impl UnlockedGatewayClientHandle<'_> {
pub(crate) fn get_mut_unchecked(
&mut self,
) -> &mut GatewayClient<nyxd::Client, PersistentStorage> {
self.0.as_mut().unwrap()
}
pub(crate) fn inner_mut(
&mut self,
) -> Option<&mut GatewayClient<nyxd::Client, PersistentStorage>> {
self.0.as_mut()
}
pub(crate) fn invalidate(&mut self) {
*self.0 = None
}
}
pub(crate) type GatewayClientsMap = HashMap<[u8; PUBLIC_KEY_LENGTH], GatewayClientHandle>;
#[derive(Clone)]
pub(crate) struct ActiveGatewayClients {
// there is no point in using an RwLock here as there will only ever be two readers here and both
// potentially need write access.
// A BiLock would have been slightly better than a normal Mutex since it's optimised for two
// owners, but it's behind `unstable` feature flag in futures and it would be a headache if the API
// changed.
inner: Arc<Mutex<GatewayClientsMap>>,
}
impl ActiveGatewayClients {
pub(crate) fn new() -> Self {
ActiveGatewayClients {
inner: Arc::new(Mutex::new(HashMap::new())),
}
}
pub(crate) async fn lock(&self) -> MutexGuard<'_, GatewayClientsMap> {
self.inner.lock().await
}
}

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